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,212 @@
|
|
|
1
|
+
import {execFileSync} from 'node:child_process';
|
|
2
|
+
import {mkdtempSync, writeFileSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {Cli} from 'incur';
|
|
6
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CountReport,
|
|
10
|
+
ShrinkoAdapter,
|
|
11
|
+
ShrinkoResult,
|
|
12
|
+
} from '../engine/shrinko/index.js';
|
|
13
|
+
import {shrinkoNotFound} from '../engine/shrinko/index.js';
|
|
14
|
+
import {type ShrinkoAdapterFactory, registerTokens} from './tokens.js';
|
|
15
|
+
|
|
16
|
+
/** A trivial but valid cart the command reads (existence + a path to hand shrinko). */
|
|
17
|
+
const CART_TEXT =
|
|
18
|
+
'pico-8 cartridge // http://www.pico-8.com\nversion 42\n__lua__\nprint("hi")\n';
|
|
19
|
+
|
|
20
|
+
let dir: string;
|
|
21
|
+
let cartPath: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
dir = mkdtempSync(join(tmpdir(), 'picopilot-tokens-'));
|
|
25
|
+
cartPath = join(dir, 'main.p8');
|
|
26
|
+
writeFileSync(cartPath, CART_TEXT);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
try {
|
|
31
|
+
execFileSync('rm', ['-rf', dir]);
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A stub {@link ShrinkoAdapter} standing in for BOTH a shrinko-present shell
|
|
39
|
+
* adapter and a hypothetical native-TS adapter. It never spawns anything, so no
|
|
40
|
+
* test depends on shrinko being installed. `count` returns whatever result the
|
|
41
|
+
* test wired.
|
|
42
|
+
*/
|
|
43
|
+
function stubAdapter(result: ShrinkoResult<CountReport>): ShrinkoAdapter {
|
|
44
|
+
return {
|
|
45
|
+
async count() {
|
|
46
|
+
return result;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function present(over: boolean): ShrinkoResult<CountReport> {
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
// Over: 9001 tokens > the 8192 budget. Under: a small cart.
|
|
55
|
+
value: over
|
|
56
|
+
? {
|
|
57
|
+
tokens: 9001,
|
|
58
|
+
tokensPct: 109,
|
|
59
|
+
chars: 34000,
|
|
60
|
+
charsPct: 52,
|
|
61
|
+
compressed: 13000,
|
|
62
|
+
compressedPct: 82,
|
|
63
|
+
}
|
|
64
|
+
: {
|
|
65
|
+
tokens: 512,
|
|
66
|
+
tokensPct: 6,
|
|
67
|
+
chars: 1840,
|
|
68
|
+
charsPct: 2,
|
|
69
|
+
compressed: 980,
|
|
70
|
+
compressedPct: 6,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Drives `picopilot tokens` through incur's `serve` DI with an INJECTED adapter
|
|
77
|
+
* factory (the seam), capturing stdout + exit without the real environment or
|
|
78
|
+
* the real binary. `env` defaults to a bare PATH the (stub) adapter ignores.
|
|
79
|
+
*/
|
|
80
|
+
async function runTokens(
|
|
81
|
+
factory: ShrinkoAdapterFactory,
|
|
82
|
+
argv: string[] = ['tokens', cartPath, '--json'],
|
|
83
|
+
env: Record<string, string | undefined> = {PATH: '/does/not/matter'},
|
|
84
|
+
) {
|
|
85
|
+
// Use a UNIQUE cli name (not the real 'picopilot'), so incur's skills-freshness
|
|
86
|
+
// check (`SyncSkills.readHash(name)` + `hasInstalledSkills(name)`) finds NO
|
|
87
|
+
// stored hash for it and never attaches a "Skills are out of date" CTA. Under
|
|
88
|
+
// the full parallel run the real `picopilot` skill hash (in $XDG_DATA_HOME) goes
|
|
89
|
+
// stale (the skills test installs/removes skills concurrently), which would
|
|
90
|
+
// otherwise inject that ambient CTA and flakily break `expect(cta).toBeUndefined()`.
|
|
91
|
+
// A unit test of `tokens` must not depend on the machine's installed-skills state.
|
|
92
|
+
const cli = Cli.create('picopilot-tokens-test', {version: '0.0.0'});
|
|
93
|
+
registerTokens(cli, factory);
|
|
94
|
+
let stdout = '';
|
|
95
|
+
let exitCode = 0;
|
|
96
|
+
await cli.serve(argv, {
|
|
97
|
+
stdout(s) {
|
|
98
|
+
stdout += s;
|
|
99
|
+
},
|
|
100
|
+
exit(code) {
|
|
101
|
+
exitCode = code;
|
|
102
|
+
},
|
|
103
|
+
env,
|
|
104
|
+
});
|
|
105
|
+
return {stdout, exitCode};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe('picopilot tokens: present (parses --count into the budget struct)', () => {
|
|
109
|
+
it('reports {tokens, pct, chars, compressed} under budget with no minify CTA', async () => {
|
|
110
|
+
const {stdout, exitCode} = await runTokens(() =>
|
|
111
|
+
stubAdapter(present(false)),
|
|
112
|
+
);
|
|
113
|
+
expect(exitCode).toBe(0);
|
|
114
|
+
const out = JSON.parse(stdout);
|
|
115
|
+
expect(out.tokens).toBe(512);
|
|
116
|
+
expect(out.chars).toBe(1840);
|
|
117
|
+
expect(out.compressed).toBe(980);
|
|
118
|
+
expect(out.budget).toBe(8192);
|
|
119
|
+
expect(out.overBudget).toBe(false);
|
|
120
|
+
// 512 / 8192 ≈ 6%.
|
|
121
|
+
expect(out.pct).toBe(6);
|
|
122
|
+
// Under budget: no minify CTA.
|
|
123
|
+
expect(out.cta).toBeUndefined();
|
|
124
|
+
expect(JSON.stringify(out)).not.toContain('minify');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('reports over budget and CTAs to minify', async () => {
|
|
128
|
+
const {stdout, exitCode} = await runTokens(() =>
|
|
129
|
+
stubAdapter(present(true)),
|
|
130
|
+
);
|
|
131
|
+
// Over budget is still a successful count (exit 0); the CTA drives the fix.
|
|
132
|
+
expect(exitCode).toBe(0);
|
|
133
|
+
const out = JSON.parse(stdout);
|
|
134
|
+
expect(out.tokens).toBe(9001);
|
|
135
|
+
expect(out.overBudget).toBe(true);
|
|
136
|
+
// 9001 / 8192 ≈ 110%.
|
|
137
|
+
expect(out.pct).toBe(110);
|
|
138
|
+
// The over-budget → minify CTA is present (incur prefixes the CLI name).
|
|
139
|
+
expect(out.cta.commands[0].command).toContain('minify');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('picopilot tokens: absent (structured shrinko-not-found + nonzero exit)', () => {
|
|
144
|
+
it('returns the exact structured failure and a nonzero exit, never a crash', async () => {
|
|
145
|
+
const {stdout, exitCode} = await runTokens(() =>
|
|
146
|
+
stubAdapter(shrinkoNotFound()),
|
|
147
|
+
);
|
|
148
|
+
expect(exitCode).not.toBe(0);
|
|
149
|
+
const out = JSON.parse(stdout);
|
|
150
|
+
// incur error envelope: the reason is the machine code, the exact remedy is
|
|
151
|
+
// carried in the message so an agent reading only the message learns the fix.
|
|
152
|
+
expect(out.code).toBe('shrinko-not-found');
|
|
153
|
+
expect(out.message).toContain('uv pip install shrinko');
|
|
154
|
+
expect(out.message).toContain('python>=3.8');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('picopilot tokens: seam-swap (a stub adapter leaves output/exit unchanged)', () => {
|
|
159
|
+
it('produces byte-identical public output whether the adapter is A or B', async () => {
|
|
160
|
+
// Two DIFFERENT adapter implementations returning the SAME count value must
|
|
161
|
+
// yield the SAME command output + exit: the command depends only on the
|
|
162
|
+
// interface, so a native-TS shrinko drops in without a visible change.
|
|
163
|
+
const value = present(true);
|
|
164
|
+
// A: returns the value synchronously-resolved.
|
|
165
|
+
const adapterA: ShrinkoAdapter = {
|
|
166
|
+
async count() {
|
|
167
|
+
return value;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
// B: a DIFFERENT implementation (extra async work, freshly-built struct) that
|
|
171
|
+
// yields the same count. Stands in for a native-TS shrinko behind the seam.
|
|
172
|
+
const adapterB: ShrinkoAdapter = {
|
|
173
|
+
async count() {
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
const v = value.ok ? value.value : undefined;
|
|
176
|
+
if (v === undefined) throw new Error('unreachable');
|
|
177
|
+
return {ok: true, value: {...v}};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const a = await runTokens(() => adapterA);
|
|
182
|
+
const b = await runTokens(() => adapterB);
|
|
183
|
+
expect(a.stdout).toBe(b.stdout);
|
|
184
|
+
expect(a.exitCode).toBe(b.exitCode);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('the injected factory receives the incur-resolved env (PATH isolation lever)', async () => {
|
|
188
|
+
let seenEnv: NodeJS.ProcessEnv | undefined;
|
|
189
|
+
const factory: ShrinkoAdapterFactory = (env) => {
|
|
190
|
+
seenEnv = env;
|
|
191
|
+
return stubAdapter(present(false));
|
|
192
|
+
};
|
|
193
|
+
await runTokens(factory, ['tokens', cartPath, '--json'], {
|
|
194
|
+
PATH: '/isolated/bin',
|
|
195
|
+
});
|
|
196
|
+
// The command handed the adapter exactly the overridden env, so the child's
|
|
197
|
+
// PATH is controllable from the test without touching the real environment.
|
|
198
|
+
expect(seenEnv?.PATH).toBe('/isolated/bin');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('picopilot tokens: a missing cart is a distinct error (not shrinko-not-found)', () => {
|
|
203
|
+
it('errors with cart-not-found + nonzero exit when the cart path does not exist', async () => {
|
|
204
|
+
const {stdout, exitCode} = await runTokens(
|
|
205
|
+
() => stubAdapter(present(false)),
|
|
206
|
+
['tokens', join(dir, 'nope.p8'), '--json'],
|
|
207
|
+
);
|
|
208
|
+
expect(exitCode).not.toBe(0);
|
|
209
|
+
const out = JSON.parse(stdout);
|
|
210
|
+
expect(out.code).toBe('cart-not-found');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {existsSync} from 'node:fs';
|
|
2
|
+
import {isAbsolute, resolve} from 'node:path';
|
|
3
|
+
import {type Cli, z} from 'incur';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type ShrinkoAdapter,
|
|
7
|
+
ShrinkoParseError,
|
|
8
|
+
ShellShrinkoAdapter,
|
|
9
|
+
} from '../engine/shrinko/index.js';
|
|
10
|
+
|
|
11
|
+
/** PICO-8's Lua token budget. A cart over this will not load in PICO-8 (US #3). */
|
|
12
|
+
export const TOKEN_BUDGET = 8192;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds the {@link ShrinkoAdapter} a command uses, given the incur-resolved
|
|
16
|
+
* env. The default is the shell-out {@link ShellShrinkoAdapter}, whose child
|
|
17
|
+
* process inherits ONLY this `env` (its `PATH` is the test isolation lever).
|
|
18
|
+
*
|
|
19
|
+
* A test passes its own factory to {@link registerTokens} to SWAP a stub
|
|
20
|
+
* adapter (the seam-swap contract): the command's public output/exit must be
|
|
21
|
+
* unchanged whether the real shell adapter or a native-TS stub is behind the
|
|
22
|
+
* seam, because the command only ever talks to the {@link ShrinkoAdapter}
|
|
23
|
+
* interface.
|
|
24
|
+
*/
|
|
25
|
+
export type ShrinkoAdapterFactory = (env: NodeJS.ProcessEnv) => ShrinkoAdapter;
|
|
26
|
+
|
|
27
|
+
const defaultAdapterFactory: ShrinkoAdapterFactory = (env) =>
|
|
28
|
+
new ShellShrinkoAdapter({env});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Registers `picopilot tokens` (US #3), the first shrinko-backed command.
|
|
32
|
+
*
|
|
33
|
+
* It reports `{tokens, pct, chars, compressed}` for a cart against the 8,192
|
|
34
|
+
* token budget via shrinko8 `--count`, and CTAs to `minify` when over budget so
|
|
35
|
+
* the agent is led straight to the fix (the detect-then-fix loop, ADR CTAs).
|
|
36
|
+
*
|
|
37
|
+
* Two-tier contract (ADR-0002): `tokens` is shrinko-BACKED, so with shrinko
|
|
38
|
+
* ABSENT it returns the structured `shrinko-not-found` error (nonzero exit,
|
|
39
|
+
* exact remedy `uv pip install shrinko`) rather than crashing or reporting a
|
|
40
|
+
* hollow zero. The dependency is reached entirely through the {@link
|
|
41
|
+
* ShrinkoAdapter} seam, so a native-TS shrinko would satisfy this command
|
|
42
|
+
* unchanged.
|
|
43
|
+
*
|
|
44
|
+
* @param adapterFactory injects the adapter (defaults to the shell adapter);
|
|
45
|
+
* tests pass a stub to swap the seam and to drive present/absent without the
|
|
46
|
+
* real binary.
|
|
47
|
+
*/
|
|
48
|
+
export function registerTokens(
|
|
49
|
+
cli: Cli.Cli,
|
|
50
|
+
adapterFactory: ShrinkoAdapterFactory = defaultAdapterFactory,
|
|
51
|
+
): void {
|
|
52
|
+
cli.command('tokens', {
|
|
53
|
+
description:
|
|
54
|
+
'Count a cart against the 8,192-token PICO-8 budget (via shrinko8 --count). Requires shrinko.',
|
|
55
|
+
args: z.object({
|
|
56
|
+
cart: z
|
|
57
|
+
.string()
|
|
58
|
+
.default('main.p8')
|
|
59
|
+
.describe(
|
|
60
|
+
'The .p8 cart to count. Defaults to main.p8 in the current folder.',
|
|
61
|
+
),
|
|
62
|
+
}),
|
|
63
|
+
// Declaring PATH here makes incur resolve it from the (test-overridable)
|
|
64
|
+
// env source and hand it to us, so we pass a controlled PATH to the child.
|
|
65
|
+
env: z.object({
|
|
66
|
+
PATH: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Used to locate shrinko8 in the child process.'),
|
|
70
|
+
}),
|
|
71
|
+
output: z.object({
|
|
72
|
+
tokens: z.number().describe('The token count shrinko reported.'),
|
|
73
|
+
pct: z
|
|
74
|
+
.number()
|
|
75
|
+
.describe(
|
|
76
|
+
'tokens as a percentage of the 8,192 budget (rounded); >100 means over budget.',
|
|
77
|
+
),
|
|
78
|
+
chars: z.number().describe('The character count shrinko reported.'),
|
|
79
|
+
compressed: z
|
|
80
|
+
.number()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('The compressed byte count (absent for some input formats).'),
|
|
83
|
+
budget: z
|
|
84
|
+
.number()
|
|
85
|
+
.describe('The token budget the percentage is against (8192).'),
|
|
86
|
+
overBudget: z.boolean().describe('True when tokens exceed the budget.'),
|
|
87
|
+
}),
|
|
88
|
+
examples: [
|
|
89
|
+
{description: 'Count main.p8'},
|
|
90
|
+
{description: 'Count a specific cart', args: {cart: 'game.p8'}},
|
|
91
|
+
],
|
|
92
|
+
async run({args, env, error, ok}) {
|
|
93
|
+
const cartPath = isAbsolute(args.cart)
|
|
94
|
+
? args.cart
|
|
95
|
+
: resolve(process.cwd(), args.cart);
|
|
96
|
+
|
|
97
|
+
// A missing cart is a picopilot-side error, distinct from shrinko absence:
|
|
98
|
+
// surface it clearly instead of letting shrinko fail opaquely on the path.
|
|
99
|
+
if (!existsSync(cartPath)) {
|
|
100
|
+
return error({
|
|
101
|
+
code: 'cart-not-found',
|
|
102
|
+
message: `no cart at ${cartPath}`,
|
|
103
|
+
exitCode: 1,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const adapter = adapterFactory(env as NodeJS.ProcessEnv);
|
|
108
|
+
|
|
109
|
+
let result: Awaited<ReturnType<ShrinkoAdapter['count']>>;
|
|
110
|
+
try {
|
|
111
|
+
result = await adapter.count(cartPath);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// shrinko RAN but its output was unparseable (version/format drift):
|
|
114
|
+
// a distinct `shrinko-failed`, NOT `shrinko-not-found` (which is absence).
|
|
115
|
+
if (e instanceof ShrinkoParseError) {
|
|
116
|
+
return error({
|
|
117
|
+
code: 'shrinko-failed',
|
|
118
|
+
message: `shrinko ran but its --count output could not be parsed: ${e.message}`,
|
|
119
|
+
exitCode: 1,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
// The two-tier structured failure (ADR-0002 / US #17): shrinko absent.
|
|
127
|
+
// The message CARRIES the exact remedy + needs so an agent reading only
|
|
128
|
+
// the message still learns the fix; the code is the machine handle.
|
|
129
|
+
return error({
|
|
130
|
+
code: result.reason,
|
|
131
|
+
message: `shrinko is not installed. ${result.remedy} (needs: ${result.needs.join(', ')})`,
|
|
132
|
+
exitCode: 1,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const {tokens, chars, compressed} = result.value;
|
|
137
|
+
const pct = Math.round((tokens / TOKEN_BUDGET) * 100);
|
|
138
|
+
const overBudget = tokens > TOKEN_BUDGET;
|
|
139
|
+
|
|
140
|
+
// Over budget → CTA to `minify` (the detect-then-fix loop). Under budget
|
|
141
|
+
// needs no CTA: nothing to do.
|
|
142
|
+
const cta = overBudget
|
|
143
|
+
? {
|
|
144
|
+
description: 'Over the 8,192-token budget. Reclaim tokens with:',
|
|
145
|
+
commands: [
|
|
146
|
+
{
|
|
147
|
+
command: 'minify',
|
|
148
|
+
description:
|
|
149
|
+
'Safe-minify the cart to reduce tokens without changing behaviour.',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
: undefined;
|
|
154
|
+
|
|
155
|
+
return ok(
|
|
156
|
+
{
|
|
157
|
+
tokens,
|
|
158
|
+
pct,
|
|
159
|
+
chars,
|
|
160
|
+
compressed,
|
|
161
|
+
budget: TOKEN_BUDGET,
|
|
162
|
+
overBudget,
|
|
163
|
+
},
|
|
164
|
+
cta === undefined ? undefined : {cta},
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {execFileSync} from 'node:child_process';
|
|
2
|
+
import {mkdtempSync, writeFileSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {Cli} from 'incur';
|
|
6
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CountReport,
|
|
10
|
+
ShrinkoAdapter,
|
|
11
|
+
ShrinkoResult,
|
|
12
|
+
} from '../engine/shrinko/index.js';
|
|
13
|
+
import {shrinkoNotFound} from '../engine/shrinko/index.js';
|
|
14
|
+
import {
|
|
15
|
+
GATE_INCAPABLE_EXIT,
|
|
16
|
+
type ShrinkoAdapterFactory,
|
|
17
|
+
registerVerify,
|
|
18
|
+
} from './verify.js';
|
|
19
|
+
|
|
20
|
+
/** A well-formed cart: parses AND round-trips cleanly (passes integrity). */
|
|
21
|
+
const GOOD_CART =
|
|
22
|
+
'pico-8 cartridge // http://www.pico-8.com\nversion 42\n__lua__\nprint("hi")\n';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A MALFORMED cart: no `version N` line after the header, so `Cart.parse`
|
|
26
|
+
* throws a `missing-version` `CartParseError` and integrity fails.
|
|
27
|
+
*/
|
|
28
|
+
const MALFORMED_CART =
|
|
29
|
+
'pico-8 cartridge // http://www.pico-8.com\n__lua__\nprint("hi")\n';
|
|
30
|
+
|
|
31
|
+
let dir: string;
|
|
32
|
+
let cartPath: string;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
dir = mkdtempSync(join(tmpdir(), 'picopilot-verify-'));
|
|
36
|
+
cartPath = join(dir, 'main.p8');
|
|
37
|
+
writeFileSync(cartPath, GOOD_CART);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
try {
|
|
42
|
+
execFileSync('rm', ['-rf', dir]);
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A stub {@link ShrinkoAdapter} standing in for a shrinko-present shell adapter
|
|
50
|
+
* (or a native-TS one). It never spawns anything, so no test depends on shrinko
|
|
51
|
+
* being installed. `count` returns whatever result the test wired.
|
|
52
|
+
*/
|
|
53
|
+
function stubAdapter(result: ShrinkoResult<CountReport>): ShrinkoAdapter {
|
|
54
|
+
return {
|
|
55
|
+
async count() {
|
|
56
|
+
return result;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A present-shrinko count: `over` picks an over-budget vs under-budget value. */
|
|
62
|
+
function present(over: boolean): ShrinkoResult<CountReport> {
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
value: over
|
|
66
|
+
? {
|
|
67
|
+
tokens: 9001,
|
|
68
|
+
tokensPct: 109,
|
|
69
|
+
chars: 34000,
|
|
70
|
+
charsPct: 52,
|
|
71
|
+
compressed: 13000,
|
|
72
|
+
compressedPct: 82,
|
|
73
|
+
}
|
|
74
|
+
: {
|
|
75
|
+
tokens: 512,
|
|
76
|
+
tokensPct: 6,
|
|
77
|
+
chars: 1840,
|
|
78
|
+
charsPct: 2,
|
|
79
|
+
compressed: 980,
|
|
80
|
+
compressedPct: 6,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Drives `picopilot verify` through incur's `serve` DI with an INJECTED adapter
|
|
87
|
+
* factory (the seam), capturing stdout + exit without the real environment or
|
|
88
|
+
* binary. `--json` so the structured envelope is asserted directly.
|
|
89
|
+
*/
|
|
90
|
+
async function runVerify(
|
|
91
|
+
factory: ShrinkoAdapterFactory,
|
|
92
|
+
cart: string = cartPath,
|
|
93
|
+
env: Record<string, string | undefined> = {PATH: '/does/not/matter'},
|
|
94
|
+
) {
|
|
95
|
+
// Unique cli name (not 'picopilot') so incur's skills-freshness check finds no
|
|
96
|
+
// stored hash and never injects an ambient "Skills are out of date" CTA. Under
|
|
97
|
+
// the full parallel run that CTA would land as cta.commands[0] and flakily break
|
|
98
|
+
// the verify->run CTA assertion. (Same isolation as tokens.test.ts.)
|
|
99
|
+
const cli = Cli.create('picopilot-verify-test', {version: '0.0.0'});
|
|
100
|
+
registerVerify(cli, factory);
|
|
101
|
+
let stdout = '';
|
|
102
|
+
let exitCode = 0;
|
|
103
|
+
await cli.serve(['verify', cart, '--json'], {
|
|
104
|
+
stdout(s) {
|
|
105
|
+
stdout += s;
|
|
106
|
+
},
|
|
107
|
+
exit(code) {
|
|
108
|
+
exitCode = code;
|
|
109
|
+
},
|
|
110
|
+
env,
|
|
111
|
+
});
|
|
112
|
+
return {stdout, exitCode};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe('picopilot verify: pass (integrity + tokens both green)', () => {
|
|
116
|
+
it('returns one structured envelope with an overall pass and per-check results', async () => {
|
|
117
|
+
const {stdout, exitCode} = await runVerify(() =>
|
|
118
|
+
stubAdapter(present(false)),
|
|
119
|
+
);
|
|
120
|
+
expect(exitCode).toBe(0);
|
|
121
|
+
const out = JSON.parse(stdout);
|
|
122
|
+
expect(out.status).toBe('pass');
|
|
123
|
+
expect(out.checks.integrity).toBe(true);
|
|
124
|
+
expect(out.checks.tokens).toBe(true);
|
|
125
|
+
expect(out.tokens).toBe(512);
|
|
126
|
+
expect(out.budget).toBe(8192);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('SELF-SCOPES: the envelope states it is static and passing does not mean the cart runs', async () => {
|
|
130
|
+
const {stdout} = await runVerify(() => stubAdapter(present(false)));
|
|
131
|
+
const out = JSON.parse(stdout);
|
|
132
|
+
expect(out.scope.toLowerCase()).toContain('static');
|
|
133
|
+
// The connotation guard: passing must NOT be claimed as "the cart runs".
|
|
134
|
+
expect(out.scope.toLowerCase()).toContain('does not mean the cart runs');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('green verify CTAs to `run` (well-formed → now confirm it boots)', async () => {
|
|
138
|
+
const {stdout} = await runVerify(() => stubAdapter(present(false)));
|
|
139
|
+
const out = JSON.parse(stdout);
|
|
140
|
+
// incur prefixes the CLI name onto the CTA command.
|
|
141
|
+
expect(out.cta.commands[0].command).toContain('run');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('picopilot verify: fail (a static check flunked, nonzero exit)', () => {
|
|
146
|
+
it('fails when tokens are over budget', async () => {
|
|
147
|
+
const {stdout, exitCode} = await runVerify(() =>
|
|
148
|
+
stubAdapter(present(true)),
|
|
149
|
+
);
|
|
150
|
+
expect(exitCode).toBe(1);
|
|
151
|
+
const out = JSON.parse(stdout);
|
|
152
|
+
expect(out.code).toBe('verify-failed');
|
|
153
|
+
// NOT reported as pass, and NOT the gate-incapable code.
|
|
154
|
+
expect(out.code).not.toBe('gate-incapable');
|
|
155
|
+
expect(out.message).toContain('budget');
|
|
156
|
+
// Still self-scopes on failure.
|
|
157
|
+
expect(out.message.toLowerCase()).toContain('static');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('fails when the cart is malformed (integrity), independent of PICO-8', async () => {
|
|
161
|
+
writeFileSync(cartPath, MALFORMED_CART);
|
|
162
|
+
const {stdout, exitCode} = await runVerify(() =>
|
|
163
|
+
stubAdapter(present(false)),
|
|
164
|
+
);
|
|
165
|
+
expect(exitCode).toBe(1);
|
|
166
|
+
const out = JSON.parse(stdout);
|
|
167
|
+
expect(out.code).toBe('verify-failed');
|
|
168
|
+
expect(out.message).toContain('integrity');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('picopilot verify: gate-incapable (shrinko absent, the regression guard)', () => {
|
|
173
|
+
it('shrinko absent → DISTINCT gate-incapable outcome with a nonzero exit, NEVER green', async () => {
|
|
174
|
+
const {stdout, exitCode} = await runVerify(() =>
|
|
175
|
+
stubAdapter(shrinkoNotFound()),
|
|
176
|
+
);
|
|
177
|
+
// Nonzero, and specifically the distinct gate-incapable exit code.
|
|
178
|
+
expect(exitCode).not.toBe(0);
|
|
179
|
+
expect(exitCode).toBe(GATE_INCAPABLE_EXIT);
|
|
180
|
+
const out = JSON.parse(stdout);
|
|
181
|
+
// A categorically DISTINCT outcome: not pass, not the ordinary fail.
|
|
182
|
+
expect(out.code).toBe('gate-incapable');
|
|
183
|
+
expect(out.code).not.toBe('verify-failed');
|
|
184
|
+
// It names the missing capability + the remedy so the agent can enable it.
|
|
185
|
+
expect(out.message).toContain('shrinko');
|
|
186
|
+
expect(out.message).toContain('uv pip install shrinko');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('REGRESSION GUARD: shrinko absent NEVER reports green, even for a perfect cart', async () => {
|
|
190
|
+
// The cart is well-formed (integrity would pass). If verify ever let a clean
|
|
191
|
+
// cart go green without checking tokens, THIS is the gate-as-theatre bug.
|
|
192
|
+
const {stdout, exitCode} = await runVerify(() =>
|
|
193
|
+
stubAdapter(shrinkoNotFound()),
|
|
194
|
+
);
|
|
195
|
+
expect(exitCode).not.toBe(0);
|
|
196
|
+
const out = JSON.parse(stdout);
|
|
197
|
+
// Must NOT be a success envelope: no `status: 'pass'`.
|
|
198
|
+
expect(out.status).not.toBe('pass');
|
|
199
|
+
expect(out.code).toBe('gate-incapable');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('gate-incapable is a distinct exit code from fail (an agent can branch on it)', async () => {
|
|
203
|
+
const incapable = await runVerify(() => stubAdapter(shrinkoNotFound()));
|
|
204
|
+
const failed = await runVerify(() => stubAdapter(present(true)));
|
|
205
|
+
expect(incapable.exitCode).toBe(GATE_INCAPABLE_EXIT);
|
|
206
|
+
expect(failed.exitCode).toBe(1);
|
|
207
|
+
expect(incapable.exitCode).not.toBe(failed.exitCode);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('picopilot verify: no PICO-8 dependency (it never runs the cart)', () => {
|
|
212
|
+
it('passes with no PICO8_PATH / pico8 anywhere in env (static gate)', async () => {
|
|
213
|
+
// A bare env with only a PATH that has no pico8: verify still passes, proving
|
|
214
|
+
// it never reaches for PICO-8.
|
|
215
|
+
const {stdout, exitCode} = await runVerify(
|
|
216
|
+
() => stubAdapter(present(false)),
|
|
217
|
+
cartPath,
|
|
218
|
+
{
|
|
219
|
+
PATH: '/empty',
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
expect(exitCode).toBe(0);
|
|
223
|
+
const out = JSON.parse(stdout);
|
|
224
|
+
expect(out.status).toBe('pass');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('picopilot verify: a missing cart is a distinct error (not fail, not gate-incapable)', () => {
|
|
229
|
+
it('errors with cart-not-found + nonzero exit when the cart path does not exist', async () => {
|
|
230
|
+
const {stdout, exitCode} = await runVerify(
|
|
231
|
+
() => stubAdapter(present(false)),
|
|
232
|
+
join(dir, 'nope.p8'),
|
|
233
|
+
);
|
|
234
|
+
expect(exitCode).not.toBe(0);
|
|
235
|
+
const out = JSON.parse(stdout);
|
|
236
|
+
expect(out.code).toBe('cart-not-found');
|
|
237
|
+
});
|
|
238
|
+
});
|