@vgai/live 0.5.22 → 0.5.24
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/.tsbuildinfo +1 -1
- package/dist/{editor.d.ts → vgai-live/src/editor.d.ts} +5 -8
- package/dist/{editor.js → vgai-live/src/editor.js} +5 -15
- package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.d.ts +10 -1
- package/dist/{game-client → vgai-live/src/game-client}/client.d.ts +36 -6
- package/dist/{game-client → vgai-live/src/game-client}/client.js +80 -9
- package/dist/{game-client → vgai-live/src/game-client}/relay-transport.d.ts +15 -2
- package/dist/{game-client → vgai-live/src/game-client}/relay-transport.js +38 -3
- package/dist/{game-client → vgai-live/src/game-client}/types.d.ts +5 -5
- package/dist/{game-client → vgai-live/src/game-client}/wait-for.js +2 -2
- package/dist/{game.d.ts → vgai-live/src/game.d.ts} +1 -1
- package/dist/{index.d.ts → vgai-live/src/index.d.ts} +2 -2
- package/dist/{index.js → vgai-live/src/index.js} +2 -2
- package/dist/{recording.js → vgai-live/src/recording.js} +3 -10
- package/dist/{session.d.ts → vgai-live/src/session.d.ts} +0 -7
- package/dist/{session.js → vgai-live/src/session.js} +56 -19
- package/dist/{tools.d.ts → vgai-live/src/tools.d.ts} +1 -1
- package/dist/{tools.js → vgai-live/src/tools.js} +1 -1
- package/dist/vgai-sdk/src/editor/session-registry-format.d.ts +81 -0
- package/dist/vgai-sdk/src/editor/session-registry-format.js +158 -0
- package/package.json +3 -3
- package/src/editor.ts +5 -16
- package/src/game-client/bridge-transport.ts +14 -1
- package/src/game-client/client.ts +98 -9
- package/src/game-client/relay-transport.ts +46 -3
- package/src/game-client/types.ts +5 -5
- package/src/game-client/wait-for.ts +2 -2
- package/src/game.ts +1 -1
- package/src/index.ts +2 -2
- package/src/recording.ts +3 -10
- package/src/session.ts +67 -24
- package/src/tools.ts +1 -1
- /package/dist/{editor-document.d.ts → vgai-live/src/editor-document.d.ts} +0 -0
- /package/dist/{editor-document.js → vgai-live/src/editor-document.js} +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/errors.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/errors.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/failure-block.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/failure-block.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/index.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/index.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/state-cap.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/state-cap.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/types.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/wait-for.d.ts +0 -0
- /package/dist/{game.js → vgai-live/src/game.js} +0 -0
- /package/dist/{lazy-proxy.d.ts → vgai-live/src/lazy-proxy.d.ts} +0 -0
- /package/dist/{lazy-proxy.js → vgai-live/src/lazy-proxy.js} +0 -0
- /package/dist/{recording.d.ts → vgai-live/src/recording.d.ts} +0 -0
- /package/dist/{singleton.d.ts → vgai-live/src/singleton.d.ts} +0 -0
- /package/dist/{singleton.js → vgai-live/src/singleton.js} +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
|
|
3
|
-
* page }
|
|
3
|
+
* page }`. Methods are named after the
|
|
4
4
|
* ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
|
|
5
5
|
* separate `selectAll`, `showPanel('viewport-play')` rather than
|
|
6
6
|
* `vgai show viewport play`'s two-token shape) — see each method's own doc
|
|
7
7
|
* comment for the exact CLI verb / `EditorClient` call it mirrors.
|
|
8
8
|
*
|
|
9
9
|
* Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
|
|
10
|
-
* hand-rolls a `fetch` to `/__editor/command` itself
|
|
11
|
-
*
|
|
10
|
+
* hand-rolls a `fetch` to `/__editor/command` itself, and has no FILE-mode
|
|
11
|
+
* escape hatch: every method goes over the wire.
|
|
12
12
|
*/
|
|
13
13
|
import type { ActiveDocumentCapture, AnimationCaptureAction, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, HistoryStep, InspectedFieldWrite, InspectedHierarchy, InspectedInspection, OpenedDocument, PresentedEditorView, RagdollGenerationResult, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
|
|
14
14
|
import { LiveEditorDocument } from './editor-document.js';
|
|
@@ -40,11 +40,8 @@ export declare class LiveEditor {
|
|
|
40
40
|
constructor(client: EditorClient);
|
|
41
41
|
/**
|
|
42
42
|
* The active authoring adapter's persistence destination — where a save would
|
|
43
|
-
* land (`status().savePath`).
|
|
44
|
-
*
|
|
45
|
-
* WO-8: this also took a `path` and OPENED that scene through
|
|
46
|
-
* `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
|
|
47
|
-
* (the `.vscn.json` format is deleted), so only the read remains.
|
|
43
|
+
* land (`status().savePath`). A read only: a three root has no scene document
|
|
44
|
+
* to open, and its root is activated instead.
|
|
48
45
|
*/
|
|
49
46
|
scene(): Promise<string | null>;
|
|
50
47
|
/**
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
|
|
3
|
-
* page }
|
|
3
|
+
* page }`. Methods are named after the
|
|
4
4
|
* ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
|
|
5
5
|
* separate `selectAll`, `showPanel('viewport-play')` rather than
|
|
6
6
|
* `vgai show viewport play`'s two-token shape) — see each method's own doc
|
|
7
7
|
* comment for the exact CLI verb / `EditorClient` call it mirrors.
|
|
8
8
|
*
|
|
9
9
|
* Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
|
|
10
|
-
* hand-rolls a `fetch` to `/__editor/command` itself
|
|
11
|
-
*
|
|
10
|
+
* hand-rolls a `fetch` to `/__editor/command` itself, and has no FILE-mode
|
|
11
|
+
* escape hatch: every method goes over the wire.
|
|
12
12
|
*/
|
|
13
13
|
import { LiveEditorDocument } from './editor-document.js';
|
|
14
14
|
import { LiveGameplayRecording } from './recording.js';
|
|
@@ -72,11 +72,8 @@ export class LiveEditor {
|
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* The active authoring adapter's persistence destination — where a save would
|
|
75
|
-
* land (`status().savePath`).
|
|
76
|
-
*
|
|
77
|
-
* WO-8: this also took a `path` and OPENED that scene through
|
|
78
|
-
* `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
|
|
79
|
-
* (the `.vscn.json` format is deleted), so only the read remains.
|
|
75
|
+
* land (`status().savePath`). A read only: a three root has no scene document
|
|
76
|
+
* to open, and its root is activated instead.
|
|
80
77
|
*/
|
|
81
78
|
async scene() {
|
|
82
79
|
const state = await this.#client.getState();
|
|
@@ -98,13 +95,6 @@ export class LiveEditor {
|
|
|
98
95
|
async captureActiveDocument(size) {
|
|
99
96
|
return this.#client.captureActiveDocument(size);
|
|
100
97
|
}
|
|
101
|
-
/*
|
|
102
|
-
* `applyDiff` lived here. It dispatched `project.scene.apply`, one of the
|
|
103
|
-
* twelve `.vscn` document tools deleted with the format, so there is nothing
|
|
104
|
-
* left for it to call. `ApplyDiffInput` / `ApplyDiffResult` went with it, and
|
|
105
|
-
* so did `projectRoot` — this was the ONE method that used it (every other
|
|
106
|
-
* method goes over the wire).
|
|
107
|
-
*/
|
|
108
98
|
async play(opts) {
|
|
109
99
|
await this.#client.play(opts);
|
|
110
100
|
}
|
|
@@ -52,7 +52,7 @@ export interface BridgeTransport {
|
|
|
52
52
|
* blank/corrupt file. */
|
|
53
53
|
screenshot(path: string): Promise<CaptureNotes>;
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* One dialect, full capability — runs a UI-automation step
|
|
56
56
|
* written as a literal `async (page) => {...}` (`GameClient.page()`,
|
|
57
57
|
* `client.ts`). `src` is `step.toString()`; `step` is the ORIGINAL
|
|
58
58
|
* function, wrapped so its own parameter type is erased to `unknown` (only
|
|
@@ -76,6 +76,15 @@ export interface BridgeTransport {
|
|
|
76
76
|
* serialized — inline every value the step needs.
|
|
77
77
|
*/
|
|
78
78
|
runPageScript(src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
|
|
79
|
+
/**
|
|
80
|
+
* THE MODULE LANE — run a step INSIDE the editor page against
|
|
81
|
+
* `{ page, modules, instanceId }`, where `modules(path)` imports the
|
|
82
|
+
* RUNNING mount's own instance of a project module (never a phantom
|
|
83
|
+
* second copy). Same serialization contract as `runPageScript`: the
|
|
84
|
+
* step's source travels as text, closures do not survive, and the return
|
|
85
|
+
* value must be plain data. `instance` scopes multi-instance sessions.
|
|
86
|
+
*/
|
|
87
|
+
runGameScript(src: string, step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
|
|
79
88
|
/**
|
|
80
89
|
* P20 — reload the document showing the game, resolving only once the page
|
|
81
90
|
* is BACK and taking commands again.
|
|
@@ -33,12 +33,19 @@ export declare class PageTransport implements BridgeTransport {
|
|
|
33
33
|
* (`capture-notes.ts`) exists on this leg. `{}` says that honestly rather
|
|
34
34
|
* than inventing a clean bill of health. */
|
|
35
35
|
screenshot(path: string): Promise<CaptureNotes>;
|
|
36
|
-
/**
|
|
36
|
+
/** The ONE transport that runs a `game.page()` step against a REAL
|
|
37
37
|
* Playwright `Page` — no serialization, so `step`'s own closures work
|
|
38
38
|
* here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
|
|
39
39
|
* full honesty-boundary contract; `src` is unused on this leg, kept only
|
|
40
40
|
* to satisfy the shared interface). */
|
|
41
41
|
runPageScript(_src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
|
|
42
|
+
/** THE MODULE LANE under a real Playwright host: the step's source is
|
|
43
|
+
* evaluated INSIDE the editor page via the same in-page handler the relay
|
|
44
|
+
* op uses (`window.__vgaiGameEval`), because `modules()` only means
|
|
45
|
+
* anything in the page's own module space — a Node-side call could never
|
|
46
|
+
* hand back the running mount's instances. Same serialization contract
|
|
47
|
+
* as the relay leg. */
|
|
48
|
+
runGameScript(src: string, _step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
|
|
42
49
|
/** Playwright's own reload already waits for the new document's `load`
|
|
43
50
|
* event, which is exactly the completion signal this method's contract
|
|
44
51
|
* asks for — nothing to reconstruct on this leg. */
|
|
@@ -275,14 +282,12 @@ export declare class GameClient {
|
|
|
275
282
|
* `events.expect`'s failure path each shoot a frame on their own
|
|
276
283
|
* (`toSessionFailure`, `GameEvents.expect`), and a route's `whileHeld`
|
|
277
284
|
* observer may shoot one from inside a callback the driver never sees. The
|
|
278
|
-
* only place that sees ALL of them is here, which is why
|
|
279
|
-
* stamps its captures by listening rather than by intercepting
|
|
280
|
-
* (`src/tools/route-context.ts` in a scaffolded project is the shipped
|
|
281
|
-
* listener).
|
|
285
|
+
* only place that sees ALL of them is here, which is why a driving tool
|
|
286
|
+
* stamps its captures by listening rather than by intercepting.
|
|
282
287
|
*/
|
|
283
288
|
onCapture(listener: CaptureListener): () => void;
|
|
284
289
|
/**
|
|
285
|
-
*
|
|
290
|
+
* One dialect, full capability — runs a UI-automation step
|
|
286
291
|
* written as a literal Playwright `async (page) => {...}` (interface
|
|
287
292
|
* doctrine §3.2/§4 rung 4: "the AI should think it is basically just
|
|
288
293
|
* executing Playwright"). Under `PageTransport` this drives the REAL
|
|
@@ -298,6 +303,31 @@ export declare class GameClient {
|
|
|
298
303
|
* `runPageScript` doc comment for the full contract this method wraps.
|
|
299
304
|
*/
|
|
300
305
|
page<T = unknown>(step: (page: Page) => T | Promise<T>): Promise<T>;
|
|
306
|
+
/**
|
|
307
|
+
* THE MODULE LANE — run literal JS INSIDE the game's page, with the
|
|
308
|
+
* running mount's modules in reach:
|
|
309
|
+
*
|
|
310
|
+
* ```js
|
|
311
|
+
* await game.run(async ({ modules }) => {
|
|
312
|
+
* const { simHost } = await modules('src/sim/host.ts');
|
|
313
|
+
* return simHost().state.day;
|
|
314
|
+
* })
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* `scope` is `{ page, modules, instanceId }`. Serialization contract as
|
|
318
|
+
* `game.page()`: the step travels as source (no closures), and the return
|
|
319
|
+
* value must be plain data. `modules(path)` resolves through the ACTIVE
|
|
320
|
+
* mount's own url space, so what you touch IS the running game — never a
|
|
321
|
+
* phantom second copy. Dev-server sessions only; a shipped build's curated
|
|
322
|
+
* surface is its adapter exports.
|
|
323
|
+
*/
|
|
324
|
+
run<T = unknown>(step: (scope: {
|
|
325
|
+
page: Page;
|
|
326
|
+
modules: (path: string) => Promise<Record<string, unknown>>;
|
|
327
|
+
instanceId: string;
|
|
328
|
+
}) => T | Promise<T>, opts?: {
|
|
329
|
+
instance?: string;
|
|
330
|
+
}): Promise<T>;
|
|
301
331
|
/**
|
|
302
332
|
* Reload the document showing the game, resolving only once it is back and
|
|
303
333
|
* answering commands (see `bridge-transport.ts`'s `reloadPage`).
|
|
@@ -111,7 +111,7 @@ export class PageTransport {
|
|
|
111
111
|
await this.page.screenshot({ path });
|
|
112
112
|
return {};
|
|
113
113
|
}
|
|
114
|
-
/**
|
|
114
|
+
/** The ONE transport that runs a `game.page()` step against a REAL
|
|
115
115
|
* Playwright `Page` — no serialization, so `step`'s own closures work
|
|
116
116
|
* here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
|
|
117
117
|
* full honesty-boundary contract; `src` is unused on this leg, kept only
|
|
@@ -128,6 +128,30 @@ export class PageTransport {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
/** THE MODULE LANE under a real Playwright host: the step's source is
|
|
132
|
+
* evaluated INSIDE the editor page via the same in-page handler the relay
|
|
133
|
+
* op uses (`window.__vgaiGameEval`), because `modules()` only means
|
|
134
|
+
* anything in the page's own module space — a Node-side call could never
|
|
135
|
+
* hand back the running mount's instances. Same serialization contract
|
|
136
|
+
* as the relay leg. */
|
|
137
|
+
async runGameScript(src, _step, instance) {
|
|
138
|
+
try {
|
|
139
|
+
const result = await this.page.evaluate(async (args) => {
|
|
140
|
+
const hook = window['__vgaiGameEval'];
|
|
141
|
+
if (typeof hook !== 'function') {
|
|
142
|
+
throw new Error('game-eval: this page has no __vgaiGameEval hook — is the editor page loaded?');
|
|
143
|
+
}
|
|
144
|
+
return hook(args.src, args.instance);
|
|
145
|
+
}, { src, ...(instance === undefined ? {} : { instance }) });
|
|
146
|
+
return { ok: true, result };
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: { code: undefined, message: err instanceof Error ? err.message : String(err) },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
131
155
|
/** Playwright's own reload already waits for the new document's `load`
|
|
132
156
|
* event, which is exactly the completion signal this method's contract
|
|
133
157
|
* asks for — nothing to reconstruct on this leg. */
|
|
@@ -422,6 +446,9 @@ export class GameClient {
|
|
|
422
446
|
/** Per-test tick-rate samples, fed by every `snapshot()` read (a poll the
|
|
423
447
|
* client was making anyway — zero extra page.evaluate round trips). */
|
|
424
448
|
#tps = new TpsAccumulator();
|
|
449
|
+
/** True after the settled run-ticks door proved absent on this page (an older exported
|
|
450
|
+
* game's engine) — see `fastForward`'s `runTicksBatch`. */
|
|
451
|
+
#legacyRunTicksDoor = false;
|
|
425
452
|
/** Hidden-tab recovery (hollowstone field lesson: the engine hard-stops
|
|
426
453
|
* while `document.hidden`). Client-lifetime state so `bringToFront()`
|
|
427
454
|
* fires at most once per test, across ALL waitFor/waitSimTime loops. */
|
|
@@ -504,7 +531,30 @@ export class GameClient {
|
|
|
504
531
|
// names neither the method nor the shape).
|
|
505
532
|
assertValidWaitForBudget(budget, 'fastForward');
|
|
506
533
|
const clock = {
|
|
507
|
-
|
|
534
|
+
// The SETTLED door (`runTicksSettled`, an async bridge method): ticks never race a scene
|
|
535
|
+
// remount's async commit, so which tick first runs a freshly reloaded world is
|
|
536
|
+
// deterministic (see engine/runtime/run-ticks-settled.ts — measured: without it, one
|
|
537
|
+
// drive script produced 7 or 8 post-respawn walked ticks depending on wall timing).
|
|
538
|
+
// Falls back ONCE to the plain sync door for a page whose engine predates the method
|
|
539
|
+
// (an older exported game), and remembers the verdict for the rest of the burst.
|
|
540
|
+
runTicksBatch: async (n, render) => {
|
|
541
|
+
if (this.#legacyRunTicksDoor) {
|
|
542
|
+
await this.callBridgeVoid('runTicks', n, { render });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
await this.callBridgeAsync('runTicksSettled', n, { render });
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
const code = error.code;
|
|
550
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
551
|
+
const doorAbsent = code === 'UNKNOWN_BRIDGE_METHOD' || /is not a function|undefined/i.test(message);
|
|
552
|
+
if (!doorAbsent)
|
|
553
|
+
throw error;
|
|
554
|
+
this.#legacyRunTicksDoor = true;
|
|
555
|
+
await this.callBridgeVoid('runTicks', n, { render });
|
|
556
|
+
}
|
|
557
|
+
},
|
|
508
558
|
readTime: async () => {
|
|
509
559
|
// Raw bridge read — deliberately NOT `this.snapshot()`, which would
|
|
510
560
|
// feed the TpsAccumulator (see fast-forward.ts's module doc, point 2).
|
|
@@ -529,7 +579,7 @@ export class GameClient {
|
|
|
529
579
|
snapshot: () => this.snapshot(),
|
|
530
580
|
now: () => Date.now(),
|
|
531
581
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
532
|
-
// Fixture heartbeat
|
|
582
|
+
// Fixture heartbeat — real stdout, the
|
|
533
583
|
// same channel a caller watching stdout already treats as
|
|
534
584
|
// liveness (see wait-for.ts's module doc for the emission
|
|
535
585
|
// invariant `maybeHeartbeat` enforces).
|
|
@@ -565,7 +615,7 @@ export class GameClient {
|
|
|
565
615
|
const start = await this.snapshot();
|
|
566
616
|
let lastTick = null;
|
|
567
617
|
let stalledPolls = 0;
|
|
568
|
-
// Fixture heartbeat
|
|
618
|
+
// Fixture heartbeat — same invariant as
|
|
569
619
|
// `wait-for.ts`'s `runWaitFor`: a heartbeat requires BOTH 60s of wall
|
|
570
620
|
// silence AND the tick having advanced since the last one emitted, so a
|
|
571
621
|
// genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
|
|
@@ -646,10 +696,8 @@ export class GameClient {
|
|
|
646
696
|
* `events.expect`'s failure path each shoot a frame on their own
|
|
647
697
|
* (`toSessionFailure`, `GameEvents.expect`), and a route's `whileHeld`
|
|
648
698
|
* observer may shoot one from inside a callback the driver never sees. The
|
|
649
|
-
* only place that sees ALL of them is here, which is why
|
|
650
|
-
* stamps its captures by listening rather than by intercepting
|
|
651
|
-
* (`src/tools/route-context.ts` in a scaffolded project is the shipped
|
|
652
|
-
* listener).
|
|
699
|
+
* only place that sees ALL of them is here, which is why a driving tool
|
|
700
|
+
* stamps its captures by listening rather than by intercepting.
|
|
653
701
|
*/
|
|
654
702
|
onCapture(listener) {
|
|
655
703
|
this.#captureListeners.add(listener);
|
|
@@ -658,7 +706,7 @@ export class GameClient {
|
|
|
658
706
|
};
|
|
659
707
|
}
|
|
660
708
|
/**
|
|
661
|
-
*
|
|
709
|
+
* One dialect, full capability — runs a UI-automation step
|
|
662
710
|
* written as a literal Playwright `async (page) => {...}` (interface
|
|
663
711
|
* doctrine §3.2/§4 rung 4: "the AI should think it is basically just
|
|
664
712
|
* executing Playwright"). Under `PageTransport` this drives the REAL
|
|
@@ -678,6 +726,29 @@ export class GameClient {
|
|
|
678
726
|
const outcome = await this.#transport.runPageScript(step.toString(), erased);
|
|
679
727
|
return this.unwrap(outcome);
|
|
680
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* THE MODULE LANE — run literal JS INSIDE the game's page, with the
|
|
731
|
+
* running mount's modules in reach:
|
|
732
|
+
*
|
|
733
|
+
* ```js
|
|
734
|
+
* await game.run(async ({ modules }) => {
|
|
735
|
+
* const { simHost } = await modules('src/sim/host.ts');
|
|
736
|
+
* return simHost().state.day;
|
|
737
|
+
* })
|
|
738
|
+
* ```
|
|
739
|
+
*
|
|
740
|
+
* `scope` is `{ page, modules, instanceId }`. Serialization contract as
|
|
741
|
+
* `game.page()`: the step travels as source (no closures), and the return
|
|
742
|
+
* value must be plain data. `modules(path)` resolves through the ACTIVE
|
|
743
|
+
* mount's own url space, so what you touch IS the running game — never a
|
|
744
|
+
* phantom second copy. Dev-server sessions only; a shipped build's curated
|
|
745
|
+
* surface is its adapter exports.
|
|
746
|
+
*/
|
|
747
|
+
async run(step, opts) {
|
|
748
|
+
const erased = (arg) => step(arg);
|
|
749
|
+
const outcome = await this.#transport.runGameScript(step.toString(), erased, opts?.instance);
|
|
750
|
+
return this.unwrap(outcome);
|
|
751
|
+
}
|
|
681
752
|
/**
|
|
682
753
|
* Reload the document showing the game, resolving only once it is back and
|
|
683
754
|
* answering commands (see `bridge-transport.ts`'s `reloadPage`).
|
|
@@ -52,6 +52,17 @@ export declare class RelayTransport implements BridgeTransport {
|
|
|
52
52
|
private hiddenCache;
|
|
53
53
|
private readonly instance;
|
|
54
54
|
constructor(opts: RelayTransportOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Deliberately NOT guarded against a page-fallback answer the way the browser
|
|
57
|
+
* (`packages/editor/src/editor-server-response.ts`) and the CLI client
|
|
58
|
+
* (`@vgai/editor-sdk`'s `EditorClient.readJson`) are. Those two can be pointed
|
|
59
|
+
* at an arbitrary URL — a share tunnel, a static host — where a `200
|
|
60
|
+
* text/html` for an unserved route is real. This `baseUrl` is
|
|
61
|
+
* `http://127.0.0.1:<port>` and nothing else, and `session.ts` has already
|
|
62
|
+
* PROVED that port is this project's editor server (`probeServedProject`, whose
|
|
63
|
+
* own unparseable-answer path is exactly the refusal): a foreign occupant is
|
|
64
|
+
* refused there, before any command reaches here.
|
|
65
|
+
*/
|
|
55
66
|
private postCommand;
|
|
56
67
|
/** Maps the relay's wire body back onto the transport-neutral
|
|
57
68
|
* `BridgeCallOutcome` — `result` on success, `code`/`error`/the rest of
|
|
@@ -67,7 +78,7 @@ export declare class RelayTransport implements BridgeTransport {
|
|
|
67
78
|
*
|
|
68
79
|
* It runs on every leg, not just `call('snapshot')`, because that is where
|
|
69
80
|
* the measured gap was: a bot that drives the game with `hold`/`command` and
|
|
70
|
-
* reads through them
|
|
81
|
+
* reads through them could run
|
|
71
82
|
* its entire session against a backgrounded tab and never be told, so a
|
|
72
83
|
* later failure read as a generic relay timeout instead of "your tab is
|
|
73
84
|
* hidden". Returns whether the tab is hidden so `call` can decide whether to
|
|
@@ -89,7 +100,7 @@ export declare class RelayTransport implements BridgeTransport {
|
|
|
89
100
|
bringToFront(): Promise<void>;
|
|
90
101
|
screenshot(path: string): Promise<CaptureNotes>;
|
|
91
102
|
/**
|
|
92
|
-
*
|
|
103
|
+
* Ships `src` (`step.toString()`) to the editor dev server's
|
|
93
104
|
* `page-script` op — a STANDALONE relay command (like `bridge-screenshot`
|
|
94
105
|
* above), not a `bridge-call` method (see `command-listener.ts`'s
|
|
95
106
|
* `handlePageScript` doc comment for why). `step` itself is unused on this
|
|
@@ -98,6 +109,8 @@ export declare class RelayTransport implements BridgeTransport {
|
|
|
98
109
|
* `PageTransport` (which DOES call it directly) also implements.
|
|
99
110
|
*/
|
|
100
111
|
runPageScript(src: string, _step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
|
|
112
|
+
/** THE MODULE LANE over the relay — same wire shape as `page-script`. */
|
|
113
|
+
runGameScript(src: string, _step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
|
|
101
114
|
/**
|
|
102
115
|
* P20 — order the tab to reload, then wait for EVIDENCE that it came back.
|
|
103
116
|
*
|
|
@@ -57,6 +57,17 @@ export class RelayTransport {
|
|
|
57
57
|
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
58
58
|
this.instance = opts.instance;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Deliberately NOT guarded against a page-fallback answer the way the browser
|
|
62
|
+
* (`packages/editor/src/editor-server-response.ts`) and the CLI client
|
|
63
|
+
* (`@vgai/editor-sdk`'s `EditorClient.readJson`) are. Those two can be pointed
|
|
64
|
+
* at an arbitrary URL — a share tunnel, a static host — where a `200
|
|
65
|
+
* text/html` for an unserved route is real. This `baseUrl` is
|
|
66
|
+
* `http://127.0.0.1:<port>` and nothing else, and `session.ts` has already
|
|
67
|
+
* PROVED that port is this project's editor server (`probeServedProject`, whose
|
|
68
|
+
* own unparseable-answer path is exactly the refusal): a foreign occupant is
|
|
69
|
+
* refused there, before any command reaches here.
|
|
70
|
+
*/
|
|
60
71
|
async postCommand(body, timeoutMs) {
|
|
61
72
|
const res = await fetch(`${this.baseUrl}/__editor/command`, {
|
|
62
73
|
method: 'POST',
|
|
@@ -119,7 +130,7 @@ export class RelayTransport {
|
|
|
119
130
|
*
|
|
120
131
|
* It runs on every leg, not just `call('snapshot')`, because that is where
|
|
121
132
|
* the measured gap was: a bot that drives the game with `hold`/`command` and
|
|
122
|
-
* reads through them
|
|
133
|
+
* reads through them could run
|
|
123
134
|
* its entire session against a backgrounded tab and never be told, so a
|
|
124
135
|
* later failure read as a generic relay timeout instead of "your tab is
|
|
125
136
|
* hidden". Returns whether the tab is hidden so `call` can decide whether to
|
|
@@ -132,7 +143,14 @@ export class RelayTransport {
|
|
|
132
143
|
// P21: what was measured, and what this run is doing about it — not a
|
|
133
144
|
// claim about where the tab is. The reading is a `presence` snapshot
|
|
134
145
|
// (`isHidden` below), which ages between the tab's own reports.
|
|
135
|
-
|
|
146
|
+
// STDERR, not stdout. `vgai status` and every other `--json`-shaped verb
|
|
147
|
+
// put their PAYLOAD on stdout and every banner on stderr; this notice went
|
|
148
|
+
// to stdout and prepended a prose sentence to the JSON, so any machine
|
|
149
|
+
// consumer piping `vgai status` into a parser got a SyntaxError the moment
|
|
150
|
+
// the tab happened to be hidden — measured while reading the coverage
|
|
151
|
+
// table on a backgrounded session. A diagnostic that breaks the payload it
|
|
152
|
+
// annotates is worse than no diagnostic.
|
|
153
|
+
process.stderr.write('vgai: the editor page last REPORTED document.visibilityState "hidden" — the engine ' +
|
|
136
154
|
'stops its loop while the page reports itself hidden, so this run drives ' +
|
|
137
155
|
'deterministic runTicks through the session relay instead of wall clock. Sim time ' +
|
|
138
156
|
'advances either way; `vgai status` prints that reading with its age.\n');
|
|
@@ -228,7 +246,7 @@ export class RelayTransport {
|
|
|
228
246
|
return notes;
|
|
229
247
|
}
|
|
230
248
|
/**
|
|
231
|
-
*
|
|
249
|
+
* Ships `src` (`step.toString()`) to the editor dev server's
|
|
232
250
|
* `page-script` op — a STANDALONE relay command (like `bridge-screenshot`
|
|
233
251
|
* above), not a `bridge-call` method (see `command-listener.ts`'s
|
|
234
252
|
* `handlePageScript` doc comment for why). `step` itself is unused on this
|
|
@@ -252,6 +270,23 @@ export class RelayTransport {
|
|
|
252
270
|
};
|
|
253
271
|
}
|
|
254
272
|
}
|
|
273
|
+
/** THE MODULE LANE over the relay — same wire shape as `page-script`. */
|
|
274
|
+
async runGameScript(src, _step, instance) {
|
|
275
|
+
try {
|
|
276
|
+
const body = await this.postCommand({ type: 'game-eval', src, ...(instance === undefined ? {} : { instance }) }, PAGE_SCRIPT_TIMEOUT_MS);
|
|
277
|
+
return this.toBridgeOutcome(body);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
error: {
|
|
283
|
+
code: 'RELAY_UNREACHABLE',
|
|
284
|
+
message: `vgai: could not reach the editor dev server relay at ${this.baseUrl} — ` +
|
|
285
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
255
290
|
/**
|
|
256
291
|
* P20 — order the tab to reload, then wait for EVIDENCE that it came back.
|
|
257
292
|
*
|
|
@@ -41,15 +41,15 @@ export interface DebugBridgeInput {
|
|
|
41
41
|
setVirtualAction(action: string, value: VirtualActionValue): VirtualActionResult;
|
|
42
42
|
tapVirtualAction(action: string): VirtualActionResult;
|
|
43
43
|
clearVirtualActions(): void;
|
|
44
|
-
/**
|
|
45
|
-
* at the start of that tick's input phase. Declared here
|
|
46
|
-
* bridge↔wire coverage-parity gate
|
|
44
|
+
/** Schedule a virtual actuation for a specific tick, applied
|
|
45
|
+
* at the start of that tick's input phase. Declared here for the
|
|
46
|
+
* bridge↔wire coverage-parity gate — type-shape completeness with
|
|
47
47
|
* `runtime/debug-bridge.ts`'s `VgaiDebugInputHandle` — this package still
|
|
48
48
|
* exposes no client-side convenience wrapper around it (deliberately
|
|
49
49
|
* parked; see `GameInput` in `client.ts`), this is pure type-shape
|
|
50
50
|
* mirroring. */
|
|
51
51
|
scheduleActionAtTick(tick: number, action: string, value: VirtualActionValue): void;
|
|
52
|
-
/**
|
|
52
|
+
/** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
|
|
53
53
|
* `VgaiDebugInputHandle.injectPointerDelta`: accumulates a synthetic
|
|
54
54
|
* pointer delta for a named test source (sums within a frame, clears each
|
|
55
55
|
* frame). Declared here for type-shape completeness with the bridge, same
|
|
@@ -59,7 +59,7 @@ export interface DebugBridgeInput {
|
|
|
59
59
|
x: number;
|
|
60
60
|
y: number;
|
|
61
61
|
}): void;
|
|
62
|
-
/**
|
|
62
|
+
/** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
|
|
63
63
|
* `VgaiDebugInputHandle.injectPointerPosition`: sets a synthetic absolute
|
|
64
64
|
* pointer position for a named test source (last-write-wins, persists
|
|
65
65
|
* until changed). Same type-shape-only precedent as `scheduleActionAtTick`. */
|
|
@@ -96,7 +96,7 @@ export class WaitForTimeoutError extends Error {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
99
|
-
// Fixture heartbeat (
|
|
99
|
+
// Fixture heartbeat ("Watchdog sim-awareness / fixture
|
|
100
100
|
// heartbeat for long silent tests" — Session C hit exit-5 on the runner's
|
|
101
101
|
// 2x90s stdout-liveness watchdog during a legitimately silent 5-minute test).
|
|
102
102
|
//
|
|
@@ -107,7 +107,7 @@ export class WaitForTimeoutError extends Error {
|
|
|
107
107
|
// advancing test can never false-wedge regardless of duration, no matter how
|
|
108
108
|
// long a single `simSeconds` budget runs.
|
|
109
109
|
//
|
|
110
|
-
// INVARIANT (tested below
|
|
110
|
+
// INVARIANT (tested below): a heartbeat requires BOTH
|
|
111
111
|
// (a) >= HEARTBEAT_INTERVAL_MS of wall time since the last heartbeat, AND
|
|
112
112
|
// (b) the tick has ADVANCED since the last heartbeat (not merely since the
|
|
113
113
|
// last poll). Without (b), the poll loop itself — which keeps running
|
|
@@ -20,7 +20,7 @@ import { GameClient } from './game-client/index.js';
|
|
|
20
20
|
/** A `GameClient` addressing ONE mounted instance, tagged with the mount `id`
|
|
21
21
|
* it drives. `id` is the whole point of the handle to a CALLER: it is what
|
|
22
22
|
* `tools.run(name, args, { instance: handle.id })` passes to scope a
|
|
23
|
-
* node-hosted tool
|
|
23
|
+
* node-hosted tool to this seat — the two-seat
|
|
24
24
|
* multiplayer verification pattern. Without it the caller has a client it can
|
|
25
25
|
* drive but no id to hand a tool, so the seat-scoped path silently degrades to
|
|
26
26
|
* the sole-instance one. */
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* and driven through the product. It is scoped to the ACTIVE document and
|
|
35
35
|
* refuses anything outside it by name (`editor-document.ts`).
|
|
36
36
|
*
|
|
37
|
-
* `page(step)` = `GameClient.page(step)
|
|
38
|
-
*
|
|
37
|
+
* `page(step)` = `GameClient.page(step)`, the playwright-shim surface —
|
|
38
|
+
* write `step` as a literal `async (page) => {...}` and inline
|
|
39
39
|
* every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
|
|
40
40
|
* binds to, `step` is shipped to the editor dev server as `step.toString()`
|
|
41
41
|
* and reconstructed there — closures over outer variables do NOT survive that
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* and driven through the product. It is scoped to the ACTIVE document and
|
|
35
35
|
* refuses anything outside it by name (`editor-document.ts`).
|
|
36
36
|
*
|
|
37
|
-
* `page(step)` = `GameClient.page(step)
|
|
38
|
-
*
|
|
37
|
+
* `page(step)` = `GameClient.page(step)`, the playwright-shim surface —
|
|
38
|
+
* write `step` as a literal `async (page) => {...}` and inline
|
|
39
39
|
* every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
|
|
40
40
|
* binds to, `step` is shipped to the editor dev server as `step.toString()`
|
|
41
41
|
* and reconstructed there — closures over outer variables do NOT survive that
|
|
@@ -36,17 +36,10 @@ export class LiveGameplayRecording {
|
|
|
36
36
|
await copyFile(capture.path, path);
|
|
37
37
|
}
|
|
38
38
|
return {
|
|
39
|
+
...capture,
|
|
40
|
+
// The caller's `destination` copy, when there was one — otherwise the
|
|
41
|
+
// project path the recorder wrote. Spread first so this wins.
|
|
39
42
|
path,
|
|
40
|
-
startedAt: capture.startedAt,
|
|
41
|
-
mimeType: capture.mimeType,
|
|
42
|
-
width: capture.width,
|
|
43
|
-
height: capture.height,
|
|
44
|
-
fps: capture.fps,
|
|
45
|
-
audio: capture.audio,
|
|
46
|
-
durationMs: capture.durationMs,
|
|
47
|
-
droppedFrames: capture.droppedFrames,
|
|
48
|
-
frameErrors: capture.frameErrors,
|
|
49
|
-
layers: capture.layers,
|
|
50
43
|
};
|
|
51
44
|
}
|
|
52
45
|
}
|
|
@@ -76,11 +76,4 @@ export interface ProjectSessionHint {
|
|
|
76
76
|
url: string;
|
|
77
77
|
startedAt: string;
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Resolve `projectDir` (default `process.cwd()`) to the port of its already-
|
|
81
|
-
* running `vgai edit` session. Throws a descriptive error (never hangs
|
|
82
|
-
* indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
|
|
83
|
-
* silently attaches to an unrelated project's session — see the module doc
|
|
84
|
-
* above) when no vgai.project.json is found, or no live session covers it.
|
|
85
|
-
*/
|
|
86
79
|
export declare function resolveSession(projectDir?: string, deps?: SessionResolutionDeps): Promise<ResolvedSession>;
|