picopilot 0.0.0 → 0.1.1
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/LICENSE +661 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audio.d.ts +17 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +315 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +257 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/gfx.d.ts +28 -0
- package/dist/commands/gfx.d.ts.map +1 -0
- package/dist/commands/gfx.js +521 -0
- package/dist/commands/gfx.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +20 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +90 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/minify.d.ts +20 -0
- package/dist/commands/minify.d.ts.map +1 -0
- package/dist/commands/minify.js +129 -0
- package/dist/commands/minify.js.map +1 -0
- package/dist/commands/music.d.ts +13 -0
- package/dist/commands/music.d.ts.map +1 -0
- package/dist/commands/music.js +241 -0
- package/dist/commands/music.js.map +1 -0
- package/dist/commands/playtest.d.ts +29 -0
- package/dist/commands/playtest.d.ts.map +1 -0
- package/dist/commands/playtest.js +631 -0
- package/dist/commands/playtest.js.map +1 -0
- package/dist/commands/run.d.ts +30 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +224 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +50 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +213 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/sfx.d.ts +13 -0
- package/dist/commands/sfx.d.ts.map +1 -0
- package/dist/commands/sfx.js +254 -0
- package/dist/commands/sfx.js.map +1 -0
- package/dist/commands/tokens.d.ts +36 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +129 -0
- package/dist/commands/tokens.js.map +1 -0
- package/dist/commands/verify.d.ts +57 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +213 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +24 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/engine/audio/index.d.ts +14 -0
- package/dist/engine/audio/index.d.ts.map +1 -0
- package/dist/engine/audio/index.js +14 -0
- package/dist/engine/audio/index.js.map +1 -0
- package/dist/engine/audio/music.d.ts +116 -0
- package/dist/engine/audio/music.d.ts.map +1 -0
- package/dist/engine/audio/music.js +138 -0
- package/dist/engine/audio/music.js.map +1 -0
- package/dist/engine/audio/section.d.ts +34 -0
- package/dist/engine/audio/section.d.ts.map +1 -0
- package/dist/engine/audio/section.js +67 -0
- package/dist/engine/audio/section.js.map +1 -0
- package/dist/engine/audio/sfx.d.ts +141 -0
- package/dist/engine/audio/sfx.d.ts.map +1 -0
- package/dist/engine/audio/sfx.js +506 -0
- package/dist/engine/audio/sfx.js.map +1 -0
- package/dist/engine/cart/cart.d.ts +75 -0
- package/dist/engine/cart/cart.d.ts.map +1 -0
- package/dist/engine/cart/cart.js +153 -0
- package/dist/engine/cart/cart.js.map +1 -0
- package/dist/engine/cart/errors.d.ts +29 -0
- package/dist/engine/cart/errors.d.ts.map +1 -0
- package/dist/engine/cart/errors.js +26 -0
- package/dist/engine/cart/errors.js.map +1 -0
- package/dist/engine/cart/gfx.d.ts +79 -0
- package/dist/engine/cart/gfx.d.ts.map +1 -0
- package/dist/engine/cart/gfx.js +171 -0
- package/dist/engine/cart/gfx.js.map +1 -0
- package/dist/engine/cart/index.d.ts +15 -0
- package/dist/engine/cart/index.d.ts.map +1 -0
- package/dist/engine/cart/index.js +15 -0
- package/dist/engine/cart/index.js.map +1 -0
- package/dist/engine/cart/map.d.ts +46 -0
- package/dist/engine/cart/map.d.ts.map +1 -0
- package/dist/engine/cart/map.js +118 -0
- package/dist/engine/cart/map.js.map +1 -0
- package/dist/engine/config/config.d.ts +74 -0
- package/dist/engine/config/config.d.ts.map +1 -0
- package/dist/engine/config/config.js +103 -0
- package/dist/engine/config/config.js.map +1 -0
- package/dist/engine/config/index.d.ts +7 -0
- package/dist/engine/config/index.d.ts.map +1 -0
- package/dist/engine/config/index.js +7 -0
- package/dist/engine/config/index.js.map +1 -0
- package/dist/engine/gfx/grid.d.ts +51 -0
- package/dist/engine/gfx/grid.d.ts.map +1 -0
- package/dist/engine/gfx/grid.js +116 -0
- package/dist/engine/gfx/grid.js.map +1 -0
- package/dist/engine/gfx/index.d.ts +17 -0
- package/dist/engine/gfx/index.d.ts.map +1 -0
- package/dist/engine/gfx/index.js +17 -0
- package/dist/engine/gfx/index.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +34 -0
- package/dist/engine/gfx/label.d.ts.map +1 -0
- package/dist/engine/gfx/label.js +190 -0
- package/dist/engine/gfx/label.js.map +1 -0
- package/dist/engine/gfx/overlap.d.ts +42 -0
- package/dist/engine/gfx/overlap.d.ts.map +1 -0
- package/dist/engine/gfx/overlap.js +57 -0
- package/dist/engine/gfx/overlap.js.map +1 -0
- package/dist/engine/gfx/palette.d.ts +29 -0
- package/dist/engine/gfx/palette.d.ts.map +1 -0
- package/dist/engine/gfx/palette.js +46 -0
- package/dist/engine/gfx/palette.js.map +1 -0
- package/dist/engine/gfx/png.d.ts +31 -0
- package/dist/engine/gfx/png.d.ts.map +1 -0
- package/dist/engine/gfx/png.js +165 -0
- package/dist/engine/gfx/png.js.map +1 -0
- package/dist/engine/gfx/render.d.ts +42 -0
- package/dist/engine/gfx/render.d.ts.map +1 -0
- package/dist/engine/gfx/render.js +72 -0
- package/dist/engine/gfx/render.js.map +1 -0
- package/dist/engine/index.d.ts +22 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +22 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/pico8/adapter.d.ts +267 -0
- package/dist/engine/pico8/adapter.d.ts.map +1 -0
- package/dist/engine/pico8/adapter.js +37 -0
- package/dist/engine/pico8/adapter.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts +308 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -0
- package/dist/engine/pico8/drive.js +581 -0
- package/dist/engine/pico8/drive.js.map +1 -0
- package/dist/engine/pico8/harness.d.ts +146 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -0
- package/dist/engine/pico8/harness.js +251 -0
- package/dist/engine/pico8/harness.js.map +1 -0
- package/dist/engine/pico8/index.d.ts +21 -0
- package/dist/engine/pico8/index.d.ts.map +1 -0
- package/dist/engine/pico8/index.js +21 -0
- package/dist/engine/pico8/index.js.map +1 -0
- package/dist/engine/pico8/sentinel.d.ts +63 -0
- package/dist/engine/pico8/sentinel.d.ts.map +1 -0
- package/dist/engine/pico8/sentinel.js +86 -0
- package/dist/engine/pico8/sentinel.js.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.js +11 -0
- package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
- package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
- package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-main.js +71 -0
- package/dist/engine/pico8/session-daemon-main.js.map +1 -0
- package/dist/engine/pico8/session-daemon.d.ts +68 -0
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon.js +264 -0
- package/dist/engine/pico8/session-daemon.js.map +1 -0
- package/dist/engine/pico8/session.d.ts +140 -0
- package/dist/engine/pico8/session.d.ts.map +1 -0
- package/dist/engine/pico8/session.js +250 -0
- package/dist/engine/pico8/session.js.map +1 -0
- package/dist/engine/pico8/shell.d.ts +103 -0
- package/dist/engine/pico8/shell.d.ts.map +1 -0
- package/dist/engine/pico8/shell.js +324 -0
- package/dist/engine/pico8/shell.js.map +1 -0
- package/dist/engine/pico8/supervisor.d.ts +137 -0
- package/dist/engine/pico8/supervisor.d.ts.map +1 -0
- package/dist/engine/pico8/supervisor.js +140 -0
- package/dist/engine/pico8/supervisor.js.map +1 -0
- package/dist/engine/scaffold/index.d.ts +8 -0
- package/dist/engine/scaffold/index.d.ts.map +1 -0
- package/dist/engine/scaffold/index.js +8 -0
- package/dist/engine/scaffold/index.js.map +1 -0
- package/dist/engine/scaffold/scaffold.d.ts +49 -0
- package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
- package/dist/engine/scaffold/scaffold.js +259 -0
- package/dist/engine/scaffold/scaffold.js.map +1 -0
- package/dist/engine/shrinko/adapter.d.ts +167 -0
- package/dist/engine/shrinko/adapter.d.ts.map +1 -0
- package/dist/engine/shrinko/adapter.js +107 -0
- package/dist/engine/shrinko/adapter.js.map +1 -0
- package/dist/engine/shrinko/index.d.ts +13 -0
- package/dist/engine/shrinko/index.d.ts.map +1 -0
- package/dist/engine/shrinko/index.js +13 -0
- package/dist/engine/shrinko/index.js.map +1 -0
- package/dist/engine/shrinko/shell.d.ts +69 -0
- package/dist/engine/shrinko/shell.d.ts.map +1 -0
- package/dist/engine/shrinko/shell.js +156 -0
- package/dist/engine/shrinko/shell.js.map +1 -0
- package/dist/engine/skills/index.d.ts +8 -0
- package/dist/engine/skills/index.d.ts.map +1 -0
- package/dist/engine/skills/index.js +8 -0
- package/dist/engine/skills/index.js.map +1 -0
- package/dist/engine/skills/skills.d.ts +104 -0
- package/dist/engine/skills/skills.d.ts.map +1 -0
- package/dist/engine/skills/skills.js +149 -0
- package/dist/engine/skills/skills.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -2
- package/src/bin.ts +4 -0
- package/src/cli.ts +61 -0
- package/src/commands/audio.test.ts +280 -0
- package/src/commands/audio.ts +410 -0
- package/src/commands/export.test.ts +310 -0
- package/src/commands/export.ts +316 -0
- package/src/commands/gfx-io.test.ts +178 -0
- package/src/commands/gfx.test.ts +470 -0
- package/src/commands/gfx.ts +645 -0
- package/src/commands/init.test.ts +482 -0
- package/src/commands/init.ts +193 -0
- package/src/commands/lint.test.ts +137 -0
- package/src/commands/lint.ts +117 -0
- package/src/commands/minify.test.ts +166 -0
- package/src/commands/minify.ts +156 -0
- package/src/commands/music.test.ts +272 -0
- package/src/commands/music.ts +288 -0
- package/src/commands/playtest.test.ts +317 -0
- package/src/commands/playtest.ts +773 -0
- package/src/commands/run.test.ts +264 -0
- package/src/commands/run.ts +285 -0
- package/src/commands/serve.test.ts +227 -0
- package/src/commands/serve.ts +272 -0
- package/src/commands/sfx.test.ts +349 -0
- package/src/commands/sfx.ts +300 -0
- package/src/commands/tokens.test.ts +212 -0
- package/src/commands/tokens.ts +168 -0
- package/src/commands/verify.test.ts +238 -0
- package/src/commands/verify.ts +256 -0
- package/src/commands/version.test.ts +70 -0
- package/src/commands/version.ts +24 -0
- package/src/engine/audio/index.ts +47 -0
- package/src/engine/audio/music.test.ts +188 -0
- package/src/engine/audio/music.ts +218 -0
- package/src/engine/audio/section.test.ts +124 -0
- package/src/engine/audio/section.ts +79 -0
- package/src/engine/audio/sfx.test.ts +410 -0
- package/src/engine/audio/sfx.ts +694 -0
- package/src/engine/cart/cart.test.ts +304 -0
- package/src/engine/cart/cart.ts +211 -0
- package/src/engine/cart/errors.ts +39 -0
- package/src/engine/cart/fixtures/crlf.p8 +6 -0
- package/src/engine/cart/fixtures/full.p8 +174 -0
- package/src/engine/cart/fixtures/header-only.p8 +2 -0
- package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
- package/src/engine/cart/fixtures/minimal.p8 +10 -0
- package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
- package/src/engine/cart/gfx.ts +195 -0
- package/src/engine/cart/index.ts +27 -0
- package/src/engine/cart/map.ts +134 -0
- package/src/engine/config/config.ts +115 -0
- package/src/engine/config/index.ts +12 -0
- package/src/engine/gfx/grid.test.ts +128 -0
- package/src/engine/gfx/grid.ts +136 -0
- package/src/engine/gfx/index.ts +34 -0
- package/src/engine/gfx/label.test.ts +105 -0
- package/src/engine/gfx/label.ts +229 -0
- package/src/engine/gfx/overlap.test.ts +71 -0
- package/src/engine/gfx/overlap.ts +66 -0
- package/src/engine/gfx/palette.test.ts +35 -0
- package/src/engine/gfx/palette.ts +54 -0
- package/src/engine/gfx/png.test.ts +111 -0
- package/src/engine/gfx/png.ts +196 -0
- package/src/engine/gfx/render.test.ts +180 -0
- package/src/engine/gfx/render.ts +106 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pico8/adapter.ts +303 -0
- package/src/engine/pico8/drive.test.ts +415 -0
- package/src/engine/pico8/drive.ts +729 -0
- package/src/engine/pico8/harness.test.ts +181 -0
- package/src/engine/pico8/harness.ts +354 -0
- package/src/engine/pico8/index.ts +20 -0
- package/src/engine/pico8/sentinel.test.ts +80 -0
- package/src/engine/pico8/sentinel.ts +93 -0
- package/src/engine/pico8/session-daemon-entry.ts +12 -0
- package/src/engine/pico8/session-daemon-main.ts +98 -0
- package/src/engine/pico8/session-daemon.test.ts +212 -0
- package/src/engine/pico8/session-daemon.ts +306 -0
- package/src/engine/pico8/session.test.ts +239 -0
- package/src/engine/pico8/session.ts +321 -0
- package/src/engine/pico8/shell.test.ts +583 -0
- package/src/engine/pico8/shell.ts +412 -0
- package/src/engine/pico8/supervisor.test.ts +105 -0
- package/src/engine/pico8/supervisor.ts +197 -0
- package/src/engine/scaffold/index.ts +15 -0
- package/src/engine/scaffold/scaffold.ts +278 -0
- package/src/engine/shrinko/adapter.ts +241 -0
- package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
- package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
- package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
- package/src/engine/shrinko/index.ts +33 -0
- package/src/engine/shrinko/shell.ts +224 -0
- package/src/engine/shrinko/shrinko.test.ts +182 -0
- package/src/engine/skills/index.ts +14 -0
- package/src/engine/skills/skills.test.ts +376 -0
- package/src/engine/skills/skills.ts +205 -0
- package/src/index.ts +13 -0
- package/src/skills/game-design-reference/SKILL.md +70 -0
- package/src/skills/game-jam/SKILL.md +45 -0
- package/src/skills/picopilot-art/SKILL.md +62 -0
- package/src/skills/picopilot-audio/SKILL.md +194 -0
- package/src/skills/picopilot-code/SKILL.md +97 -0
- package/src/skills/picopilot-code/reference/README.md +42 -0
- package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
- package/src/skills/picopilot-code/reference/platformer.md +119 -0
- package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
- package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
- package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
- package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
- package/src/skills/picopilot-debug/SKILL.md +195 -0
- package/src/skills/picopilot-overview/SKILL.md +52 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playtest session DAEMON runtime + CLIENT (ADR-0011, prd US #6). This is the
|
|
3
|
+
* IO half of the supervisor: the daemon LISTENS on a session's Unix domain socket
|
|
4
|
+
* and drives a live {@link DriveSession} on behalf of each verb sub-invocation;
|
|
5
|
+
* the {@link SessionClient} CONNECTS to that socket to send a verb and read back
|
|
6
|
+
* the structured response.
|
|
7
|
+
*
|
|
8
|
+
* The daemon owns the live process the whole session, so the game stays ALIVE +
|
|
9
|
+
* PAUSED between the agent's turns. It self-reaps on an IDLE timeout (the orphan
|
|
10
|
+
* path) and on `stop`. The live daemon end-to-end is the manual/opt-in tier; the
|
|
11
|
+
* pure pieces it composes (the handshake {@link DriveSession}, the registry, the
|
|
12
|
+
* frame codec) are unit-tested without a real socket or binary.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {spawn} from 'node:child_process';
|
|
16
|
+
import {createConnection, createServer, type Socket} from 'node:net';
|
|
17
|
+
import {existsSync, readdirSync, rmSync, writeFileSync} from 'node:fs';
|
|
18
|
+
import {dirname, join} from 'node:path';
|
|
19
|
+
import {fileURLToPath} from 'node:url';
|
|
20
|
+
import {
|
|
21
|
+
DriveSession,
|
|
22
|
+
SessionError,
|
|
23
|
+
type ShotResult,
|
|
24
|
+
type StepResult,
|
|
25
|
+
} from './session.js';
|
|
26
|
+
import type {Pico8Process} from './shell.js';
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_IDLE_TIMEOUT_MS,
|
|
29
|
+
encodeFrame,
|
|
30
|
+
FrameDecoder,
|
|
31
|
+
type SessionPaths,
|
|
32
|
+
type SessionRequest,
|
|
33
|
+
type SessionResponse,
|
|
34
|
+
type SessionResponseValue,
|
|
35
|
+
} from './supervisor.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A client for a running session daemon: connects to its socket, sends ONE verb
|
|
39
|
+
* request, awaits the single response, and closes. Each `playtest step/input/shot
|
|
40
|
+
* /stop` sub-invocation makes exactly one of these round-trips.
|
|
41
|
+
*/
|
|
42
|
+
export class SessionClient {
|
|
43
|
+
constructor(private readonly socketPath: string) {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sends `request` to the daemon and resolves its {@link SessionResponse}. A
|
|
47
|
+
* connection failure (no daemon / dead socket) rejects with a plain Error the
|
|
48
|
+
* command maps to `playtest-session-not-found`.
|
|
49
|
+
*/
|
|
50
|
+
send(request: SessionRequest): Promise<SessionResponse> {
|
|
51
|
+
return new Promise<SessionResponse>((resolve, reject) => {
|
|
52
|
+
const decoder = new FrameDecoder<SessionResponse>();
|
|
53
|
+
let settled = false;
|
|
54
|
+
const sock = createConnection(this.socketPath);
|
|
55
|
+
sock.setEncoding('utf8');
|
|
56
|
+
sock.on('connect', () => sock.write(encodeFrame(request)));
|
|
57
|
+
sock.on('data', (chunk: string) => {
|
|
58
|
+
const [msg] = decoder.push(chunk);
|
|
59
|
+
if (msg !== undefined && !settled) {
|
|
60
|
+
settled = true;
|
|
61
|
+
resolve(msg);
|
|
62
|
+
sock.end();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
sock.on('error', (err) => {
|
|
66
|
+
if (!settled) {
|
|
67
|
+
settled = true;
|
|
68
|
+
reject(err);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
sock.on('close', () => {
|
|
72
|
+
if (!settled) {
|
|
73
|
+
settled = true;
|
|
74
|
+
reject(new Error('daemon closed the connection with no response'));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether a session daemon is reachable (its socket exists). */
|
|
82
|
+
export function sessionExists(paths: SessionPaths): boolean {
|
|
83
|
+
return existsSync(paths.socket);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Resolves the sibling daemon-entry module (`.ts` in dev, `.js` when built). */
|
|
87
|
+
function daemonEntryPath(): string {
|
|
88
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
89
|
+
// This module and the entry are compiled/loaded together, so the extension
|
|
90
|
+
// here (`.ts` under tsx, `.js` under dist) is the entry's extension too.
|
|
91
|
+
const ext = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
92
|
+
return join(here, `session-daemon-entry${ext}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Spawns the session DAEMON as a DETACHED background process that outlives this
|
|
97
|
+
* (the `start`) invocation, so the driven PICO-8 stays alive across the agent's
|
|
98
|
+
* separate verb turns. It re-invokes the current executable + `execArgv` (so the
|
|
99
|
+
* daemon runs under the SAME loader, dev `tsx` or built node) on the daemon entry
|
|
100
|
+
* file, passing the config as a single JSON arg. `unref` + `detached` cut it free
|
|
101
|
+
* from this process's lifetime; its stdio is ignored (the daemon talks over the
|
|
102
|
+
* session socket, not this process's pipes).
|
|
103
|
+
*/
|
|
104
|
+
export function spawnSessionDaemon(
|
|
105
|
+
config: import('./session-daemon-main.js').DaemonConfig,
|
|
106
|
+
): void {
|
|
107
|
+
const child = spawn(
|
|
108
|
+
process.execPath,
|
|
109
|
+
[...process.execArgv, daemonEntryPath(), JSON.stringify(config)],
|
|
110
|
+
{detached: true, stdio: 'ignore'},
|
|
111
|
+
);
|
|
112
|
+
child.unref();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Options for {@link serveSession}. */
|
|
116
|
+
export interface ServeSessionOptions {
|
|
117
|
+
readonly id: string;
|
|
118
|
+
readonly paths: SessionPaths;
|
|
119
|
+
/** The live driven process to own for this session (spawned by the caller). */
|
|
120
|
+
readonly process: Pico8Process;
|
|
121
|
+
/** The screenshot basename the driven cart names each SHOT (matches transform). */
|
|
122
|
+
readonly shotBasename: string;
|
|
123
|
+
/** Idle-reap window (defaults to {@link DEFAULT_IDLE_TIMEOUT_MS}). */
|
|
124
|
+
readonly idleTimeoutMs?: number;
|
|
125
|
+
/** Per-command ACK deadline forwarded to {@link DriveSession}. */
|
|
126
|
+
readonly ackTimeoutMs?: number;
|
|
127
|
+
/** Called when the daemon has fully torn down (for the process to exit). */
|
|
128
|
+
readonly onExit?: () => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Runs the session daemon: wraps the live process in a {@link DriveSession},
|
|
133
|
+
* listens on the session socket, and dispatches each incoming verb request to the
|
|
134
|
+
* handshake (send-command -> wait-ack -> respond). Resets an IDLE-reap timer on
|
|
135
|
+
* every verb, so an orphaned session tears down on its own. Returns the net
|
|
136
|
+
* server so the caller (the daemon entrypoint) can await its lifetime.
|
|
137
|
+
*/
|
|
138
|
+
export function serveSession(options: ServeSessionOptions): {
|
|
139
|
+
close: () => void;
|
|
140
|
+
} {
|
|
141
|
+
const session = new DriveSession({
|
|
142
|
+
process: options.process,
|
|
143
|
+
shotBasename: options.shotBasename,
|
|
144
|
+
ackTimeoutMs: options.ackTimeoutMs,
|
|
145
|
+
});
|
|
146
|
+
const idleMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
147
|
+
|
|
148
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
149
|
+
let tornDown = false;
|
|
150
|
+
|
|
151
|
+
const teardown = (): void => {
|
|
152
|
+
if (tornDown) return;
|
|
153
|
+
tornDown = true;
|
|
154
|
+
if (idleTimer !== undefined) clearTimeout(idleTimer);
|
|
155
|
+
try {
|
|
156
|
+
session.stop();
|
|
157
|
+
} catch {
|
|
158
|
+
// best-effort; the process kill below is the real backstop
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
server.close();
|
|
162
|
+
} catch {
|
|
163
|
+
// ignore
|
|
164
|
+
}
|
|
165
|
+
// Reap the WHOLE session dir (socket + pidfile + driven cart + shots), so an
|
|
166
|
+
// idle/orphan teardown leaves nothing behind, not just the socket. (The CLI
|
|
167
|
+
// `stop` also sweeps the dir; doing it here covers the no-`stop` orphan path.)
|
|
168
|
+
try {
|
|
169
|
+
rmSync(options.paths.dir, {recursive: true, force: true});
|
|
170
|
+
} catch {
|
|
171
|
+
// ignore
|
|
172
|
+
}
|
|
173
|
+
options.onExit?.();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const armIdle = (): void => {
|
|
177
|
+
if (idleTimer !== undefined) clearTimeout(idleTimer);
|
|
178
|
+
idleTimer = setTimeout(teardown, idleMs);
|
|
179
|
+
idleTimer.unref?.();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handle = async (req: SessionRequest): Promise<SessionResponse> => {
|
|
183
|
+
armIdle();
|
|
184
|
+
try {
|
|
185
|
+
switch (req.verb) {
|
|
186
|
+
case 'input': {
|
|
187
|
+
const r: StepResult = await session.input(req.held);
|
|
188
|
+
return ok(options.id, {frame: r.frame, ack: r.ack, alive: true});
|
|
189
|
+
}
|
|
190
|
+
case 'step': {
|
|
191
|
+
// A frames span may exceed one byte; the daemon issues it as repeated
|
|
192
|
+
// single-byte steps, each awaiting its own STEP-DONE ack, so the total
|
|
193
|
+
// is exact and lossless regardless of size.
|
|
194
|
+
let last: StepResult = {frame: session.frame, ack: ''};
|
|
195
|
+
let remaining = Math.max(0, Math.floor(req.frames));
|
|
196
|
+
while (remaining > 0) {
|
|
197
|
+
const chunk = Math.min(remaining, 0xff);
|
|
198
|
+
last = await session.step(chunk);
|
|
199
|
+
remaining -= chunk;
|
|
200
|
+
}
|
|
201
|
+
return ok(options.id, {
|
|
202
|
+
frame: last.frame,
|
|
203
|
+
ack: last.ack,
|
|
204
|
+
alive: true,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
case 'shot': {
|
|
208
|
+
const r: ShotResult = await session.shot();
|
|
209
|
+
const screenshot = collectShot(options.paths.shotDir, r.shotName);
|
|
210
|
+
return ok(options.id, {
|
|
211
|
+
frame: r.frame,
|
|
212
|
+
ack: r.ack,
|
|
213
|
+
shotName: r.shotName,
|
|
214
|
+
screenshot,
|
|
215
|
+
alive: true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
case 'status': {
|
|
219
|
+
return ok(options.id, {
|
|
220
|
+
frame: session.frame,
|
|
221
|
+
alive: !session.isDead,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
case 'stop': {
|
|
225
|
+
// The connection handler tears the daemon down AFTER this response has
|
|
226
|
+
// flushed to the client (so `stop` returns cleanly, no close-race).
|
|
227
|
+
return ok(options.id, {frame: session.frame, alive: false});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {
|
|
231
|
+
if (e instanceof SessionError) {
|
|
232
|
+
return {ok: false, code: e.code, message: e.message};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
code: 'playtest-session-error',
|
|
237
|
+
message: e instanceof Error ? e.message : String(e),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const server = createServer((sock: Socket) => {
|
|
243
|
+
sock.setEncoding('utf8');
|
|
244
|
+
const decoder = new FrameDecoder<SessionRequest>();
|
|
245
|
+
sock.on('data', (chunk: string) => {
|
|
246
|
+
for (const req of decoder.push(chunk)) {
|
|
247
|
+
const isStop = req.verb === 'stop';
|
|
248
|
+
void handle(req).then((res) => {
|
|
249
|
+
try {
|
|
250
|
+
// Flush the response, THEN (for `stop`) end the socket and tear the
|
|
251
|
+
// daemon down, so the client always reads the stop reply first.
|
|
252
|
+
sock.write(encodeFrame(res), () => {
|
|
253
|
+
if (isStop) {
|
|
254
|
+
sock.end();
|
|
255
|
+
teardown();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
} catch {
|
|
259
|
+
if (isStop) teardown();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
sock.on('error', () => {
|
|
265
|
+
/* a dropped client is not fatal to the daemon */
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// If the driven process dies, the next verb reports it (process-dead); we do
|
|
270
|
+
// NOT auto-teardown here, so the agent can still `stop`/`status` and get the
|
|
271
|
+
// structured error rather than a vanished socket.
|
|
272
|
+
server.listen(options.paths.socket, () => {
|
|
273
|
+
writeFileSync(options.paths.pidfile, String(process.pid));
|
|
274
|
+
armIdle();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return {close: teardown};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Builds an ok {@link SessionResponse} from a partial value + the id. */
|
|
281
|
+
function ok(
|
|
282
|
+
id: string,
|
|
283
|
+
partial: Omit<SessionResponseValue, 'id'>,
|
|
284
|
+
): SessionResponse {
|
|
285
|
+
return {ok: true, value: {id, ...partial}};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolves the absolute path of the screenshot a SHOT just produced in the
|
|
290
|
+
* `-desktop` dir. The cart named it `<shotName>` (basename) and PICO-8 appends
|
|
291
|
+
* `.png`; if the exact file is not yet visible (a filesystem race) it falls back
|
|
292
|
+
* to the most-recent PNG, so the caller still gets a path to read.
|
|
293
|
+
*/
|
|
294
|
+
function collectShot(shotDir: string, shotName: string): string | undefined {
|
|
295
|
+
const exact = join(shotDir, `${shotName}.png`);
|
|
296
|
+
if (existsSync(exact)) return exact;
|
|
297
|
+
try {
|
|
298
|
+
const pngs = readdirSync(shotDir)
|
|
299
|
+
.filter((f) => f.toLowerCase().endsWith('.png'))
|
|
300
|
+
.sort();
|
|
301
|
+
const last = pngs.at(-1);
|
|
302
|
+
return last !== undefined ? join(shotDir, last) : undefined;
|
|
303
|
+
} catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {ACK, BLOCK_SIZE, BlockDecoder, OPCODE} from './drive.js';
|
|
3
|
+
import {DriveSession, SessionError} from './session.js';
|
|
4
|
+
import type {Pico8Process} from './shell.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The RESUMABLE session HANDSHAKE core is tested WITHOUT the paid PICO-8 binary
|
|
8
|
+
* (ADR-0011, prd US #6): a FAKE driven process lets us assert the load-bearing
|
|
9
|
+
* transport contract, each verb WRITES its fixed-size command block and WAITS for
|
|
10
|
+
* the cart's ACK before returning (send -> wait-ack -> return), and the failure
|
|
11
|
+
* paths (a lost/late ack, a process that dies mid-command) surface as STRUCTURED
|
|
12
|
+
* errors, never a hang. A live multi-turn session is the manual/opt-in tier.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A controllable fake driven process: records the command blocks written to its
|
|
17
|
+
* stdin, and lets the test emit ACK lines back on stdout (or die), so the
|
|
18
|
+
* handshake is driven deterministically with no real binary.
|
|
19
|
+
*/
|
|
20
|
+
class FakeDriven implements Pico8Process {
|
|
21
|
+
private stdoutCb: (c: string) => void = () => {};
|
|
22
|
+
private closeCb: (code: number | null) => void = () => {};
|
|
23
|
+
private errorCb: (e: NodeJS.ErrnoException) => void = () => {};
|
|
24
|
+
killed = false;
|
|
25
|
+
/** Every block written to stdin, decoded back into commands for assertions. */
|
|
26
|
+
readonly writes: Uint8Array[] = [];
|
|
27
|
+
|
|
28
|
+
onStdout(cb: (c: string) => void): void {
|
|
29
|
+
this.stdoutCb = cb;
|
|
30
|
+
}
|
|
31
|
+
onClose(cb: (code: number | null) => void): void {
|
|
32
|
+
this.closeCb = cb;
|
|
33
|
+
}
|
|
34
|
+
onError(cb: (e: NodeJS.ErrnoException) => void): void {
|
|
35
|
+
this.errorCb = cb;
|
|
36
|
+
}
|
|
37
|
+
writeStdin(bytes: Uint8Array): void {
|
|
38
|
+
this.writes.push(bytes);
|
|
39
|
+
}
|
|
40
|
+
kill(): void {
|
|
41
|
+
this.killed = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- test drivers ---
|
|
45
|
+
/** Emit a cart ACK line (the handshake's other half). */
|
|
46
|
+
ack(line: string): void {
|
|
47
|
+
this.stdoutCb(`${line}\n`);
|
|
48
|
+
}
|
|
49
|
+
/** Simulate PICO-8 exiting (crash/quit). */
|
|
50
|
+
die(): void {
|
|
51
|
+
this.closeCb(0);
|
|
52
|
+
}
|
|
53
|
+
/** The commands the session wrote so far (decoded from the fixed-size blocks). */
|
|
54
|
+
get commands() {
|
|
55
|
+
const dec = new BlockDecoder();
|
|
56
|
+
const all: number[] = [];
|
|
57
|
+
for (const w of this.writes) for (const b of w) all.push(b);
|
|
58
|
+
return dec.push(all);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const session = (proc: FakeDriven, ackTimeoutMs = 1000): DriveSession =>
|
|
63
|
+
new DriveSession({process: proc, shotBasename: 'play', ackTimeoutMs});
|
|
64
|
+
|
|
65
|
+
describe('DriveSession: the send -> wait-ack -> return handshake', () => {
|
|
66
|
+
it('input WRITES a fixed-size INPUT block and returns only after its ACK', async () => {
|
|
67
|
+
const proc = new FakeDriven();
|
|
68
|
+
const s = session(proc);
|
|
69
|
+
const p = s.input(0b10010); // hold Right + O
|
|
70
|
+
|
|
71
|
+
// The command was written as exactly one fixed-size block, and the call has
|
|
72
|
+
// NOT resolved yet (it is waiting for the ACK).
|
|
73
|
+
expect(proc.writes).toHaveLength(1);
|
|
74
|
+
expect(proc.writes[0]!.length).toBe(BLOCK_SIZE);
|
|
75
|
+
expect(proc.commands).toEqual([{op: 'input', held: 0b10010}]);
|
|
76
|
+
|
|
77
|
+
proc.ack(ACK.input);
|
|
78
|
+
const r = await p;
|
|
79
|
+
expect(r.ack).toBe(ACK.input);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('step advances the frame count only after the STEP-DONE ack (settled frame)', async () => {
|
|
83
|
+
const proc = new FakeDriven();
|
|
84
|
+
const s = session(proc);
|
|
85
|
+
const p = s.step(5);
|
|
86
|
+
expect(proc.commands).toEqual([{op: 'step', frames: 5}]);
|
|
87
|
+
|
|
88
|
+
// The step-complete ACK (budget drained) is what resolves it, NOT read time.
|
|
89
|
+
proc.ack(ACK.stepDone);
|
|
90
|
+
const r = await p;
|
|
91
|
+
expect(r.frame).toBe(5);
|
|
92
|
+
expect(r.ack).toBe(ACK.stepDone);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('an ACK that races AHEAD of the wait is not lost (queued)', async () => {
|
|
96
|
+
const proc = new FakeDriven();
|
|
97
|
+
const s = session(proc);
|
|
98
|
+
// Emit the ACK synchronously right after the write, before the race parks.
|
|
99
|
+
const p = s.step(3);
|
|
100
|
+
proc.ack(ACK.stepDone);
|
|
101
|
+
const r = await p;
|
|
102
|
+
expect(r.frame).toBe(3);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('shot returns the basename+index it queued, after the SHOT ack', async () => {
|
|
106
|
+
const proc = new FakeDriven();
|
|
107
|
+
const s = session(proc);
|
|
108
|
+
const p = s.shot();
|
|
109
|
+
expect(proc.commands).toEqual([{op: 'shot'}]);
|
|
110
|
+
proc.ack(ACK.shot);
|
|
111
|
+
const r = await p;
|
|
112
|
+
expect(r.shotName).toBe('play0');
|
|
113
|
+
// A second shot increments the index.
|
|
114
|
+
const p2 = s.shot();
|
|
115
|
+
proc.ack(ACK.shot);
|
|
116
|
+
expect((await p2).shotName).toBe('play1');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('steps are lossless across turns (the frame tally accumulates)', async () => {
|
|
120
|
+
const proc = new FakeDriven();
|
|
121
|
+
const s = session(proc);
|
|
122
|
+
const p1 = s.step(4);
|
|
123
|
+
proc.ack(ACK.stepDone);
|
|
124
|
+
await p1;
|
|
125
|
+
const p2 = s.step(6);
|
|
126
|
+
proc.ack(ACK.stepDone);
|
|
127
|
+
await p2;
|
|
128
|
+
expect(s.frame).toBe(10);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('DriveSession: the lost/late-ack path (never a hang)', () => {
|
|
133
|
+
it('a missing ACK within the deadline is a structured ack-timeout error', async () => {
|
|
134
|
+
vi.useFakeTimers();
|
|
135
|
+
try {
|
|
136
|
+
const proc = new FakeDriven();
|
|
137
|
+
const s = session(proc, 500);
|
|
138
|
+
const p = s.step(1).then(
|
|
139
|
+
() => 'resolved',
|
|
140
|
+
(e) => e,
|
|
141
|
+
);
|
|
142
|
+
// No ACK is ever emitted; advance past the deadline.
|
|
143
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
144
|
+
const outcome = await p;
|
|
145
|
+
expect(outcome).toBeInstanceOf(SessionError);
|
|
146
|
+
expect((outcome as SessionError).code).toBe(
|
|
147
|
+
'playtest-session-ack-timeout',
|
|
148
|
+
);
|
|
149
|
+
} finally {
|
|
150
|
+
vi.useRealTimers();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('a LATE ack (after the deadline) still does not resolve the timed-out call', async () => {
|
|
155
|
+
vi.useFakeTimers();
|
|
156
|
+
try {
|
|
157
|
+
const proc = new FakeDriven();
|
|
158
|
+
const s = session(proc, 300);
|
|
159
|
+
const p = s.step(1).then(
|
|
160
|
+
() => 'resolved',
|
|
161
|
+
(e) => (e as SessionError).code,
|
|
162
|
+
);
|
|
163
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
164
|
+
proc.ack(ACK.stepDone); // arrives too late
|
|
165
|
+
expect(await p).toBe('playtest-session-ack-timeout');
|
|
166
|
+
} finally {
|
|
167
|
+
vi.useRealTimers();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('DriveSession: PICO-8 dying mid-session (structured, not a hang)', () => {
|
|
173
|
+
it('a process death while a command is in flight rejects with process-dead', async () => {
|
|
174
|
+
const proc = new FakeDriven();
|
|
175
|
+
const s = session(proc);
|
|
176
|
+
const p = s.step(10).then(
|
|
177
|
+
() => 'resolved',
|
|
178
|
+
(e) => (e as SessionError).code,
|
|
179
|
+
);
|
|
180
|
+
proc.die(); // PICO-8 exits before the ACK
|
|
181
|
+
expect(await p).toBe('playtest-session-process-dead');
|
|
182
|
+
expect(s.isDead).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('a command issued AFTER the process died is a structured process-dead', async () => {
|
|
186
|
+
const proc = new FakeDriven();
|
|
187
|
+
const s = session(proc);
|
|
188
|
+
proc.die();
|
|
189
|
+
await expect(s.step(1)).rejects.toMatchObject({
|
|
190
|
+
code: 'playtest-session-process-dead',
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('DriveSession: stop (teardown)', () => {
|
|
196
|
+
it('writes a QUIT block, kills the process, and closes the session', async () => {
|
|
197
|
+
const proc = new FakeDriven();
|
|
198
|
+
const s = session(proc);
|
|
199
|
+
s.stop();
|
|
200
|
+
expect(proc.commands).toEqual([{op: 'quit'}]);
|
|
201
|
+
expect(proc.killed).toBe(true);
|
|
202
|
+
// A verb after stop is a structured closed error, not a hang.
|
|
203
|
+
await expect(s.input(1)).rejects.toMatchObject({
|
|
204
|
+
code: 'playtest-session-closed',
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('is idempotent + safe on an already-dead process', () => {
|
|
209
|
+
const proc = new FakeDriven();
|
|
210
|
+
const s = session(proc);
|
|
211
|
+
proc.die();
|
|
212
|
+
expect(() => {
|
|
213
|
+
s.stop();
|
|
214
|
+
s.stop();
|
|
215
|
+
}).not.toThrow();
|
|
216
|
+
expect(proc.killed).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('DriveSession: every command is a FIXED-SIZE block (the coalesce guard)', () => {
|
|
221
|
+
it('input/step/shot/quit each write exactly BLOCK_SIZE bytes', async () => {
|
|
222
|
+
const proc = new FakeDriven();
|
|
223
|
+
const s = session(proc);
|
|
224
|
+
const pi = s.input(1);
|
|
225
|
+
proc.ack(ACK.input);
|
|
226
|
+
await pi;
|
|
227
|
+
const ps = s.step(2);
|
|
228
|
+
proc.ack(ACK.stepDone);
|
|
229
|
+
await ps;
|
|
230
|
+
const psh = s.shot();
|
|
231
|
+
proc.ack(ACK.shot);
|
|
232
|
+
await psh;
|
|
233
|
+
for (const w of proc.writes) expect(w.length).toBe(BLOCK_SIZE);
|
|
234
|
+
// And the opcodes are the tagged protocol (not raw wall-clock writes).
|
|
235
|
+
expect(proc.writes[0]![0]).toBe(OPCODE.input);
|
|
236
|
+
expect(proc.writes[1]![0]).toBe(OPCODE.step);
|
|
237
|
+
expect(proc.writes[2]![0]).toBe(OPCODE.shot);
|
|
238
|
+
});
|
|
239
|
+
});
|