AGENTS.md@apps/mobile · git:20261103.ca690a7 · 2026-11-03 · sha256 284034df154fa65c
AGENTS.md@apps/mobile git:20261103.ca690a7A
Immutable. This exact content never changes and is served at /api/v1/blob/284034df154fa65c.
# Mobile app (`apps/mobile`)
Expo / React Native app. The root `AGENTS.md` still applies; this guide covers mobile-only workflows.
## Tests
Mobile tests are host tests, not Vitest: `scripts/*.test.mjs` run under `node --test` on Node 24. They need no simulator, emulator or Metro.
```sh
pnpm --filter mobile test # every file
pnpm --filter mobile test:ci # same run, plus JUnit at apps/mobile/reports/node-junit.xml
node --test apps/mobile/scripts/layout.test.mjs # one file
node --test --test-name-pattern "Undo" apps/mobile/scripts/career-undo.test.mjs # matching cases
```
CI's unit job runs `test:ci` through Turbo, so a failing mobile test fails the pull request.
Tests never import app source directly. `scripts/harness.mjs` compiles one source file with esbuild and runs it against a mock map:
- `load(path, mocks, { globals, append, ...esbuildOptions })` compiles one file. `path` is relative to `scripts/` (`"../src/…"`). `globals` become free variables (`React`, `__DEV__`, `setTimeout`).
- `bundle(path, mocks, options)` also bundles the relative imports the file reaches; only `mocks` keys stay external.
- `run(code, mocks, globals)` evaluates already-compiled CommonJS. `tag` stands in for Lingui `t`. `mobileRequire` loads real packages from the app's dependencies. `esbuild` is the compiler.
`AssertionError: Missing boundary: X` means the file under test imports `X` and the test's mock map has no key for it. Most files keep a shared map near the top (`mocks`, `dependencies`, `common`, `defaults`) that their local `load` merges with per-call `extra`. Add the stub there:
```js
"@/ui/new-thing": { NewThing: "NewThing" }, // component: render as a string tag
"expo-haptics": { impactAsync: async () => {} }, // native module: no-op
immer: mobileRequire("immer"), // pure library: real module
```
When a source file gains an import, grep `scripts/` for its path (for example `features/write/dates`) to find every test that loads it; each one needs the stub. A few files keep their own loader because their setup differs (real-module fallback, `.po` handling, source-string mocks); edit those in place.
### `experience.test.mjs`
About 4,700 lines, written as one script rather than separate `test()` cases. The first failing assertion stops the rest.
- Lines 1–190 hold the shared setup: a fake `react` whose hooks are keyed by call order, `ui` and `mocks` (the shared boundary map), and `load(path, extra, append)` with `path` relative to `src/`.
- Helpers: `render(Component, props)` calls the component and returns its element tree. `nodes(tree)` flattens it. `control(tree, label)` returns the props of the element with that `label`, or fails. `reset()` clears hook state and the `calls` log. `settle()` flushes pending promises.
- Each case ends with `console.log("PASS …")`. Find a case by grepping its PASS line or the source path it loads (`load("features/…`).
### Adding cases
Put tests for a new feature in a new `scripts/<feature>.test.mjs` that uses `test()` from `node:test` and the harness. Extend `experience.test.mjs` only when a case needs its fake React and shared mocks: add the block beside related cases, start it with `reset()`, and end it with `console.log("PASS …")`.
## Bundle check
```sh
pnpm --filter mobile check:bundle
```
This runs `expo export` for iOS and Android: Metro bundles the whole app and compiles Hermes bytecode. It uses a fresh Metro cache in a temporary directory and deletes it afterwards, so it needs no `TMPDIR` or `--clear` flags and leaves nothing in the checkout. It takes about 1.5 minutes on an Apple-silicon Mac.
Run it after changing imports, dependencies, Babel or Metro config, or shared-package code the app reaches, and before handing off mobile work. CI runs it when `apps/mobile/` or `packages/` changes. `scripts/hermes-compat.test.mjs` is the quick per-file Hermes check inside the test suite.
## Where web features live on mobile
Names differ between the apps; start from this map instead of guessing paths.
| Web (`apps/web/src/…`) | Mobile (`apps/mobile/…`) |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `routes/builder/$resumeId`, `features/resume` | `app/builder/[id]`, `src/features/{write,design,check,share,history,document-menu}` |
| `routes/builder/letter`, `features/letters` | `app/letter/[id]`, `src/features/letters` |
| `routes/dashboard/applications`, `features/applications` | `app/(tabs)/applications`, `app/applications/[id]`, `src/features/{applications,applications-board,application-detail,reminders}` |
| `routes/dashboard/career`, `features/career` (incl. `workspace`, `knowledge`) | `app/(tabs)/career`, `src/features/{career,workspace}` |
| `routes/agent`, `features/assistant` | `app/builder/[id]/assistant.tsx`, `src/features/assistant` |
| `routes/dashboard/resumes`, `features/documents` | `app/(tabs)/documents`, `src/features/documents` |
| `routes/dashboard/settings`, `features/settings` | `app/(tabs)/settings`, `src/features/settings` |
| `routes/_home/ats-checker.tsx`, `features/ats-checker` | `app/ats-checker.tsx`, `src/features/ats-checker` |
| `features/command-palette` | `app/search.tsx`, `src/features/search` |
| `features/keyboard-shortcuts` | `src/shell/key-commands.ts`, `src/shell/shortcut-help.tsx` |
| `routes/auth`, `features/auth` | `app/(auth)`, `src/auth` |
| Public resume page (server: `apps/server/src/static/resume-page.ts`) | `app/r/[username]/[slug].tsx` |
| `packages/ui` primitives | `src/ui` (tokens in `src/ui/tokens.ts`) |
## OTA update or store build
The runtime version is an Expo fingerprint (`fingerprint.config.js`). JavaScript, styling, copy, translations and
bundled assets ship as an OTA update with the next stable release. Native dependencies or config plugins, permissions,
`app.config.ts` native fields, Expo SDK upgrades, `patches/`, and Forme native sources or
`packages/forme-native/scripts/prebuilt.json` need a new store build. Say which one a change needs in its PR. Release
steps: `.agents/docs/runbooks/mobile-releases.md`; workflow and signing setup: `.agents/docs/runbooks/mobile-ci.md`.
## Parity exceptions
The owner's explicit decisions; every other web/mobile gap is a bug. Add new exceptions here with the date.
- iOS hides the Donate row (store policy, first release).
- Applications CSV import and export are web-only (2026-10-19).
- Employer-form autofill and the embedded browser are not on Android or iOS (2026-11-01, RX-75). Mobile keeps review
packets, copyable answers, exact approved PDFs and opening the form in the system browser.