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,583 @@
|
|
|
1
|
+
import {mkdtempSync, rmSync, writeFileSync} from 'node:fs';
|
|
2
|
+
import {tmpdir} from 'node:os';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
5
|
+
import {DONE_SENTINEL, RECORD_WAV_BASENAME} from './adapter.js';
|
|
6
|
+
import {
|
|
7
|
+
type Pico8Process,
|
|
8
|
+
pico8HomeDir,
|
|
9
|
+
ShellPico8Adapter,
|
|
10
|
+
type SpawnRunner,
|
|
11
|
+
withPico8Home,
|
|
12
|
+
} from './shell.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The shell adapter's orchestration (launch, sentinel-kill, backstop, collect)
|
|
16
|
+
* is exercised with a FAKE {@link SpawnRunner}, so every path is covered WITHOUT
|
|
17
|
+
* the paid PICO-8 binary. Live runs against a real PICO-8 are a manual/opt-in
|
|
18
|
+
* tier (mirroring the shrinko runner-absent discipline).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A controllable fake PICO-8 process: the test drives it by emitting stdout,
|
|
23
|
+
* closing, or erroring, and records whether it was killed.
|
|
24
|
+
*/
|
|
25
|
+
class FakeProcess implements Pico8Process {
|
|
26
|
+
private stdoutCb: (c: string) => void = () => {};
|
|
27
|
+
private closeCb: (code: number | null) => void = () => {};
|
|
28
|
+
private errorCb: (e: NodeJS.ErrnoException) => void = () => {};
|
|
29
|
+
killed = false;
|
|
30
|
+
/** The bytes written to stdin (the `drive` command-block transport). */
|
|
31
|
+
stdinBytes: number[] = [];
|
|
32
|
+
|
|
33
|
+
onStdout(cb: (c: string) => void): void {
|
|
34
|
+
this.stdoutCb = cb;
|
|
35
|
+
}
|
|
36
|
+
onClose(cb: (code: number | null) => void): void {
|
|
37
|
+
this.closeCb = cb;
|
|
38
|
+
}
|
|
39
|
+
onError(cb: (e: NodeJS.ErrnoException) => void): void {
|
|
40
|
+
this.errorCb = cb;
|
|
41
|
+
}
|
|
42
|
+
writeStdin(bytes: Uint8Array): void {
|
|
43
|
+
for (const b of bytes) this.stdinBytes.push(b);
|
|
44
|
+
}
|
|
45
|
+
kill(): void {
|
|
46
|
+
this.killed = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Test drivers:
|
|
50
|
+
emit(chunk: string): void {
|
|
51
|
+
this.stdoutCb(chunk);
|
|
52
|
+
}
|
|
53
|
+
close(code: number | null = 0): void {
|
|
54
|
+
this.closeCb(code);
|
|
55
|
+
}
|
|
56
|
+
fail(code: string): void {
|
|
57
|
+
const err = new Error(code) as NodeJS.ErrnoException;
|
|
58
|
+
err.code = code;
|
|
59
|
+
this.errorCb(err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('ShellPico8Adapter: PICO-8 absent (the CI-testable boundary)', () => {
|
|
64
|
+
it('returns structured pico8-not-found when the binary ENOENTs (never a crash/hang)', async () => {
|
|
65
|
+
const proc = new FakeProcess();
|
|
66
|
+
const spawn: SpawnRunner = () => proc;
|
|
67
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn});
|
|
68
|
+
|
|
69
|
+
const resultP = adapter.run({
|
|
70
|
+
cartPath: '/x/main.p8',
|
|
71
|
+
shotDir: '/x/shots',
|
|
72
|
+
backstopMs: 1000,
|
|
73
|
+
});
|
|
74
|
+
proc.fail('ENOENT'); // spawn failure = binary not installed
|
|
75
|
+
const result = await resultP;
|
|
76
|
+
|
|
77
|
+
expect(result.ok).toBe(false);
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
expect(result.reason).toBe('pico8-not-found');
|
|
80
|
+
expect(result.remedy).toBe('set PICO8_PATH or install PICO-8');
|
|
81
|
+
expect(result.needs).toContain('pico8');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('ShellPico8Adapter: sentinel-driven termination', () => {
|
|
87
|
+
let shotDir: string;
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
shotDir = mkdtempSync(join(tmpdir(), 'pico8-shots-'));
|
|
90
|
+
});
|
|
91
|
+
afterEach(() => rmSync(shotDir, {recursive: true, force: true}));
|
|
92
|
+
|
|
93
|
+
it('kills PICO-8 the moment the sentinel is printed (exitReason: sentinel)', async () => {
|
|
94
|
+
const proc = new FakeProcess();
|
|
95
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
96
|
+
const resultP = adapter.run({
|
|
97
|
+
cartPath: '/x/main.p8',
|
|
98
|
+
shotDir,
|
|
99
|
+
backstopMs: 10_000, // long: prove the sentinel, not the backstop, ends it
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
proc.emit('RUNNING:\n');
|
|
103
|
+
proc.emit(`${DONE_SENTINEL}\n`); // the cart signals done
|
|
104
|
+
|
|
105
|
+
const result = await resultP;
|
|
106
|
+
expect(result.ok).toBe(true);
|
|
107
|
+
if (result.ok) expect(result.value.exitReason).toBe('sentinel');
|
|
108
|
+
expect(proc.killed).toBe(true); // killed promptly, not after the backstop
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('passes --input through as `-p <input>` in the spawn args (playtest channel)', async () => {
|
|
112
|
+
const proc = new FakeProcess();
|
|
113
|
+
let spawnArgs: string[] = [];
|
|
114
|
+
const spawn: SpawnRunner = (_file, args) => {
|
|
115
|
+
spawnArgs = args;
|
|
116
|
+
return proc;
|
|
117
|
+
};
|
|
118
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn});
|
|
119
|
+
const resultP = adapter.run({
|
|
120
|
+
cartPath: '/x/m.p8',
|
|
121
|
+
shotDir,
|
|
122
|
+
backstopMs: 9999,
|
|
123
|
+
input: 'rrrrz',
|
|
124
|
+
});
|
|
125
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
126
|
+
await resultP;
|
|
127
|
+
expect(spawnArgs).toContain('-p');
|
|
128
|
+
expect(spawnArgs[spawnArgs.indexOf('-p') + 1]).toBe('rrrrz');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('prepends `-home <isolated tmp dir>` so PICO-8 config/data never litters the CWD', async () => {
|
|
132
|
+
const proc = new FakeProcess();
|
|
133
|
+
let spawnArgs: string[] = [];
|
|
134
|
+
const spawn: SpawnRunner = (_file, args) => {
|
|
135
|
+
spawnArgs = args;
|
|
136
|
+
return proc;
|
|
137
|
+
};
|
|
138
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn});
|
|
139
|
+
const resultP = adapter.run({
|
|
140
|
+
cartPath: '/x/m.p8',
|
|
141
|
+
shotDir,
|
|
142
|
+
backstopMs: 9999,
|
|
143
|
+
});
|
|
144
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
145
|
+
await resultP;
|
|
146
|
+
expect(spawnArgs).toContain('-home');
|
|
147
|
+
const home = spawnArgs[spawnArgs.indexOf('-home') + 1];
|
|
148
|
+
// Points at an isolated temp dir, NOT the process CWD.
|
|
149
|
+
expect(home.startsWith(tmpdir())).toBe(true);
|
|
150
|
+
expect(home).not.toBe(process.cwd());
|
|
151
|
+
// The pure helper is stable and prepends, not appends.
|
|
152
|
+
expect(withPico8Home(['-x', 'c.p8'])).toEqual([
|
|
153
|
+
'-home',
|
|
154
|
+
pico8HomeDir(),
|
|
155
|
+
'-x',
|
|
156
|
+
'c.p8',
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('omits `-p` when no input is given', async () => {
|
|
161
|
+
const proc = new FakeProcess();
|
|
162
|
+
let spawnArgs: string[] = [];
|
|
163
|
+
const spawn: SpawnRunner = (_file, args) => {
|
|
164
|
+
spawnArgs = args;
|
|
165
|
+
return proc;
|
|
166
|
+
};
|
|
167
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn});
|
|
168
|
+
const resultP = adapter.run({
|
|
169
|
+
cartPath: '/x/m.p8',
|
|
170
|
+
shotDir,
|
|
171
|
+
backstopMs: 9999,
|
|
172
|
+
});
|
|
173
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
174
|
+
await resultP;
|
|
175
|
+
expect(spawnArgs).not.toContain('-p');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('captures printh stdout in the report', async () => {
|
|
179
|
+
const proc = new FakeProcess();
|
|
180
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
181
|
+
const resultP = adapter.run({
|
|
182
|
+
cartPath: '/x/m.p8',
|
|
183
|
+
shotDir,
|
|
184
|
+
backstopMs: 9999,
|
|
185
|
+
});
|
|
186
|
+
proc.emit('score=42\n');
|
|
187
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
188
|
+
const result = await resultP;
|
|
189
|
+
if (result.ok) expect(result.value.printh).toContain('score=42');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('collects the PNG screenshots the run produced, sorted', async () => {
|
|
193
|
+
const proc = new FakeProcess();
|
|
194
|
+
writeFileSync(join(shotDir, 'frame_1.png'), 'x');
|
|
195
|
+
writeFileSync(join(shotDir, 'frame_0.png'), 'x');
|
|
196
|
+
writeFileSync(join(shotDir, 'notes.txt'), 'ignore me');
|
|
197
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
198
|
+
const resultP = adapter.run({
|
|
199
|
+
cartPath: '/x/m.p8',
|
|
200
|
+
shotDir,
|
|
201
|
+
backstopMs: 9999,
|
|
202
|
+
});
|
|
203
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
204
|
+
const result = await resultP;
|
|
205
|
+
if (result.ok) {
|
|
206
|
+
expect(result.value.screenshots.map((p) => p.split('/').pop())).toEqual([
|
|
207
|
+
'frame_0.png',
|
|
208
|
+
'frame_1.png',
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('ShellPico8Adapter: backstop + natural exit', () => {
|
|
215
|
+
it('fires the backstop (exitReason: timeout) when the cart never signals', async () => {
|
|
216
|
+
const proc = new FakeProcess();
|
|
217
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
218
|
+
const shotDir = mkdtempSync(join(tmpdir(), 'pico8-shots-'));
|
|
219
|
+
try {
|
|
220
|
+
const resultP = adapter.run({
|
|
221
|
+
cartPath: '/x/m.p8',
|
|
222
|
+
shotDir,
|
|
223
|
+
backstopMs: 20, // short: the backstop is the only terminator here
|
|
224
|
+
});
|
|
225
|
+
proc.emit('RUNNING:\n'); // ... and then nothing (a hung cart)
|
|
226
|
+
const result = await resultP;
|
|
227
|
+
expect(result.ok).toBe(true);
|
|
228
|
+
if (result.ok) expect(result.value.exitReason).toBe('timeout');
|
|
229
|
+
expect(proc.killed).toBe(true);
|
|
230
|
+
} finally {
|
|
231
|
+
rmSync(shotDir, {recursive: true, force: true});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('reports exitReason: exit when PICO-8 closes on its own', async () => {
|
|
236
|
+
const proc = new FakeProcess();
|
|
237
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
238
|
+
const shotDir = mkdtempSync(join(tmpdir(), 'pico8-shots-'));
|
|
239
|
+
try {
|
|
240
|
+
const resultP = adapter.run({
|
|
241
|
+
cartPath: '/x/m.p8',
|
|
242
|
+
shotDir,
|
|
243
|
+
backstopMs: 9999,
|
|
244
|
+
});
|
|
245
|
+
proc.close(0); // PICO-8 exited before any sentinel
|
|
246
|
+
const result = await resultP;
|
|
247
|
+
if (result.ok) expect(result.value.exitReason).toBe('exit');
|
|
248
|
+
} finally {
|
|
249
|
+
rmSync(shotDir, {recursive: true, force: true});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/** A valid (non-empty) RIFF/WAVE payload: header + a few PCM sample bytes. */
|
|
255
|
+
function writeNonEmptyWav(path: string): void {
|
|
256
|
+
// 44-byte header + 8 bytes of data so statSync(path).size > 44 (a real capture).
|
|
257
|
+
writeFileSync(path, Buffer.concat([Buffer.alloc(44), Buffer.alloc(8, 1)]));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
describe('ShellPico8Adapter.record: PICO-8 absent (the CI-testable boundary)', () => {
|
|
261
|
+
it('returns structured pico8-not-found when the binary ENOENTs', async () => {
|
|
262
|
+
const proc = new FakeProcess();
|
|
263
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
264
|
+
const resultP = adapter.record({
|
|
265
|
+
cartPath: '/x/main.p8',
|
|
266
|
+
wavDir: '/x/wav',
|
|
267
|
+
backstopMs: 1000,
|
|
268
|
+
});
|
|
269
|
+
proc.fail('ENOENT');
|
|
270
|
+
const result = await resultP;
|
|
271
|
+
expect(result.ok).toBe(false);
|
|
272
|
+
if (!result.ok) {
|
|
273
|
+
expect(result.reason).toBe('pico8-not-found');
|
|
274
|
+
expect(result.remedy).toBe('set PICO8_PATH or install PICO-8');
|
|
275
|
+
expect(result.needs).toContain('pico8');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('ShellPico8Adapter.record: real A/V session + WAV isolation', () => {
|
|
281
|
+
let wavDir: string;
|
|
282
|
+
beforeEach(() => {
|
|
283
|
+
wavDir = mkdtempSync(join(tmpdir(), 'pico8-wav-'));
|
|
284
|
+
});
|
|
285
|
+
afterEach(() => rmSync(wavDir, {recursive: true, force: true}));
|
|
286
|
+
|
|
287
|
+
it('launches with `-run` (real A/V), NOT headless `-x`, and `-root_path <wavDir>`', async () => {
|
|
288
|
+
const proc = new FakeProcess();
|
|
289
|
+
let spawnArgs: string[] = [];
|
|
290
|
+
const spawn: SpawnRunner = (_file, args) => {
|
|
291
|
+
spawnArgs = args;
|
|
292
|
+
return proc;
|
|
293
|
+
};
|
|
294
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn});
|
|
295
|
+
const resultP = adapter.record({
|
|
296
|
+
cartPath: '/x/harness.p8',
|
|
297
|
+
wavDir,
|
|
298
|
+
backstopMs: 9999,
|
|
299
|
+
});
|
|
300
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
301
|
+
await resultP;
|
|
302
|
+
// The record session must be a real A/V run (recording needs audio to
|
|
303
|
+
// capture; `-x` yields an empty WAV, ADR-0009).
|
|
304
|
+
expect(spawnArgs).toContain('-run');
|
|
305
|
+
expect(spawnArgs).not.toContain('-x');
|
|
306
|
+
// The WAV location is steered to the isolated dir via root_path (the
|
|
307
|
+
// audio_end(1)-to-current-folder quirk), never ~/Desktop.
|
|
308
|
+
expect(spawnArgs).toContain('-root_path');
|
|
309
|
+
expect(spawnArgs[spawnArgs.indexOf('-root_path') + 1]).toBe(wavDir);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('collects the <basename>.wav the cooperating cart wrote (sentinel-ended)', async () => {
|
|
313
|
+
const proc = new FakeProcess();
|
|
314
|
+
writeNonEmptyWav(join(wavDir, `${RECORD_WAV_BASENAME}.wav`));
|
|
315
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
316
|
+
const resultP = adapter.record({
|
|
317
|
+
cartPath: '/x/harness.p8',
|
|
318
|
+
wavDir,
|
|
319
|
+
backstopMs: 9999,
|
|
320
|
+
});
|
|
321
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
322
|
+
const result = await resultP;
|
|
323
|
+
expect(result.ok).toBe(true);
|
|
324
|
+
if (result.ok) {
|
|
325
|
+
expect(result.value.wavPath).toBe(
|
|
326
|
+
join(wavDir, `${RECORD_WAV_BASENAME}.wav`),
|
|
327
|
+
);
|
|
328
|
+
expect(result.value.exitReason).toBe('sentinel');
|
|
329
|
+
}
|
|
330
|
+
expect(proc.killed).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('treats a 44-byte (0-frame) WAV as NO usable audio (the headless empty-WAV trap)', async () => {
|
|
334
|
+
const proc = new FakeProcess();
|
|
335
|
+
// A well-formed but EMPTY RIFF header (what headless -x produces).
|
|
336
|
+
writeFileSync(join(wavDir, `${RECORD_WAV_BASENAME}.wav`), Buffer.alloc(44));
|
|
337
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
338
|
+
const resultP = adapter.record({
|
|
339
|
+
cartPath: '/x/harness.p8',
|
|
340
|
+
wavDir,
|
|
341
|
+
backstopMs: 9999,
|
|
342
|
+
});
|
|
343
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
344
|
+
const result = await resultP;
|
|
345
|
+
if (result.ok) expect(result.value.wavPath).toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('reports no WAV when none was produced', async () => {
|
|
349
|
+
const proc = new FakeProcess();
|
|
350
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
351
|
+
const resultP = adapter.record({
|
|
352
|
+
cartPath: '/x/harness.p8',
|
|
353
|
+
wavDir,
|
|
354
|
+
backstopMs: 9999,
|
|
355
|
+
});
|
|
356
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
357
|
+
const result = await resultP;
|
|
358
|
+
if (result.ok) expect(result.value.wavPath).toBeUndefined();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('fires the record backstop when the cart never signals (never a hang)', async () => {
|
|
362
|
+
const proc = new FakeProcess();
|
|
363
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
364
|
+
const resultP = adapter.record({
|
|
365
|
+
cartPath: '/x/harness.p8',
|
|
366
|
+
wavDir,
|
|
367
|
+
backstopMs: 20,
|
|
368
|
+
});
|
|
369
|
+
const result = await resultP;
|
|
370
|
+
if (result.ok) expect(result.value.exitReason).toBe('timeout');
|
|
371
|
+
expect(proc.killed).toBe(true);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('ShellPico8Adapter.drive: PICO-8 absent (the CI-testable boundary)', () => {
|
|
376
|
+
it('returns structured pico8-not-found when the binary ENOENTs', async () => {
|
|
377
|
+
const proc = new FakeProcess();
|
|
378
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
379
|
+
const resultP = adapter.drive({
|
|
380
|
+
cartPath: '/x/driven.p8',
|
|
381
|
+
shotDir: '/x/shots',
|
|
382
|
+
blocks: new Uint8Array([1, 0, 0, 0]),
|
|
383
|
+
backstopMs: 1000,
|
|
384
|
+
});
|
|
385
|
+
proc.fail('ENOENT');
|
|
386
|
+
const result = await resultP;
|
|
387
|
+
expect(result.ok).toBe(false);
|
|
388
|
+
if (!result.ok) {
|
|
389
|
+
expect(result.reason).toBe('pico8-not-found');
|
|
390
|
+
expect(result.needs).toContain('pico8');
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe('ShellPico8Adapter.drive: headless capture + block transport', () => {
|
|
396
|
+
let shotDir: string;
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
shotDir = mkdtempSync(join(tmpdir(), 'pico8-drive-'));
|
|
399
|
+
});
|
|
400
|
+
afterEach(() => rmSync(shotDir, {recursive: true, force: true}));
|
|
401
|
+
|
|
402
|
+
it('launches headless `-x` + `-desktop <shotDir>` (never a real ~/Desktop write)', async () => {
|
|
403
|
+
const proc = new FakeProcess();
|
|
404
|
+
let spawnArgs: string[] = [];
|
|
405
|
+
let stdinRequested = false;
|
|
406
|
+
const spawn: SpawnRunner = (_file, args, _env, options) => {
|
|
407
|
+
spawnArgs = args;
|
|
408
|
+
stdinRequested = options?.stdin === true;
|
|
409
|
+
return proc;
|
|
410
|
+
};
|
|
411
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn});
|
|
412
|
+
const resultP = adapter.drive({
|
|
413
|
+
cartPath: '/x/driven.p8',
|
|
414
|
+
shotDir,
|
|
415
|
+
blocks: new Uint8Array([2, 16, 0, 0]),
|
|
416
|
+
backstopMs: 9999,
|
|
417
|
+
});
|
|
418
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
419
|
+
await resultP;
|
|
420
|
+
expect(spawnArgs).toContain('-x');
|
|
421
|
+
expect(spawnArgs).toContain('-desktop');
|
|
422
|
+
expect(spawnArgs[spawnArgs.indexOf('-desktop') + 1]).toBe(shotDir);
|
|
423
|
+
// The driven run needs a LIVE stdin to pipe the command blocks into.
|
|
424
|
+
expect(stdinRequested).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('writes the whole command-block stream to the cart stdin (one-shot up front)', async () => {
|
|
428
|
+
const proc = new FakeProcess();
|
|
429
|
+
const blocks = new Uint8Array([2, 16, 0, 0, 1, 1, 0, 0, 5, 0, 0, 0]);
|
|
430
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
431
|
+
const resultP = adapter.drive({
|
|
432
|
+
cartPath: '/x/driven.p8',
|
|
433
|
+
shotDir,
|
|
434
|
+
blocks,
|
|
435
|
+
backstopMs: 9999,
|
|
436
|
+
});
|
|
437
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
438
|
+
await resultP;
|
|
439
|
+
// Every byte of the block stream reached stdin, in order (fixed-size blocks).
|
|
440
|
+
expect(proc.stdinBytes).toEqual([...blocks]);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('collects the SHOT screenshots + ends on the sentinel', async () => {
|
|
444
|
+
const proc = new FakeProcess();
|
|
445
|
+
writeFileSync(join(shotDir, 'play1.png'), 'x');
|
|
446
|
+
writeFileSync(join(shotDir, 'play0.png'), 'x');
|
|
447
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
448
|
+
const resultP = adapter.drive({
|
|
449
|
+
cartPath: '/x/driven.p8',
|
|
450
|
+
shotDir,
|
|
451
|
+
blocks: new Uint8Array([5, 0, 0, 0]),
|
|
452
|
+
backstopMs: 9999,
|
|
453
|
+
});
|
|
454
|
+
proc.emit(`${DONE_SENTINEL}\n`);
|
|
455
|
+
const result = await resultP;
|
|
456
|
+
expect(result.ok).toBe(true);
|
|
457
|
+
if (result.ok) {
|
|
458
|
+
expect(result.value.screenshots.map((p) => p.split('/').pop())).toEqual([
|
|
459
|
+
'play0.png',
|
|
460
|
+
'play1.png',
|
|
461
|
+
]);
|
|
462
|
+
expect(result.value.exitReason).toBe('sentinel');
|
|
463
|
+
}
|
|
464
|
+
expect(proc.killed).toBe(true);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('fires the drive backstop when the driven cart never signals (never a hang)', async () => {
|
|
468
|
+
const proc = new FakeProcess();
|
|
469
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
470
|
+
const resultP = adapter.drive({
|
|
471
|
+
cartPath: '/x/driven.p8',
|
|
472
|
+
shotDir,
|
|
473
|
+
blocks: new Uint8Array([1, 1, 0, 0]),
|
|
474
|
+
backstopMs: 20,
|
|
475
|
+
});
|
|
476
|
+
const result = await resultP;
|
|
477
|
+
if (result.ok) expect(result.value.exitReason).toBe('timeout');
|
|
478
|
+
expect(proc.killed).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('ShellPico8Adapter.export: PICO-8 absent (the CI-testable boundary)', () => {
|
|
483
|
+
it('returns structured pico8-not-found when the binary ENOENTs', async () => {
|
|
484
|
+
const proc = new FakeProcess();
|
|
485
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
486
|
+
const resultP = adapter.export({
|
|
487
|
+
cartPath: '/x/main.p8',
|
|
488
|
+
outDir: '/x/out',
|
|
489
|
+
backstopMs: 1000,
|
|
490
|
+
});
|
|
491
|
+
proc.fail('ENOENT');
|
|
492
|
+
const result = await resultP;
|
|
493
|
+
expect(result.ok).toBe(false);
|
|
494
|
+
if (!result.ok) {
|
|
495
|
+
expect(result.reason).toBe('pico8-not-found');
|
|
496
|
+
expect(result.remedy).toBe('set PICO8_PATH or install PICO-8');
|
|
497
|
+
expect(result.needs).toContain('pico8');
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe('ShellPico8Adapter.export: the headless -export -x invocation', () => {
|
|
503
|
+
let outDir: string;
|
|
504
|
+
beforeEach(() => {
|
|
505
|
+
outDir = mkdtempSync(join(tmpdir(), 'pico8-export-'));
|
|
506
|
+
});
|
|
507
|
+
afterEach(() => rmSync(outDir, {recursive: true, force: true}));
|
|
508
|
+
|
|
509
|
+
it('invokes `-export <outDir>/index.html -x <cart>` (headless, no display)', async () => {
|
|
510
|
+
const proc = new FakeProcess();
|
|
511
|
+
let spawnArgs: string[] = [];
|
|
512
|
+
const spawn: SpawnRunner = (_file, args) => {
|
|
513
|
+
spawnArgs = args;
|
|
514
|
+
return proc;
|
|
515
|
+
};
|
|
516
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn});
|
|
517
|
+
const resultP = adapter.export({
|
|
518
|
+
cartPath: '/x/game.p8',
|
|
519
|
+
outDir,
|
|
520
|
+
htmlName: 'index.html',
|
|
521
|
+
backstopMs: 9999,
|
|
522
|
+
});
|
|
523
|
+
proc.close(0); // -x exits on its own once the export is written
|
|
524
|
+
await resultP;
|
|
525
|
+
expect(spawnArgs).toContain('-export');
|
|
526
|
+
expect(spawnArgs[spawnArgs.indexOf('-export') + 1]).toBe(
|
|
527
|
+
join(outDir, 'index.html'),
|
|
528
|
+
);
|
|
529
|
+
expect(spawnArgs).toContain('-x'); // headless
|
|
530
|
+
expect(spawnArgs).toContain('/x/game.p8');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('collects the html + js pair PICO-8 produced', async () => {
|
|
534
|
+
const proc = new FakeProcess();
|
|
535
|
+
writeFileSync(join(outDir, 'index.html'), '<html></html>');
|
|
536
|
+
writeFileSync(join(outDir, 'index.js'), '// runtime');
|
|
537
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
538
|
+
const resultP = adapter.export({
|
|
539
|
+
cartPath: '/x/game.p8',
|
|
540
|
+
outDir,
|
|
541
|
+
backstopMs: 9999,
|
|
542
|
+
});
|
|
543
|
+
proc.close(0);
|
|
544
|
+
const result = await resultP;
|
|
545
|
+
expect(result.ok).toBe(true);
|
|
546
|
+
if (result.ok) {
|
|
547
|
+
expect(result.value.htmlPath?.endsWith('index.html')).toBe(true);
|
|
548
|
+
expect(result.value.jsPath?.endsWith('index.js')).toBe(true);
|
|
549
|
+
expect(result.value.files.length).toBe(2);
|
|
550
|
+
expect(result.value.labelWarning).toBe(false);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('flags labelWarning when PICO-8 prints "please capture a label first"', async () => {
|
|
555
|
+
const proc = new FakeProcess();
|
|
556
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
557
|
+
const resultP = adapter.export({
|
|
558
|
+
cartPath: '/x/game.p8',
|
|
559
|
+
outDir,
|
|
560
|
+
backstopMs: 9999,
|
|
561
|
+
});
|
|
562
|
+
proc.emit('EXPORT: index.html\nplease capture a label first\n');
|
|
563
|
+
proc.close(0);
|
|
564
|
+
const result = await resultP;
|
|
565
|
+
if (result.ok) expect(result.value.labelWarning).toBe(true);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('reports an undefined jsPath when the export wrote nothing', async () => {
|
|
569
|
+
const proc = new FakeProcess();
|
|
570
|
+
const adapter = new ShellPico8Adapter({env: {}, spawn: () => proc});
|
|
571
|
+
const resultP = adapter.export({
|
|
572
|
+
cartPath: '/x/game.p8',
|
|
573
|
+
outDir, // stays empty
|
|
574
|
+
backstopMs: 9999,
|
|
575
|
+
});
|
|
576
|
+
proc.close(0);
|
|
577
|
+
const result = await resultP;
|
|
578
|
+
if (result.ok) {
|
|
579
|
+
expect(result.value.jsPath).toBeUndefined();
|
|
580
|
+
expect(result.value.files.length).toBe(0);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
});
|