@unotest/protocol 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -68,6 +68,18 @@ declare const STEPS_FILE = "steps.jsonl";
68
68
  /** mtime-updated periodically by the runner; viewer treats run as
69
69
  * `interrupted` after `> 30s` without mtime change (per design-v0). */
70
70
  declare const HEARTBEAT_FILE = "heartbeat";
71
+ /** Per-day derived index: one JSON line per run, inside the day's shard
72
+ * directory (`<runsRoot>/YYYY/MM/DD/_day.jsonl`). Derived from the run
73
+ * manifests beside it — losing it costs a rebuild, never data. */
74
+ declare const DAY_INDEX_FILE = "_day.jsonl";
75
+ /** Directory of per-scenario derived indexes, at the runs root. Holds one
76
+ * JSONL per scenario plus `_latest.json`. Named with a leading underscore
77
+ * so it sorts away from `YYYY` shards and can never collide with one. */
78
+ declare const SCENARIOS_INDEX_DIR = "_scenarios";
79
+ /** One entry per scenario: its most recent run, status and failure
80
+ * streak. Feeds the activity-bar badge and the "failing now" section,
81
+ * neither of which may cost a history scan. */
82
+ declare const SCENARIOS_LATEST_FILE = "_latest.json";
71
83
  /** Captured child-process stdout when run is spawned by viewer. */
72
84
  declare const STDOUT_FILE = "stdout.log";
73
85
  /** Captured child-process stderr when run is spawned by viewer. */
@@ -148,1043 +160,1192 @@ declare function makeRunId(name: string): string;
148
160
  * random suffix appended by `makeRunId`. */
149
161
  declare function sanitizeRunIdSegment(name: string): string;
150
162
 
151
- /** Filesystem-unsafe characters in a collection name — see
152
- * `FS_UNSAFE_NAME_RE` for the block-list rationale. Kept as a named
153
- * re-export for the published surface. */
154
- declare const COLLECTION_NAME_FORBIDDEN_RE: RegExp;
155
- /** Validate a collection name. Returns `null` on success, or a
156
- * human-readable reason string on failure. Shared by the viewer-server
157
- * writer (authoritative) and any client form (pre-flight) so both sides
158
- * agree on what's acceptable. Pure function — safe to bundle in the
159
- * browser. */
160
- declare function validateCollectionName(name: string): string | null;
161
- /** Parsed YAML collection manifest from `unotest/e2e/_collections/*.yaml`.
162
- * Per design-v0/decisions.md, Phase 1 keeps the manifest minimal — only
163
- * `description` and `scenarios`. No `parallel`/`env`/`retry`/`timeout`
164
- * fields until a real use-case appears. */
165
- interface CollectionMeta {
166
- /** Filename stem (single source of truth — no `name:` field inside YAML). */
167
- name: string;
168
- /** Project-relative path to the `.yaml` file. */
169
- path: string;
170
- /** Optional one-line description from the YAML's top-level `description:`. */
171
- description: string | null;
172
- /** Scenario names referenced in the YAML's `scenarios:` array. Names are
173
- * scenario-meta `name` values (file stem relative to `e2e/`). Orphan
174
- * references (scenario file missing) are NOT filtered out — the viewer
175
- * surfaces them as red entries with «file not found» tooltip; running
176
- * a collection skips orphans with a warning, not blocks. */
177
- scenarios: string[];
178
- }
163
+ /** Epoch millis encoded in a runId, or null when the id does not follow
164
+ * `makeRunId`'s shape. Callers must handle null rather than guess — an
165
+ * id we cannot place has no shard. */
166
+ declare function runIdStartedAt(runId: string): number | null;
167
+ /** `YYYY/MM/DD` (UTC) for a timestamp. POSIX separators — these are
168
+ * project-relative artifact paths, not OS paths. */
169
+ declare function shardPathForTimestamp(ms: number): string;
170
+ /** `YYYY/MM/DD` for a runId, or null when the id carries no timestamp. */
171
+ declare function shardPathForRunId(runId: string): string | null;
172
+ /** Project-relative directory of one run: `<runsRoot>/YYYY/MM/DD/<runId>`.
173
+ * Null for an id we cannot place — callers decide whether that is a skip
174
+ * or an error, but nobody may invent a location. */
175
+ declare function runDirFor(runsRoot: string, runId: string): string | null;
176
+ /** Thrown when a runId carries no usable timestamp and the caller cannot
177
+ * proceed without one. Writers hit this only if they were handed an id
178
+ * they did not make. */
179
+ declare class UnplaceableRunIdError extends Error {
180
+ constructor(runId: string);
181
+ }
182
+ /** `runDirFor` for callers that own the id and therefore know it parses.
183
+ * Throws instead of returning null so a writer can never quietly invent
184
+ * a location and split one run across two layouts. */
185
+ declare function requireRunDirFor(runsRoot: string, runId: string): string;
186
+ /** Project-relative runs root for a target: `unotest/.runs<suffix>`. */
187
+ declare function projectRunsRoot(suffix?: string): string;
188
+ /** Project-relative directory of one run, shard included. The single
189
+ * place that knows how a run id becomes a path — every writer and reader
190
+ * goes through here, so the layout cannot drift between them. */
191
+ declare function projectRunDirFor(runId: string, suffix?: string): string;
192
+ /** Day directory: `<runsRoot>/YYYY/MM/DD`. */
193
+ declare function shardDirFor(runsRoot: string, ms: number): string;
194
+ /** Walk day-shard paths backwards from `fromMs`, newest first. Used by
195
+ * listing (fill a page from the newest days) and by retention (drop days
196
+ * older than the window) — both want days in order, neither wants to
197
+ * readdir the whole tree. */
198
+ declare function shardDaysDescending(runsRoot: string, fromMs: number, count: number): Generator<string>;
179
199
 
180
- /** Run a single scenario by its `name` (file stem under `e2e/`). */
181
- interface ScenarioRunRequest {
182
- kind: "scenario";
183
- /** Scenario `name` — file stem relative to `unotest/e2e/`. */
184
- scenario: string;
185
- /** Run with browser visible (web) / device window visible (mobile).
186
- * Adapter maps this to the runner's preferred env / arg. */
187
- headed?: boolean;
188
- /** Spawn with debugger enabled (breakpoints from
189
- * `unotest/.debugger.json` + inline override). */
190
- debug?: boolean;
191
- /** Inline breakpoint override as `"line:col"` strings. Only honored
192
- * when `debug=true`. */
193
- breakpoints?: string[];
200
+ /** Which kind of locator the in-page picker overlay should retarget to and
201
+ * what DSL snippet to emit on pick. `locator` → an interactive element →
202
+ * `click(<loc>)`; `text`/`visibility` → any named/interactive element →
203
+ * `assertText`/`assertVisible`. See docs/recoder/plan.md. */
204
+ type PickerMode = "locator" | "text" | "visibility";
205
+ interface DbgCommandStep {
206
+ id: string;
207
+ cmd: "step";
194
208
  }
195
- /** Run a collection — the runner's CLI orchestrates scenarios inside.
196
- * Viewer doesn't see the per-scenario subprocesses; it sees one direct
197
- * child plus the artifacts each scenario writes to its own `.runs/`
198
- * dir, linked to this parent via `parentRunId` in their manifests. */
199
- interface CollectionRunRequest {
200
- kind: "collection";
201
- /** Collection `name` — filename stem under `_collections/`. */
202
- collection: string;
203
- /** Forward to each child scenario. */
204
- headed?: boolean;
205
- /** Worker count for parallel scenario execution. Default `1`
206
- * (serial). The adapter passes it to the CLI; the runner enforces. */
207
- workers?: number;
208
- /** Stop on first failure. Default `false` (continue-on-fail). */
209
- bail?: boolean;
209
+ interface DbgCommandContinue {
210
+ id: string;
211
+ cmd: "continue";
210
212
  }
211
- /** Open a live AUTHORING-HOLD session (collaborative model, U1/U3): a
212
- * headed shared browser + draft stay open with NO running test; the human
213
- * and agent co-author the draft. Runs `unotest-web author <scenario>`. */
214
- interface AuthoringRunRequest {
215
- kind: "authoring";
216
- /** Scenario `name` — file stem relative to `unotest/e2e/`. A stub is
217
- * created if the file doesn't exist yet. */
218
- scenario: string;
219
- /** Authoring is visible by default — the human must see the live browser. */
220
- headed?: boolean;
213
+ interface DbgCommandPause {
214
+ id: string;
215
+ cmd: "pause";
221
216
  }
222
- /** Discriminated union of every run-shape the viewer can request. */
223
- type RunRequest = ScenarioRunRequest | CollectionRunRequest | AuthoringRunRequest;
224
- /** All `kind` literals — useful for switch-completeness checks and for
225
- * adapter `supportedKinds` lists. */
226
- type RunKind = RunRequest["kind"];
227
-
228
- declare const RUN_MANIFEST_FILE = "manifest.json";
229
- interface RunManifest {
230
- /** Format-version of this manifest. Bump only on breaking schema
231
- * changes — additive optional fields don't require it. Starts at 1. */
232
- schemaVersion: 1;
233
- /** Identifies which RunRequest variant produced this run. */
234
- kind: RunKind;
235
- /** Original (un-sanitized) `ref` from the request — scenario `name`
236
- * for `kind:"scenario"`, collection `name` for `kind:"collection"`.
237
- * The `runId` directory uses a sanitized form; this carries the
238
- * human-readable original. */
239
- ref: string;
240
- /** Run-id of the parent collection-run when this is a child scenario
241
- * spawned inside a collection. Absent for top-level scenario-runs
242
- * and for collection-runs themselves. */
243
- parentRunId?: string;
244
- /** Adapter `name` that produced this run (`"@unotest/web"`,
245
- * `"@unotest/mobile"`). Lets the viewer pick rendering details for
246
- * runner-specific bundle contents without runner branches in core
247
- * logic. */
248
- adapter: string;
249
- /** Wallclock at runner start. Distinct from the first event's `t`
250
- * (which may have leading delay for adapter setup). */
251
- startedAt: number;
252
- /** Opt-in artifact-capture flags as resolved from the runner's env at
253
- * start (`UNOTEST_CONSOLE` / `UNOTEST_NETWORK`). Recorded here so the
254
- * viewer can tell "capture was disabled" apart from "capture was on but
255
- * the run aborted before writing console.json / network.json" — a
256
- * distinction the mere presence of those files can't make. Optional: runs
257
- * produced before this field existed simply omit it, and the viewer falls
258
- * back to a file-presence heuristic. */
259
- capture?: {
260
- console: boolean;
261
- network: boolean;
262
- };
217
+ interface DbgCommandSetBp {
218
+ id: string;
219
+ cmd: "set-bp";
220
+ line: number;
221
+ col: number;
263
222
  }
264
-
265
- /** Snapshot file holding entry + every helper source that was loaded. */
266
- declare const RUN_SOURCES_FILE = "sources.json";
267
- interface RunSources {
268
- /** Project-root-relative posix path of the entry scenario file,
269
- * e.g. `unotest/e2e/foo.test.js`. Also a key in `files`. */
270
- entry: string;
271
- /** Project-root-relative posix path → full source text. Includes the
272
- * entry file and every helper file pulled in by the loader. */
273
- files: Record<string, string>;
223
+ interface DbgCommandClearBp {
224
+ id: string;
225
+ cmd: "clear-bp";
226
+ line: number;
227
+ col: number;
274
228
  }
275
-
276
- /** The argv/env builder for a specific runner CLI. Pure functions —
277
- * no spawning, no fs access. The viewer owns the actual subprocess
278
- * lifecycle; the adapter merely tells it WHAT to spawn. */
279
- interface IRunnerAdapter {
280
- /** Human-readable identity for logs and error messages
281
- * (e.g. `"@unotest/web"`). NOT used for control flow — viewer
282
- * must not branch on this. */
283
- readonly name: string;
284
- /** Per-target artifact suffix — the single axis that scopes EVERY
285
- * per-target filesystem name (M-10): `""` for web (the first target
286
- * keeps the bare names), `"-mobile"` for mobile, `"-unity"` for a
287
- * future unity runner. Derived names (via `paths.ts` helpers):
288
- * scenario tree `e2e<suffix>`, run artifacts `.runs<suffix>`, debug
289
- * logs `.debug<suffix>`, breakpoints `.debugger<suffix>.json`, env
290
- * overlays `.env<suffix>`. The viewer scopes discovery/runs/
291
- * breakpoints by the active target's suffix so no folder name is
292
- * ever hardcoded in viewer source (OCP: a new target just declares
293
- * its suffix). */
294
- readonly suffix: string;
295
- /** Debug-toolbar capabilities the viewer gates its controls on (OCP — a
296
- * new target just declares what it supports; viewer never branches on the
297
- * runner name). See `DebugCapabilities`. */
298
- readonly debug: DebugCapabilities;
299
- /** Which `RunRequest.kind` values this adapter can handle. Viewer
300
- * rejects unsupported kinds with `UnsupportedRunKindError` before
301
- * building argv. */
302
- readonly supportedKinds: ReadonlyArray<RunKind>;
303
- /** Absolute path to the bin entry that should be spawned with the
304
- * current Node binary: `spawn(process.execPath, [resolveBin(), ...buildArgs(req)])`.
305
- * Throws if the runner package isn't installed reachably from the
306
- * viewer's project root. */
307
- resolveBin(): string;
308
- /** Argv to pass after the bin path. Pure — depends only on `req`.
309
- * Example for web: `["e2e", "smoke", "--debug", "--break", "10:4"]`. */
310
- buildArgs(req: RunRequest): string[];
311
- /** Environment overrides merged on top of `process.env` by the
312
- * caller. Pure — no `process.env` reads here. Example for web:
313
- * `{ UNOTEST_HEADED: "1" }`. Worker-isolation env
314
- * (`UNOTEST_WORKER_INDEX/COUNT`) is NOT the adapter's
315
- * responsibility — see `D-C4` in the collections plan. */
316
- buildEnv(req: RunRequest): NodeJS.ProcessEnv;
317
- /** Argv (after the bin path) that starts this runner's LONG-LIVED
318
- * app-under-test holder (M-12/M-16) — the process that prepares the
319
- * app (web: spawns the dev server; mobile: boots devices + installs
320
- * the build), emits `AppServerEvent` JSON lines on stdout, and tears
321
- * down what it owns when terminated. Absent → the runner has no
322
- * app-launch capability and the viewer hides the feature for this
323
- * target. Example: `["app-server"]`. */
324
- appServerArgs?(): string[];
325
- /** Path inputs the App panel renders for this target (M-17) — each
326
- * spec becomes an input + file-picker button whose value is saved
327
- * into the target's env file (`envKey`). The viewer stays
328
- * platform-blind: extensions/labels are vocab the adapter owns
329
- * (mobile iOS: `.app`; a future Android entry adds `.apk`). */
330
- appServerPickers?(): readonly AppPathPickerSpec[];
331
- /** Host-OS requirement for THIS runner. The viewer resolves it against
332
- * the server's real `process.platform` and gates the switcher: an
333
- * unsupported host shows `reason` and blocks activation (e.g. mobile is
334
- * macOS-only — iOS Simulator is Apple-licensed). Absent → runs on any
335
- * host (web). OCP: a new platform-bound runner just declares this; the
336
- * viewer never hardcodes which target needs which OS. */
337
- readonly host?: HostRequirement;
338
- /** Structured environment preflight ("doctor") — what must be installed/
339
- * configured for this runner to actually run here (mobile: Xcode +
340
- * simulators; web: Node + a system browser). Pure-ish but MAY shell out
341
- * to probe the host, hence async; the viewer invokes it on demand (a
342
- * "Check" button), never on every switch. Absent → nothing to verify
343
- * beyond the package being installed. The viewer renders the returned
344
- * list verbatim (severity + message + fix `detail`) — it owns no runner
345
- * vocab. */
346
- doctor?(ctx: DoctorContext): Promise<DoctorCheck[]>;
229
+ interface DbgCommandAbort {
230
+ id: string;
231
+ cmd: "abort";
347
232
  }
348
- /** Severity of a single {@link DoctorCheck}. `error` = the runner cannot
349
- * run until fixed; `warning` = may work, review; `ok` = satisfied. */
350
- type CheckSeverity = "ok" | "warning" | "error";
351
- /** One environment-preflight result. The single shape both `@unotest/web`
352
- * and `@unotest/mobile` already produce from their `runEnvironmentChecks`
353
- * — lifted here so the viewer (and any future surface) consumes one type,
354
- * not a per-runner copy. */
355
- interface DoctorCheck {
356
- /** Short check id, e.g. `"xcode-cli"`, `"Node.js version"`. */
357
- name: string;
358
- severity: CheckSeverity;
359
- /** One-line outcome, e.g. `"Xcode CLI tools at /Applications/…"`. */
360
- message: string;
361
- /** Human-readable remediation when not `ok` — a command or concrete
362
- * next step. Surfaced under the message in the UI. */
363
- detail?: string;
233
+ /** Start (or, if already active, re-mode) the in-page picker on the live
234
+ * paused page. `execute=false` (default) is capture-only: clicks are
235
+ * swallowed, never run — picks just record a locator/assertion.
236
+ * `execute=true` turns the overlay into a Playwright-style recorder: real
237
+ * clicks / typing / selects RUN on the page and are recorded as DSL actions
238
+ * (`click`/`fill`/`press`/`selectOption`/`check`). In execute mode the
239
+ * `text`/`visibility` modes still record an assertion without running the
240
+ * click (swap mode to drop an assertion mid-flow). */
241
+ interface DbgCommandPickerStart {
242
+ id: string;
243
+ cmd: "picker:start";
244
+ mode: PickerMode;
245
+ execute?: boolean;
364
246
  }
365
- /** What an adapter's {@link IRunnerAdapter.doctor} receives. Kept as an
366
- * object so new context (e.g. a target's env file) can be added without
367
- * touching every implementor's signature. */
368
- interface DoctorContext {
369
- /** Absolute path to the project root (the dir containing `unotest/`) —
370
- * for project-aware checks (mobile reads `package.json` to hint RN/Expo). */
371
- projectRoot: string;
247
+ /** Tear down the picker overlay. */
248
+ interface DbgCommandPickerStop {
249
+ id: string;
250
+ cmd: "picker:stop";
372
251
  }
373
- /** A runner's host-OS requirement. The viewer compares `platforms` against
374
- * `process.platform`; a mismatch shows `reason` and blocks the switch. */
375
- interface HostRequirement {
376
- /** `process.platform` values this runner can execute on, e.g. `["darwin"]`
377
- * for iOS. */
378
- platforms: ReadonlyArray<NodeJS.Platform>;
379
- /** Shown when the current host isn't in `platforms` — one sentence the
380
- * user reads in the switcher / setup panel. */
381
- reason: string;
382
- }
383
- /** What a runner's debug session supports — drives which debug-toolbar
384
- * controls the viewer shows. A target that lacks a capability gets the
385
- * control hidden (no dead buttons). */
386
- interface DebugCapabilities {
387
- /** In-page locator picker / recorder — click an element in the SHARED,
388
- * inspectable page to insert its locator/assertion (web). Runners that
389
- * drive an opaque device without a clickable DOM (mobile via WDA) set this
390
- * false → the viewer hides the picker cluster (Capture/Execute +
391
- * target/A/eye). NOTE: distinct from `attach` — the wheel + agent join
392
- * apply to mobile too. */
393
- picker: boolean;
394
- /** Collaborative attach: a second client (the MCP agent) can join the live
395
- * debug session and co-drive it, coordinated by the control wheel. True for
396
- * web (shared browser) AND mobile (shared device via WDA). The viewer shows
397
- * the wheel / "who's driving" badge when set. */
398
- attach: boolean;
399
- /** A failure-pause is resumable as a RETRY of the failed step (mobile D-17:
400
- * the user fixes the device/app state, Continue re-executes the same
401
- * statement). When false (web: resume past a failure means "skip & pretend
402
- * it passed"), the viewer hides Continue/Step on a failure-pause so the
403
- * only action is Stop. */
404
- resumeFromFailure: boolean;
405
- }
406
- /** One path-input row in the viewer's App panel (M-17). */
407
- interface AppPathPickerSpec {
408
- /** Env var the picked path is saved to (e.g. `"APP_PATH"`). Routed to
409
- * the target's env file by the viewer's variables API (M-11). */
410
- envKey: string;
411
- /** Selectable leaf extensions (lowercase, with dot). macOS `.app`
412
- * bundles are directories — the file browser treats a matching dir
413
- * as a selectable leaf. */
414
- extensions: readonly string[];
415
- /** Human label rendered above the input (e.g. `"iOS app bundle"`). */
416
- label: string;
417
- }
418
- /** Thrown by the viewer when a `RunRequest.kind` isn't in the
419
- * adapter's `supportedKinds`. The exception filter maps to HTTP 400
420
- * with the kind name + adapter name in the message. */
421
- declare class UnsupportedRunKindError extends Error {
422
- constructor(kind: RunKind, adapterName: string);
252
+ /** On-demand: capture the current page outline (what the picker hit-tests
253
+ * against) and write it to `picker-snapshot.txt`. Lets the viewer's Snapshot
254
+ * tab show / refresh the snapshot without arming the picker. Cheap — runs
255
+ * only when the user opens/refreshes the tab (the outline is expensive on
256
+ * huge pages, so it is never captured on every pause). */
257
+ interface DbgCommandSnapshot {
258
+ id: string;
259
+ cmd: "snapshot";
423
260
  }
424
- /** Thrown by the viewer when the configured runner package can't be
425
- * located on disk. Message names the package + how to install. */
426
- declare class RunnerAdapterNotFoundError extends Error {
427
- constructor(packageName: string, hint: string);
261
+ /** The DSL line a recorded action / pick renders to. Mirrors the in-page
262
+ * recorder's verbs. `locator` → bare locator; `assertText`/`assertVisible`
263
+ * → assertions; the rest → Playwright-style actions. */
264
+ type RecordedActionKind = "locator" | "assertText" | "assertVisible" | "click" | "fill" | "press" | "selectOption" | "check" | "uncheck";
265
+ /** Agent-driven, ref-safe record (collaborative authoring). The agent picks
266
+ * an element by `ref` (from a snapshot of the SHARED page) + an action; the
267
+ * runner resolves `ref→locator` (`RefResolver`) and emits a `locator:picked`
268
+ * event the viewer inserts — so the agent NEVER hand-writes a locator. Same
269
+ * resolution/insertion pipeline as the human's in-page picker. */
270
+ interface DbgCommandRecord {
271
+ id: string;
272
+ cmd: "record";
273
+ ref: string;
274
+ action: RecordedActionKind;
275
+ /** Typed/selected value for `fill` / `selectOption`. */
276
+ value?: string;
277
+ /** Key for `press` (e.g. "Enter", "Control+A"). */
278
+ key?: string;
279
+ /** Expected text for `assertText` (else the element's textContent is used). */
280
+ text?: string;
281
+ /** Where the viewer drops the line. Default `append` (agent records a flow). */
282
+ placement?: "cursor" | "append";
428
283
  }
429
- /** Thrown by the viewer when `unotest.config.*` doesn't declare a
430
- * `runner:` field. Surfaces what packages exist so the user can
431
- * pick. */
432
- declare class RunnerNotConfiguredError extends Error {
433
- constructor(message: string);
284
+ /** Collaborative control token (Slice 3). Sets who may act on the shared
285
+ * page: `agent` lets the agent record; `human` (default) blocks the agent.
286
+ * Written by the viewer (Take control button) or the agent (take_wheel). */
287
+ interface DbgCommandWheel {
288
+ id: string;
289
+ cmd: "wheel";
290
+ owner: "agent" | "human";
434
291
  }
435
-
436
- /** Parsed scenario metadata from the mandatory header lines
437
- * (per design-v0/decisions.md, "Test source format"):
438
- *
439
- * // id-<kebab-slug>
440
- * // <human title>
441
- * // #<6-hex-color>
442
- * function test_<...>() { ... }
443
- *
444
- * All three lines are required; a file without a complete header is
445
- * rejected by the discovery service as parse-error (no legacy fallback). */
446
- interface ScenarioMeta {
447
- /** Slug from `// id-<slug>`. Stable test identity across rename. */
292
+ /** Evaluate a DSL locator STRING against the live (paused or running) page and
293
+ * report what it matches — count, the N a trailing .first()/.last()/.nth()
294
+ * collapses, and per-match identity — via a `probe:result` event. The runner
295
+ * parses `locator` with the SAME DSL parser the agent's `check_locator` uses,
296
+ * so agent and human probe identically. READ-ONLY (never acts on the page),
297
+ * so it is intentionally NOT wheel-gated: any client may probe at any time. */
298
+ interface DbgCommandProbe {
448
299
  id: string;
449
- /** Human-readable title from `// <text>`. */
450
- title: string;
451
- /** Color identifier from `// #RRGGBB`. Used only in tab headers as a
452
- * thin colored strip — NOT a status signal. */
453
- color: string;
454
- /** File stem relative to `unotest/e2e/`, dirs as slashes.
455
- * Examples: `smoke-welcome`, `auth/signin-flow`.
456
- * Source-of-truth identity for CLI invocations and tab keys. */
457
- name: string;
458
- /** Project-relative path to the source file. */
459
- path: string;
300
+ cmd: "probe";
301
+ /** Locator exactly as written in a test, e.g.
302
+ * getByRole('row').filter({hasText: 'Uma Quinn'}).first(). */
303
+ locator: string;
304
+ /** Cap on the number of match details returned (default 20). */
305
+ limit?: number;
460
306
  }
307
+ type DbgCommand = DbgCommandStep | DbgCommandContinue | DbgCommandPause | DbgCommandSetBp | DbgCommandClearBp | DbgCommandAbort | DbgCommandPickerStart | DbgCommandPickerStop | DbgCommandSnapshot | DbgCommandRecord | DbgCommandWheel | DbgCommandProbe;
308
+ type DbgCommandKind = DbgCommand["cmd"];
461
309
 
462
- /** Response of `GET /api/scenarios`. Folders are listed independently of
463
- * the scenarios so an EMPTY directory under `unotest/e2e/` still renders
464
- * in the tree (the folder list isn't inferred from scenario paths). */
465
- interface ScenariosPayload {
466
- scenarios: ScenarioMeta[];
467
- /** Directory paths relative to `unotest/e2e/`, posix-separated, any
468
- * depth (e.g. `auth`, `auth/signup`, `autopark`). Excludes reserved
469
- * subtrees (`_helpers`, `_collections`) and any `_`/`.`-prefixed dir. */
470
- folders: string[];
310
+ interface ProbeMatch {
311
+ /** 0-based position among the locator's matches. */
312
+ index: number;
313
+ role?: string;
314
+ name?: string;
315
+ /** Trimmed/clipped textContent of the element. */
316
+ text?: string;
317
+ testId?: string;
318
+ href?: string;
319
+ /** Currently visible? Non-waiting check (a match can exist but be off-screen). */
320
+ visible: boolean;
471
321
  }
472
- /** Filesystem-unsafe characters in a single path segment — see
473
- * `FS_UNSAFE_NAME_RE` (it includes both path separators, so a valid
474
- * segment is guaranteed to be ONE level). Kept as a named re-export for
475
- * the published surface. */
476
- declare const SEGMENT_NAME_FORBIDDEN_RE: RegExp;
477
- /** Validate a single scenario-file stem or folder name. Returns `null` on
478
- * success, else a human-readable reason. Shared by the viewer-server
479
- * writer (authoritative) and the client form (pre-flight) so both agree.
480
- * Pure — safe to bundle in the browser.
481
- *
482
- * Rejects: empty, `.`/`..`, `.`-prefix (hidden), `_`-prefix (reserved for
483
- * `_helpers`/`_collections`/templates), path separators + FS-unsafe
484
- * punctuation + control chars. */
485
- declare function validateSegmentName(name: string): string | null;
486
-
487
- /** Session profile — the kind of process a tab spawns. Mirrored by the
488
- * viewer's TerminalStore.TermKind. */
489
- type TerminalKind = "local" | "claude" | "codex";
490
- interface TerminalSpawnMessage {
491
- type: "spawn";
492
- id: string;
493
- kind: TerminalKind;
494
- cols: number;
495
- rows: number;
322
+ interface ProbePinned {
323
+ /** The terminal index op that collapsed a multi-match to a single element. */
324
+ op: "first" | "last" | "nth";
325
+ /** How many elements the chain matched BEFORE the terminal op — i.e. how many
326
+ * the .first()/.last()/.nth() silently picked from. `ofCount > 1` flags an
327
+ * index op hiding ambiguity. */
328
+ ofCount: number;
496
329
  }
497
- interface TerminalDataMessage {
498
- type: "data";
499
- id: string;
500
- /** Raw bytes the user typed, forwarded to the PTY stdin. */
501
- data: string;
330
+ interface ProbeResult {
331
+ /** Human-readable chain echo. NOT a selector. */
332
+ chain: string;
333
+ /** Elements the full chain matches. */
334
+ count: number;
335
+ /** Present only when the chain ends in first/last/nth. */
336
+ pinned?: ProbePinned;
337
+ /** Details of up to the requested limit of matches. */
338
+ matches: ProbeMatch[];
339
+ /** True count before truncation (equal to `count`). */
340
+ totalCount: number;
341
+ truncated: boolean;
342
+ /** Actionable hints: nothing matched, hidden ambiguity, truncation, stale ref. */
343
+ warnings: string[];
502
344
  }
503
- interface TerminalResizeMessage {
504
- type: "resize";
505
- id: string;
506
- cols: number;
507
- rows: number;
345
+
346
+ /** Terminal outcome reported by the runner via `run:finished`. */
347
+ type RunOutcome = "completed" | "failed" | "aborted" | "interrupted";
348
+ /** Scenario-level run-id. Format opaque to consumers; treat as string. */
349
+ type RunId = string;
350
+ /** Common fields on every event. */
351
+ interface RunArtifactBase {
352
+ /** Wallclock timestamp (Date.now()) at emit. */
353
+ t: number;
508
354
  }
509
- interface TerminalCloseMessage {
510
- type: "close";
511
- id: string;
355
+ interface RunStartedEvent extends RunArtifactBase {
356
+ kind: "run:started";
357
+ /** Same as the `<runId>` directory name; included for self-describing
358
+ * events when reassembled out-of-band. */
359
+ runId: RunId;
360
+ /** Scenario `name` (file stem relative to `e2e/`). */
361
+ scenario: string;
512
362
  }
513
- type TerminalClientMessage = TerminalSpawnMessage | TerminalDataMessage | TerminalResizeMessage | TerminalCloseMessage;
514
- interface TerminalOutputMessage {
515
- type: "output";
516
- id: string;
517
- /** Raw PTY stdout/stderr bytes to write to the xterm instance. */
518
- data: string;
363
+ interface StepStartedEvent extends RunArtifactBase {
364
+ kind: "step:started";
365
+ /** Project-root-relative posix path of the file this step lives in
366
+ * (entry or a helper). Matches a key in the run's `sources.json`.
367
+ * The viewer slices the source text by `line` from the snapshot. */
368
+ file: string;
369
+ /** Line number of the DSL call in the source file (1-indexed). */
370
+ line: number;
371
+ /** Column number (1-indexed since 0.11 — the DSL lexer's token
372
+ * columns; earlier releases emitted 0-indexed columns for
373
+ * word-initial statements). */
374
+ col: number;
519
375
  }
520
- interface TerminalExitMessage {
521
- type: "exit";
522
- id: string;
523
- exitCode: number | null;
376
+ interface StepOkEvent extends RunArtifactBase {
377
+ kind: "step:ok";
378
+ /** Wall-clock duration of the step that just finished. */
379
+ durationMs: number;
524
380
  }
525
- /** Spawn failure (e.g. `claude` not on PATH) reported to the tab so it can
526
- * print a friendly line instead of silently dying. */
527
- interface TerminalErrorMessage {
528
- type: "error";
529
- id: string;
530
- message: string;
381
+ interface StepFailEvent extends RunArtifactBase {
382
+ kind: "step:fail";
383
+ /** Human-readable error message (single line preferred). */
384
+ error: string;
385
+ /** Error class name (e.g. `"AssertionError"`, `"LocatorError"`,
386
+ * `"TimeoutError"`). Lets the viewer's `ErrorCard` show a typed
387
+ * pill instead of a generic "Error" label. Optional because some
388
+ * thrown errors lack a meaningful class name. */
389
+ errorName?: string;
390
+ /** Project-relative path to a Playwright trace zip, if captured. */
391
+ trace?: string;
531
392
  }
532
- type TerminalServerMessage = TerminalOutputMessage | TerminalExitMessage | TerminalErrorMessage;
533
- /** The `ws` adapter routes inbound messages by an `event` envelope; both
534
- * directions use this single event name on `/ws/terminal`. */
535
- declare const TERMINAL_WS_EVENT = "terminal";
536
-
537
- type VariableCategory = "secret" | "app" | "runner";
538
- /** User-facing runner-config knobs documented in `unotest/.env`. Single source
539
- * of truth for "is this name our config?" — drives the viewer's "Runner
540
- * config" group and the add-variable dropdown.
541
- *
542
- * NOT listed (so they never surface as user config): launcher-injected
543
- * run-control vars (UNOTEST_RUN_ID, UNOTEST_PARENT_RUN_ID,
544
- * UNOTEST_BUSY_SCENARIOS, UNOTEST_WORKER_*), session-log internals
545
- * (UNOTEST_SESSION_LOG_*), and path bootstrap (UNOTEST_DIR,
546
- * UNOTEST_PROJECT_ROOT, UNOTEST_CONFIG, UNOTEST_VIEWER_NO_OPEN). A user would
547
- * never put those in `unotest/.env`. */
548
- declare const RUNNER_CONFIG_KEYS: ReadonlySet<string>;
549
- /** True when `name` is a key the runner consumes as its own configuration. */
550
- declare function isRunnerConfigKey(name: string): boolean;
551
- /** Group a variable for the viewer. Secrecy from the source file; runner-vs-app
552
- * from the owned-key set. */
553
- declare function categorizeVariable(v: {
554
- name: string;
555
- source: "env" | "secrets";
556
- }): VariableCategory;
557
- /** An external scenario variable is referenced by a bare UPPER_SNAKE
558
- * identifier (`APP_BASE_URL`). App / secret names must match this to be
559
- * resolvable from a scenario; runner keys are validated against the set
560
- * above instead. Convention only — no value inspection. */
561
- declare function isExternalVarName(name: string): boolean;
562
-
563
- /** Parse `KEY=VALUE` env-file CONTENT into a plain record. Pure — the
564
- * caller does the fs read. Blank lines and `#` comments are skipped;
565
- * balanced surrounding single/double quotes are stripped; keys not
566
- * matching `[A-Za-z_][A-Za-z0-9_]*` are ignored. Shared by the web and
567
- * mobile runners' layered env loaders (M-11) so both parse `.env` files
568
- * identically. */
569
- declare function parseEnvContent(raw: string): Record<string, string>;
570
- /** Quote on write only when the value would otherwise be ambiguous (spaces,
571
- * `#`, quotes, `=`, backslash, leading backtick, or newlines). Plain tokens
572
- * stay unquoted. Mirrors the viewer env-dotenv serializer. */
573
- declare function serializeEnvValue(value: string): string;
574
- /** Active (non-commented) assignment keys present in env-file `content`.
575
- * Used to detect duplicates / cross-file shadowing before an add. */
576
- declare function envKeys(content: string): Set<string>;
577
- /**
578
- * Insert `KEY=VALUE` into env-file `content`. With `sectionLabel`, the line
579
- * goes into the first `# --- <label> --- ` divider section — after the last
580
- * assignment there (or its description comments if it has no vars yet). The
581
- * label is matched as a whole word against DIVIDER lines only, so prose
582
- * comments are never mistaken for a section. No such section → it is created
583
- * at end-of-file. With no label the line is appended. Does NOT dedup — reject
584
- * duplicates before calling.
585
- */
586
- declare function insertEnvVar(content: string, key: string, value: string, sectionLabel?: string): string;
587
- /**
588
- * Remove the assignment line for `key`, leaving every comment and other line
589
- * untouched (we do not guess which comments "belong" to the key). Returns the
590
- * new content and whether anything was removed.
591
- */
592
- declare function removeEnvVar(content: string, key: string): {
593
- content: string;
594
- removed: boolean;
595
- };
596
-
597
- /** Which group the user is adding into. App/runner → `.env`, secret →
598
- * `.secrets`. Mirrors VariableCategory as an INPUT (the user's choice). */
599
- type VariableKind = VariableCategory;
600
- interface AddVariablePlanInput {
601
- name: string;
602
- kind: VariableKind;
603
- /** Current text of `unotest/.env` (`""` if absent). */
604
- envContent: string;
605
- /** Current text of `unotest/.secrets` (`""` if absent). */
606
- secretsContent: string;
393
+ interface ScreenshotEvent extends RunArtifactBase {
394
+ kind: "screenshot";
395
+ /** Project-relative path to the PNG (under `unotest/.runs/<runId>/screenshot/`). */
396
+ path: string;
397
+ /** Step identity for captures bound to a specific step (per-step
398
+ * screenshots under `UNOTEST_STEP_SCREENSHOTS`). Same semantics as
399
+ * `StepStartedEvent` — the viewer joins on `line:col`. Absent for
400
+ * unbound captures (`screenshot()` DSL calls); consumers may fall
401
+ * back to attributing those to the step in flight at emit time. */
402
+ file?: string;
403
+ line?: number;
404
+ col?: number;
607
405
  }
608
- type VariablePlanError = {
609
- ok: false;
610
- code: "duplicate" | "invalid-name" | "unknown-runner-key";
611
- message: string;
612
- };
613
- type AddVariablePlan = {
614
- ok: true;
615
- target: "env" | "secrets";
616
- sectionLabel?: string;
617
- warnings: string[];
618
- } | VariablePlanError;
619
- /**
620
- * Decide where a new variable goes and what to warn about — or reject it.
621
- * Reject reasons: duplicate key in the target file; a non-UPPER_SNAKE app/
622
- * secret name (a scenario could never reference it); a runner key not in the
623
- * owned-config allowlist. Warnings (non-fatal): secret shadowing a same-named
624
- * `.env` var, or vice-versa.
625
- */
626
- declare function planAddVariable(input: AddVariablePlanInput): AddVariablePlan;
627
- /**
628
- * Warning to show after deleting `name` from the file: if the shell
629
- * environment still defines it with a DIFFERENT value, it will keep
630
- * resolving from there (set-if-absent means ambient wins). Comparing values
631
- * avoids a false positive when `.env` was merely flattened into process.env.
632
- */
633
- declare function deleteShadowWarning(name: string, fileValue: string | undefined, ambientValue: string | undefined): string[];
634
-
635
- /** A single editor diagnostic. Superset of the `@unotest/dsl`
636
- * validator diagnostic: it unifies parse errors, the vocab validator,
637
- * and the platform linter behind one shape, tagged by `source`/`rule`
638
- * so consumers can theme or filter. Positions are 1-based; a parse
639
- * error with no recoverable position falls back to `1:1`. */
640
- interface DslDiagnostic {
641
- severity: "error" | "warning" | "info";
642
- /** 1-based line. */
406
+ /** Emitted ONCE after a failing run when the runner has finished writing
407
+ * the D-16 failure bundle to disk. Viewer's inspector reads
408
+ * `bundlePath` to fetch artifacts (screenshot, semantic DOM, network,
409
+ * trace) via `/api/runs/<runId>/asset?path=...`. The bundle directory
410
+ * is keyed by its own id (`<scenario>__<isoDate>__<stepIdx>`) under
411
+ * `.unotest/failures/`, not under `unotest/.runs/`, because the
412
+ * bundle layout pre-dates per-run dirs and is still consumed by the
413
+ * agent-fix MCP tools. */
414
+ interface FailureBundleWrittenEvent extends RunArtifactBase {
415
+ kind: "failure-bundle:written";
416
+ /** Project-relative path to the bundle directory. */
417
+ bundlePath: string;
418
+ /** Project-relative path to the screenshot PNG inside the bundle,
419
+ * when tier-2 capture succeeded. */
420
+ screenshotPath?: string;
421
+ /** Page URL at the moment of failure (same value captured into the
422
+ * bundle's `snapshot.json`/`outline.txt`), for the viewer's ErrorCard. */
423
+ url?: string;
424
+ }
425
+ interface RunFinishedEvent extends RunArtifactBase {
426
+ kind: "run:finished";
427
+ outcome: RunOutcome;
428
+ }
429
+ /** Debugger pause. Reason discriminates:
430
+ * - `breakpoint` — executor halted before a statement whose `line:col` is
431
+ * in `RunOptions.breakpoints`.
432
+ * - `step` — `pauseRequested` flag was set by a `pause` debug command,
433
+ * or the run was started in step-mode and just completed a statement.
434
+ * - `failure` — executor caught an error and `pauseOnFailure` is true.
435
+ *
436
+ * When the runner is hosting a debug session it also writes a parallel
437
+ * `runtime.json` file with the same snapshot. The snapshot is included
438
+ * inline here for replay / single-frame consumers that don't want to do a
439
+ * separate fs read. */
440
+ interface PausedEvent extends RunArtifactBase {
441
+ kind: "paused";
442
+ reason: "breakpoint" | "step" | "failure";
443
+ /** Project-root-relative posix path of the file the pause point lives
444
+ * in (entry or a helper). Matches a key in the run's `sources.json`. */
445
+ file: string;
643
446
  line: number;
644
- /** 1-based column. */
645
- column: number;
646
- /** 1-based end line, when the producer knows the span. */
647
- endLine?: number;
648
- /** 1-based end column, when the producer knows the span. */
649
- endColumn?: number;
650
- message: string;
651
- /** Stable rule id, e.g. `"validator:unknown-function"`, `"validator:dsl"`,
652
- * a linter rule id, or `"parse"`. */
653
- rule: string;
654
- /** Which stage produced it — lets the UI group / colour by origin. */
655
- source: "parse" | "validate" | "lint";
447
+ col: number;
448
+ /** Inline snapshot of the runtime state at the pause point. `vars` is
449
+ * per-value JSON-stringified (LocatorValue → "<Locator role=... name=...>"
450
+ * human-readable string) and per-var truncated to 4KB to keep the JSONL
451
+ * line manageable. `callStack` is outermost→innermost user-function frames. */
452
+ snapshot?: {
453
+ vars: Record<string, string>;
454
+ callStack: {
455
+ fn: string;
456
+ file: string;
457
+ line: number;
458
+ }[];
459
+ };
656
460
  }
657
- /** Coarse grouping for a vocab entry — drives completion icons and
658
- * ordering. `keyword` covers DSL control-flow / block words (`step`,
659
- * `if`, `for`, …); `helper` covers project-defined `flow_*` / mock
660
- * helpers surfaced from the workspace. */
661
- type DslVocabCategory = "action" | "assertion" | "selector" | "chain" | "navigation" | "read" | "wait" | "state" | "storage" | "multi-context" | "sandbox" | "diagnostic" | "escape-hatch" | "keyword" | "helper";
662
- /** One declared argument of a vocab entry — feeds signature help and
663
- * the snippet placeholders in `insertText`. `kind` is the validator's
664
- * arg-kind string (`"locatorLike"`, `"stringLike"`, `"number"`,
665
- * `"any"`, …); the editor treats it as an opaque label. */
666
- interface DslVocabArg {
667
- label: string;
668
- kind: string;
669
- required: boolean;
670
- /** Literal members when `kind` is `"enum"` — the label alone loses
671
- * them, and consumers (typings generator, completion) need the
672
- * actual values. */
673
- enumValues?: readonly string[];
461
+ /** Emitted by the in-page locator picker / action recorder (debug-pause)
462
+ * when the user picks an element or performs a recorded action. The runner
463
+ * resolved the picked `ref` to a locator (`RefResolver`) and rendered the
464
+ * DSL snippet. The viewer inserts `snippet` per `placement`.
465
+ * See docs/recoder/plan.md. */
466
+ interface LocatorPickedEvent extends RunArtifactBase {
467
+ kind: "locator:picked";
468
+ mode: PickerMode;
469
+ /** Ready-to-insert DSL line, e.g. a bare locator
470
+ * `getByRole("button", {name: "Sign in"})` (locator mode), an assertion
471
+ * (`assertText(…)`), or a recorded action (`click(…)`, `fill(…)`). */
472
+ snippet: string;
473
+ /** Where the viewer drops the snippet:
474
+ * - `cursor` — at the editor's caret (deliberate picks; user-placed).
475
+ * - `append` — after the previous recorded line (execute-mode action
476
+ * recorder, preserving order). */
477
+ placement: "cursor" | "append";
478
+ /** `stable` (testId / role+name / label / …) or `fragile` (css/nth-only).
479
+ * Lets the viewer flag brittle picks. */
480
+ stability: "stable" | "fragile";
674
481
  }
675
- /** A single autocomplete-able item: a vocab function, a DSL keyword, or
676
- * a project helper. Serializable — crosses the REST boundary verbatim
677
- * and is cached by the browser (the vocab is static per platform). */
678
- interface DslVocabEntry {
679
- name: string;
680
- category: DslVocabCategory;
681
- args: DslVocabArg[];
682
- /** True when the signature accepts extra positional args after the
683
- * declared ones (`shell(cmd, ...args)`, `dbQuery(sql, ...params)`). */
684
- variadic?: boolean;
685
- /** Return-kind string (`"void"`, `"string"`, `"locator"`, …). */
686
- returns: string;
687
- /** Snippet body with `${n:label}` placeholders, e.g.
688
- * `click(${1:locator})`. */
689
- insertText: string;
690
- /** Human-readable signature for hover / signature-help, e.g.
691
- * `getByRole(role, { name?, exact? })`. Richer than the bare arg
692
- * list — includes option-object shape. */
693
- signature?: string;
694
- /** Prose description (markdown) for hover and the completion info
695
- * panel. */
696
- doc?: string;
697
- /** Locator capability from the function contract. `"chainableLocator"`
698
- * entries can be chained off a locator (offered after `.`); any value
699
- * other than `"none"` produces a locator (so it fits a `locator`
700
- * argument slot). Absent for keywords / helpers that have no contract. */
701
- locatorCapability?: "none" | "locator" | "chainableLocator";
702
- /** Where the entry is defined in the USER's project — set for project
703
- * helpers (`flow_*` in `_helpers/`), absent for platform functions,
704
- * which have no source on disk there. Feeds go-to-definition.
705
- * `path` is project-relative posix; `line`/`column` are 1-based. */
706
- source?: {
707
- path: string;
708
- line: number;
709
- column: number;
482
+ /** Emitted when execution resumes after a `paused` event. The `by` field
483
+ * records which debug command unblocked it — purely informational; the
484
+ * next `step:started` event carries the actual line/col. */
485
+ interface ResumedEvent extends RunArtifactBase {
486
+ kind: "resumed";
487
+ by: "step" | "continue";
488
+ }
489
+ /** Emitted when the collaborative control token changes owner (Slice 3), so
490
+ * the viewer can live-update the "who's driving" badge over WS. */
491
+ interface WheelChangedEvent extends RunArtifactBase {
492
+ kind: "wheel:changed";
493
+ owner: "agent" | "human";
494
+ }
495
+ /** Emitted in response to a `probe` debug command: the result of evaluating a
496
+ * locator string against the live page. `id` echoes the command's id so the
497
+ * client that issued the probe can correlate the reply. READ-ONLY — producing
498
+ * it never changed the page. */
499
+ interface ProbeResultEvent extends RunArtifactBase {
500
+ kind: "probe:result";
501
+ /** Correlates with the issuing `DbgCommandProbe.id`. */
502
+ id: string;
503
+ result: ProbeResult;
504
+ }
505
+ /** A single MCP tool invocation by the agent, surfaced into the session's
506
+ * event stream so the human sees — in the viewer's System console — what the
507
+ * agent called and how it ended. Emitted ONLY while the MCP server is attached
508
+ * to this run (own-browser calls have no session to attach to). Paired:
509
+ * `tool:call` at invocation, `tool:result` at completion, correlated by
510
+ * `callId`. Read-only telemetry — emitting it never touches the page. */
511
+ interface ToolCallEvent extends RunArtifactBase {
512
+ kind: "tool:call";
513
+ /** Correlates with the matching `tool:result` (one MCP call). */
514
+ callId: string;
515
+ /** MCP tool name, e.g. "check_locator", "explore_step". */
516
+ tool: string;
517
+ /** Arguments as a secret-redacted, length-capped JSON string — NOT the raw
518
+ * object. Capped upstream so the line stays a single atomic append. */
519
+ args: string;
520
+ }
521
+ interface ToolResultEvent extends RunArtifactBase {
522
+ kind: "tool:result";
523
+ /** Correlates with the issuing `ToolCallEvent.callId`. */
524
+ callId: string;
525
+ tool: string;
526
+ ok: boolean;
527
+ durationMs: number;
528
+ /** Secret-redacted, length-capped preview of the result text. */
529
+ preview: string;
530
+ /** Present only when `ok` is false. */
531
+ error?: {
532
+ class: string;
533
+ message: string;
710
534
  };
711
535
  }
712
- /** Per-request file context for {@link IDslLanguageService.validate}.
713
- * The viewer owns workspace discovery, so it supplies the things the
714
- * platform validator can't know on its own. */
715
- interface DslValidateContext {
716
- /** Names of project helpers (`flow_*`, mocks) so their calls are not
717
- * flagged as unknown functions — mirrors how the lint runner
718
- * registers `unotest/e2e/_helpers/`. */
719
- helperNames?: readonly string[];
720
- /** Whether the source is a scenario or a helper definition file. */
721
- fileKind?: "scenario" | "helper";
536
+ /** Discriminated union of every JSONL event the scenario runner emits. */
537
+ type RunArtifact = RunStartedEvent | StepStartedEvent | StepOkEvent | StepFailEvent | ScreenshotEvent | FailureBundleWrittenEvent | PausedEvent | ResumedEvent | LocatorPickedEvent | WheelChangedEvent | ProbeResultEvent | ToolCallEvent | ToolResultEvent | RunFinishedEvent;
538
+ interface CollectionRunStartedEvent extends RunArtifactBase {
539
+ kind: "collection-run:started";
540
+ runId: RunId;
541
+ /** Collection `name` (filename stem). */
542
+ collection: string;
543
+ /** Scenario names that will be executed, in order. */
544
+ scenarios: string[];
722
545
  }
723
- /** The DSL language service a platform exposes to the viewer. Two
724
- * concerns only: the static vocabulary (for completion / signature
725
- * help) and full source validation (parse + vocab validator + linter,
726
- * identical to the platform's own lint gate). */
727
- interface IDslLanguageService {
728
- /** Static, serializable vocabulary for autocomplete and signature
729
- * help. Stable for the lifetime of the process — safe to cache. */
730
- getVocab(): DslVocabEntry[];
731
- /** Full diagnostics for one source string. Pure with respect to the
732
- * filesystem — all workspace knowledge arrives via `ctx`. */
733
- validate(source: string, ctx?: DslValidateContext): DslDiagnostic[];
546
+ interface CollectionScenarioStartedEvent extends RunArtifactBase {
547
+ kind: "collection-run:scenario-started";
548
+ /** The scenario-run-id created when this scenario started executing. */
549
+ scenarioRunId: RunId;
550
+ scenario: string;
734
551
  }
735
-
736
- /** A spawned runner process. Structural superset of the bits of a Node
737
- * `ChildProcess` the viewer actually consumes — pid for the active-run
738
- * snapshot, stdout/stderr for log capture, exit/error for finalize,
739
- * kill for abort. Both `child_process` and Electron `utilityProcess`
740
- * satisfy this via a thin adapter. */
741
- interface ILaunchedProcess {
742
- /** OS process id, or -1 if the spawn failed to produce one. */
743
- readonly pid: number;
744
- /** Captured stdout stream, or null when not piped. */
745
- readonly stdout: Readable | null;
746
- /** Captured stderr stream, or null when not piped. */
747
- readonly stderr: Readable | null;
748
- /** Register the exit handler. Called once when the process exits. */
749
- onExit(cb: (code: number | null, signal: NodeJS.Signals | null) => void): void;
750
- /** Register the spawn-error handler (e.g. bin not found). */
751
- onError(cb: (err: Error) => void): void;
752
- /** Terminate the process. Default signal SIGTERM. */
753
- kill(signal?: NodeJS.Signals): void;
552
+ interface CollectionScenarioFinishedEvent extends RunArtifactBase {
553
+ kind: "collection-run:scenario-finished";
554
+ scenarioRunId: RunId;
555
+ scenario: string;
556
+ outcome: RunOutcome;
557
+ }
558
+ interface CollectionRunFinishedEvent extends RunArtifactBase {
559
+ kind: "collection-run:finished";
560
+ outcome: RunOutcome;
754
561
  }
755
- /** Everything the launcher needs to spawn one runner process. The caller
756
- * (RunnerService) has already merged the environment and resolved the bin
757
- * + argv from the `IRunnerAdapter`; the launcher only decides HOW to run
758
- * it. Pure data — no behaviour. */
759
- interface LaunchSpec {
760
- /** Absolute path to the JS bin entry — `IRunnerAdapter.resolveBin()`. */
761
- bin: string;
762
- /** Argv to pass after the bin — `IRunnerAdapter.buildArgs(req)`. */
763
- args: string[];
764
- /** Fully-merged environment for the child. */
765
- env: NodeJS.ProcessEnv;
766
- /** Working directory — the workspace project root. */
767
- cwd: string;
562
+ type CollectionRunArtifact = CollectionRunStartedEvent | CollectionScenarioStartedEvent | CollectionScenarioFinishedEvent | CollectionRunFinishedEvent;
563
+
564
+ /** Run a single scenario by its `name` (file stem under `e2e/`). */
565
+ interface ScenarioRunRequest {
566
+ kind: "scenario";
567
+ /** Scenario `name` — file stem relative to `unotest/e2e/`. */
568
+ scenario: string;
569
+ /** Run with browser visible (web) / device window visible (mobile).
570
+ * Adapter maps this to the runner's preferred env / arg. */
571
+ headed?: boolean;
572
+ /** Spawn with debugger enabled (breakpoints from
573
+ * `unotest/.debugger.json` + inline override). */
574
+ debug?: boolean;
575
+ /** Inline breakpoint override as `"line:col"` strings. Only honored
576
+ * when `debug=true`. */
577
+ breakpoints?: string[];
768
578
  }
769
- /** Spawns the runner JS under a Node-capable runtime. Injected into the
770
- * viewer's RunnerService so the spawn mechanism (system Node vs Electron
771
- * utilityProcess) is a deployment choice, not baked into the service. */
772
- interface IProcessLauncher {
773
- launch(spec: LaunchSpec): ILaunchedProcess;
579
+ /** Run a collection — the runner's CLI orchestrates scenarios inside.
580
+ * Viewer doesn't see the per-scenario subprocesses; it sees one direct
581
+ * child plus the artifacts each scenario writes to its own `.runs/`
582
+ * dir, linked to this parent via `parentRunId` in their manifests. */
583
+ interface CollectionRunRequest {
584
+ kind: "collection";
585
+ /** Collection `name` — filename stem under `_collections/`. */
586
+ collection: string;
587
+ /** Forward to each child scenario. */
588
+ headed?: boolean;
589
+ /** Worker count for parallel scenario execution. Default `1`
590
+ * (serial). The adapter passes it to the CLI; the runner enforces. */
591
+ workers?: number;
592
+ /** Stop on first failure. Default `false` (continue-on-fail). */
593
+ bail?: boolean;
594
+ }
595
+ /** Open a live AUTHORING-HOLD session (collaborative model, U1/U3): a
596
+ * headed shared browser + draft stay open with NO running test; the human
597
+ * and agent co-author the draft. Runs `unotest-web author <scenario>`. */
598
+ interface AuthoringRunRequest {
599
+ kind: "authoring";
600
+ /** Scenario `name` — file stem relative to `unotest/e2e/`. A stub is
601
+ * created if the file doesn't exist yet. */
602
+ scenario: string;
603
+ /** Authoring is visible by default — the human must see the live browser. */
604
+ headed?: boolean;
774
605
  }
606
+ /** Discriminated union of every run-shape the viewer can request. */
607
+ type RunRequest = ScenarioRunRequest | CollectionRunRequest | AuthoringRunRequest;
608
+ /** All `kind` literals — useful for switch-completeness checks and for
609
+ * adapter `supportedKinds` lists. */
610
+ type RunKind = RunRequest["kind"];
775
611
 
776
- /** Contents of `unotest/.viewer.lock`. Written by viewer-server on
777
- * startup, removed on graceful shutdown. Stale detection: `process.kill(
778
- * pid, 0)` throws ESRCH if PID is dead → lockfile is stale, safe to
779
- * overwrite. */
780
- interface LockFile {
781
- /** PID of the viewer-server process. */
782
- pid: number;
783
- /** Full URL (with port) to reach the viewer. e.g. `http://localhost:5173`. */
784
- url: string;
785
- /** ISO 8601 timestamp of `pid` start. Used for tie-breaking and stale-age
786
- * heuristics if `kill(pid, 0)` is inconclusive. */
787
- startedAt: string;
612
+ /** Lifecycle stage as inferred by the server when listing runs. */
613
+ type RunStatus = "running" | "passed" | "failed" | "aborted" | "interrupted";
614
+ interface RunMeta {
615
+ runId: RunId;
616
+ kind: RunKind;
617
+ /** Scenario `name` (file stem under `e2e/`) for `kind:"scenario"`,
618
+ * collection `name` (yaml stem) for `kind:"collection"`. */
619
+ ref: string;
620
+ /** Wallclock from the `run:started` event. */
621
+ startedAt: number;
622
+ /** Wallclock from the `run:finished` event, or null if still active. */
623
+ finishedAt: number | null;
624
+ /** Total wall-clock duration, or null if still running. */
625
+ durationMs: number | null;
626
+ /** Status derived from outcome / liveness — `running` while the run's
627
+ * heartbeat is fresh and no `run:finished` has landed. */
628
+ status: RunStatus;
629
+ /** Terminal outcome from `run:finished`, or null if still running. */
630
+ outcome: RunOutcome | null;
631
+ /** Run-id of the parent collection-run when this is a child scenario
632
+ * spawned inside one. Absent for top-level scenarios and for
633
+ * collection-runs themselves. Populated by the writer from the
634
+ * child's `manifest.json`. */
635
+ parentRunId?: string;
788
636
  }
789
637
 
790
- /** Helper file metadata. Helpers live in `unotest/e2e/_helpers/**` as
791
- * `snake_case` functions (`signin_as`, `seed_user`, …). The runner
792
- * auto-discovers them; no import system in scenarios. */
793
- interface HelperMeta {
794
- /** File stem relative to `_helpers/`, dirs as slashes. */
638
+ /** Bumped when a line's shape changes incompatibly. A file whose header
639
+ * carries a different version is discarded and rebuilt rather than
640
+ * half-parsed. */
641
+ declare const RUN_INDEX_SCHEMA_VERSION = 2;
642
+ /** How many past outcomes a scenario's rolling window keeps. Twelve is
643
+ * enough to see a flake and small enough that the whole field of them
644
+ * fits one JSON file. Exported so the fold and everything that renders
645
+ * the window agree on one number. */
646
+ declare const SCENARIO_RECENT_WINDOW = 12;
647
+ /** First line of every index JSONL. */
648
+ interface RunIndexHeader {
649
+ v: number;
650
+ }
651
+ /** One run, as stored in a day index. Identical to what the API returns,
652
+ * deliberately: the index exists to avoid recomputing `RunMeta`, so
653
+ * storing anything narrower would just force a second read. */
654
+ type RunIndexEntry = RunMeta;
655
+ /** Rolling per-scenario state. `failStreak`, `failingSince` and `recent`
656
+ * are the only fields not derivable from a single run — they are folded
657
+ * forward run by run, and a rebuild replays the scenario's history to
658
+ * restore them. */
659
+ interface ScenarioLatestEntry {
660
+ lastRunId: string;
661
+ lastStatus: RunStatus;
662
+ lastAt: number;
663
+ /** Wall-clock of the last run, or null if it never finished. */
664
+ lastDurationMs: number | null;
665
+ /** Up to {@link SCENARIO_RECENT_WINDOW} past outcomes, OLDEST FIRST.
666
+ * Answers the one question a single last status cannot: does this
667
+ * test flake. Kept here rather than fetched per scenario because the
668
+ * overview needs it for every test at once, and a page request per
669
+ * test would be one round trip per mouse movement. */
670
+ recent: RunStatus[];
671
+ /** Consecutive failing runs ending at `lastRunId`. Zero when the last
672
+ * run passed. Distinguishes "just broke" from "broken for weeks", and
673
+ * a value of 1 after green runs is the signature of a flake. */
674
+ failStreak: number;
675
+ /** Start of the current failing streak, or null when not failing. */
676
+ failingSince: number | null;
677
+ }
678
+ interface ScenariosLatest {
679
+ v: number;
680
+ scenarios: Record<string, ScenarioLatestEntry>;
681
+ }
682
+ /** Status values that count as a failure for streak purposes. `aborted`
683
+ * is excluded: the user stopped it, the test did not fail. */
684
+ declare function isFailingStatus(status: RunStatus): boolean;
685
+ /** Fold one newer run into a scenario's rolling state. Pure so the live
686
+ * path and the rebuild path cannot disagree about what a streak is. */
687
+ declare function foldScenarioRun(prev: ScenarioLatestEntry | undefined, run: Pick<RunMeta, "runId" | "status" | "startedAt" | "durationMs">): ScenarioLatestEntry;
688
+ /** Filename for a scenario's index. `encodeURIComponent` because scenario
689
+ * refs carry `/` (`big-table/edit-dialog`) and must not become nested
690
+ * directories — and because it is reversible, so the ref can be read
691
+ * back off the filename without a side table. */
692
+ declare function scenarioIndexFileName(ref: string): string;
693
+ /** Inverse of `scenarioIndexFileName`. */
694
+ declare function scenarioRefFromFileName(fileName: string): string | null;
695
+
696
+ /** Blob root under the runs root. Leading underscore for the same reason
697
+ * as `_scenarios`: derived, internal, and can never be mistaken for a
698
+ * `YYYY` shard directory. */
699
+ declare const BLOBS_DIR = "_blobs";
700
+ /** Thrown rather than returning a path built from a malformed hash — a
701
+ * writer that guessed here would scatter blobs outside the fanout. */
702
+ declare class InvalidBlobHashError extends Error {
703
+ constructor(hash: string);
704
+ }
705
+ /** Project-relative path of one blob.
706
+ *
707
+ * `generation` exists because a hardlink count is bounded — ext4 caps an
708
+ * inode at 65000 links. A frame present in every run crosses that at
709
+ * 65k runs, well inside the six months we size for, and `link()` then
710
+ * fails with EMLINK. Rather than silently degrading to full copies from
711
+ * that point on, the store opens `<hash>~1`, `<hash>~2`, … Each
712
+ * generation is an ordinary blob: same bytes, own inode, own link
713
+ * budget, and garbage-collected by the same `st_nlink` rule. */
714
+ declare function blobPathFor(runsRoot: string, hash: string, ext: string, generation?: number): string;
715
+ /** The same path relative to the runs root, as POSIX segments. Callers
716
+ * holding an OS-absolute root join these themselves rather than pasting
717
+ * an absolute path into a POSIX string. */
718
+ declare function blobRelSegmentsFor(hash: string, ext: string, generation?: number): string[];
719
+ /** Convenience over `blobRelSegmentsFor` for POSIX contexts. */
720
+ declare function blobRelPathFor(hash: string, ext: string, generation?: number): string;
721
+ /** Blob root for a runs root — what the garbage collector walks. */
722
+ declare function blobsRootFor(runsRoot: string): string;
723
+
724
+ /** Filesystem-unsafe characters in a collection name — see
725
+ * `FS_UNSAFE_NAME_RE` for the block-list rationale. Kept as a named
726
+ * re-export for the published surface. */
727
+ declare const COLLECTION_NAME_FORBIDDEN_RE: RegExp;
728
+ /** Validate a collection name. Returns `null` on success, or a
729
+ * human-readable reason string on failure. Shared by the viewer-server
730
+ * writer (authoritative) and any client form (pre-flight) so both sides
731
+ * agree on what's acceptable. Pure function — safe to bundle in the
732
+ * browser. */
733
+ declare function validateCollectionName(name: string): string | null;
734
+ /** Parsed YAML collection manifest from `unotest/e2e/_collections/*.yaml`.
735
+ * Per design-v0/decisions.md the manifest stays minimal — `description`,
736
+ * `scenarios`, and (since workers-support) `workers`. No `env`/`retry`/
737
+ * `timeout` fields until a real use-case appears. */
738
+ interface CollectionMeta {
739
+ /** Filename stem (single source of truth — no `name:` field inside YAML). */
795
740
  name: string;
796
- /** Project-relative path to the source file. */
741
+ /** Project-relative path to the `.yaml` file. */
797
742
  path: string;
743
+ /** Optional one-line description from the YAML's top-level `description:`. */
744
+ description: string | null;
745
+ /** Scenario names referenced in the YAML's `scenarios:` array. Names are
746
+ * scenario-meta `name` values (file stem relative to `e2e/`). Orphan
747
+ * references (scenario file missing) are NOT filtered out — the viewer
748
+ * surfaces them as red entries with «file not found» tooltip; running
749
+ * a collection skips orphans with a warning, not blocks. */
750
+ scenarios: string[];
751
+ /** Optional `workers:` from the YAML — how many scenarios of this
752
+ * collection may run concurrently. Integer ≥ 1; `null` when the field
753
+ * is absent (callers default to 1 = sequential). A CLI `--workers`
754
+ * flag overrides this value for one run. */
755
+ workers: number | null;
798
756
  }
799
757
 
800
- /** Which kind of locator the in-page picker overlay should retarget to and
801
- * what DSL snippet to emit on pick. `locator` → an interactive element →
802
- * `click(<loc>)`; `text`/`visibility` → any named/interactive element →
803
- * `assertText`/`assertVisible`. See docs/recoder/plan.md. */
804
- type PickerMode = "locator" | "text" | "visibility";
805
- interface DbgCommandStep {
806
- id: string;
807
- cmd: "step";
808
- }
809
- interface DbgCommandContinue {
810
- id: string;
811
- cmd: "continue";
758
+ declare const RUN_MANIFEST_FILE = "manifest.json";
759
+ interface RunManifest {
760
+ /** Format-version of this manifest. Bump only on breaking schema
761
+ * changes — additive optional fields don't require it. Starts at 1. */
762
+ schemaVersion: 1;
763
+ /** Identifies which RunRequest variant produced this run. */
764
+ kind: RunKind;
765
+ /** Original (un-sanitized) `ref` from the request — scenario `name`
766
+ * for `kind:"scenario"`, collection `name` for `kind:"collection"`.
767
+ * The `runId` directory uses a sanitized form; this carries the
768
+ * human-readable original. */
769
+ ref: string;
770
+ /** Run-id of the parent collection-run when this is a child scenario
771
+ * spawned inside a collection. Absent for top-level scenario-runs
772
+ * and for collection-runs themselves. */
773
+ parentRunId?: string;
774
+ /** Adapter `name` that produced this run (`"@unotest/web"`,
775
+ * `"@unotest/mobile"`). Lets the viewer pick rendering details for
776
+ * runner-specific bundle contents without runner branches in core
777
+ * logic. */
778
+ adapter: string;
779
+ /** Wallclock at runner start. Distinct from the first event's `t`
780
+ * (which may have leading delay for adapter setup). */
781
+ startedAt: number;
782
+ /** Opt-in artifact-capture flags as resolved from the runner's env at
783
+ * start (`UNOTEST_CONSOLE` / `UNOTEST_NETWORK`). Recorded here so the
784
+ * viewer can tell "capture was disabled" apart from "capture was on but
785
+ * the run aborted before writing console.json / network.json" — a
786
+ * distinction the mere presence of those files can't make. Optional: runs
787
+ * produced before this field existed simply omit it, and the viewer falls
788
+ * back to a file-presence heuristic. */
789
+ capture?: {
790
+ console: boolean;
791
+ network: boolean;
792
+ };
812
793
  }
813
- interface DbgCommandPause {
814
- id: string;
815
- cmd: "pause";
794
+
795
+ /** Snapshot file holding entry + every helper source that was loaded. */
796
+ declare const RUN_SOURCES_FILE = "sources.json";
797
+ interface RunSources {
798
+ /** Project-root-relative posix path of the entry scenario file,
799
+ * e.g. `unotest/e2e/foo.test.js`. Also a key in `files`. */
800
+ entry: string;
801
+ /** Project-root-relative posix path → full source text. Includes the
802
+ * entry file and every helper file pulled in by the loader. */
803
+ files: Record<string, string>;
816
804
  }
817
- interface DbgCommandSetBp {
818
- id: string;
819
- cmd: "set-bp";
820
- line: number;
821
- col: number;
805
+
806
+ /** The argv/env builder for a specific runner CLI. Pure functions —
807
+ * no spawning, no fs access. The viewer owns the actual subprocess
808
+ * lifecycle; the adapter merely tells it WHAT to spawn. */
809
+ interface IRunnerAdapter {
810
+ /** Human-readable identity for logs and error messages
811
+ * (e.g. `"@unotest/web"`). NOT used for control flow — viewer
812
+ * must not branch on this. */
813
+ readonly name: string;
814
+ /** Per-target artifact suffix — the single axis that scopes EVERY
815
+ * per-target filesystem name (M-10): `""` for web (the first target
816
+ * keeps the bare names), `"-mobile"` for mobile, `"-unity"` for a
817
+ * future unity runner. Derived names (via `paths.ts` helpers):
818
+ * scenario tree `e2e<suffix>`, run artifacts `.runs<suffix>`, debug
819
+ * logs `.debug<suffix>`, breakpoints `.debugger<suffix>.json`, env
820
+ * overlays `.env<suffix>`. The viewer scopes discovery/runs/
821
+ * breakpoints by the active target's suffix so no folder name is
822
+ * ever hardcoded in viewer source (OCP: a new target just declares
823
+ * its suffix). */
824
+ readonly suffix: string;
825
+ /** Debug-toolbar capabilities the viewer gates its controls on (OCP — a
826
+ * new target just declares what it supports; viewer never branches on the
827
+ * runner name). See `DebugCapabilities`. */
828
+ readonly debug: DebugCapabilities;
829
+ /** Which `RunRequest.kind` values this adapter can handle. Viewer
830
+ * rejects unsupported kinds with `UnsupportedRunKindError` before
831
+ * building argv. */
832
+ readonly supportedKinds: ReadonlyArray<RunKind>;
833
+ /** Absolute path to the bin entry that should be spawned with the
834
+ * current Node binary: `spawn(process.execPath, [resolveBin(), ...buildArgs(req)])`.
835
+ * Throws if the runner package isn't installed reachably from the
836
+ * viewer's project root. */
837
+ resolveBin(): string;
838
+ /** Argv to pass after the bin path. Pure — depends only on `req`.
839
+ * Example for web: `["e2e", "smoke", "--debug", "--break", "10:4"]`. */
840
+ buildArgs(req: RunRequest): string[];
841
+ /** Environment overrides merged on top of `process.env` by the
842
+ * caller. Pure — no `process.env` reads here. Example for web:
843
+ * `{ UNOTEST_HEADED: "1" }`. Worker-isolation env
844
+ * (`UNOTEST_WORKER_INDEX/COUNT`) is NOT the adapter's
845
+ * responsibility — see `D-C4` in the collections plan. */
846
+ buildEnv(req: RunRequest): NodeJS.ProcessEnv;
847
+ /** Argv (after the bin path) that starts this runner's LONG-LIVED
848
+ * app-under-test holder (M-12/M-16) — the process that prepares the
849
+ * app (web: spawns the dev server; mobile: boots devices + installs
850
+ * the build), emits `AppServerEvent` JSON lines on stdout, and tears
851
+ * down what it owns when terminated. Absent → the runner has no
852
+ * app-launch capability and the viewer hides the feature for this
853
+ * target. Example: `["app-server"]`. */
854
+ appServerArgs?(): string[];
855
+ /** Path inputs the App panel renders for this target (M-17) — each
856
+ * spec becomes an input + file-picker button whose value is saved
857
+ * into the target's env file (`envKey`). The viewer stays
858
+ * platform-blind: extensions/labels are vocab the adapter owns
859
+ * (mobile iOS: `.app`; a future Android entry adds `.apk`). */
860
+ appServerPickers?(): readonly AppPathPickerSpec[];
861
+ /** Host-OS requirement for THIS runner. The viewer resolves it against
862
+ * the server's real `process.platform` and gates the switcher: an
863
+ * unsupported host shows `reason` and blocks activation (e.g. mobile is
864
+ * macOS-only — iOS Simulator is Apple-licensed). Absent → runs on any
865
+ * host (web). OCP: a new platform-bound runner just declares this; the
866
+ * viewer never hardcodes which target needs which OS. */
867
+ readonly host?: HostRequirement;
868
+ /** Structured environment preflight ("doctor") — what must be installed/
869
+ * configured for this runner to actually run here (mobile: Xcode +
870
+ * simulators; web: Node + a system browser). Pure-ish but MAY shell out
871
+ * to probe the host, hence async; the viewer invokes it on demand (a
872
+ * "Check" button), never on every switch. Absent → nothing to verify
873
+ * beyond the package being installed. The viewer renders the returned
874
+ * list verbatim (severity + message + fix `detail`) — it owns no runner
875
+ * vocab. */
876
+ doctor?(ctx: DoctorContext): Promise<DoctorCheck[]>;
822
877
  }
823
- interface DbgCommandClearBp {
824
- id: string;
825
- cmd: "clear-bp";
826
- line: number;
827
- col: number;
878
+ /** Severity of a single {@link DoctorCheck}. `error` = the runner cannot
879
+ * run until fixed; `warning` = may work, review; `ok` = satisfied. */
880
+ type CheckSeverity = "ok" | "warning" | "error";
881
+ /** One environment-preflight result. The single shape both `@unotest/web`
882
+ * and `@unotest/mobile` already produce from their `runEnvironmentChecks`
883
+ * — lifted here so the viewer (and any future surface) consumes one type,
884
+ * not a per-runner copy. */
885
+ interface DoctorCheck {
886
+ /** Short check id, e.g. `"xcode-cli"`, `"Node.js version"`. */
887
+ name: string;
888
+ severity: CheckSeverity;
889
+ /** One-line outcome, e.g. `"Xcode CLI tools at /Applications/…"`. */
890
+ message: string;
891
+ /** Human-readable remediation when not `ok` — a command or concrete
892
+ * next step. Surfaced under the message in the UI. */
893
+ detail?: string;
828
894
  }
829
- interface DbgCommandAbort {
830
- id: string;
831
- cmd: "abort";
895
+ /** What an adapter's {@link IRunnerAdapter.doctor} receives. Kept as an
896
+ * object so new context (e.g. a target's env file) can be added without
897
+ * touching every implementor's signature. */
898
+ interface DoctorContext {
899
+ /** Absolute path to the project root (the dir containing `unotest/`) —
900
+ * for project-aware checks (mobile reads `package.json` to hint RN/Expo). */
901
+ projectRoot: string;
832
902
  }
833
- /** Start (or, if already active, re-mode) the in-page picker on the live
834
- * paused page. `execute=false` (default) is capture-only: clicks are
835
- * swallowed, never run — picks just record a locator/assertion.
836
- * `execute=true` turns the overlay into a Playwright-style recorder: real
837
- * clicks / typing / selects RUN on the page and are recorded as DSL actions
838
- * (`click`/`fill`/`press`/`selectOption`/`check`). In execute mode the
839
- * `text`/`visibility` modes still record an assertion without running the
840
- * click (swap mode to drop an assertion mid-flow). */
841
- interface DbgCommandPickerStart {
842
- id: string;
843
- cmd: "picker:start";
844
- mode: PickerMode;
845
- execute?: boolean;
903
+ /** A runner's host-OS requirement. The viewer compares `platforms` against
904
+ * `process.platform`; a mismatch shows `reason` and blocks the switch. */
905
+ interface HostRequirement {
906
+ /** `process.platform` values this runner can execute on, e.g. `["darwin"]`
907
+ * for iOS. */
908
+ platforms: ReadonlyArray<NodeJS.Platform>;
909
+ /** Shown when the current host isn't in `platforms` — one sentence the
910
+ * user reads in the switcher / setup panel. */
911
+ reason: string;
846
912
  }
847
- /** Tear down the picker overlay. */
848
- interface DbgCommandPickerStop {
849
- id: string;
850
- cmd: "picker:stop";
913
+ /** What a runner's debug session supports — drives which debug-toolbar
914
+ * controls the viewer shows. A target that lacks a capability gets the
915
+ * control hidden (no dead buttons). */
916
+ interface DebugCapabilities {
917
+ /** In-page locator picker / recorder — click an element in the SHARED,
918
+ * inspectable page to insert its locator/assertion (web). Runners that
919
+ * drive an opaque device without a clickable DOM (mobile via WDA) set this
920
+ * false → the viewer hides the picker cluster (Capture/Execute +
921
+ * target/A/eye). NOTE: distinct from `attach` — the wheel + agent join
922
+ * apply to mobile too. */
923
+ picker: boolean;
924
+ /** Collaborative attach: a second client (the MCP agent) can join the live
925
+ * debug session and co-drive it, coordinated by the control wheel. True for
926
+ * web (shared browser) AND mobile (shared device via WDA). The viewer shows
927
+ * the wheel / "who's driving" badge when set. */
928
+ attach: boolean;
929
+ /** A failure-pause is resumable as a RETRY of the failed step (mobile D-17:
930
+ * the user fixes the device/app state, Continue re-executes the same
931
+ * statement). When false (web: resume past a failure means "skip & pretend
932
+ * it passed"), the viewer hides Continue/Step on a failure-pause so the
933
+ * only action is Stop. */
934
+ resumeFromFailure: boolean;
851
935
  }
852
- /** On-demand: capture the current page outline (what the picker hit-tests
853
- * against) and write it to `picker-snapshot.txt`. Lets the viewer's Snapshot
854
- * tab show / refresh the snapshot without arming the picker. Cheap — runs
855
- * only when the user opens/refreshes the tab (the outline is expensive on
856
- * huge pages, so it is never captured on every pause). */
857
- interface DbgCommandSnapshot {
858
- id: string;
859
- cmd: "snapshot";
936
+ /** One path-input row in the viewer's App panel (M-17). */
937
+ interface AppPathPickerSpec {
938
+ /** Env var the picked path is saved to (e.g. `"APP_PATH"`). Routed to
939
+ * the target's env file by the viewer's variables API (M-11). */
940
+ envKey: string;
941
+ /** Selectable leaf extensions (lowercase, with dot). macOS `.app`
942
+ * bundles are directories — the file browser treats a matching dir
943
+ * as a selectable leaf. */
944
+ extensions: readonly string[];
945
+ /** Human label rendered above the input (e.g. `"iOS app bundle"`). */
946
+ label: string;
860
947
  }
861
- /** The DSL line a recorded action / pick renders to. Mirrors the in-page
862
- * recorder's verbs. `locator` → bare locator; `assertText`/`assertVisible`
863
- * → assertions; the rest → Playwright-style actions. */
864
- type RecordedActionKind = "locator" | "assertText" | "assertVisible" | "click" | "fill" | "press" | "selectOption" | "check" | "uncheck";
865
- /** Agent-driven, ref-safe record (collaborative authoring). The agent picks
866
- * an element by `ref` (from a snapshot of the SHARED page) + an action; the
867
- * runner resolves `ref→locator` (`RefResolver`) and emits a `locator:picked`
868
- * event the viewer inserts — so the agent NEVER hand-writes a locator. Same
869
- * resolution/insertion pipeline as the human's in-page picker. */
870
- interface DbgCommandRecord {
871
- id: string;
872
- cmd: "record";
873
- ref: string;
874
- action: RecordedActionKind;
875
- /** Typed/selected value for `fill` / `selectOption`. */
876
- value?: string;
877
- /** Key for `press` (e.g. "Enter", "Control+A"). */
878
- key?: string;
879
- /** Expected text for `assertText` (else the element's textContent is used). */
880
- text?: string;
881
- /** Where the viewer drops the line. Default `append` (agent records a flow). */
882
- placement?: "cursor" | "append";
948
+ /** Thrown by the viewer when a `RunRequest.kind` isn't in the
949
+ * adapter's `supportedKinds`. The exception filter maps to HTTP 400
950
+ * with the kind name + adapter name in the message. */
951
+ declare class UnsupportedRunKindError extends Error {
952
+ constructor(kind: RunKind, adapterName: string);
883
953
  }
884
- /** Collaborative control token (Slice 3). Sets who may act on the shared
885
- * page: `agent` lets the agent record; `human` (default) blocks the agent.
886
- * Written by the viewer (Take control button) or the agent (take_wheel). */
887
- interface DbgCommandWheel {
888
- id: string;
889
- cmd: "wheel";
890
- owner: "agent" | "human";
954
+ /** Thrown by the viewer when the configured runner package can't be
955
+ * located on disk. Message names the package + how to install. */
956
+ declare class RunnerAdapterNotFoundError extends Error {
957
+ constructor(packageName: string, hint: string);
891
958
  }
892
- /** Evaluate a DSL locator STRING against the live (paused or running) page and
893
- * report what it matches — count, the N a trailing .first()/.last()/.nth()
894
- * collapses, and per-match identity — via a `probe:result` event. The runner
895
- * parses `locator` with the SAME DSL parser the agent's `check_locator` uses,
896
- * so agent and human probe identically. READ-ONLY (never acts on the page),
897
- * so it is intentionally NOT wheel-gated: any client may probe at any time. */
898
- interface DbgCommandProbe {
899
- id: string;
900
- cmd: "probe";
901
- /** Locator exactly as written in a test, e.g.
902
- * getByRole('row').filter({hasText: 'Uma Quinn'}).first(). */
903
- locator: string;
904
- /** Cap on the number of match details returned (default 20). */
905
- limit?: number;
959
+ /** Thrown by the viewer when `unotest.config.*` doesn't declare a
960
+ * `runner:` field. Surfaces what packages exist so the user can
961
+ * pick. */
962
+ declare class RunnerNotConfiguredError extends Error {
963
+ constructor(message: string);
906
964
  }
907
- type DbgCommand = DbgCommandStep | DbgCommandContinue | DbgCommandPause | DbgCommandSetBp | DbgCommandClearBp | DbgCommandAbort | DbgCommandPickerStart | DbgCommandPickerStop | DbgCommandSnapshot | DbgCommandRecord | DbgCommandWheel | DbgCommandProbe;
908
- type DbgCommandKind = DbgCommand["cmd"];
909
965
 
910
- interface ProbeMatch {
911
- /** 0-based position among the locator's matches. */
912
- index: number;
913
- role?: string;
914
- name?: string;
915
- /** Trimmed/clipped textContent of the element. */
916
- text?: string;
917
- testId?: string;
918
- href?: string;
919
- /** Currently visible? Non-waiting check (a match can exist but be off-screen). */
920
- visible: boolean;
921
- }
922
- interface ProbePinned {
923
- /** The terminal index op that collapsed a multi-match to a single element. */
924
- op: "first" | "last" | "nth";
925
- /** How many elements the chain matched BEFORE the terminal op — i.e. how many
926
- * the .first()/.last()/.nth() silently picked from. `ofCount > 1` flags an
927
- * index op hiding ambiguity. */
928
- ofCount: number;
929
- }
930
- interface ProbeResult {
931
- /** Human-readable chain echo. NOT a selector. */
932
- chain: string;
933
- /** Elements the full chain matches. */
934
- count: number;
935
- /** Present only when the chain ends in first/last/nth. */
936
- pinned?: ProbePinned;
937
- /** Details of up to the requested limit of matches. */
938
- matches: ProbeMatch[];
939
- /** True count before truncation (equal to `count`). */
940
- totalCount: number;
941
- truncated: boolean;
942
- /** Actionable hints: nothing matched, hidden ambiguity, truncation, stale ref. */
943
- warnings: string[];
966
+ /** A test that is SUPPOSED to fail: the failure itself is the assertion.
967
+ * Its last run being red is the healthy state — and its last run being
968
+ * green is the broken one, because the thing it was guarding stopped
969
+ * being broken without anyone noticing. */
970
+ declare const EXPECT_FAIL_ANNOTATION = "expect-fail";
971
+ interface ScenarioAnnotations {
972
+ expectFail: boolean;
973
+ }
974
+ /** True for a line that is an annotation rather than prose or code. Used
975
+ * by the header parser to step over annotations while keeping the
976
+ * header's "no loose comments" rule for everything else. */
977
+ declare function isAnnotationLine(line: string): boolean;
978
+ /** Read the markers out of a scenario's source. Scans the leading run of
979
+ * comments and stops at the first line of code — an `// @expect-fail`
980
+ * further down the file is a comment about a step, not about the test. */
981
+ declare function parseScenarioAnnotations(source: string): ScenarioAnnotations;
982
+
983
+ /** Parsed scenario metadata from the mandatory header lines
984
+ * (per design-v0/decisions.md, "Test source format"):
985
+ *
986
+ * // id-<kebab-slug>
987
+ * // <human title>
988
+ * // #<6-hex-color>
989
+ * function test_<...>() { ... }
990
+ *
991
+ * All three lines are required; a file without a complete header is
992
+ * rejected by the discovery service as parse-error (no legacy fallback). */
993
+ interface ScenarioMeta {
994
+ /** Slug from `// id-<slug>`. Stable test identity across rename. */
995
+ id: string;
996
+ /** Human-readable title from `// <text>`. */
997
+ title: string;
998
+ /** Color identifier from `// #RRGGBB`. Used only in tab headers as a
999
+ * thin colored strip — NOT a status signal. */
1000
+ color: string;
1001
+ /** File stem relative to `unotest/e2e/`, dirs as slashes.
1002
+ * Examples: `smoke-welcome`, `auth/signin-flow`.
1003
+ * Source-of-truth identity for CLI invocations and tab keys. */
1004
+ name: string;
1005
+ /** Project-relative path to the source file. */
1006
+ path: string;
1007
+ /** The file carries `// @expect-fail` — a red run is this test's
1008
+ * healthy state, and a green one is the alarm. See
1009
+ * {@link ./scenario-annotations.ts}. */
1010
+ expectFail: boolean;
944
1011
  }
945
1012
 
946
- /** Terminal outcome reported by the runner via `run:finished`. */
947
- type RunOutcome = "completed" | "failed" | "aborted" | "interrupted";
948
- /** Scenario-level run-id. Format opaque to consumers; treat as string. */
949
- type RunId = string;
950
- /** Common fields on every event. */
951
- interface RunArtifactBase {
952
- /** Wallclock timestamp (Date.now()) at emit. */
953
- t: number;
1013
+ /** Response of `GET /api/scenarios`. Folders are listed independently of
1014
+ * the scenarios so an EMPTY directory under `unotest/e2e/` still renders
1015
+ * in the tree (the folder list isn't inferred from scenario paths). */
1016
+ interface ScenariosPayload {
1017
+ scenarios: ScenarioMeta[];
1018
+ /** Directory paths relative to `unotest/e2e/`, posix-separated, any
1019
+ * depth (e.g. `auth`, `auth/signup`, `autopark`). Excludes reserved
1020
+ * subtrees (`_helpers`, `_collections`) and any `_`/`.`-prefixed dir. */
1021
+ folders: string[];
954
1022
  }
955
- interface RunStartedEvent extends RunArtifactBase {
956
- kind: "run:started";
957
- /** Same as the `<runId>` directory name; included for self-describing
958
- * events when reassembled out-of-band. */
959
- runId: RunId;
960
- /** Scenario `name` (file stem relative to `e2e/`). */
961
- scenario: string;
1023
+ /** Filesystem-unsafe characters in a single path segment — see
1024
+ * `FS_UNSAFE_NAME_RE` (it includes both path separators, so a valid
1025
+ * segment is guaranteed to be ONE level). Kept as a named re-export for
1026
+ * the published surface. */
1027
+ declare const SEGMENT_NAME_FORBIDDEN_RE: RegExp;
1028
+ /** Validate a single scenario-file stem or folder name. Returns `null` on
1029
+ * success, else a human-readable reason. Shared by the viewer-server
1030
+ * writer (authoritative) and the client form (pre-flight) so both agree.
1031
+ * Pure — safe to bundle in the browser.
1032
+ *
1033
+ * Rejects: empty, `.`/`..`, `.`-prefix (hidden), `_`-prefix (reserved for
1034
+ * `_helpers`/`_collections`/templates), path separators + FS-unsafe
1035
+ * punctuation + control chars. */
1036
+ declare function validateSegmentName(name: string): string | null;
1037
+
1038
+ /** Session profile — the kind of process a tab spawns. Mirrored by the
1039
+ * viewer's TerminalStore.TermKind. */
1040
+ type TerminalKind = "local" | "claude" | "codex";
1041
+ interface TerminalSpawnMessage {
1042
+ type: "spawn";
1043
+ id: string;
1044
+ kind: TerminalKind;
1045
+ cols: number;
1046
+ rows: number;
962
1047
  }
963
- interface StepStartedEvent extends RunArtifactBase {
964
- kind: "step:started";
965
- /** Project-root-relative posix path of the file this step lives in
966
- * (entry or a helper). Matches a key in the run's `sources.json`.
967
- * The viewer slices the source text by `line` from the snapshot. */
968
- file: string;
969
- /** Line number of the DSL call in the source file (1-indexed). */
970
- line: number;
971
- /** Column number (1-indexed since 0.11 — the DSL lexer's token
972
- * columns; earlier releases emitted 0-indexed columns for
973
- * word-initial statements). */
974
- col: number;
1048
+ interface TerminalDataMessage {
1049
+ type: "data";
1050
+ id: string;
1051
+ /** Raw bytes the user typed, forwarded to the PTY stdin. */
1052
+ data: string;
975
1053
  }
976
- interface StepOkEvent extends RunArtifactBase {
977
- kind: "step:ok";
978
- /** Wall-clock duration of the step that just finished. */
979
- durationMs: number;
1054
+ interface TerminalResizeMessage {
1055
+ type: "resize";
1056
+ id: string;
1057
+ cols: number;
1058
+ rows: number;
980
1059
  }
981
- interface StepFailEvent extends RunArtifactBase {
982
- kind: "step:fail";
983
- /** Human-readable error message (single line preferred). */
984
- error: string;
985
- /** Error class name (e.g. `"AssertionError"`, `"LocatorError"`,
986
- * `"TimeoutError"`). Lets the viewer's `ErrorCard` show a typed
987
- * pill instead of a generic "Error" label. Optional because some
988
- * thrown errors lack a meaningful class name. */
989
- errorName?: string;
990
- /** Project-relative path to a Playwright trace zip, if captured. */
991
- trace?: string;
1060
+ interface TerminalCloseMessage {
1061
+ type: "close";
1062
+ id: string;
992
1063
  }
993
- interface ScreenshotEvent extends RunArtifactBase {
994
- kind: "screenshot";
995
- /** Project-relative path to the PNG (under `unotest/.runs/<runId>/screenshot/`). */
996
- path: string;
997
- /** Step identity for captures bound to a specific step (per-step
998
- * screenshots under `UNOTEST_STEP_SCREENSHOTS`). Same semantics as
999
- * `StepStartedEvent` — the viewer joins on `line:col`. Absent for
1000
- * unbound captures (`screenshot()` DSL calls); consumers may fall
1001
- * back to attributing those to the step in flight at emit time. */
1002
- file?: string;
1003
- line?: number;
1004
- col?: number;
1064
+ type TerminalClientMessage = TerminalSpawnMessage | TerminalDataMessage | TerminalResizeMessage | TerminalCloseMessage;
1065
+ interface TerminalOutputMessage {
1066
+ type: "output";
1067
+ id: string;
1068
+ /** Raw PTY stdout/stderr bytes to write to the xterm instance. */
1069
+ data: string;
1005
1070
  }
1006
- /** Emitted ONCE after a failing run when the runner has finished writing
1007
- * the D-16 failure bundle to disk. Viewer's inspector reads
1008
- * `bundlePath` to fetch artifacts (screenshot, semantic DOM, network,
1009
- * trace) via `/api/runs/<runId>/asset?path=...`. The bundle directory
1010
- * is keyed by its own id (`<scenario>__<isoDate>__<stepIdx>`) under
1011
- * `.unotest/failures/`, not under `unotest/.runs/`, because the
1012
- * bundle layout pre-dates per-run dirs and is still consumed by the
1013
- * agent-fix MCP tools. */
1014
- interface FailureBundleWrittenEvent extends RunArtifactBase {
1015
- kind: "failure-bundle:written";
1016
- /** Project-relative path to the bundle directory. */
1017
- bundlePath: string;
1018
- /** Project-relative path to the screenshot PNG inside the bundle,
1019
- * when tier-2 capture succeeded. */
1020
- screenshotPath?: string;
1021
- /** Page URL at the moment of failure (same value captured into the
1022
- * bundle's `snapshot.json`/`outline.txt`), for the viewer's ErrorCard. */
1023
- url?: string;
1071
+ interface TerminalExitMessage {
1072
+ type: "exit";
1073
+ id: string;
1074
+ exitCode: number | null;
1024
1075
  }
1025
- interface RunFinishedEvent extends RunArtifactBase {
1026
- kind: "run:finished";
1027
- outcome: RunOutcome;
1076
+ /** Spawn failure (e.g. `claude` not on PATH) reported to the tab so it can
1077
+ * print a friendly line instead of silently dying. */
1078
+ interface TerminalErrorMessage {
1079
+ type: "error";
1080
+ id: string;
1081
+ message: string;
1028
1082
  }
1029
- /** Debugger pause. Reason discriminates:
1030
- * - `breakpoint` — executor halted before a statement whose `line:col` is
1031
- * in `RunOptions.breakpoints`.
1032
- * - `step` — `pauseRequested` flag was set by a `pause` debug command,
1033
- * or the run was started in step-mode and just completed a statement.
1034
- * - `failure` — executor caught an error and `pauseOnFailure` is true.
1083
+ type TerminalServerMessage = TerminalOutputMessage | TerminalExitMessage | TerminalErrorMessage;
1084
+ /** The `ws` adapter routes inbound messages by an `event` envelope; both
1085
+ * directions use this single event name on `/ws/terminal`. */
1086
+ declare const TERMINAL_WS_EVENT = "terminal";
1087
+
1088
+ type VariableCategory = "secret" | "app" | "runner";
1089
+ /** User-facing runner-config knobs documented in `unotest/.env`. Single source
1090
+ * of truth for "is this name our config?" — drives the viewer's "Runner
1091
+ * config" group and the add-variable dropdown.
1035
1092
  *
1036
- * When the runner is hosting a debug session it also writes a parallel
1037
- * `runtime.json` file with the same snapshot. The snapshot is included
1038
- * inline here for replay / single-frame consumers that don't want to do a
1039
- * separate fs read. */
1040
- interface PausedEvent extends RunArtifactBase {
1041
- kind: "paused";
1042
- reason: "breakpoint" | "step" | "failure";
1043
- /** Project-root-relative posix path of the file the pause point lives
1044
- * in (entry or a helper). Matches a key in the run's `sources.json`. */
1045
- file: string;
1046
- line: number;
1047
- col: number;
1048
- /** Inline snapshot of the runtime state at the pause point. `vars` is
1049
- * per-value JSON-stringified (LocatorValue → "<Locator role=... name=...>"
1050
- * human-readable string) and per-var truncated to 4KB to keep the JSONL
1051
- * line manageable. `callStack` is outermost→innermost user-function frames. */
1052
- snapshot?: {
1053
- vars: Record<string, string>;
1054
- callStack: {
1055
- fn: string;
1056
- file: string;
1057
- line: number;
1058
- }[];
1059
- };
1060
- }
1061
- /** Emitted by the in-page locator picker / action recorder (debug-pause)
1062
- * when the user picks an element or performs a recorded action. The runner
1063
- * resolved the picked `ref` to a locator (`RefResolver`) and rendered the
1064
- * DSL snippet. The viewer inserts `snippet` per `placement`.
1065
- * See docs/recoder/plan.md. */
1066
- interface LocatorPickedEvent extends RunArtifactBase {
1067
- kind: "locator:picked";
1068
- mode: PickerMode;
1069
- /** Ready-to-insert DSL line, e.g. a bare locator
1070
- * `getByRole("button", {name: "Sign in"})` (locator mode), an assertion
1071
- * (`assertText(…)`), or a recorded action (`click(…)`, `fill(…)`). */
1072
- snippet: string;
1073
- /** Where the viewer drops the snippet:
1074
- * - `cursor` — at the editor's caret (deliberate picks; user-placed).
1075
- * - `append` — after the previous recorded line (execute-mode action
1076
- * recorder, preserving order). */
1077
- placement: "cursor" | "append";
1078
- /** `stable` (testId / role+name / label / …) or `fragile` (css/nth-only).
1079
- * Lets the viewer flag brittle picks. */
1080
- stability: "stable" | "fragile";
1093
+ * NOT listed (so they never surface as user config): launcher-injected
1094
+ * run-control vars (UNOTEST_RUN_ID, UNOTEST_PARENT_RUN_ID,
1095
+ * UNOTEST_BUSY_SCENARIOS, UNOTEST_WORKER_*), session-log internals
1096
+ * (UNOTEST_SESSION_LOG_*), and path bootstrap (UNOTEST_DIR,
1097
+ * UNOTEST_PROJECT_ROOT, UNOTEST_CONFIG, UNOTEST_VIEWER_NO_OPEN). A user would
1098
+ * never put those in `unotest/.env`. */
1099
+ declare const RUNNER_CONFIG_KEYS: ReadonlySet<string>;
1100
+ /** True when `name` is a key the runner consumes as its own configuration. */
1101
+ declare function isRunnerConfigKey(name: string): boolean;
1102
+ /** Group a variable for the viewer. Secrecy from the source file; runner-vs-app
1103
+ * from the owned-key set. */
1104
+ declare function categorizeVariable(v: {
1105
+ name: string;
1106
+ source: "env" | "secrets";
1107
+ }): VariableCategory;
1108
+ /** An external scenario variable is referenced by a bare UPPER_SNAKE
1109
+ * identifier (`APP_BASE_URL`). App / secret names must match this to be
1110
+ * resolvable from a scenario; runner keys are validated against the set
1111
+ * above instead. Convention only — no value inspection. */
1112
+ declare function isExternalVarName(name: string): boolean;
1113
+
1114
+ /** Parse `KEY=VALUE` env-file CONTENT into a plain record. Pure — the
1115
+ * caller does the fs read. Blank lines and `#` comments are skipped;
1116
+ * balanced surrounding single/double quotes are stripped; keys not
1117
+ * matching `[A-Za-z_][A-Za-z0-9_]*` are ignored. Shared by the web and
1118
+ * mobile runners' layered env loaders (M-11) so both parse `.env` files
1119
+ * identically. */
1120
+ declare function parseEnvContent(raw: string): Record<string, string>;
1121
+ /** Quote on write only when the value would otherwise be ambiguous (spaces,
1122
+ * `#`, quotes, `=`, backslash, leading backtick, or newlines). Plain tokens
1123
+ * stay unquoted. Mirrors the viewer env-dotenv serializer. */
1124
+ declare function serializeEnvValue(value: string): string;
1125
+ /** Active (non-commented) assignment keys present in env-file `content`.
1126
+ * Used to detect duplicates / cross-file shadowing before an add. */
1127
+ declare function envKeys(content: string): Set<string>;
1128
+ /**
1129
+ * Insert `KEY=VALUE` into env-file `content`. With `sectionLabel`, the line
1130
+ * goes into the first `# --- <label> --- ` divider section — after the last
1131
+ * assignment there (or its description comments if it has no vars yet). The
1132
+ * label is matched as a whole word against DIVIDER lines only, so prose
1133
+ * comments are never mistaken for a section. No such section → it is created
1134
+ * at end-of-file. With no label the line is appended. Does NOT dedup — reject
1135
+ * duplicates before calling.
1136
+ */
1137
+ declare function insertEnvVar(content: string, key: string, value: string, sectionLabel?: string): string;
1138
+ /**
1139
+ * Remove the assignment line for `key`, leaving every comment and other line
1140
+ * untouched (we do not guess which comments "belong" to the key). Returns the
1141
+ * new content and whether anything was removed.
1142
+ */
1143
+ declare function removeEnvVar(content: string, key: string): {
1144
+ content: string;
1145
+ removed: boolean;
1146
+ };
1147
+
1148
+ /** Which group the user is adding into. App/runner → `.env`, secret →
1149
+ * `.secrets`. Mirrors VariableCategory as an INPUT (the user's choice). */
1150
+ type VariableKind = VariableCategory;
1151
+ interface AddVariablePlanInput {
1152
+ name: string;
1153
+ kind: VariableKind;
1154
+ /** Current text of `unotest/.env` (`""` if absent). */
1155
+ envContent: string;
1156
+ /** Current text of `unotest/.secrets` (`""` if absent). */
1157
+ secretsContent: string;
1081
1158
  }
1082
- /** Emitted when execution resumes after a `paused` event. The `by` field
1083
- * records which debug command unblocked it — purely informational; the
1084
- * next `step:started` event carries the actual line/col. */
1085
- interface ResumedEvent extends RunArtifactBase {
1086
- kind: "resumed";
1087
- by: "step" | "continue";
1159
+ type VariablePlanError = {
1160
+ ok: false;
1161
+ code: "duplicate" | "invalid-name" | "unknown-runner-key";
1162
+ message: string;
1163
+ };
1164
+ type AddVariablePlan = {
1165
+ ok: true;
1166
+ target: "env" | "secrets";
1167
+ sectionLabel?: string;
1168
+ warnings: string[];
1169
+ } | VariablePlanError;
1170
+ /**
1171
+ * Decide where a new variable goes and what to warn about — or reject it.
1172
+ * Reject reasons: duplicate key in the target file; a non-UPPER_SNAKE app/
1173
+ * secret name (a scenario could never reference it); a runner key not in the
1174
+ * owned-config allowlist. Warnings (non-fatal): secret shadowing a same-named
1175
+ * `.env` var, or vice-versa.
1176
+ */
1177
+ declare function planAddVariable(input: AddVariablePlanInput): AddVariablePlan;
1178
+ /**
1179
+ * Warning to show after deleting `name` from the file: if the shell
1180
+ * environment still defines it with a DIFFERENT value, it will keep
1181
+ * resolving from there (set-if-absent means ambient wins). Comparing values
1182
+ * avoids a false positive when `.env` was merely flattened into process.env.
1183
+ */
1184
+ declare function deleteShadowWarning(name: string, fileValue: string | undefined, ambientValue: string | undefined): string[];
1185
+
1186
+ /** A single editor diagnostic. Superset of the `@unotest/dsl`
1187
+ * validator diagnostic: it unifies parse errors, the vocab validator,
1188
+ * and the platform linter behind one shape, tagged by `source`/`rule`
1189
+ * so consumers can theme or filter. Positions are 1-based; a parse
1190
+ * error with no recoverable position falls back to `1:1`. */
1191
+ interface DslDiagnostic {
1192
+ severity: "error" | "warning" | "info";
1193
+ /** 1-based line. */
1194
+ line: number;
1195
+ /** 1-based column. */
1196
+ column: number;
1197
+ /** 1-based end line, when the producer knows the span. */
1198
+ endLine?: number;
1199
+ /** 1-based end column, when the producer knows the span. */
1200
+ endColumn?: number;
1201
+ message: string;
1202
+ /** Stable rule id, e.g. `"validator:unknown-function"`, `"validator:dsl"`,
1203
+ * a linter rule id, or `"parse"`. */
1204
+ rule: string;
1205
+ /** Which stage produced it — lets the UI group / colour by origin. */
1206
+ source: "parse" | "validate" | "lint";
1088
1207
  }
1089
- /** Emitted when the collaborative control token changes owner (Slice 3), so
1090
- * the viewer can live-update the "who's driving" badge over WS. */
1091
- interface WheelChangedEvent extends RunArtifactBase {
1092
- kind: "wheel:changed";
1093
- owner: "agent" | "human";
1208
+ /** Coarse grouping for a vocab entry — drives completion icons and
1209
+ * ordering. `keyword` covers DSL control-flow / block words (`step`,
1210
+ * `if`, `for`, …); `helper` covers project-defined `flow_*` / mock
1211
+ * helpers surfaced from the workspace. */
1212
+ type DslVocabCategory = "action" | "assertion" | "selector" | "chain" | "navigation" | "read" | "wait" | "state" | "storage" | "multi-context" | "sandbox" | "diagnostic" | "escape-hatch" | "keyword" | "helper";
1213
+ /** One declared argument of a vocab entry — feeds signature help and
1214
+ * the snippet placeholders in `insertText`. `kind` is the validator's
1215
+ * arg-kind string (`"locatorLike"`, `"stringLike"`, `"number"`,
1216
+ * `"any"`, …); the editor treats it as an opaque label. */
1217
+ interface DslVocabArg {
1218
+ label: string;
1219
+ kind: string;
1220
+ required: boolean;
1221
+ /** Literal members when `kind` is `"enum"` — the label alone loses
1222
+ * them, and consumers (typings generator, completion) need the
1223
+ * actual values. */
1224
+ enumValues?: readonly string[];
1094
1225
  }
1095
- /** Emitted in response to a `probe` debug command: the result of evaluating a
1096
- * locator string against the live page. `id` echoes the command's id so the
1097
- * client that issued the probe can correlate the reply. READ-ONLY — producing
1098
- * it never changed the page. */
1099
- interface ProbeResultEvent extends RunArtifactBase {
1100
- kind: "probe:result";
1101
- /** Correlates with the issuing `DbgCommandProbe.id`. */
1102
- id: string;
1103
- result: ProbeResult;
1226
+ /** A single autocomplete-able item: a vocab function, a DSL keyword, or
1227
+ * a project helper. Serializable — crosses the REST boundary verbatim
1228
+ * and is cached by the browser (the vocab is static per platform). */
1229
+ interface DslVocabEntry {
1230
+ name: string;
1231
+ category: DslVocabCategory;
1232
+ args: DslVocabArg[];
1233
+ /** True when the signature accepts extra positional args after the
1234
+ * declared ones (`shell(cmd, ...args)`, `dbQuery(sql, ...params)`). */
1235
+ variadic?: boolean;
1236
+ /** Return-kind string (`"void"`, `"string"`, `"locator"`, …). */
1237
+ returns: string;
1238
+ /** Snippet body with `${n:label}` placeholders, e.g.
1239
+ * `click(${1:locator})`. */
1240
+ insertText: string;
1241
+ /** Human-readable signature for hover / signature-help, e.g.
1242
+ * `getByRole(role, { name?, exact? })`. Richer than the bare arg
1243
+ * list — includes option-object shape. */
1244
+ signature?: string;
1245
+ /** Prose description (markdown) for hover and the completion info
1246
+ * panel. */
1247
+ doc?: string;
1248
+ /** Locator capability from the function contract. `"chainableLocator"`
1249
+ * entries can be chained off a locator (offered after `.`); any value
1250
+ * other than `"none"` produces a locator (so it fits a `locator`
1251
+ * argument slot). Absent for keywords / helpers that have no contract. */
1252
+ locatorCapability?: "none" | "locator" | "chainableLocator";
1253
+ /** Where the entry is defined in the USER's project — set for project
1254
+ * helpers (`flow_*` in `_helpers/`), absent for platform functions,
1255
+ * which have no source on disk there. Feeds go-to-definition.
1256
+ * `path` is project-relative posix; `line`/`column` are 1-based. */
1257
+ source?: {
1258
+ path: string;
1259
+ line: number;
1260
+ column: number;
1261
+ };
1104
1262
  }
1105
- /** A single MCP tool invocation by the agent, surfaced into the session's
1106
- * event stream so the human sees — in the viewer's System console — what the
1107
- * agent called and how it ended. Emitted ONLY while the MCP server is attached
1108
- * to this run (own-browser calls have no session to attach to). Paired:
1109
- * `tool:call` at invocation, `tool:result` at completion, correlated by
1110
- * `callId`. Read-only telemetry — emitting it never touches the page. */
1111
- interface ToolCallEvent extends RunArtifactBase {
1112
- kind: "tool:call";
1113
- /** Correlates with the matching `tool:result` (one MCP call). */
1114
- callId: string;
1115
- /** MCP tool name, e.g. "check_locator", "explore_step". */
1116
- tool: string;
1117
- /** Arguments as a secret-redacted, length-capped JSON string — NOT the raw
1118
- * object. Capped upstream so the line stays a single atomic append. */
1119
- args: string;
1263
+ /** Per-request file context for {@link IDslLanguageService.validate}.
1264
+ * The viewer owns workspace discovery, so it supplies the things the
1265
+ * platform validator can't know on its own. */
1266
+ interface DslValidateContext {
1267
+ /** Names of project helpers (`flow_*`, mocks) so their calls are not
1268
+ * flagged as unknown functions — mirrors how the lint runner
1269
+ * registers `unotest/e2e/_helpers/`. */
1270
+ helperNames?: readonly string[];
1271
+ /** Whether the source is a scenario or a helper definition file. */
1272
+ fileKind?: "scenario" | "helper";
1120
1273
  }
1121
- interface ToolResultEvent extends RunArtifactBase {
1122
- kind: "tool:result";
1123
- /** Correlates with the issuing `ToolCallEvent.callId`. */
1124
- callId: string;
1125
- tool: string;
1126
- ok: boolean;
1127
- durationMs: number;
1128
- /** Secret-redacted, length-capped preview of the result text. */
1129
- preview: string;
1130
- /** Present only when `ok` is false. */
1131
- error?: {
1132
- class: string;
1133
- message: string;
1134
- };
1274
+ /** The DSL language service a platform exposes to the viewer. Two
1275
+ * concerns only: the static vocabulary (for completion / signature
1276
+ * help) and full source validation (parse + vocab validator + linter,
1277
+ * identical to the platform's own lint gate). */
1278
+ interface IDslLanguageService {
1279
+ /** Static, serializable vocabulary for autocomplete and signature
1280
+ * help. Stable for the lifetime of the process — safe to cache. */
1281
+ getVocab(): DslVocabEntry[];
1282
+ /** Full diagnostics for one source string. Pure with respect to the
1283
+ * filesystem — all workspace knowledge arrives via `ctx`. */
1284
+ validate(source: string, ctx?: DslValidateContext): DslDiagnostic[];
1135
1285
  }
1136
- /** Discriminated union of every JSONL event the scenario runner emits. */
1137
- type RunArtifact = RunStartedEvent | StepStartedEvent | StepOkEvent | StepFailEvent | ScreenshotEvent | FailureBundleWrittenEvent | PausedEvent | ResumedEvent | LocatorPickedEvent | WheelChangedEvent | ProbeResultEvent | ToolCallEvent | ToolResultEvent | RunFinishedEvent;
1138
- interface CollectionRunStartedEvent extends RunArtifactBase {
1139
- kind: "collection-run:started";
1140
- runId: RunId;
1141
- /** Collection `name` (filename stem). */
1142
- collection: string;
1143
- /** Scenario names that will be executed, in order. */
1144
- scenarios: string[];
1286
+
1287
+ /** A spawned runner process. Structural superset of the bits of a Node
1288
+ * `ChildProcess` the viewer actually consumes — pid for the active-run
1289
+ * snapshot, stdout/stderr for log capture, exit/error for finalize,
1290
+ * kill for abort. Both `child_process` and Electron `utilityProcess`
1291
+ * satisfy this via a thin adapter. */
1292
+ interface ILaunchedProcess {
1293
+ /** OS process id, or -1 if the spawn failed to produce one. */
1294
+ readonly pid: number;
1295
+ /** Captured stdout stream, or null when not piped. */
1296
+ readonly stdout: Readable | null;
1297
+ /** Captured stderr stream, or null when not piped. */
1298
+ readonly stderr: Readable | null;
1299
+ /** Register the exit handler. Called once when the process exits. */
1300
+ onExit(cb: (code: number | null, signal: NodeJS.Signals | null) => void): void;
1301
+ /** Register the spawn-error handler (e.g. bin not found). */
1302
+ onError(cb: (err: Error) => void): void;
1303
+ /** Terminate the process. Default signal SIGTERM. */
1304
+ kill(signal?: NodeJS.Signals): void;
1145
1305
  }
1146
- interface CollectionScenarioStartedEvent extends RunArtifactBase {
1147
- kind: "collection-run:scenario-started";
1148
- /** The scenario-run-id created when this scenario started executing. */
1149
- scenarioRunId: RunId;
1150
- scenario: string;
1306
+ /** Everything the launcher needs to spawn one runner process. The caller
1307
+ * (RunnerService) has already merged the environment and resolved the bin
1308
+ * + argv from the `IRunnerAdapter`; the launcher only decides HOW to run
1309
+ * it. Pure data — no behaviour. */
1310
+ interface LaunchSpec {
1311
+ /** Absolute path to the JS bin entry — `IRunnerAdapter.resolveBin()`. */
1312
+ bin: string;
1313
+ /** Argv to pass after the bin — `IRunnerAdapter.buildArgs(req)`. */
1314
+ args: string[];
1315
+ /** Fully-merged environment for the child. */
1316
+ env: NodeJS.ProcessEnv;
1317
+ /** Working directory — the workspace project root. */
1318
+ cwd: string;
1151
1319
  }
1152
- interface CollectionScenarioFinishedEvent extends RunArtifactBase {
1153
- kind: "collection-run:scenario-finished";
1154
- scenarioRunId: RunId;
1155
- scenario: string;
1156
- outcome: RunOutcome;
1320
+ /** Spawns the runner JS under a Node-capable runtime. Injected into the
1321
+ * viewer's RunnerService so the spawn mechanism (system Node vs Electron
1322
+ * utilityProcess) is a deployment choice, not baked into the service. */
1323
+ interface IProcessLauncher {
1324
+ launch(spec: LaunchSpec): ILaunchedProcess;
1157
1325
  }
1158
- interface CollectionRunFinishedEvent extends RunArtifactBase {
1159
- kind: "collection-run:finished";
1160
- outcome: RunOutcome;
1326
+
1327
+ /** Contents of `unotest/.viewer.lock`. Written by viewer-server on
1328
+ * startup, removed on graceful shutdown. Stale detection: `process.kill(
1329
+ * pid, 0)` throws ESRCH if PID is dead → lockfile is stale, safe to
1330
+ * overwrite. */
1331
+ interface LockFile {
1332
+ /** PID of the viewer-server process. */
1333
+ pid: number;
1334
+ /** Full URL (with port) to reach the viewer. e.g. `http://localhost:5173`. */
1335
+ url: string;
1336
+ /** ISO 8601 timestamp of `pid` start. Used for tie-breaking and stale-age
1337
+ * heuristics if `kill(pid, 0)` is inconclusive. */
1338
+ startedAt: string;
1161
1339
  }
1162
- type CollectionRunArtifact = CollectionRunStartedEvent | CollectionScenarioStartedEvent | CollectionScenarioFinishedEvent | CollectionRunFinishedEvent;
1163
1340
 
1164
- /** Lifecycle stage as inferred by the server when listing runs. */
1165
- type RunStatus = "running" | "passed" | "failed" | "aborted" | "interrupted";
1166
- interface RunMeta {
1167
- runId: RunId;
1168
- kind: RunKind;
1169
- /** Scenario `name` (file stem under `e2e/`) for `kind:"scenario"`,
1170
- * collection `name` (yaml stem) for `kind:"collection"`. */
1171
- ref: string;
1172
- /** Wallclock from the `run:started` event. */
1173
- startedAt: number;
1174
- /** Wallclock from the `run:finished` event, or null if still active. */
1175
- finishedAt: number | null;
1176
- /** Total wall-clock duration, or null if still running. */
1177
- durationMs: number | null;
1178
- /** Status derived from outcome / liveness — `running` while the run's
1179
- * heartbeat is fresh and no `run:finished` has landed. */
1180
- status: RunStatus;
1181
- /** Terminal outcome from `run:finished`, or null if still running. */
1182
- outcome: RunOutcome | null;
1183
- /** Run-id of the parent collection-run when this is a child scenario
1184
- * spawned inside one. Absent for top-level scenarios and for
1185
- * collection-runs themselves. Populated by the writer from the
1186
- * child's `manifest.json`. */
1187
- parentRunId?: string;
1341
+ /** Helper file metadata. Helpers live in `unotest/e2e/_helpers/**` as
1342
+ * `snake_case` functions (`signin_as`, `seed_user`, …). The runner
1343
+ * auto-discovers them; no import system in scenarios. */
1344
+ interface HelperMeta {
1345
+ /** File stem relative to `_helpers/`, dirs as slashes. */
1346
+ name: string;
1347
+ /** Project-relative path to the source file. */
1348
+ path: string;
1188
1349
  }
1189
1350
 
1190
1351
  interface FsScenariosChangedMessage {
@@ -1392,4 +1553,4 @@ interface RuntimeStateFile {
1392
1553
  updatedAt: number;
1393
1554
  }
1394
1555
 
1395
- export { type AddVariablePlan, type AddVariablePlanInput, type AppPathPickerSpec, type AppServerEvent, type AuthoringRunRequest, type BreakpointEntry, type BreakpointsFile, COLLECTIONS_SUBDIR, COLLECTION_NAME_FORBIDDEN_RE, COMMANDS_FILE, type CheckSeverity, type CollectionMeta, type CollectionRunArtifact, type CollectionRunFinishedEvent, type CollectionRunRequest, type CollectionRunStartedEvent, type CollectionRunTab, type CollectionScenarioFinishedEvent, type CollectionScenarioStartedEvent, type CollectionTab, type DbgCommand, type DbgCommandAbort, type DbgCommandClearBp, type DbgCommandContinue, type DbgCommandKind, type DbgCommandPause, type DbgCommandPickerStart, type DbgCommandPickerStop, type DbgCommandProbe, type DbgCommandRecord, type DbgCommandSetBp, type DbgCommandSnapshot, type DbgCommandStep, type DbgCommandWheel, type DebugCapabilities, type DoctorCheck, type DoctorContext, type DslDiagnostic, type DslValidateContext, type DslVocabArg, type DslVocabCategory, type DslVocabEntry, E2E_SUBDIR, ENV_FILE, type EnvFile, type EnvUpdate, type EnvVar, type ErrorMessage, type FailureBundleWrittenEvent, type FsBreakpointsChangedMessage, type FsCollectionRunStepMessage, type FsCollectionsChangedMessage, type FsHelpersChangedMessage, type FsRunFinishedMessage, type FsRunStartedMessage, type FsRunStepMessage, type FsScenariosChangedMessage, HEARTBEAT_FILE, HELPERS_SUBDIR, type HelperMeta, type HelperTab, type HostRequirement, type IDslLanguageService, type ILaunchedProcess, type IProcessLauncher, type IRunnerAdapter, LOCK_FILE, LOG_FILE, type LaunchSpec, type LocatorPickedEvent, type LockFile, type LogLineMessage, type Matcher, PICKER_SNAPSHOT_FILE, type PausedEvent, type PickerMode, type ProbeMatch, type ProbePinned, type ProbeResult, type ProbeResultEvent, RUNNER_CONFIG_KEYS, RUNS_SUBDIR, RUNTIME_FILE, RUN_MANIFEST_FILE, RUN_SOURCES_FILE, type RecordedActionKind, type RegexLiteral, type ResumedEvent, type RunArtifact, type RunFinishedEvent, type RunId, type RunKind, type RunManifest, type RunMeta, type RunOutcome, type RunRequest, type RunSources, type RunStartedEvent, type RunStatus, RunnerAdapterNotFoundError, RunnerNotConfiguredError, type RuntimeState, type RuntimeStateFile, SECRETS_FILE, SEGMENT_NAME_FORBIDDEN_RE, SNAPSHOTS_SUBDIR, STDERR_FILE, STDOUT_FILE, STEPS_FILE, type ScenarioMeta, type ScenarioRunRequest, type ScenarioRunTab, type ScenarioTab, type ScenariosPayload, type ScreenshotEvent, type SnapshotTab, type StepFailEvent, type StepOkEvent, type StepStartedEvent, TERMINAL_RUNTIME_STATES, TERMINAL_WS_EVENT, type TabKind, type TabKindName, type TerminalClientMessage, type TerminalCloseMessage, type TerminalDataMessage, type TerminalErrorMessage, type TerminalExitMessage, type TerminalKind, type TerminalOutputMessage, type TerminalResizeMessage, type TerminalServerMessage, type TerminalSpawnMessage, type ToolCallEvent, type ToolResultEvent, UNOTEST_DIR, UnsupportedRunKindError, type VariableCategory, type VariableKind, type VariablePlanError, type WheelChangedEvent, type WsMessage, type WsMessageKind, activeEnvName, categorizeVariable, debugDirFor, debuggerFileFor, deleteShadowWarning, envFileFor, envKeys, envLayerFilesFor, insertEnvVar, isExternalVarName, isRegexLiteral, isRunnerConfigKey, isTerminalRuntimeState, makeRunId, parseAppServerEvent, parseEnvContent, planAddVariable, regexLiteral, removeEnvVar, runsDirFor, sanitizeRunIdSegment, secretsFileFor, secretsLayerFilesFor, serializeEnvValue, testDirFor, validateCollectionName, validateSegmentName };
1556
+ export { type AddVariablePlan, type AddVariablePlanInput, type AppPathPickerSpec, type AppServerEvent, type AuthoringRunRequest, BLOBS_DIR, type BreakpointEntry, type BreakpointsFile, COLLECTIONS_SUBDIR, COLLECTION_NAME_FORBIDDEN_RE, COMMANDS_FILE, type CheckSeverity, type CollectionMeta, type CollectionRunArtifact, type CollectionRunFinishedEvent, type CollectionRunRequest, type CollectionRunStartedEvent, type CollectionRunTab, type CollectionScenarioFinishedEvent, type CollectionScenarioStartedEvent, type CollectionTab, DAY_INDEX_FILE, type DbgCommand, type DbgCommandAbort, type DbgCommandClearBp, type DbgCommandContinue, type DbgCommandKind, type DbgCommandPause, type DbgCommandPickerStart, type DbgCommandPickerStop, type DbgCommandProbe, type DbgCommandRecord, type DbgCommandSetBp, type DbgCommandSnapshot, type DbgCommandStep, type DbgCommandWheel, type DebugCapabilities, type DoctorCheck, type DoctorContext, type DslDiagnostic, type DslValidateContext, type DslVocabArg, type DslVocabCategory, type DslVocabEntry, E2E_SUBDIR, ENV_FILE, EXPECT_FAIL_ANNOTATION, type EnvFile, type EnvUpdate, type EnvVar, type ErrorMessage, type FailureBundleWrittenEvent, type FsBreakpointsChangedMessage, type FsCollectionRunStepMessage, type FsCollectionsChangedMessage, type FsHelpersChangedMessage, type FsRunFinishedMessage, type FsRunStartedMessage, type FsRunStepMessage, type FsScenariosChangedMessage, HEARTBEAT_FILE, HELPERS_SUBDIR, type HelperMeta, type HelperTab, type HostRequirement, type IDslLanguageService, type ILaunchedProcess, type IProcessLauncher, type IRunnerAdapter, InvalidBlobHashError, LOCK_FILE, LOG_FILE, type LaunchSpec, type LocatorPickedEvent, type LockFile, type LogLineMessage, type Matcher, PICKER_SNAPSHOT_FILE, type PausedEvent, type PickerMode, type ProbeMatch, type ProbePinned, type ProbeResult, type ProbeResultEvent, RUNNER_CONFIG_KEYS, RUNS_SUBDIR, RUNTIME_FILE, RUN_INDEX_SCHEMA_VERSION, RUN_MANIFEST_FILE, RUN_SOURCES_FILE, type RecordedActionKind, type RegexLiteral, type ResumedEvent, type RunArtifact, type RunFinishedEvent, type RunId, type RunIndexEntry, type RunIndexHeader, type RunKind, type RunManifest, type RunMeta, type RunOutcome, type RunRequest, type RunSources, type RunStartedEvent, type RunStatus, RunnerAdapterNotFoundError, RunnerNotConfiguredError, type RuntimeState, type RuntimeStateFile, SCENARIOS_INDEX_DIR, SCENARIOS_LATEST_FILE, SCENARIO_RECENT_WINDOW, SECRETS_FILE, SEGMENT_NAME_FORBIDDEN_RE, SNAPSHOTS_SUBDIR, STDERR_FILE, STDOUT_FILE, STEPS_FILE, type ScenarioAnnotations, type ScenarioLatestEntry, type ScenarioMeta, type ScenarioRunRequest, type ScenarioRunTab, type ScenarioTab, type ScenariosLatest, type ScenariosPayload, type ScreenshotEvent, type SnapshotTab, type StepFailEvent, type StepOkEvent, type StepStartedEvent, TERMINAL_RUNTIME_STATES, TERMINAL_WS_EVENT, type TabKind, type TabKindName, type TerminalClientMessage, type TerminalCloseMessage, type TerminalDataMessage, type TerminalErrorMessage, type TerminalExitMessage, type TerminalKind, type TerminalOutputMessage, type TerminalResizeMessage, type TerminalServerMessage, type TerminalSpawnMessage, type ToolCallEvent, type ToolResultEvent, UNOTEST_DIR, UnplaceableRunIdError, UnsupportedRunKindError, type VariableCategory, type VariableKind, type VariablePlanError, type WheelChangedEvent, type WsMessage, type WsMessageKind, activeEnvName, blobPathFor, blobRelPathFor, blobRelSegmentsFor, blobsRootFor, categorizeVariable, debugDirFor, debuggerFileFor, deleteShadowWarning, envFileFor, envKeys, envLayerFilesFor, foldScenarioRun, insertEnvVar, isAnnotationLine, isExternalVarName, isFailingStatus, isRegexLiteral, isRunnerConfigKey, isTerminalRuntimeState, makeRunId, parseAppServerEvent, parseEnvContent, parseScenarioAnnotations, planAddVariable, projectRunDirFor, projectRunsRoot, regexLiteral, removeEnvVar, requireRunDirFor, runDirFor, runIdStartedAt, runsDirFor, sanitizeRunIdSegment, scenarioIndexFileName, scenarioRefFromFileName, secretsFileFor, secretsLayerFilesFor, serializeEnvValue, shardDaysDescending, shardDirFor, shardPathForRunId, shardPathForTimestamp, testDirFor, validateCollectionName, validateSegmentName };