@bongos/core 1.19.710 → 1.19.712
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/.bongos-core.json +62 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +16 -0
- package/clients/bongos-client/index.cjs +16 -0
- package/clients/bongos-client/index.d.ts +24 -0
- package/clients/bongos-client/index.mjs +16 -0
- package/docs/api/openapi.json +506 -3
- package/docs/api-reference.md +14 -2
- package/docs/module-api-changelog.md +4 -0
- package/modules/agents/lib/answer-hold.js +91 -0
- package/modules/agents/lib/authoring.js +155 -0
- package/modules/agents/lib/fire-budget.js +109 -0
- package/modules/agents/lib/gate.js +64 -0
- package/modules/agents/routes/agents.js +494 -0
- package/modules/agents/spawn.js +12 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +16 -0
- package/modules/government/catalog.js +10 -0
- package/modules/government/migrations/government_013_agent_atoms.sql +66 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/agent-invoke.js +5 -27
- package/scripts/gds/sandbox-stage.js +56 -12
- package/src/module-api.js +1 -1
- package/tests/agents_authoring.mjs +306 -0
- package/tests/agents_routes.mjs +76 -15
- package/tests/agents_write_routes.mjs +461 -0
- package/tests/profile_route.mjs +0 -1
- package/tests/sandbox_stage.mjs +111 -30
package/tests/sandbox_stage.mjs
CHANGED
|
@@ -57,7 +57,7 @@ t('previewUrl: reads box.env via injected fs; null when unreadable', () => {
|
|
|
57
57
|
|
|
58
58
|
// ---------- previewable surfaces ----------
|
|
59
59
|
t('isPreviewableFile: game surfaces yes, internal surfaces no', () => {
|
|
60
|
-
for (const f of ['public/game/main.js', '
|
|
60
|
+
for (const f of ['modules/game/public/game/main.js', 'modules/game/world/terrain.js', 'modules/game/rooms/WorldRoom.js', 'server.js']) {
|
|
61
61
|
assert.equal(sbx.isPreviewableFile(f), true, `${f} should be previewable`);
|
|
62
62
|
}
|
|
63
63
|
for (const f of ['scripts/gds/ship.js', 'modules/dev-box/routes/box.js', 'docs/adr/0046.md', 'modules/status-ui/public/index.html', 'modules/hall-ui/public/hall.js', 'migrations/075.sql', 'tests/sandbox_stage.mjs', 'server.js.bak']) {
|
|
@@ -66,7 +66,7 @@ t('isPreviewableFile: game surfaces yes, internal surfaces no', () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
t('anyPreviewable: mixed diff counts as previewable; empty/none does not', () => {
|
|
69
|
-
assert.equal(sbx.anyPreviewable(['docs/x.md', 'public/game/a.js']), true);
|
|
69
|
+
assert.equal(sbx.anyPreviewable(['docs/x.md', 'modules/game/public/game/a.js']), true);
|
|
70
70
|
assert.equal(sbx.anyPreviewable(['docs/x.md', 'scripts/gds/y.js']), false);
|
|
71
71
|
assert.equal(sbx.anyPreviewable([]), false);
|
|
72
72
|
assert.equal(sbx.anyPreviewable(null), false);
|
|
@@ -162,9 +162,9 @@ function git(...args) { execFileSync('git', args, { cwd: tmp, encoding: 'utf8' }
|
|
|
162
162
|
git('init', '-q');
|
|
163
163
|
git('config', 'user.email', 'test@example.com');
|
|
164
164
|
git('config', 'user.name', 'test');
|
|
165
|
-
fs.mkdirSync(path.join(tmp, 'public'));
|
|
165
|
+
fs.mkdirSync(path.join(tmp, 'modules', 'game', 'public'), { recursive: true });
|
|
166
166
|
fs.mkdirSync(path.join(tmp, 'docs'));
|
|
167
|
-
fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'one\n');
|
|
167
|
+
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'one\n');
|
|
168
168
|
fs.writeFileSync(path.join(tmp, 'server.js'), 'srv\n');
|
|
169
169
|
fs.writeFileSync(path.join(tmp, 'docs', 'notes.md'), 'doc\n');
|
|
170
170
|
git('add', '.');
|
|
@@ -181,7 +181,7 @@ t('computeTreeStamp: content-stable, scoped to game surfaces', () => {
|
|
|
181
181
|
assert.equal(sbx.computeTreeStamp({ cwd: tmp }), s1, 'docs edit → stamp unchanged');
|
|
182
182
|
|
|
183
183
|
// A game-surface edit produces a new stamp...
|
|
184
|
-
fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'two\n');
|
|
184
|
+
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'two\n');
|
|
185
185
|
const s3 = sbx.computeTreeStamp({ cwd: tmp });
|
|
186
186
|
assert.notEqual(s3, s1, 'game edit → new stamp');
|
|
187
187
|
|
|
@@ -192,10 +192,10 @@ t('computeTreeStamp: content-stable, scoped to game surfaces', () => {
|
|
|
192
192
|
assert.equal(sbx.computeTreeStamp({ cwd: tmp }), s3, 'commit of same content → stamp unchanged');
|
|
193
193
|
|
|
194
194
|
// An untracked game file counts.
|
|
195
|
-
fs.writeFileSync(path.join(tmp, 'public', 'new-tile.js'), 'untracked\n');
|
|
195
|
+
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'new-tile.js'), 'untracked\n');
|
|
196
196
|
const s4 = sbx.computeTreeStamp({ cwd: tmp });
|
|
197
197
|
assert.notEqual(s4, s3, 'untracked game file → new stamp');
|
|
198
|
-
fs.rmSync(path.join(tmp, 'public', 'new-tile.js'));
|
|
198
|
+
fs.rmSync(path.join(tmp, 'modules', 'game', 'public', 'new-tile.js'));
|
|
199
199
|
assert.equal(sbx.computeTreeStamp({ cwd: tmp }), s3, 'removing it restores the stamp');
|
|
200
200
|
|
|
201
201
|
assert.equal(sbx.computeTreeStamp({ cwd: os.tmpdir() }), null, 'not a repo → null');
|
|
@@ -265,7 +265,7 @@ await ta('runShipGate: no sandbox available (not a box, no local game entry) →
|
|
|
265
265
|
// is present — a partial clone with no game. (A full local clone WITH the game
|
|
266
266
|
// is covered by the LOCAL tests below, where it DOES stage.)
|
|
267
267
|
const r = await sbx.runShipGate(
|
|
268
|
-
{ taskId: 1, files: ['public/game/a.js'], skipStage: false, tty: true },
|
|
268
|
+
{ taskId: 1, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true },
|
|
269
269
|
{ fs: { existsSync: () => false } }
|
|
270
270
|
);
|
|
271
271
|
assert.equal(r.ok, true);
|
|
@@ -281,7 +281,7 @@ await ta('runShipGate: box + non-game diff → ok, no staging', async () => {
|
|
|
281
281
|
await ta('runShipGate: box + game diff + TTY, reviewer says yes → stages then ok, records tty sign-off', async () => {
|
|
282
282
|
const mp = path.join(markerDir, 'm2.json');
|
|
283
283
|
const deps = boxDeps(mp, { promptYesNo: async () => true });
|
|
284
|
-
const r = await sbx.runShipGate({ taskId: 927, files: ['
|
|
284
|
+
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/world/terrain.js'], skipStage: false, tty: true }, deps);
|
|
285
285
|
assert.equal(r.ok, true);
|
|
286
286
|
const m = JSON.parse(fs.readFileSync(mp, 'utf8'));
|
|
287
287
|
assert.equal(m.url, 'https://sandbox-test.example.com');
|
|
@@ -295,7 +295,7 @@ await ta('runShipGate: box + game diff + TTY, reviewer says yes → stages then
|
|
|
295
295
|
await ta('runShipGate: reviewer says NO → aborts with claim-preserving reason', async () => {
|
|
296
296
|
const mp = path.join(markerDir, 'm3.json');
|
|
297
297
|
const deps = boxDeps(mp, { promptYesNo: async () => false });
|
|
298
|
-
const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
298
|
+
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
299
299
|
assert.equal(r.ok, false);
|
|
300
300
|
assert.match(r.reason, /sandbox review declined/);
|
|
301
301
|
});
|
|
@@ -321,14 +321,14 @@ await ta('runShipGate: stale marker forces a re-stage (stamp mismatch)', async (
|
|
|
321
321
|
await ta('runShipGate: non-TTY → stages then ABORTS with the review URL (no prompt, no hang)', async () => {
|
|
322
322
|
const mp = path.join(markerDir, 'm6.json');
|
|
323
323
|
const deps = boxDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
|
|
324
|
-
const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: false }, deps);
|
|
324
|
+
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, deps);
|
|
325
325
|
assert.equal(r.ok, false, 'unreviewed game work must not ship non-interactively');
|
|
326
326
|
assert.equal(r.review, true, 'abort is flagged as a review pause');
|
|
327
327
|
assert.match(r.reason, /staged at https:\/\/sandbox-test\.example\.com/i);
|
|
328
328
|
assert.ok(fs.existsSync(mp), 'work is staged so the human can review at the URL');
|
|
329
329
|
// ...and after the (notional) review, a bare re-run passes on the fresh marker
|
|
330
330
|
// but records NO sign-off (no --approved) — back-compat / failure-open.
|
|
331
|
-
const r2 = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: false }, deps);
|
|
331
|
+
const r2 = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, deps);
|
|
332
332
|
assert.equal(r2.ok, true, 're-run after staging passes (marker fresh)');
|
|
333
333
|
assert.equal(r2.sandboxReview, undefined, 'bare re-run records no sign-off');
|
|
334
334
|
});
|
|
@@ -336,16 +336,16 @@ await ta('runShipGate: non-TTY → stages then ABORTS with the review URL (no pr
|
|
|
336
336
|
await ta('runShipGate: uncommitted game edit counts as previewable even when committed diff does not', async () => {
|
|
337
337
|
const mp = path.join(markerDir, 'm9.json');
|
|
338
338
|
const deps = boxDeps(mp, { promptYesNo: async () => true });
|
|
339
|
-
fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'dirty working tree\n');
|
|
339
|
+
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'dirty working tree\n');
|
|
340
340
|
const r = await sbx.runShipGate({ taskId: 927, files: ['docs/x.md'], skipStage: false, tty: true }, deps);
|
|
341
341
|
assert.equal(r.ok, true);
|
|
342
342
|
assert.ok(fs.existsSync(mp), 'gate staged the dirty game surface');
|
|
343
|
-
git('checkout', '--', 'public/game.js');
|
|
343
|
+
git('checkout', '--', 'modules/game/public/game.js');
|
|
344
344
|
});
|
|
345
345
|
|
|
346
346
|
await ta('runShipGate: --skip-stage → ok, untouched', async () => {
|
|
347
347
|
const mp = path.join(markerDir, 'm7.json');
|
|
348
|
-
const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: true, tty: true }, boxDeps(mp));
|
|
348
|
+
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: true, tty: true }, boxDeps(mp));
|
|
349
349
|
assert.equal(r.ok, true);
|
|
350
350
|
assert.equal(fs.existsSync(mp), false);
|
|
351
351
|
});
|
|
@@ -357,7 +357,7 @@ await ta('runShipGate: --skip-stage → ok, untouched', async () => {
|
|
|
357
357
|
await ta('1002607 runShipGate: staging failure (health never green) ABORTS — review gate cannot silently turn itself off', async () => {
|
|
358
358
|
const mp = path.join(markerDir, 'm8.json');
|
|
359
359
|
const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
|
|
360
|
-
const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
360
|
+
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
361
361
|
assert.equal(r.ok, false, 'a failed stage must not ship as if the review happened');
|
|
362
362
|
assert.equal(r.review, undefined, 'a failure to present is NOT a calm review pause');
|
|
363
363
|
assert.match(r.reason, /DID NOT run/, 'says plainly the review step did not run');
|
|
@@ -384,7 +384,7 @@ await ta('runShipGate: --approved cannot pre-approve an UNstaged change (still s
|
|
|
384
384
|
const deps = boxDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
|
|
385
385
|
// No marker → not staged-fresh → --approved is ignored; the gate still stages
|
|
386
386
|
// + aborts for review. You cannot sign off on something never shown.
|
|
387
|
-
const r = await sbx.runShipGate({ taskId: 1048, files: ['public/game/a.js'], skipStage: false, tty: false, approved: true }, deps);
|
|
387
|
+
const r = await sbx.runShipGate({ taskId: 1048, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false, approved: true }, deps);
|
|
388
388
|
assert.equal(r.ok, false, '--approved must not bypass the first stage + review');
|
|
389
389
|
assert.equal(r.review, true);
|
|
390
390
|
assert.equal(r.sandboxReview, undefined, 'nothing recorded — nothing was staged to approve');
|
|
@@ -402,13 +402,13 @@ await ta('syncWorktreeToServed: brings the branch content into the served checko
|
|
|
402
402
|
const g = (...a) => execFileSync('git', a, { cwd: served, encoding: 'utf8' });
|
|
403
403
|
g('init', '-q');
|
|
404
404
|
g('config', 'user.email', 't@e.com'); g('config', 'user.name', 't');
|
|
405
|
-
fs.mkdirSync(path.join(served, 'public'));
|
|
406
|
-
fs.writeFileSync(path.join(served, 'public', 'game.js'), 'MAIN content\n');
|
|
405
|
+
fs.mkdirSync(path.join(served, 'modules', 'game', 'public'), { recursive: true });
|
|
406
|
+
fs.writeFileSync(path.join(served, 'modules', 'game', 'public', 'game.js'), 'MAIN content\n');
|
|
407
407
|
g('add', '.'); g('commit', '-qm', 'main');
|
|
408
408
|
// a linked worktree on a feature branch with different game content
|
|
409
409
|
const wt = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-wt-')) + '/co';
|
|
410
410
|
g('worktree', 'add', '-q', '-b', 'claude/feat', wt);
|
|
411
|
-
fs.writeFileSync(path.join(wt, 'public', 'game.js'), 'BRANCH content\n');
|
|
411
|
+
fs.writeFileSync(path.join(wt, 'modules', 'game', 'public', 'game.js'), 'BRANCH content\n');
|
|
412
412
|
execFileSync('git', ['add', '.'], { cwd: wt });
|
|
413
413
|
execFileSync('git', ['commit', '-qm', 'branch edit'], { cwd: wt });
|
|
414
414
|
|
|
@@ -419,7 +419,7 @@ await ta('syncWorktreeToServed: brings the branch content into the served checko
|
|
|
419
419
|
// The claim is WHICH content landed, not how its lines end: git checks the fixture
|
|
420
420
|
// out under the machine's own autocrlf, so on Windows the file comes back CRLF and a
|
|
421
421
|
// byte-exact compare failed for a reason unrelated to the sync (task 1003529).
|
|
422
|
-
assert.ok(sameContent(fs.readFileSync(path.join(served, 'public', 'game.js'), 'utf8'), 'BRANCH content\n'),
|
|
422
|
+
assert.ok(sameContent(fs.readFileSync(path.join(served, 'modules', 'game', 'public', 'game.js'), 'utf8'), 'BRANCH content\n'),
|
|
423
423
|
'served /workspace now shows the SESSION branch content, not stale main');
|
|
424
424
|
|
|
425
425
|
g('worktree', 'remove', '--force', wt);
|
|
@@ -469,26 +469,26 @@ t('syncWorktreeToServed: detached HEAD / unresolvable branch → not ok', () =>
|
|
|
469
469
|
|
|
470
470
|
await ta('verifyServedFresh: served bytes match worktree → ok/verified', async () => {
|
|
471
471
|
// dirty a tracked game file so pickVerifyTarget selects it
|
|
472
|
-
fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'served-match\n');
|
|
472
|
+
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'served-match\n');
|
|
473
473
|
const r = await sbx.verifyServedFresh({
|
|
474
474
|
cwd: tmp,
|
|
475
475
|
healthUrl: 'http://127.0.0.1:3000/',
|
|
476
476
|
fetchImpl: async (url) => { assert.match(url, /\/game\.js$/); return { status: 200, text: async () => 'served-match\n' }; },
|
|
477
477
|
});
|
|
478
478
|
assert.equal(r.ok, true);
|
|
479
|
-
assert.equal(r.verified, 'public/game.js');
|
|
480
|
-
execFileSync('git', ['checkout', '--', 'public/game.js'], { cwd: tmp });
|
|
479
|
+
assert.equal(r.verified, 'modules/game/public/game.js');
|
|
480
|
+
execFileSync('git', ['checkout', '--', 'modules/game/public/game.js'], { cwd: tmp });
|
|
481
481
|
});
|
|
482
482
|
|
|
483
483
|
await ta('verifyServedFresh: served bytes DIFFER → not ok (serving stale code)', async () => {
|
|
484
|
-
fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'worktree-new\n');
|
|
484
|
+
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'worktree-new\n');
|
|
485
485
|
const r = await sbx.verifyServedFresh({
|
|
486
486
|
cwd: tmp,
|
|
487
487
|
fetchImpl: async () => ({ status: 200, text: async () => 'stale-old\n' }),
|
|
488
488
|
});
|
|
489
489
|
assert.equal(r.ok, false);
|
|
490
490
|
assert.match(r.reason, /serving stale code/);
|
|
491
|
-
execFileSync('git', ['checkout', '--', 'public/game.js'], { cwd: tmp });
|
|
491
|
+
execFileSync('git', ['checkout', '--', 'modules/game/public/game.js'], { cwd: tmp });
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
await ta('verifyServedFresh: no served static change → skipped (ok)', async () => {
|
|
@@ -506,7 +506,7 @@ await ta('stage(box): a HARD sync failure → {ok:false,hard:true}, no marker; r
|
|
|
506
506
|
assert.equal(staged.hard, true);
|
|
507
507
|
assert.equal(fs.existsSync(mp), false, 'no marker on a hard sync failure');
|
|
508
508
|
// and the gate turns a hard failure into a claim-preserving abort
|
|
509
|
-
const r = await sbx.runShipGate({ taskId: 1068, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
509
|
+
const r = await sbx.runShipGate({ taskId: 1068, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
510
510
|
assert.equal(r.ok, false);
|
|
511
511
|
assert.equal(r.review, true);
|
|
512
512
|
assert.match(r.reason, /could not be trusted/);
|
|
@@ -526,7 +526,7 @@ await ta('1002607 runShipGate(box): a SOFT failure (preview never healthy) now f
|
|
|
526
526
|
// sync ok (injected), but health never green → the ship must NOT continue
|
|
527
527
|
// as if the review ran (that was the silent-disable defect).
|
|
528
528
|
const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
|
|
529
|
-
const r = await sbx.runShipGate({ taskId: 1068, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
529
|
+
const r = await sbx.runShipGate({ taskId: 1068, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
530
530
|
assert.equal(r.ok, false, 'the soft failure aborts instead of silently skipping the review');
|
|
531
531
|
assert.match(r.reason, /sandbox staging failed/);
|
|
532
532
|
});
|
|
@@ -610,7 +610,7 @@ function localDeps(markerFile, overrides = {}) {
|
|
|
610
610
|
|
|
611
611
|
await ta('runShipGate(local): game diff + non-TTY → stages then ABORTS with the localhost URL', async () => {
|
|
612
612
|
const mp = path.join(markerDir, 'lm1.json');
|
|
613
|
-
const r = await sbx.runShipGate({ taskId: 1056, files: ['public/game/a.js'], skipStage: false, tty: false }, localDeps(mp));
|
|
613
|
+
const r = await sbx.runShipGate({ taskId: 1056, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, localDeps(mp));
|
|
614
614
|
assert.equal(r.ok, false, 'unreviewed game work must not ship non-interactively on a laptop');
|
|
615
615
|
assert.equal(r.review, true);
|
|
616
616
|
assert.match(r.reason, /staged at http:\/\/localhost:3100/i);
|
|
@@ -619,7 +619,7 @@ await ta('runShipGate(local): game diff + non-TTY → stages then ABORTS with th
|
|
|
619
619
|
|
|
620
620
|
await ta('runShipGate(local): TTY yes → records a tty sign-off with the localhost URL + kind=local marker', async () => {
|
|
621
621
|
const mp = path.join(markerDir, 'lm2.json');
|
|
622
|
-
const r = await sbx.runShipGate({ taskId: 1056, files: ['
|
|
622
|
+
const r = await sbx.runShipGate({ taskId: 1056, files: ['modules/game/world/terrain.js'], skipStage: false, tty: true }, localDeps(mp, { promptYesNo: async () => true }));
|
|
623
623
|
assert.equal(r.ok, true);
|
|
624
624
|
assert.equal(r.sandboxReview.method, 'tty');
|
|
625
625
|
assert.equal(r.sandboxReview.url, 'http://localhost:3100');
|
|
@@ -648,5 +648,86 @@ await ta('runShipGate(local): non-game diff (clean tree) → skip, no staging',
|
|
|
648
648
|
|
|
649
649
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
650
650
|
|
|
651
|
+
// ---------- 1002062: the surface list must describe a REAL tree ----------
|
|
652
|
+
//
|
|
653
|
+
// WHY THESE EXIST. Every case above feeds sandbox-stage its own fixture paths,
|
|
654
|
+
// so the previewable list matched NOTHING in the real repo for 69 days and all
|
|
655
|
+
// 47 tests stayed green. A suite that supplies the paths it tests cannot notice
|
|
656
|
+
// that the paths are wrong. These four look at the real thing instead.
|
|
657
|
+
|
|
658
|
+
t('1002062 the three surface spellings are DERIVED, so they cannot drift apart', () => {
|
|
659
|
+
// They used to be three hand-maintained copies. The module carve updated none
|
|
660
|
+
// of them, and nothing compared them. Derivation is the fix; this pins it.
|
|
661
|
+
assert.deepEqual(
|
|
662
|
+
sbx.PREVIEWABLE_PATHSPECS,
|
|
663
|
+
[...sbx.PREVIEWABLE_DIRS, ...sbx.PREVIEWABLE_ENTRYPOINTS],
|
|
664
|
+
"PATHSPECS must be derived from DIRS + ENTRYPOINTS, not hand-listed"
|
|
665
|
+
);
|
|
666
|
+
for (const d of sbx.PREVIEWABLE_DIRS) {
|
|
667
|
+
assert.equal(sbx.isPreviewableFile(`${d}/some/file.js`), true, `${d} must match via the derived prefixes`);
|
|
668
|
+
}
|
|
669
|
+
for (const e of sbx.PREVIEWABLE_ENTRYPOINTS) {
|
|
670
|
+
assert.equal(sbx.isPreviewableFile(e), true, `${e} must match via the derived file set`);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
t('1002062 no PRE-CARVE relic survives in the surface list', () => {
|
|
675
|
+
// The exact spellings that were stale: the carve (ADR 0083 / task 1418) moved
|
|
676
|
+
// them under modules/game/ and this list kept the old ones.
|
|
677
|
+
for (const relic of ['public', 'src/world', 'src/rooms']) {
|
|
678
|
+
assert.ok(
|
|
679
|
+
!sbx.PREVIEWABLE_PATHSPECS.includes(relic),
|
|
680
|
+
`${relic} is the pre-carve spelling and matches nothing — it must not be back`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
assert.ok(sbx.PREVIEWABLE_STATIC_DIR.startsWith('modules/'), 'the served static dir is module-carved');
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
t('1002062 a client directory that matched NOTHING is reported, not swallowed', () => {
|
|
687
|
+
// THE ACTUAL BUG. The sync collapsed to one boolean, so a run that synced only
|
|
688
|
+
// the entrypoints and missed every client dir reported plain success — the
|
|
689
|
+
// reporter saw "synced previewable surfaces" and got a stale sandbox.
|
|
690
|
+
const served = fs.mkdtempSync(path.join(os.tmpdir(), "sbx-missed-"));
|
|
691
|
+
const session = fs.mkdtempSync(path.join(os.tmpdir(), "sbx-sess-"));
|
|
692
|
+
const calls = [];
|
|
693
|
+
const execFileSyncStub = (bin, args) => {
|
|
694
|
+
const a = args.join(" ");
|
|
695
|
+
if (a.includes("rev-parse")) return "feature-branch\n";
|
|
696
|
+
// Entrypoints resolve; every client directory is absent from the branch.
|
|
697
|
+
const ps = args[args.length - 1];
|
|
698
|
+
if (sbx.PREVIEWABLE_DIRS.includes(ps) && a.includes("feature-branch")) {
|
|
699
|
+
throw new Error(`pathspec ${ps} did not match any file(s) known to git`);
|
|
700
|
+
}
|
|
701
|
+
calls.push(a);
|
|
702
|
+
return "";
|
|
703
|
+
};
|
|
704
|
+
const r = sbx.syncWorktreeToServed({
|
|
705
|
+
execFileSync: execFileSyncStub,
|
|
706
|
+
servedRoot: served,
|
|
707
|
+
sessionRoot: session,
|
|
708
|
+
cwd: session,
|
|
709
|
+
});
|
|
710
|
+
assert.equal(r.ok, true, "the entrypoints synced, so the sync itself is not a hard failure");
|
|
711
|
+
assert.deepEqual(
|
|
712
|
+
r.missedDirs, sbx.PREVIEWABLE_DIRS,
|
|
713
|
+
"every client directory missed, and the caller must be told which"
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
t('1002062 pickVerifyTarget maps the CARVED static dir onto the served URL', () => {
|
|
718
|
+
// modules/game/public/game.js is served at /game.js. The old code stripped a
|
|
719
|
+
// hardcoded "public/" prefix, which post-carve would have left the whole
|
|
720
|
+
// modules/game/ path in the URL.
|
|
721
|
+
const rel = `${sbx.PREVIEWABLE_STATIC_DIR}/game.js`;
|
|
722
|
+
const target = sbx.pickVerifyTarget({
|
|
723
|
+
cwd: "/nowhere",
|
|
724
|
+
fs: { existsSync: () => true },
|
|
725
|
+
execFileSync: (bin, args) => (args.includes("--porcelain") ? ` M ${rel}\n` : ""),
|
|
726
|
+
});
|
|
727
|
+
assert.ok(target, "a changed file under the static dir must be pickable");
|
|
728
|
+
assert.equal(target.relPath, rel);
|
|
729
|
+
assert.equal(target.urlPath, "/game.js", "the carved prefix is stripped, not just \"public/\"");
|
|
730
|
+
});
|
|
731
|
+
|
|
651
732
|
console.log(`\nsandbox_stage: ${passed} passed, ${failed} failed`);
|
|
652
733
|
process.exit(failed ? 1 : 0);
|