Skip to content
Source
Sign inSign up

scenarios.md

173 lines7.6 KB

1# Scenarios
2​
3A scenario is an [e2e](https://e2e.com/docs) test that shows one behavior in the real app: a real
4server on its own T3 home, real HTTP and WebSocket, and a real browser on the web client or a
5pooled iPhone simulator running the mobile app. The
6dev replay agent stands in for the model, so a run is free and repeatable. Use one as the proof
7for a bug fix: it fails on the base commit and passes on the fix.
8​
9Scenarios live in [`scripts/scenarios`](../../scripts/scenarios) as `*.e2e.ts`. e2e is the
10runner; its docs ship in `scripts/node_modules/e2e/docs` and `e2e guide` prints them.
11​
12## Write one
13​
14```ts
15import { expect } from "e2e";
16​
17import { test, WEB } from "./t3.ts";
18​
19test(
20 "Tab moves from the add-project Git URL field to its submit button",
21 WEB,
22 async ({ t3, app, screen, browser }) => {
23 await t3.project("demo");
24 await t3.open();
25​
26 await screen.getByRole("button", "Add project").tap();
27 await screen.getByText("Git URL").tap();
28 await screen.getByRole("combobox").fill("https://github.com/pingdotgg/t3code.git");
29 const submit = screen.getByRole("button", "Continue (Enter)");
30 await expect(submit).toBeEnabled();
31​
32 await browser.keyboard.press("Tab");
33 await app.screenshot("after-tab");
34 await expect(submit).toBeFocused();
35 },
36);
37```
38​
39`screen`, `browser`, `app`, and `expect` are e2e's own. [`t3.ts`](../../scripts/scenarios/t3.ts)
40adds one fixture for what e2e cannot know about T3 Code:
41​
42- `t3.project(title, files)` creates a git repository and adds it as a project.
43- `t3.thread({ project, title, messages })` starts a thread and plays one replayed turn per
44 message before returning, so the thread has real history. `model` picks the recording.
45- `t3.open(path)` opens the app, already signed in.
46​
47State is created through the app's own CLI, so it is real events and works on a base commit
48whose schema differs. New threads default to the replay agent's `showcase` recording: type a
49message and send it. To play something else, add a recording to the
50[replay agent](development.md#replay-provider).
51​
52Write one test per file: a proof runs the file and needs exactly one test in it.
53​
54Take an `app.screenshot("label")` where the bug shows. The proof pairs base and head captures by
55label. e2e adds a capture at the point of failure itself.
56​
57Write only `expect` assertions for the thing the bug breaks. A proof counts a failed assertion as
58the bug. A control that cannot be found, or an app that does not start, counts as a scenario that
59could not run.
60​
61Keep `agent.act` and `agent.assert` out of scenarios: they call a model, and no model is
62configured.
63​
64## Run
65​
66```sh
67vp run scenarios # all of them
68cd scripts && pnpm exec e2e run scenarios/replay-turn-streams.e2e.ts --headed
69```
70​
71e2e starts the dev stack through [`start-app.ts`](../../scripts/scenarios/start-app.ts) on a free
72port, runs the tests, and stops it. Results land in `scripts/.e2e`: `report.json`, `summary.md`,
73and per test a `trace.md`, its screenshots, and a video. `vp run scenarios --target web` runs one
74target; flags go straight after the script name, without `--`.
75​
76The first run downloads e2e's browser (about 120 MB). To fetch it ahead of time, such as in a
77machine image, run this from the repository root after `vp i`:
78​
79```sh
80node scripts/node_modules/playwright-core/cli.js install --only-shell chromium ffmpeg
81```
82​
83e2e opens the web app at `127.0.0.1`, and the start script tells the web dev server to bind that
84address. It does not use `localhost`, which some machines resolve to `::1` first.
85​
86`e2e run --help` lists the rest: `--grep`, `--watch`, `--last-failed`, `--changed-since`.
87​
88Run one at a time per checkout: a run recreates the checkout's scenario T3 home and its `.e2e`
89output. Use a second worktree for a second run.
90​
91## Prove a fix
92​
93```sh
94vp run scenarios:prove scripts/scenarios/add-project-tab-reaches-submit.e2e.ts \
95 --base e509db3a2f^ --head e509db3a2f --upload-command "ghdrop upload"
96```
97​
98[`prove-scenario.ts`](../../scripts/prove-scenario.ts) runs `e2e run` for that scenario on both
99commits and compares the two reports. It prints, and writes to `scripts/.e2e/prove/proof.json`,
100`failsOnBase`, `passesOnHead`, `baseSha`, `headSha`, `verdict`, each side's `steps`, `captures`,
101`videos`, and `markdown`: a table with base and head captures side by side, each labelled with
102its commit, ready for a pull request description. `--upload-command` names a command that takes
103the capture paths and prints one URL per line. `--head` defaults to `HEAD`.
104​
105It exits 0 only when the scenario fails on base and passes on head, 1 when it ran and did not
106prove the fix, and 2 when it could not run. A scenario that passes on base does not reproduce the
107bug, and the verdict says so.
108​
109The scenario and e2e always come from your checkout. Only the app comes from the commit under
110test: a temporary worktree beside your checkout, installed with `vp i` and removed afterwards. A
111commit that is your clean `HEAD` runs in place. So a new scenario can prove a fix against a base
112that predates it.
113​
114## iOS
115​
116An iOS scenario imports from [`t3-mobile.ts`](../../scripts/scenarios/t3-mobile.ts) and passes
117`IOS` where a web one passes `WEB`:
118​
119```ts
120import { expect } from "e2e";
121​
122import { IOS, test } from "./t3-mobile.ts";
123​
124test(
125 "The iOS app lists a seeded thread and opens its history",
126 IOS,
127 async ({ t3, app, screen }) => {
128 const project = await t3.project("demo");
129 await t3.thread({ project, title: "Earlier work", messages: ["first", "second"] });
130 await t3.open();
131​
132 const thread = screen.getByText("Earlier work", { exact: false }).first();
133 await expect(thread).toBeVisible({ timeout: 120_000 });
134 await thread.tap();
135 await expect(screen.getByText("second fixture turn complete", { exact: false })).toBeVisible();
136 },
137);
138```
139​
140`t3.project` and `t3.thread` are the same. `t3.open()` installs the dev client for the checkout's
141native fingerprint, loads it from Metro, and pairs it with the scenario's server. `device` is
142e2e's [device fixture](https://e2e.com/docs/mobile#the-device-fixture) for system actions.
143​
144The run leases an iPhone 18 Pro from the Simlock pool, so it needs `SIMLOCK_AGENT_DEVICE_URL`
145and `AGENT_DEVICE_DAEMON_AUTH_TOKEN`, as `mobile:e2e` does, and a gateway on Simlock 0.7.0 or
146later. A web-only run leases nothing. The device reaches this machine over the tailnet on ports
14713990 (server) and 8190 (Metro); set `T3_SCENARIO_SERVER_PORT` and `T3_SCENARIO_METRO_PORT` when
148another run on the machine holds them. An iOS run takes two to three minutes, most of it the
149device and the first bundle. `e2e run --target web` or `--target ios` runs one side.
150​
151`scenarios:prove` works the same for an iOS scenario. Each commit gets the dev client built for
152its own native code.
153​
154## Other tiers
155​
156- **Android.** Add a target with `mobile({ platform: "android", device: simlock("Pixel 9") })`
157 and open the dev client with the Android form of the Expo link. Not built.
158- **Live provider.** Point the default model in `start-app.ts` at a real provider's cheapest
159 model. Assertions on exact reply text would not hold.
160​
161## In CI
162​
163No job runs scenarios, and none should run all of them on every pull request. e2e can select the
164ones a branch touches with `e2e run --changed-since origin/main` once
165[test filters](https://e2e.com/docs/filters) are configured, and `@e2e-dev/github` posts the
166result as a pull request comment.
167​
168## What a scenario cannot cover
169​
170- Desktop shell behavior (menus, windows, IPC), and Android.
171- Provider-specific behavior. The replay agent speaks generic ACP.
172- State no command can create, such as a run stopped by a usage limit.
173- Timing. Replay streaming is simulated, so a scenario cannot show a performance regression.