# Affected e2e (/docs/e2e/affected)



`warden affected` works out which specs a branch needs and `warden e2e` runs just those, failing when a required one fails; use it on branches and in CI instead of running the whole suite.

```bash
warden affected mobile --base origin/main --explain   # what would run, and why
warden e2e mobile --base origin/main --count 2        # run it: exit 0 = every required spec passed
```

Specs stay in your repo's own `*.e2e.ts` files, written with [`@delacour/warden-e2e`](/docs/e2e/specs). The relative imports of a spec count as helpers; warden picks the specs, leases devices, runs them with `bun test` and reports.

## How specs are picked [#how-specs-are-picked]

1. **Changed files.** Everything that differs from `merge-base(<base>, HEAD)`: commits on the branch, staged and unstaged edits, untracked files, both sides of a rename. `--files a,b` skips git.
2. **`runAll`.** A changed file matching one of the suite's `runAll` globs (lockfile, native project, bundler config: you list them, there are no defaults) selects every spec and skips the import walk. `ignore` globs drop a change before any step; `scope` keeps only matching changes.
3. **`paths`.** Globs on a spec select it when a changed file matches.
4. **Import graph.** Each spec lists its `entries`: the screens or routes it drives. Warden follows their imports, resolved the way the TypeScript compiler does (each file's nearest `tsconfig.json`: `paths`, `baseUrl`, `extends`, `customConditions`; workspace packages followed to their source). A changed file anywhere in that graph selects the spec.
   * **Per platform.** React Native platform files resolve per platform (`x.ios.tsx` → `x.native.tsx` → `x.tsx`), so an `.android.tsx` change only selects Android specs.
   * **Runtime imports only.** `import type` / `export type` are dropped, as the bundler drops them.
   * **Router layouts.** With `routerRoot`, every `_layout` above an entry counts as an entry too (file-based routers render them around the route).
   * **`maxDepth`** limits how many import hops from an entry still count. Apps with hub modules (a session context, a shared API client) otherwise reach almost everything from every screen; `3` is a good start.
   * **Needs TypeScript 5.0 or later.** The graph is walked with a TypeScript compiler: warden's own when it runs from source, else (the compiled `warden` binary, which does not carry one) the analysed project's, found as `node_modules/typescript` from the config's directory upward. Add `typescript` as a devDependency; without it (or with one older than 5.0) a run that needs the graph exits 1 and says so. `warden e2e --all` / `--specs` and runs with no changes do not need it. Yarn Plug'n'Play installs have no `node_modules`, so they are not supported (use `nodeLinker: node-modules`).
5. **Spec files.** A changed spec selects itself; a changed helper (any other `.ts` file in the specs directory) selects every spec that imports it.
6. **`always`** specs run every time, and so do specs with no `entries` / `paths` unless `unmapped` is `"skip"`.

A spec runs on the platforms named in its config, else the `ios` / `android` token in its id (`store-ios-02-chats`), else both.

`--explain` prints the reason for each spec, including the shortest import chain:

```text
ios  2 of 11 spec(s)  (base origin/main, 3 changed file(s))
  ● qa-chat-attachment  required
      import   src/modules/chats/composer.tsx
               src/app/(app)/chats/[chat_id].tsx → src/modules/chats/thread.tsx → src/modules/chats/composer.tsx
  ● store-ios-03-chat-thread  optional
      import   src/modules/chats/composer.tsx
  skipped: qa-offline-banner, store-ios-01-my-day, …
  reached no spec: src/modules/voice/voice-auth.ts
```

"Reached no spec" lists changed files no spec covers: gaps in the map. `--strict` exits 3 when there are any; `unmatched: "run-all"` instead makes such a change select every spec.

Without `--platform` and a suite `platform`, `warden affected` covers both platforms (one id list, the union); `warden e2e` needs exactly one. With no `e2e` suites configured, every spec is selected (no git diff).

## Configuring a suite [#configuring-a-suite]

```ts title="warden.config.ts"
import { defineConfig } from "@delacour/warden/config";

export default defineConfig({
  e2e: {
    mobile: {
      specsDir: "specs",
      spec: { bundleId: "com.example.mobile" },
      platform: "ios",
      tsconfig: "apps/mobile/tsconfig.json",
      routerRoot: "apps/mobile/src/app",
      maxDepth: 3,
      runAll: ["bun.lock", "patches/**", "apps/mobile/app.config.ts", "apps/mobile/ios/**", "apps/mobile/android/**"],
      ignore: ["**/*.md", "**/*.test.ts"],
      passes: 2,
      count: 2,
      specs: {
        "qa-chat-attachment": { entries: ["apps/mobile/src/app/(app)/chats/[chat_id].tsx"] },
        "qa-offline-*": { entries: ["apps/mobile/src/app/+native-intent.ts"], paths: ["apps/mobile/src/modules/linking/**"] },
        "store-*": { required: false }
      }
    }
  }
});
```

A spec's id is its path under `specsDir` without the extension. Keys in `specs` are ids or globs; a spec's exact key and every matching glob merge (`entries` / `paths` add up, the exact key's `required` / `platforms` / `maxDepth` win). All paths are relative to the config file. See the [field reference](/docs/reference/configuration#e2e).

## Running and gating [#running-and-gating]

`warden e2e` is [`warden batch`](/docs/devices/scripts) with the job list coming from the selection. Every batch and claim flag works (`--count`, `--serve`, `--serve-ready`, `--app`, `--record` …); the suite's `count`, `profile`, `retry`, `passes`, `app`, `ports`, `env`, `serve`, `serveReady` and `serveTimeout` fill in anything you don't pass, and `project` (a `projects[].name`) sets where the specs and serve run and which app is installed, with the same semantics as a [batch preset](/docs/devices/scripts).

* One `bun test` process per device runs its share of the specs (`isolate: "device"`, the default unless `passes` is above 1, which defaults to `isolate: "spec"`; sharded round-robin by spec id), in the suite's `project` root, with `WARDEN_UDID`, `WARDEN_BUNDLE_ID` and the rest of the [spec environment](/docs/reference/environment#set-for-spec-processes). `isolate: "spec"` runs each spec as its own process instead.
* `passes: 2` makes a spec pass only after two consecutive green runs on the same device (`WARDEN_PASS` = 0, 1), which catches specs that only pass from a lucky start state.
* `required: false` specs are reported but never fail the gate. `--required-only` runs just the required ones.
* `e2e-report.json` (in the batch dir, and at `--report <file>`) lists every spec with its reasons, attempts and verdict, a `tests[]` entry per test (status, duration, error, artifact directory, from the spec's `results.jsonl`), plus a `device`, `log` and `screenshot` (png) for a spec whose last attempt failed, and top-level `suite`, `platform`, `base`, `isolate`, `headless`, `batchDir`, `only` and `setup`. `--json` prints it.
* Exit codes: 0 gate passed (or nothing affected), 1 a required spec failed or setup error, 130 interrupted.
* Leases are released when the run ends. Under CI a passed gate also shuts down the sims / emulators warden created (or booted for the run). Locally they stay booted, so the next run skips the 10-30 s boot, and `warden gc` shuts them down once they have sat idle (20 min). A failed run always leaves them booted so you can inspect them (`warden release --mine --shutdown` when done). `e2e.<suite>.shutdown: "always" | "idle"` overrides the CI / local default and `--no-shutdown` keeps them up after a pass whatever it says.
* Nothing affected → exit 0 without claiming a device. `--dry-run` selects and explains, then stops.

### In CI [#in-ci]

```bash
git fetch origin main
warden e2e mobile --base origin/main --count 2 --report e2e-report.json
```

The same command runs locally or in a pre-push hook. Parsed imports are cached under `$WARDEN_HOME/affected`, so repeat runs only re-read changed files. The import graph is built in a separate process (`bun graph-worker.ts` from source, the binary's hidden `__affected-graph` subcommand when compiled), so the TypeScript compiler never loads into warden, and the finished graph is kept for the current commit and working-tree state: running again without touching a source file, tsconfig or `node_modules` reads it back without building anything.

### Feeding a batch preset [#feeding-a-batch-preset]

`warden affected` prints one spec id per line, so it also works as a preset's job source:

```ts
jobsFrom: { command: "warden affected mobile --platform ios --base origin/main" }
```

## Per-device setup [#per-device-setup]

`setup` runs a shell command once on every leased device, after the app install and before that device's first spec. Typical uses: point the installed app at the suite's local API, or answer a first-launch system alert.

```ts title="warden.config.ts"
import { defineConfig } from "@delacour/warden/config";

export default defineConfig({
  e2e: {
    online: {
      ports: ["8091:5"],
      serve: "bun api",
      setup: "xcrun simctl spawn {udid} defaults write com.example.app ApiUrl http://localhost:$WARDEN_PORT_0"
    }
  }
});
```

The command gets `WARDEN_UDID`, `WARDEN_WORKER`, `WARDEN_PORT_<i>` / `WARDEN_PORTS`, `WARDEN_APP_PATH` / `WARDEN_APP_HASH` and the suite `env`, and runs in the suite's `project` root. Each device's output is `logs/setup-<worker>.log`, next to the job logs. A device whose setup exits non-zero is dropped from the pool (its specs go to the remaining devices); if none is left the run fails and names the setup logs. `e2e-report.json` records every device under `setup` (`worker`, `udid`, `ok`, `exitCode`, `log`).

`slim: true` (iOS) runs [`warden sim slim`](/docs/reference/cli#warden-sim-slim-udid) on each device first, to save RAM and CPU when several simulators run together. A slim failure only warns; the device still runs specs. The result is recorded under `setup[].slim` in `e2e-report.json`. A suite that runs on both platforms keeps `slim: true`: an Android run skips it with a note (`slim is iOS-only; skipped on android`) and never slims its emulators. Only a suite fixed to `platform: "android"` fails config validation with it.
