@vgai/live 0.5.2 → 0.5.3

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.
Files changed (62) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/editor.d.ts +78 -55
  3. package/dist/editor.js +129 -76
  4. package/dist/game-client/bridge-heartbeat.d.ts +49 -0
  5. package/dist/game-client/bridge-heartbeat.js +46 -0
  6. package/dist/game-client/bridge-transport.d.ts +75 -0
  7. package/dist/game-client/bridge-transport.js +19 -0
  8. package/dist/game-client/client.d.ts +293 -0
  9. package/dist/game-client/client.js +706 -0
  10. package/dist/game-client/errors.d.ts +57 -0
  11. package/dist/game-client/errors.js +76 -0
  12. package/dist/game-client/events-matcher.d.ts +41 -0
  13. package/dist/game-client/events-matcher.js +68 -0
  14. package/dist/game-client/failure-block.d.ts +93 -0
  15. package/dist/game-client/failure-block.js +97 -0
  16. package/dist/game-client/fast-forward.d.ts +125 -0
  17. package/dist/game-client/fast-forward.js +122 -0
  18. package/dist/game-client/hidden-recovery.d.ts +85 -0
  19. package/dist/game-client/hidden-recovery.js +105 -0
  20. package/dist/game-client/index.d.ts +40 -0
  21. package/dist/game-client/index.js +26 -0
  22. package/dist/game-client/perf-sampling.d.ts +56 -0
  23. package/dist/game-client/perf-sampling.js +85 -0
  24. package/dist/game-client/relay-transport.d.ts +100 -0
  25. package/dist/game-client/relay-transport.js +237 -0
  26. package/dist/game-client/screenshot-target.d.ts +60 -0
  27. package/dist/game-client/screenshot-target.js +68 -0
  28. package/dist/game-client/state-cap.d.ts +7 -0
  29. package/dist/game-client/state-cap.js +21 -0
  30. package/dist/game-client/types.d.ts +128 -0
  31. package/dist/game-client/types.js +15 -0
  32. package/dist/game-client/wait-for.d.ts +155 -0
  33. package/dist/game-client/wait-for.js +229 -0
  34. package/dist/game.d.ts +47 -18
  35. package/dist/game.js +59 -16
  36. package/dist/index.d.ts +46 -21
  37. package/dist/index.js +51 -20
  38. package/dist/session.d.ts +4 -4
  39. package/dist/session.js +7 -7
  40. package/dist/tools.d.ts +12 -3
  41. package/dist/tools.js +15 -6
  42. package/package.json +10 -5
  43. package/src/editor.ts +142 -96
  44. package/src/game-client/bridge-heartbeat.ts +61 -0
  45. package/src/game-client/bridge-transport.ts +73 -0
  46. package/src/game-client/client.ts +836 -0
  47. package/src/game-client/errors.ts +96 -0
  48. package/src/game-client/events-matcher.ts +106 -0
  49. package/src/game-client/failure-block.ts +199 -0
  50. package/src/game-client/fast-forward.ts +175 -0
  51. package/src/game-client/hidden-recovery.ts +149 -0
  52. package/src/game-client/index.ts +98 -0
  53. package/src/game-client/perf-sampling.ts +94 -0
  54. package/src/game-client/relay-transport.ts +311 -0
  55. package/src/game-client/screenshot-target.ts +91 -0
  56. package/src/game-client/state-cap.ts +29 -0
  57. package/src/game-client/types.ts +137 -0
  58. package/src/game-client/wait-for.ts +327 -0
  59. package/src/game.ts +96 -16
  60. package/src/index.ts +68 -31
  61. package/src/session.ts +8 -10
  62. package/src/tools.ts +19 -6
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The transport seam `GameClient` (`client.ts`) drives (#140). Two
3
+ * implementations answer the identical contract below:
4
+ * - `PageTransport` (`client.ts`) — Playwright's `page.evaluate` against
5
+ * `window.__vgai`, for a standalone game page the caller drives itself.
6
+ * - `RelayTransport` (`relay-transport.ts`) — the editor dev-server's
7
+ * session wire (`POST /__editor/command`, `bridge-call`/
8
+ * `bridge-screenshot` ops), driving the SAME live session a human already
9
+ * has open, with no new browser/window/vite instance.
10
+ *
11
+ * `wait-for.ts`/`events-matcher.ts`/`failure-block.ts`, and every method
12
+ * body on `GameClient` itself, are written against this interface only —
13
+ * none of them may know or care which transport is underneath (load-bearing
14
+ * for a planned default flip to relay-when-a-live-session-exists, and for a
15
+ * future one-shot REPL client reusing the same relay op). Deliberately NO
16
+ * `@playwright/test` import here, nor in `relay-transport.ts` — only
17
+ * `client.ts` is allowed to touch a live `Page` (see its own module doc).
18
+ */
19
+ /** Result of one generic bridge-method call — thrown page/relay-side errors
20
+ * never cross either transport boundary AS themselves (Node only keeps
21
+ * `.message` across `page.evaluate`; HTTP/JSON strips everything but what
22
+ * the server explicitly serializes), so both transports catch and report
23
+ * `code`/`data` explicitly, and `GameClient.unwrap` reconstructs a
24
+ * `SessionError` from that. */
25
+ export interface BridgeCallOutcome {
26
+ ok: boolean;
27
+ result?: unknown;
28
+ error?: {
29
+ code: string | undefined;
30
+ message: string;
31
+ data?: unknown;
32
+ };
33
+ }
34
+ export interface BridgeTransport {
35
+ /** Sync-style bridge call (state/stateAll/providers/commands/events/
36
+ * snapshot/runTicks/input.*) — "sync-style" describes the PAGE side's own
37
+ * call, not this method, which is always async across either wire. */
38
+ call(method: string, callArgs: unknown[]): Promise<BridgeCallOutcome>;
39
+ /** Async bridge call — `invoke` (debug commands may themselves be async). */
40
+ callAsync(method: string, callArgs: unknown[]): Promise<BridgeCallOutcome>;
41
+ /** Whether the surface showing the game is currently hidden from the user
42
+ * (`document.hidden` on the page transport) — feeds `HiddenRecoveryDriver`. */
43
+ isHidden(): Promise<boolean>;
44
+ /** Bring the surface showing the game to the foreground. */
45
+ bringToFront(): Promise<void>;
46
+ /** Capture a screenshot to `path` (PNG). A transport that cannot support
47
+ * this should reject with a descriptive error rather than write a blank/
48
+ * corrupt file. */
49
+ screenshot(path: string): Promise<void>;
50
+ /**
51
+ * Wave-2 "one dialect, full capability" — runs a UI-automation step
52
+ * written as a literal `async (page) => {...}` (`GameClient.page()`,
53
+ * `client.ts`). `src` is `step.toString()`; `step` is the ORIGINAL
54
+ * function, wrapped so its own parameter type is erased to `unknown` (only
55
+ * `client.ts` — the one file allowed to touch a real `Page` — ever names
56
+ * the `Page` type itself).
57
+ *
58
+ * The two implementations differ ON PURPOSE, and that difference is the
59
+ * load-bearing honesty boundary this method exists to name:
60
+ * - `PageTransport` (`client.ts`) calls `step` DIRECTLY against the real
61
+ * Playwright `Page` — no serialization, so closures over outer Node
62
+ * values work here exactly like any ordinary `page.evaluate` callback.
63
+ * - `RelayTransport` (`relay-transport.ts`) ships `src` over the wire and
64
+ * reconstructs it with `new Function` INSIDE the editor page, against
65
+ * an in-page shim (`packages/editor/src/playwright-shim.ts`) — closure
66
+ * capture over anything outside the step's own body does NOT survive
67
+ * that trip (the same limitation class as Playwright's own `evaluate`
68
+ * serialization).
69
+ *
70
+ * Because the two transports differ this way, specs meant to pass
71
+ * unmodified under both hosts must be written as though ALWAYS
72
+ * serialized — inline every value the step needs.
73
+ */
74
+ runPageScript(src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
75
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The transport seam `GameClient` (`client.ts`) drives (#140). Two
3
+ * implementations answer the identical contract below:
4
+ * - `PageTransport` (`client.ts`) — Playwright's `page.evaluate` against
5
+ * `window.__vgai`, for a standalone game page the caller drives itself.
6
+ * - `RelayTransport` (`relay-transport.ts`) — the editor dev-server's
7
+ * session wire (`POST /__editor/command`, `bridge-call`/
8
+ * `bridge-screenshot` ops), driving the SAME live session a human already
9
+ * has open, with no new browser/window/vite instance.
10
+ *
11
+ * `wait-for.ts`/`events-matcher.ts`/`failure-block.ts`, and every method
12
+ * body on `GameClient` itself, are written against this interface only —
13
+ * none of them may know or care which transport is underneath (load-bearing
14
+ * for a planned default flip to relay-when-a-live-session-exists, and for a
15
+ * future one-shot REPL client reusing the same relay op). Deliberately NO
16
+ * `@playwright/test` import here, nor in `relay-transport.ts` — only
17
+ * `client.ts` is allowed to touch a live `Page` (see its own module doc).
18
+ */
19
+ export {};
@@ -0,0 +1,293 @@
1
+ /**
2
+ * The Playwright-page-bound wiring for `game`'s client methods (D17: specs
3
+ * drive `window.__vgai` via `page.evaluate` — the standalone page has no HTTP
4
+ * channel). This module is deliberately the ONLY place that touches `Page`;
5
+ * the budget math (`wait-for.ts`), the events matcher (`events-matcher.ts`),
6
+ * and the failure-block assembler (`failure-block.ts`) are plain modules
7
+ * that never import Playwright, per Task 3.2's architecture requirement.
8
+ */
9
+ import type { Page } from '@playwright/test';
10
+ import type { BridgeCallOutcome, BridgeTransport } from './bridge-transport.js';
11
+ import { type FastForwardBudget, type FastForwardOptions } from './fast-forward.js';
12
+ import { type TpsStats } from './perf-sampling.js';
13
+ import type { DebugCommandInfo, DebugSnapshot, ProviderInfo, VirtualActionValue } from './types.js';
14
+ import { type WaitForBudget } from './wait-for.js';
15
+ /**
16
+ * #140 — the `BridgeTransport` `page.evaluate` implementation. This is the
17
+ * ONE place a `Page` is ever touched to drive `window.__vgai` (module doc
18
+ * above); `RelayTransport` (`relay-transport.ts`) is the sibling
19
+ * implementation for the live editor session, and imports no Playwright at
20
+ * all. Exported so `fixture.ts` (which already imports `@playwright/test`
21
+ * for its own `page.goto`/error-listener setup) can construct one.
22
+ */
23
+ export declare class PageTransport implements BridgeTransport {
24
+ private readonly page;
25
+ constructor(page: Page);
26
+ call(method: string, callArgs: unknown[]): Promise<BridgeCallOutcome>;
27
+ callAsync(method: string, callArgs: unknown[]): Promise<BridgeCallOutcome>;
28
+ isHidden(): Promise<boolean>;
29
+ bringToFront(): Promise<void>;
30
+ screenshot(path: string): Promise<void>;
31
+ /** Wave-2: the ONE transport that runs a `game.page()` step against a REAL
32
+ * Playwright `Page` — no serialization, so `step`'s own closures work
33
+ * here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
34
+ * full honesty-boundary contract; `src` is unused on this leg, kept only
35
+ * to satisfy the shared interface). */
36
+ runPageScript(_src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
37
+ }
38
+ export interface GameClientOptions {
39
+ /** The bridge transport — `new PageTransport(page)` for a standalone page
40
+ * (`fixture.ts`), `new RelayTransport({ port })` for `--in-editor`
41
+ * (`relay-fixture.ts`). See `bridge-transport.ts`'s module doc: every
42
+ * method below reaches `window.__vgai` (or its relay-side equivalent)
43
+ * ONLY through this seam. */
44
+ transport: BridgeTransport;
45
+ /** Populated by the fixture's `pageerror` listener, installed before goto. */
46
+ pageErrors: string[];
47
+ /** Populated by the fixture's `console` listener (type `'error'`), same timing. */
48
+ consoleErrors: string[];
49
+ /** First snapshot's `time.tick` — used only for `events.expect`'s
50
+ * `withinTicks` timing math (`events-matcher.ts`) and the D5 failure-block
51
+ * deltas below; identity fencing itself is `fenceSeq`'s job (run-4
52
+ * friction #5 — see that field's doc comment). */
53
+ fenceTick: number;
54
+ /** Run-4 friction #5: the fence snapshot's own event ring's last `seq`
55
+ * (`0` if the ring was empty at fence time — nothing emitted yet, so
56
+ * "since seq 0" is correct). `events.expect` fences on THIS, not
57
+ * `fenceTick` — the removed `tick > sinceTick` filter silently dropped any event that
58
+ * shares the fence's own tick (e.g. one emitted from a debug-command
59
+ * handler, which runs between ticks), which is exactly the bug this field
60
+ * closes. See `debug-registry.ts`'s `TickStampedEvent.seq` doc comment
61
+ * for the full mechanism. */
62
+ fenceSeq: number;
63
+ /** D5: first snapshot's `time.simSeconds` — `events.expect`'s failure block
64
+ * computes REAL elapsed sim-time as a delta off this, instead of the
65
+ * absolute (since-boot) `simSeconds` the pre-fix code passed. */
66
+ fenceSimSeconds: number;
67
+ /** D5: `Date.now()` at the same moment as the fence snapshot — the wall
68
+ * side of that same delta, instead of the pre-fix code's hardcoded `0`
69
+ * (which fabricated a "sim speed 0.00x — loop stalled" reading on every
70
+ * single `events.expect` failure, regardless of how the game was
71
+ * actually running). */
72
+ fenceWallMs: number;
73
+ artifactsDir?: string | undefined;
74
+ /** Set by the caller when it reused an already-running game server rather
75
+ * than booting a fresh one for this run. Threaded through so `unwrap` can
76
+ * append the warm-session staleness hint to a
77
+ * `DEBUG_COMMAND_NOT_REGISTERED` failure (see `errors.ts`'s
78
+ * `appendWarmSessionHint`). Defaults to `false` so every caller that never
79
+ * sets it is unaffected. */
80
+ warmSession?: boolean;
81
+ /** `testInfo.title` — names the fixture heartbeat lines `waitFor`/
82
+ * `waitSimTime` print during a long poll (see `wait-for.ts`'s module doc).
83
+ * Defaults to `'test'` so a caller that never sets it (existing direct
84
+ * `GameClient` construction in older tests) still gets a valid, if
85
+ * generic, heartbeat line rather than `undefined` in the output. */
86
+ testTitle?: string;
87
+ }
88
+ export declare class GameInput {
89
+ private readonly client;
90
+ constructor(client: GameClient);
91
+ /**
92
+ * Holds one action, or SIMULTANEOUSLY holds several — `hold('jump')` and
93
+ * `hold(['move_right', 'move_forward'])` (the natural way to drive a
94
+ * diagonal) are both first-class. `action` is deliberately typed as
95
+ * `string | string[]`, never just `string`: a bare JS array silently
96
+ * stringifies to a comma-joined name (`['move_right','move_forward']` →
97
+ * `"move_right,move_forward"`) wherever it crosses a template literal or
98
+ * the bridge's JSON boundary, which used to surface as a baffling
99
+ * `unknown action "move_right,move_forward"` — the array was accepted
100
+ * structurally (nothing rejected it) and then silently mangled instead.
101
+ * Accepting the array for real, instead of merely rejecting it, is the
102
+ * useful behavior: every 2D/3D mover needs "hold two directions at once"
103
+ * as its ordinary case, not an edge case.
104
+ *
105
+ * Single action: one `holdFor` bridge call (`runtime/debug-bridge.ts`)
106
+ * instead of the old set → `waitSimTime`'s 150ms-interval snapshot poll
107
+ * loop → clear (15+ transport round trips over the editor relay for a
108
+ * multi-second hold). The wait uses ordinary host-loop ticks while visible.
109
+ * If the browser has hidden-paused that loop, the bridge drives the held
110
+ * action through the same game phases with deterministic ticks; this
111
+ * collapses transport cost without letting a background tab deadlock it.
112
+ *
113
+ * A relay call has a bounded async-invoke timeout budget (see
114
+ * `relay-transport.ts`'s `INVOKE_TIMEOUT_MS`) — a `simSeconds` long enough
115
+ * to exceed it at worst-case sim speed is NOT silently truncated; the
116
+ * transport timeout is caught and rethrown with a hint to split the hold
117
+ * into multiple shorter `hold()` calls instead.
118
+ *
119
+ * Multiple actions: `holdFor` is a single-action bridge primitive, so this
120
+ * presses every action (`setVirtualAction(action, true)`, in order),
121
+ * waits the shared `simSeconds` once via `waitSimTime`, and releases every
122
+ * action it managed to press — in a `finally`, so a gated press (a
123
+ * `delivered: false` result throws `INPUT_GATED` immediately) or a stalled
124
+ * clock during the wait still leaves no action stuck held.
125
+ */
126
+ hold(action: string | string[], budget: {
127
+ simSeconds: number;
128
+ }): Promise<void>;
129
+ /**
130
+ * Keep one or more honest game actions pressed while `observe` runs, then
131
+ * release every action in a `finally` block. This is the evidence-oriented
132
+ * sibling of {@link hold}: `hold()` resolves after release, which is ideal
133
+ * for asserting distance travelled but cannot capture a moving pose or read
134
+ * transient "currently sprinting" state. `whileHeld()` makes that interval
135
+ * explicit without asking tests to hand-roll unsafe set/clear cleanup.
136
+ *
137
+ * The callback uses the same `GameClient` instance already in the test, so
138
+ * it may call `game.waitFor`, `game.state`, `game.screenshot`, or any other
139
+ * ordinary observation. No state is fabricated and the actions still enter
140
+ * through `input.setVirtualAction`/the project's real input map.
141
+ */
142
+ whileHeld<T>(action: string | string[], observe: () => T | Promise<T>): Promise<T>;
143
+ /**
144
+ * One honest one-shot press — pressed, carried through AT LEAST ONE FULL
145
+ * FIXED TICK, then released, as a single bridge call.
146
+ *
147
+ * The contract is deliberately "a tick services it", not "the bridge
148
+ * accepted it", because the raw primitive underneath (`InputManager.
149
+ * tapVirtualAction`) only QUEUES: `poll()` promotes the queue to that tick's
150
+ * just-pressed set, and nothing else ever does. On a visible tab a tick
151
+ * lands ~16ms later so the difference is invisible; on a HIDDEN tab the host
152
+ * loop is stopped outright, so a queue-only tap sat unserviced and then died
153
+ * — silently, having already reported `delivered: true` (measured:
154
+ * `input.trace: { ticks: [] }`, no event, while `whileHeld` on the same
155
+ * action worked every time). Routing through the `holdFor` primitive is what
156
+ * makes the press TICK-ALIGNED by construction: that call owns the wait, and
157
+ * its hidden-tab leg drives the tick deterministically
158
+ * (`runtime/debug-bridge.ts`'s `waitForHoldBudget`), so a tap can no longer
159
+ * fall between ticks on any transport or any tab state.
160
+ *
161
+ * A gated action still fails loudly and immediately with `INPUT_GATED`,
162
+ * exactly as {@link hold} does — same `delivered:false` contract, same
163
+ * error.
164
+ */
165
+ tap(action: string): Promise<void>;
166
+ /**
167
+ * Sets one action, or the SAME value on several at once — same
168
+ * `string | string[]` shape as {@link hold}, and for the same reason: a
169
+ * bare array used to silently stringify into a single bogus action name
170
+ * instead of being rejected or honored.
171
+ */
172
+ set(action: string | string[], value: VirtualActionValue): Promise<void>;
173
+ }
174
+ export declare class GameEvents {
175
+ private readonly client;
176
+ constructor(client: GameClient);
177
+ expect(names: string[], opts?: {
178
+ exact?: boolean;
179
+ withinTicks?: number;
180
+ }): Promise<void>;
181
+ }
182
+ /**
183
+ * The `game` fixture value. Every method reaches `window.__vgai` (or its
184
+ * relay-side equivalent) through `this.#transport` — see `bridge-transport.ts`'s
185
+ * module doc for the seam, and this file's own module doc for why it, alone,
186
+ * is allowed to import Playwright (via `PageTransport`, above).
187
+ */
188
+ export declare class GameClient {
189
+ #private;
190
+ readonly input: GameInput;
191
+ readonly events: GameEvents;
192
+ readonly fenceTick: number;
193
+ /** Run-4 friction #5: see `GameClientOptions.fenceSeq`. */
194
+ readonly fenceSeq: number;
195
+ /** D5: see `GameClientOptions.fenceSimSeconds`. */
196
+ readonly fenceSimSeconds: number;
197
+ /** D5: see `GameClientOptions.fenceWallMs`. */
198
+ readonly fenceWallMs: number;
199
+ readonly pageErrors: string[];
200
+ readonly consoleErrors: string[];
201
+ /** M13: hidden-tab recovery's own structured notices — kept SEPARATE from
202
+ * `consoleErrors` (see `hidden-recovery.ts`'s hook below and
203
+ * `failure-block.ts`'s `recoveryNotices` member). */
204
+ readonly recoveryNotices: string[];
205
+ /** Where a labelled `screenshot()` lands — project-scoped by `@vgai/live`, cwd-relative otherwise. Public: a caller reading it is asking a fair question, and `screenshot()` returns a path under it anyway. */
206
+ readonly artifactsDir: string;
207
+ constructor(opts: GameClientOptions);
208
+ state(name: string): Promise<unknown>;
209
+ command(name: string, ...args: unknown[]): Promise<unknown>;
210
+ providers(): Promise<ProviderInfo[]>;
211
+ commands(): Promise<DebugCommandInfo[]>;
212
+ snapshot(sinceSeq?: number): Promise<DebugSnapshot>;
213
+ /** Fixture-teardown perf read: p50/p95 effective ticks-per-second over all
214
+ * the snapshot polls this test performed. */
215
+ tpsStats(): TpsStats;
216
+ /** True if the hidden-tab recovery fired (page.bringToFront was called). */
217
+ hiddenRecoveryTriggered(): boolean;
218
+ /**
219
+ * D15/T-D15.4 — synchronously drives `budget` worth of sim time/ticks via
220
+ * the live `Game`'s `runTicks` (through the debug bridge), instead of
221
+ * waiting for real wall-clock time to pass. Doctrine (see
222
+ * `fast-forward.ts`'s module doc, which also documents the two honesty
223
+ * decisions this wraps): SETUP/STAGING traversal — reaching a known
224
+ * late-game state fast — not a substitute for real-input proofs, which
225
+ * still run in real ticks. Returns the final `{time, state, events,
226
+ * pageErrors}` snapshot (a completely ordinary `snapshot()` read, taken
227
+ * AFTER the tps baseline reset below, so it never itself corrupts the tps
228
+ * stats either).
229
+ */
230
+ fastForward(budget: FastForwardBudget, opts?: FastForwardOptions): Promise<DebugSnapshot>;
231
+ waitFor(pred: (s: (name: string) => unknown) => boolean, budget: WaitForBudget): Promise<void>;
232
+ /** Used by `input.hold` — waits for a sim-time delta to pass with no
233
+ * predicate (a predicate that is never true would misreport as a
234
+ * waitFor failure, so this is its own tiny loop, not `runWaitFor` with a
235
+ * false predicate).
236
+ *
237
+ * D2: this loop has the SAME frozen-clock bug `runWaitFor` had — with no
238
+ * predicate at all, a stalled sim clock would poll forever, since
239
+ * `current.time.simSeconds` would never advance. Carries the identical
240
+ * stall guard (`WAIT_FOR_STALL_POLL_LIMIT` consecutive polls with an
241
+ * unchanged tick — see wait-for.ts's doc comment for the rationale),
242
+ * throwing the same `WaitForTimeoutError` shape so it renders through the
243
+ * same honest failure block as an ordinary `waitFor`/`waitSimTime` budget
244
+ * exhaustion. */
245
+ waitSimTime(budget: {
246
+ simSeconds: number;
247
+ }): Promise<void>;
248
+ /**
249
+ * Capture the running game to disk and return the ABSOLUTE path written.
250
+ *
251
+ * `labelOrPath` is read as a LABEL when it is a bare identifier
252
+ * (`'waitfor-timeout'`) — numbered into the artifacts directory, the
253
+ * long-standing spec behaviour — and as a DESTINATION PATH the moment it
254
+ * carries a separator or a file extension, in which case the bytes land
255
+ * exactly there (relative to `process.cwd()`). See
256
+ * `screenshot-target.ts`'s header for the live incident that made this
257
+ * distinction mandatory: a caller naming a path used to get "success" and
258
+ * an empty path.
259
+ */
260
+ screenshot(labelOrPath: string): Promise<string>;
261
+ /**
262
+ * Wave-2 "one dialect, full capability" — runs a UI-automation step
263
+ * written as a literal Playwright `async (page) => {...}` (interface
264
+ * doctrine §3.2/§4 rung 4: "the AI should think it is basically just
265
+ * executing Playwright"). Under `PageTransport` this drives the REAL
266
+ * `Page` (real closures, real hit-testing); under `RelayTransport` it
267
+ * posts the step's own `toString()` source to the editor dev server's
268
+ * `page-script` op, which reconstructs it in-page against
269
+ * `playwright-shim.ts`'s in-page shim (`isTrusted: false` synthetic
270
+ * DOM events, no real hit testing). This is the honest input path for a
271
+ * React-only DOM game; canvas gameplay continues to use `game.input.*`.
272
+ * Write specs as if ALWAYS serialized — inline every value the step needs,
273
+ * never close over imported `expect`/helpers/outer variables, and return
274
+ * observations to assert outside the callback. See `bridge-transport.ts`'s
275
+ * `runPageScript` doc comment for the full contract this method wraps.
276
+ */
277
+ page<T = unknown>(step: (page: Page) => T | Promise<T>): Promise<T>;
278
+ private toSessionFailure;
279
+ /** run-3 friction #2 — called at the top of every bridge dispatch, before
280
+ * the await, so a burst of short calls (e.g. many `hold()`s) keeps
281
+ * printing throttled liveness even while individually near-silent. See
282
+ * bridge-heartbeat.ts's module doc for why this needs no sim-tick
283
+ * awareness the way `wait-for.ts`'s poll-loop heartbeat does. */
284
+ private emitBridgeHeartbeatIfDue;
285
+ /** @internal exposed for GameInput/GameEvents in this module only. */
286
+ callBridge<T>(method: string, ...callArgs: unknown[]): Promise<T>;
287
+ /** @internal like `callBridge`, but awaits the page-side call (for async
288
+ * bridge methods like `invoke`). */
289
+ callBridgeAsync<T>(method: string, ...callArgs: unknown[]): Promise<T>;
290
+ /** @internal void-returning convenience (clearVirtualActions). */
291
+ callBridgeVoid(method: string, ...callArgs: unknown[]): Promise<void>;
292
+ private unwrap;
293
+ }