picopilot 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +202 -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 +217 -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 +460 -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 +238 -0
- package/src/commands/run.ts +259 -0
- package/src/commands/serve.test.ts +218 -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 +236 -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,317 @@
|
|
|
1
|
+
import {execFileSync} from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import {homedir, tmpdir} from 'node:os';
|
|
10
|
+
import {join} from 'node:path';
|
|
11
|
+
import {Cli} from 'incur';
|
|
12
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
13
|
+
import type {
|
|
14
|
+
DriveOptions,
|
|
15
|
+
Pico8Adapter,
|
|
16
|
+
Pico8DriveResult,
|
|
17
|
+
} from '../engine/pico8/index.js';
|
|
18
|
+
import {BlockDecoder, pico8NotFound} from '../engine/pico8/index.js';
|
|
19
|
+
import {type Pico8AdapterFactory, registerPlaytest} from './playtest.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `picopilot playtest` command wiring tests. Like `run`, they inject a STUB
|
|
23
|
+
* adapter (never spawning the paid PICO-8 binary), so the CI-testable surface is
|
|
24
|
+
* exercised: PICO-8 absent, the structured envelope, --seed threading, input
|
|
25
|
+
* validation, shotDir isolation (never ~/Desktop), and the entry cart being
|
|
26
|
+
* untouched. Live drive-and-capture is the manual/opt-in tier.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const CART_TEXT =
|
|
30
|
+
'pico-8 cartridge // http://www.pico-8.com\nversion 42\n__lua__\n' +
|
|
31
|
+
'x=64\nfunction _update() if btnp(4) then x+=1 end end\n' +
|
|
32
|
+
'function _draw() cls() circfill(x,64,4,8) end\n';
|
|
33
|
+
|
|
34
|
+
let dir: string;
|
|
35
|
+
let cartPath: string;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
dir = mkdtempSync(join(tmpdir(), 'picopilot-playtest-test-'));
|
|
39
|
+
cartPath = join(dir, 'main.p8');
|
|
40
|
+
writeFileSync(cartPath, CART_TEXT);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
try {
|
|
45
|
+
execFileSync('rm', ['-rf', dir]);
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A stub {@link Pico8Adapter} standing in for the shell adapter. It never spawns
|
|
53
|
+
* PICO-8 and RECORDS the {@link DriveOptions} it was handed, so a test can assert
|
|
54
|
+
* the command chose an isolated shotDir + piped the encoded blocks.
|
|
55
|
+
*/
|
|
56
|
+
function stubAdapter(
|
|
57
|
+
result: Pico8DriveResult,
|
|
58
|
+
seen?: {options?: DriveOptions},
|
|
59
|
+
): Pico8Adapter {
|
|
60
|
+
return {
|
|
61
|
+
async run() {
|
|
62
|
+
throw new Error('run not used by playtest');
|
|
63
|
+
},
|
|
64
|
+
async record() {
|
|
65
|
+
throw new Error('record not used by playtest');
|
|
66
|
+
},
|
|
67
|
+
async drive(options) {
|
|
68
|
+
if (seen !== undefined) seen.options = options;
|
|
69
|
+
return result;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function drove(exitReason: 'sentinel' | 'timeout' | 'exit'): Pico8DriveResult {
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
value: {
|
|
78
|
+
screenshots: ['/tmp/play0.png', '/tmp/play1.png'],
|
|
79
|
+
printh: '__PP_ACK_STEP__\n__PICOPILOT_DONE__\n',
|
|
80
|
+
exitReason,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runPlaytest(
|
|
86
|
+
factory: Pico8AdapterFactory,
|
|
87
|
+
argv: string[] = ['playtest', 'run', cartPath, '--json'],
|
|
88
|
+
env: Record<string, string | undefined> = {PATH: '/does/not/matter'},
|
|
89
|
+
) {
|
|
90
|
+
const cli = Cli.create('picopilot', {version: '0.0.0'});
|
|
91
|
+
registerPlaytest(cli, factory);
|
|
92
|
+
let stdout = '';
|
|
93
|
+
let exitCode = 0;
|
|
94
|
+
await cli.serve(argv, {
|
|
95
|
+
stdout(s) {
|
|
96
|
+
stdout += s;
|
|
97
|
+
},
|
|
98
|
+
exit(code) {
|
|
99
|
+
exitCode = code;
|
|
100
|
+
},
|
|
101
|
+
env,
|
|
102
|
+
});
|
|
103
|
+
return {stdout, exitCode};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe('picopilot playtest: PICO-8 absent (the CI-testable boundary)', () => {
|
|
107
|
+
it('returns structured pico8-not-found + nonzero exit (mirrors run, never a crash)', async () => {
|
|
108
|
+
const {stdout, exitCode} = await runPlaytest(() =>
|
|
109
|
+
stubAdapter(pico8NotFound()),
|
|
110
|
+
);
|
|
111
|
+
expect(exitCode).not.toBe(0);
|
|
112
|
+
expect(stdout).toContain('set PICO8_PATH or install PICO-8');
|
|
113
|
+
expect(stdout).toContain('pico8');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('picopilot playtest: structured drive envelope', () => {
|
|
118
|
+
it('reports screenshots + printh + exitReason + steps on a sentinel-ended run', async () => {
|
|
119
|
+
const {stdout, exitCode} = await runPlaytest(() =>
|
|
120
|
+
stubAdapter(drove('sentinel')),
|
|
121
|
+
);
|
|
122
|
+
expect(exitCode).toBe(0);
|
|
123
|
+
const out = JSON.parse(stdout);
|
|
124
|
+
expect(out.exitReason).toBe('sentinel');
|
|
125
|
+
expect(out.screenshots).toHaveLength(2);
|
|
126
|
+
expect(out.steps).toBeGreaterThan(0);
|
|
127
|
+
expect(out.shotCount).toBeGreaterThan(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('a backstop timeout CTAs toward verify/run', async () => {
|
|
131
|
+
const {stdout, exitCode} = await runPlaytest(() =>
|
|
132
|
+
stubAdapter(drove('timeout')),
|
|
133
|
+
);
|
|
134
|
+
expect(exitCode).toBe(0);
|
|
135
|
+
const out = JSON.parse(stdout);
|
|
136
|
+
expect(out.exitReason).toBe('timeout');
|
|
137
|
+
expect(stdout.toLowerCase()).toContain('verify');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('picopilot playtest: --input + --seed threading', () => {
|
|
142
|
+
it('parses an explicit --input into the driven blocks (right-nudge present)', async () => {
|
|
143
|
+
const seen: {options?: DriveOptions} = {};
|
|
144
|
+
await runPlaytest(
|
|
145
|
+
() => stubAdapter(drove('sentinel'), seen),
|
|
146
|
+
[
|
|
147
|
+
'playtest',
|
|
148
|
+
'run',
|
|
149
|
+
cartPath,
|
|
150
|
+
'--input',
|
|
151
|
+
'3:1',
|
|
152
|
+
'--frames',
|
|
153
|
+
'5',
|
|
154
|
+
'--json',
|
|
155
|
+
],
|
|
156
|
+
);
|
|
157
|
+
// The blocks piped to the cart decode to a script that presses Right (bit 1).
|
|
158
|
+
const cmds = new BlockDecoder().push(seen.options!.blocks);
|
|
159
|
+
const inputs = cmds.filter((c) => c.op === 'input');
|
|
160
|
+
expect(inputs.some((c) => c.op === 'input' && c.held === 0b10)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('rejects a malformed --input with a structured error (no launch)', async () => {
|
|
164
|
+
let launched = false;
|
|
165
|
+
const {stdout, exitCode} = await runPlaytest(() => {
|
|
166
|
+
launched = true;
|
|
167
|
+
return stubAdapter(drove('sentinel'));
|
|
168
|
+
}, ['playtest', 'run', cartPath, '--input', '3:9', '--json']);
|
|
169
|
+
expect(exitCode).not.toBe(0);
|
|
170
|
+
expect(stdout).toContain('playtest-input-invalid');
|
|
171
|
+
expect(launched).toBe(false); // fails before the adapter is built
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('--seed sets seeded=true and injects srand into the driven cart', async () => {
|
|
175
|
+
const seen: {options?: DriveOptions} = {};
|
|
176
|
+
const {stdout} = await runPlaytest(
|
|
177
|
+
() => stubAdapter(drove('sentinel'), seen),
|
|
178
|
+
['playtest', 'run', cartPath, '--seed', '7', '--json'],
|
|
179
|
+
);
|
|
180
|
+
expect(JSON.parse(stdout).seeded).toBe(true);
|
|
181
|
+
// The driven cart written for PICO-8 carries srand(7).
|
|
182
|
+
const driven = readFileSync(seen.options!.cartPath, 'utf8');
|
|
183
|
+
expect(driven).toContain('srand(7)');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('no --seed => seeded false and no srand injected', async () => {
|
|
187
|
+
const seen: {options?: DriveOptions} = {};
|
|
188
|
+
const {stdout} = await runPlaytest(
|
|
189
|
+
() => stubAdapter(drove('sentinel'), seen),
|
|
190
|
+
['playtest', 'run', cartPath, '--json'],
|
|
191
|
+
);
|
|
192
|
+
expect(JSON.parse(stdout).seeded).toBe(false);
|
|
193
|
+
expect(readFileSync(seen.options!.cartPath, 'utf8')).not.toContain(
|
|
194
|
+
'srand(',
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('picopilot playtest: shared-write discipline (never ~/Desktop, entry untouched)', () => {
|
|
200
|
+
it('defaults to an isolated temp dir, not the real ~/Desktop', async () => {
|
|
201
|
+
const seen: {options?: DriveOptions} = {};
|
|
202
|
+
const {exitCode} = await runPlaytest(() =>
|
|
203
|
+
stubAdapter(drove('sentinel'), seen),
|
|
204
|
+
);
|
|
205
|
+
expect(exitCode).toBe(0);
|
|
206
|
+
const shotDir = seen.options?.shotDir ?? '';
|
|
207
|
+
expect(shotDir.startsWith(tmpdir())).toBe(true);
|
|
208
|
+
expect(shotDir).not.toContain(join(homedir(), 'Desktop'));
|
|
209
|
+
expect(existsSync(shotDir)).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('writes the throwaway driven cart into the temp dir, NOT next to the entry', async () => {
|
|
213
|
+
const seen: {options?: DriveOptions} = {};
|
|
214
|
+
await runPlaytest(() => stubAdapter(drove('sentinel'), seen));
|
|
215
|
+
const drivenCart = seen.options?.cartPath ?? '';
|
|
216
|
+
expect(drivenCart.startsWith(tmpdir())).toBe(true);
|
|
217
|
+
expect(drivenCart).not.toBe(cartPath);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('leaves the entry cart bytes BYTE-untouched across a playtest', async () => {
|
|
221
|
+
await runPlaytest(
|
|
222
|
+
() => stubAdapter(drove('sentinel')),
|
|
223
|
+
['playtest', 'run', cartPath, '--seed', '1', '--json'],
|
|
224
|
+
);
|
|
225
|
+
expect(readFileSync(cartPath, 'utf8')).toBe(CART_TEXT);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('leaves the real ~/Desktop BYTE-untouched across a playtest', async () => {
|
|
229
|
+
const desktop = join(homedir(), 'Desktop');
|
|
230
|
+
const snap = (): string[] =>
|
|
231
|
+
existsSync(desktop) ? readdirSync(desktop).sort() : [];
|
|
232
|
+
const before = snap();
|
|
233
|
+
await runPlaytest(() => stubAdapter(drove('sentinel')));
|
|
234
|
+
expect(snap()).toEqual(before);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('picopilot playtest: cart-not-found (picopilot-side, distinct from pico8 absence)', () => {
|
|
239
|
+
it('errors with a nonzero exit when the cart path does not exist', async () => {
|
|
240
|
+
const {stdout, exitCode} = await runPlaytest(
|
|
241
|
+
() => stubAdapter(drove('sentinel')),
|
|
242
|
+
['playtest', 'run', join(dir, 'nope.p8'), '--json'],
|
|
243
|
+
);
|
|
244
|
+
expect(exitCode).not.toBe(0);
|
|
245
|
+
expect(stdout).toContain('no cart at');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does NOT invoke the adapter when the cart is missing (fails fast)', async () => {
|
|
249
|
+
let called = false;
|
|
250
|
+
await runPlaytest(() => {
|
|
251
|
+
called = true;
|
|
252
|
+
return stubAdapter(drove('sentinel'));
|
|
253
|
+
}, ['playtest', 'run', join(dir, 'missing.p8'), '--json']);
|
|
254
|
+
expect(called).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The RESUMABLE session verbs' CI-safe surface (no daemon, no binary): a verb
|
|
260
|
+
* against an id with no live daemon is a structured `playtest-session-not-found`;
|
|
261
|
+
* a malformed id / buttons is a structured refusal before any socket work. The
|
|
262
|
+
* live daemon round-trip is covered in `session-daemon.test.ts` (fake process)
|
|
263
|
+
* and the manual/opt-in tier (real PICO-8).
|
|
264
|
+
*/
|
|
265
|
+
describe('picopilot playtest session verbs: structured boundaries (no daemon)', () => {
|
|
266
|
+
it('step/input/shot/status on an unknown id -> playtest-session-not-found', async () => {
|
|
267
|
+
for (const argv of [
|
|
268
|
+
['playtest', 'step', 'ghost', '--json'],
|
|
269
|
+
['playtest', 'input', 'ghost', 'right', '--json'],
|
|
270
|
+
['playtest', 'shot', 'ghost', '--json'],
|
|
271
|
+
['playtest', 'status', 'ghost', '--json'],
|
|
272
|
+
]) {
|
|
273
|
+
const {stdout, exitCode} = await runPlaytest(
|
|
274
|
+
() => stubAdapter(drove('sentinel')),
|
|
275
|
+
argv,
|
|
276
|
+
);
|
|
277
|
+
expect(exitCode).not.toBe(0);
|
|
278
|
+
expect(stdout).toContain('playtest-session-not-found');
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('stop on an unknown id is an idempotent no-op success (alive:false)', async () => {
|
|
283
|
+
const {stdout, exitCode} = await runPlaytest(
|
|
284
|
+
() => stubAdapter(drove('sentinel')),
|
|
285
|
+
['playtest', 'stop', 'ghost', '--json'],
|
|
286
|
+
);
|
|
287
|
+
expect(exitCode).toBe(0);
|
|
288
|
+
expect(JSON.parse(stdout).alive).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('a malformed session id is a structured refusal', async () => {
|
|
292
|
+
const {stdout, exitCode} = await runPlaytest(
|
|
293
|
+
() => stubAdapter(drove('sentinel')),
|
|
294
|
+
['playtest', 'step', '../escape', '--json'],
|
|
295
|
+
);
|
|
296
|
+
expect(exitCode).not.toBe(0);
|
|
297
|
+
expect(stdout).toContain('playtest-session-id-invalid');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('an unknown button on `input` is a structured refusal (before any socket)', async () => {
|
|
301
|
+
const {stdout, exitCode} = await runPlaytest(
|
|
302
|
+
() => stubAdapter(drove('sentinel')),
|
|
303
|
+
['playtest', 'input', 'ghost', 'jump', '--json'],
|
|
304
|
+
);
|
|
305
|
+
expect(exitCode).not.toBe(0);
|
|
306
|
+
expect(stdout).toContain('playtest-input-invalid');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('start with an invalid id refuses before spawning anything', async () => {
|
|
310
|
+
const {stdout, exitCode} = await runPlaytest(
|
|
311
|
+
() => stubAdapter(drove('sentinel')),
|
|
312
|
+
['playtest', 'start', cartPath, '--id', 'bad id', '--json'],
|
|
313
|
+
);
|
|
314
|
+
expect(exitCode).not.toBe(0);
|
|
315
|
+
expect(stdout).toContain('playtest-session-id-invalid');
|
|
316
|
+
});
|
|
317
|
+
});
|