@bongos/core 1.19.711 → 1.19.713
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 +14 -14
- package/docs/module-api-changelog.md +4 -0
- package/modules/government/config.js +22 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/sandbox-stage.js +56 -12
- package/src/module-api.js +1 -1
- package/tests/government_vanilla_boot.mjs +10 -7
- package/tests/profile_route.mjs +0 -1
- package/tests/sandbox_stage.mjs +111 -30
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.713",
|
|
6
|
+
"core_contract": "1.19.713",
|
|
7
|
+
"source_commit": "5be761d1e912b17b2dd926e9e0b592ca16fd03d7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-13T07:
|
|
9
|
+
"built_at": "2026-09-13T07:37:27.419Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 478,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2649,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "4b976390238b5dcf6d47222e20f7b6e8888937d359baebc19d1f1a0ba6f00b6e",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2807,7 +2807,7 @@
|
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/module-api-changelog.md",
|
|
2809
2809
|
"mode": "0000644",
|
|
2810
|
-
"sha256": "
|
|
2810
|
+
"sha256": "0a5717521fbdc11a214fa18fab654d79d7a99edd1bca3e593130b05bc9039ab2"
|
|
2811
2811
|
},
|
|
2812
2812
|
{
|
|
2813
2813
|
"path": "docs/modules-contract.md",
|
|
@@ -4547,7 +4547,7 @@
|
|
|
4547
4547
|
{
|
|
4548
4548
|
"path": "modules/government/config.js",
|
|
4549
4549
|
"mode": "0000644",
|
|
4550
|
-
"sha256": "
|
|
4550
|
+
"sha256": "0110e6a9421696632fa749291547045c60ace60255d55f35a88accd61dcf5c80"
|
|
4551
4551
|
},
|
|
4552
4552
|
{
|
|
4553
4553
|
"path": "modules/government/db.js",
|
|
@@ -7817,12 +7817,12 @@
|
|
|
7817
7817
|
{
|
|
7818
7818
|
"path": "package-lock.json",
|
|
7819
7819
|
"mode": "0000644",
|
|
7820
|
-
"sha256": "
|
|
7820
|
+
"sha256": "376faf36a3b58a417b8f70396e85d6c43ae3fda00a2d04b1aeac65b649068fa9"
|
|
7821
7821
|
},
|
|
7822
7822
|
{
|
|
7823
7823
|
"path": "package.json",
|
|
7824
7824
|
"mode": "0000644",
|
|
7825
|
-
"sha256": "
|
|
7825
|
+
"sha256": "08d91f7a26e6d416fb03293a5ce8cfbd54ec8c9c3b390e51896e1712327da69b"
|
|
7826
7826
|
},
|
|
7827
7827
|
{
|
|
7828
7828
|
"path": "public-docs/index.html",
|
|
@@ -8762,7 +8762,7 @@
|
|
|
8762
8762
|
{
|
|
8763
8763
|
"path": "scripts/gds/sandbox-stage.js",
|
|
8764
8764
|
"mode": "0000644",
|
|
8765
|
-
"sha256": "
|
|
8765
|
+
"sha256": "9d7911da631e8da6c72875a6385502f4dfc0d2d6160ed95d56aa3f25c3f86878"
|
|
8766
8766
|
},
|
|
8767
8767
|
{
|
|
8768
8768
|
"path": "scripts/gds/search-ingest-db.js",
|
|
@@ -9602,7 +9602,7 @@
|
|
|
9602
9602
|
{
|
|
9603
9603
|
"path": "src/module-api.js",
|
|
9604
9604
|
"mode": "0000644",
|
|
9605
|
-
"sha256": "
|
|
9605
|
+
"sha256": "87766080733fb825c08f63e2aeef763fde461f58aab511af3c5814c6316a8a04"
|
|
9606
9606
|
},
|
|
9607
9607
|
{
|
|
9608
9608
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11107,7 +11107,7 @@
|
|
|
11107
11107
|
{
|
|
11108
11108
|
"path": "tests/government_vanilla_boot.mjs",
|
|
11109
11109
|
"mode": "0000644",
|
|
11110
|
-
"sha256": "
|
|
11110
|
+
"sha256": "5c2676855575784f9fffcf909dd6179e4b588e49cb5c80b5395d6803bf89386d"
|
|
11111
11111
|
},
|
|
11112
11112
|
{
|
|
11113
11113
|
"path": "tests/government_vocabulary.mjs",
|
|
@@ -12072,7 +12072,7 @@
|
|
|
12072
12072
|
{
|
|
12073
12073
|
"path": "tests/profile_route.mjs",
|
|
12074
12074
|
"mode": "0000644",
|
|
12075
|
-
"sha256": "
|
|
12075
|
+
"sha256": "ef2a58d60f561da1e025cf0958bdcb66072545158a68f3e41b4bc683afbed9d6"
|
|
12076
12076
|
},
|
|
12077
12077
|
{
|
|
12078
12078
|
"path": "tests/profile_ui_cross_project.mjs",
|
|
@@ -12492,7 +12492,7 @@
|
|
|
12492
12492
|
{
|
|
12493
12493
|
"path": "tests/sandbox_stage.mjs",
|
|
12494
12494
|
"mode": "0000644",
|
|
12495
|
-
"sha256": "
|
|
12495
|
+
"sha256": "3873f8b93e78f026dd0dd6d8cdaf3aa68e73b3f9ce95d47bf8415cf242d42471"
|
|
12496
12496
|
},
|
|
12497
12497
|
{
|
|
12498
12498
|
"path": "tests/scouting_owner_access_and_reach.mjs",
|
|
@@ -1881,5 +1881,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1881
1881
|
landed since 1.19.709 with no explicit bump. run 34744369035. (task 1002620)
|
|
1882
1882
|
1.19.711 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1883
1883
|
landed since 1.19.710 with no explicit bump. run 34744940181. (task 1002620)
|
|
1884
|
+
1.19.712 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1885
|
+
landed since 1.19.711 with no explicit bump. run 34745412403. (task 1002620)
|
|
1886
|
+
1.19.713 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1887
|
+
landed since 1.19.712 with no explicit bump. run 34745667268. (task 1002620)
|
|
1884
1888
|
---------------------------------------------------------------------------
|
|
1885
1889
|
```
|
|
@@ -33,8 +33,13 @@
|
|
|
33
33
|
// Their grants are DERIVED from catalog floors and materialised by
|
|
34
34
|
// governance_002; config may relabel a seeded rank, never re-grant it.
|
|
35
35
|
//
|
|
36
|
-
// Stripping rather than throwing matches resolver.js: a
|
|
37
|
-
//
|
|
36
|
+
// Stripping rather than throwing matches resolver.js: a template entry that asks for
|
|
37
|
+
// authority it may not have is DROPPED, so a wrong entry fails CLOSED rather than
|
|
38
|
+
// bricking the boot.
|
|
39
|
+
//
|
|
40
|
+
// SCOPE OF THAT RULE — it is about a pack that PARSES and asks for the wrong thing.
|
|
41
|
+
// A pack that does not parse at all is the other case, and the disk loader below
|
|
42
|
+
// throws on it deliberately. See the loadGovernmentConfig header.
|
|
38
43
|
//
|
|
39
44
|
// SLICE BOUNDARY: this file is the config CONTRACT, and deliberately has no production
|
|
40
45
|
// caller yet — R96 is the "what shape is a rank template, and what can config never
|
|
@@ -321,11 +326,22 @@ function readJson(p) {
|
|
|
321
326
|
catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
322
327
|
}
|
|
323
328
|
|
|
324
|
-
// Disk loader with the documented precedence. BOTH files are optional: the
|
|
325
|
-
// pack because a vanilla instance ships only the neutral starter, and the
|
|
326
|
-
// starter because the four ranks still resolve to their canonical names without
|
|
329
|
+
// Disk loader with the documented precedence. BOTH files are optional when ABSENT: the
|
|
330
|
+
// instance pack because a vanilla instance ships only the neutral starter, and the
|
|
331
|
+
// neutral starter because the four ranks still resolve to their canonical names without
|
|
327
332
|
// it. Unlike branding — where a missing identity is fatal — no value here is required,
|
|
328
|
-
// so
|
|
333
|
+
// so an absent pack falls through to sane defaults.
|
|
334
|
+
//
|
|
335
|
+
// ABSENT and MALFORMED are NOT the same case, and this loader treats them differently
|
|
336
|
+
// on purpose. readJson() swallows only ENOENT; a pack that is present but does not
|
|
337
|
+
// parse raises a SyntaxError that propagates out of here and stops boot. A corrupt
|
|
338
|
+
// authority-adjacent file must be fixed by a human, not silently replaced by defaults
|
|
339
|
+
// nobody chose — the loud stop IS the fail-closed behaviour, since a boot that never
|
|
340
|
+
// completes grants nothing. Both halves are pinned as source: the absent half by
|
|
341
|
+
// tests/government_config.mjs ('tolerates a missing instance pack') and
|
|
342
|
+
// tests/government_vanilla_boot.mjs's no-packs-at-all case, the malformed half by
|
|
343
|
+
// that file's 'a malformed config pack fails LOUD'. Changing the throw into a
|
|
344
|
+
// fallback is therefore a deliberate decision plus a test edit, not a tidy-up.
|
|
329
345
|
//
|
|
330
346
|
// CALL THIS AT BOOT, NOT PER REQUEST. The two reads are synchronous, which is correct
|
|
331
347
|
// for a startup read of a committed file and wrong inside a request handler. If a
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.713",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.713",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.713",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -44,10 +44,27 @@ const HEALTH_POLL_MS = 2_000;
|
|
|
44
44
|
// GAME-ONLY entry (src/preview-server.js, ADR 0052), so only game-rendering paths
|
|
45
45
|
// are previewable — GDS/internal-tooling diffs have nothing to look at there.
|
|
46
46
|
// src/preview-server.js + server.js are the entrypoints that render the game.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
47
|
+
// The GAME CLIENT directories the sandbox renders. These MOVED in the module
|
|
48
|
+
// carve (ADR 0083, BV1.R49 / task 1418): public/ -> modules/game/public/,
|
|
49
|
+
// src/world/ -> modules/game/world/, src/rooms/ -> modules/game/rooms/. The
|
|
50
|
+
// pre-carve spelling sat here matching NOTHING, and staging still reported
|
|
51
|
+
// success because the two entrypoints below kept syncing — so a builder staged,
|
|
52
|
+
// saw 'synced previewable surfaces', and got a sandbox serving whatever client
|
|
53
|
+
// was already on /workspace (task 1002062; found when a new /studio page 404'd).
|
|
54
|
+
const PREVIEWABLE_DIRS = ['modules/game/public', 'modules/game/world', 'modules/game/rooms'];
|
|
55
|
+
// The single directory the preview serves AT THE URL ROOT: modules/game/public/x
|
|
56
|
+
// is served as /x. pickVerifyTarget maps between the two, so it must be exactly
|
|
57
|
+
// one dir — hence its own constant rather than PREVIEWABLE_DIRS[0].
|
|
58
|
+
const PREVIEWABLE_STATIC_DIR = 'modules/game/public';
|
|
59
|
+
// The entrypoints that render the game. A sparse box (STARTER_SPARSE_PATHS) may
|
|
60
|
+
// legitimately lack these, which is why a miss here is not on its own an error.
|
|
61
|
+
const PREVIEWABLE_ENTRYPOINTS = ['server.js', 'src/preview-server.js'];
|
|
62
|
+
// DERIVED, all three of them. They used to be three hand-maintained copies of the
|
|
63
|
+
// same list, and the carve updated none of them; deriving removes the drift class
|
|
64
|
+
// rather than just this instance of it.
|
|
65
|
+
const PREVIEWABLE_PREFIXES = PREVIEWABLE_DIRS.map((d) => `${d}/`);
|
|
66
|
+
const PREVIEWABLE_FILES = new Set(PREVIEWABLE_ENTRYPOINTS);
|
|
67
|
+
const PREVIEWABLE_PATHSPECS = [...PREVIEWABLE_DIRS, ...PREVIEWABLE_ENTRYPOINTS];
|
|
51
68
|
|
|
52
69
|
// On a box, box-game-preview.service has WorkingDirectory=/workspace and serves
|
|
53
70
|
// /workspace/public — the MAIN checkout. A Claude session, though, runs in a git
|
|
@@ -316,6 +333,11 @@ function syncWorktreeToServed(deps = {}) {
|
|
|
316
333
|
// then overlay the session branch's content.
|
|
317
334
|
let synced = false;
|
|
318
335
|
let lastErr = null;
|
|
336
|
+
// task 1002062: this collapsed to a single boolean, so a run that synced ONLY
|
|
337
|
+
// the entrypoints and missed every client directory reported plain success.
|
|
338
|
+
// Track them by name — the caller warns on a missed DIRECTORY, which is the
|
|
339
|
+
// shape a stale pathspec list takes.
|
|
340
|
+
const missed = [];
|
|
319
341
|
for (const ps of PREVIEWABLE_PATHSPECS) {
|
|
320
342
|
try {
|
|
321
343
|
run('git', ['-C', servedRoot, 'checkout', '--', ps], { stdio: 'ignore' });
|
|
@@ -327,12 +349,16 @@ function syncWorktreeToServed(deps = {}) {
|
|
|
327
349
|
synced = true;
|
|
328
350
|
} catch (e) {
|
|
329
351
|
lastErr = e; // pathspec absent in the branch (sparse/missing) — skip it
|
|
352
|
+
missed.push(ps);
|
|
330
353
|
}
|
|
331
354
|
}
|
|
332
355
|
if (!synced) {
|
|
333
|
-
return { ok: false, synced: false, reason: `git checkout ${branch} -- <previewable> in ${servedRoot} synced nothing${lastErr ? ` (${lastErr.message})` : ''}` };
|
|
356
|
+
return { ok: false, synced: false, missed, reason: `git checkout ${branch} -- <previewable> in ${servedRoot} synced nothing${lastErr ? ` (${lastErr.message})` : ''}` };
|
|
334
357
|
}
|
|
335
|
-
|
|
358
|
+
// A missed ENTRYPOINT is normal on a sparse box. A missed client DIRECTORY means
|
|
359
|
+
// the sandbox is about to serve a client this stage never touched.
|
|
360
|
+
const missedDirs = missed.filter((m) => PREVIEWABLE_DIRS.includes(m));
|
|
361
|
+
return { ok: true, synced: true, branch, missed, missedDirs };
|
|
336
362
|
}
|
|
337
363
|
|
|
338
364
|
// Pick the smallest cheap-to-compare changed previewable static file to verify,
|
|
@@ -343,21 +369,26 @@ function pickVerifyTarget(deps = {}) {
|
|
|
343
369
|
const cwd = deps.cwd || process.cwd();
|
|
344
370
|
const f = deps.fs || fs;
|
|
345
371
|
try {
|
|
346
|
-
|
|
372
|
+
// task 1002062: this hardcoded 'public' three times, independently of the
|
|
373
|
+
// constants above, so it kept the pre-carve spelling even after they moved.
|
|
374
|
+
// Derived from PREVIEWABLE_STATIC_DIR now, so there is one place to change.
|
|
375
|
+
const staticDir = deps.staticDir || PREVIEWABLE_STATIC_DIR;
|
|
376
|
+
const prefix = `${staticDir}/`;
|
|
377
|
+
const out = run('git', ['status', '--porcelain', '--', staticDir], { cwd, encoding: 'utf8' });
|
|
347
378
|
let chosen = out
|
|
348
379
|
.split('\n')
|
|
349
380
|
.map((line) => line.slice(3).replace(/^"|"$/g, '').trim())
|
|
350
|
-
.find((rel) => rel && rel.startsWith(
|
|
381
|
+
.find((rel) => rel && rel.startsWith(prefix) && f.existsSync(path.join(cwd, rel)));
|
|
351
382
|
if (!chosen) {
|
|
352
383
|
// committed-only change: fall back to the diff vs the frozen origin/main
|
|
353
|
-
const diff = run('git', ['diff', '--name-only', 'origin/main...HEAD', '--',
|
|
384
|
+
const diff = run('git', ['diff', '--name-only', 'origin/main...HEAD', '--', staticDir], { cwd, encoding: 'utf8' });
|
|
354
385
|
chosen = diff
|
|
355
386
|
.split('\n')
|
|
356
387
|
.map((s) => s.trim())
|
|
357
|
-
.find((p) => p.startsWith(
|
|
388
|
+
.find((p) => p.startsWith(prefix) && f.existsSync(path.join(cwd, p)));
|
|
358
389
|
}
|
|
359
390
|
if (!chosen) return null;
|
|
360
|
-
return { relPath: chosen, urlPath: '/' + chosen.
|
|
391
|
+
return { relPath: chosen, urlPath: '/' + chosen.slice(prefix.length) };
|
|
361
392
|
} catch {
|
|
362
393
|
return null;
|
|
363
394
|
}
|
|
@@ -374,7 +405,7 @@ async function verifyServedFresh(deps = {}) {
|
|
|
374
405
|
const cwd = deps.cwd || process.cwd();
|
|
375
406
|
const base = (deps.healthUrl || HEALTH_URL).replace(/\/$/, '');
|
|
376
407
|
const target = pickVerifyTarget(deps);
|
|
377
|
-
if (!target) return { ok: true, skipped: true, reason:
|
|
408
|
+
if (!target) return { ok: true, skipped: true, reason: `no served static change to verify (e.g. only ${PREVIEWABLE_DIRS.filter((d) => d !== PREVIEWABLE_STATIC_DIR).join(' or ')} changed)` };
|
|
378
409
|
let want;
|
|
379
410
|
try {
|
|
380
411
|
want = f.readFileSync(path.join(cwd, target.relPath), 'utf8');
|
|
@@ -455,6 +486,16 @@ async function stage({ taskId } = {}, deps = {}) {
|
|
|
455
486
|
return { ok: false, hard: true, reason: `could not sync your worktree to the served checkout (${sync.reason}); the sandbox would show stale ${deps.servedRoot || BOX_SERVED_ROOT} code` };
|
|
456
487
|
}
|
|
457
488
|
if (sync.synced) console.log(` synced previewable surfaces from ${sync.branch} into ${deps.servedRoot || BOX_SERVED_ROOT}`);
|
|
489
|
+
// task 1002062 — the tell that went unsaid for 69 days. Staging "succeeded"
|
|
490
|
+
// while every game-client directory matched nothing, so the sandbox served a
|
|
491
|
+
// client the stage never touched and the only hint was a freshness-verify skip.
|
|
492
|
+
if (sync.missedDirs && sync.missedDirs.length) {
|
|
493
|
+
console.warn(
|
|
494
|
+
` ⚠ these previewable directories matched NOTHING in ${sync.branch}: ${sync.missedDirs.join(', ')}`
|
|
495
|
+
+ ' — the sandbox will serve whatever client is already on the served checkout.'
|
|
496
|
+
+ ' If this repo carved or renamed them, PREVIEWABLE_DIRS in sandbox-stage.js is stale.'
|
|
497
|
+
);
|
|
498
|
+
}
|
|
458
499
|
}
|
|
459
500
|
try {
|
|
460
501
|
ctx.restart(deps);
|
|
@@ -699,6 +740,9 @@ module.exports = {
|
|
|
699
740
|
BOX_ENV_PATH,
|
|
700
741
|
BOX_SERVED_ROOT,
|
|
701
742
|
PREVIEWABLE_PATHSPECS,
|
|
743
|
+
PREVIEWABLE_DIRS,
|
|
744
|
+
PREVIEWABLE_ENTRYPOINTS,
|
|
745
|
+
PREVIEWABLE_STATIC_DIR,
|
|
702
746
|
sessionCheckoutRoot,
|
|
703
747
|
syncWorktreeToServed,
|
|
704
748
|
pickVerifyTarget,
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.713'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -94,13 +94,16 @@ test('a core with NO config packs at all still boots four roles rather than none
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test('a malformed config pack fails LOUD — it never degrades into a silent grant', () => {
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
97
|
+
// Pins the loader's real behaviour: readJson() returns null only for ENOENT, so a
|
|
98
|
+
// syntactically broken pack throws. That is fail-CLOSED (no authority is produced),
|
|
99
|
+
// which is the property this proof cares about.
|
|
100
|
+
//
|
|
101
|
+
// This assertion used to contradict the loader's own header note, which promised
|
|
102
|
+
// "the loader fails to sane defaults rather than throwing" for the malformed case
|
|
103
|
+
// too. The PROSE was the bug, not the throw: config.js now states the absent and
|
|
104
|
+
// malformed cases separately and names this test as the oracle for the second
|
|
105
|
+
// (task 1002876). Turning a corrupt authority-adjacent file into a silent fallback
|
|
106
|
+
// remains a decision, not a typo — it would have to change this assertion first.
|
|
104
107
|
withScratchDir((dir) => {
|
|
105
108
|
const broken = path.join(dir, 'broken.json');
|
|
106
109
|
fs.writeFileSync(broken, '{ not json');
|
package/tests/profile_route.mjs
CHANGED
|
@@ -157,7 +157,6 @@ require.cache[authPath] = {
|
|
|
157
157
|
id: authPath, filename: authPath, loaded: true,
|
|
158
158
|
exports: {
|
|
159
159
|
requireBuilder: (_req, _res, next) => next(),
|
|
160
|
-
requireRank: () => (_req, _res, next) => next(),
|
|
161
160
|
requirePermission: () => (_req, _res, next) => next(),
|
|
162
161
|
// ADR 0215 D4: the sliver route beside the directory gates on RANK, so the
|
|
163
162
|
// partial doorway must answer requireRank too or the router will not load.
|
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);
|