@bongos/core 1.19.694 → 1.19.696
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 +28 -38
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -0
- package/docs/copy-inventory.md +14 -24
- package/docs/copy-registry.json +29 -130
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/government/catalog.js +10 -2
- package/modules/hall-ui/public/panel.css +3 -3
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +4 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/worktree.js +52 -1
- package/src/bongos/auth.js +11 -25
- package/src/bongos/serve-internal.js +13 -11
- package/src/module-api.js +1 -1
- package/tests/auth_resolve_failure.mjs +9 -8
- package/tests/government_page_gates.mjs +13 -7
- package/tests/hall_page_gate_map.mjs +87 -14
- package/tests/hall_settings_world.mjs +6 -3
- package/tests/scouting_page_gate.mjs +6 -3
- package/tests/worktree_helper.mjs +50 -1
- package/modules/hall-ui/public/not-ready.html +0 -53
- package/modules/hall-ui/public/not-ready.states.json +0 -14
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.696'; // 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');
|
|
@@ -92,19 +92,20 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
|
|
|
92
92
|
// The enumeration, kept as an executable assertion rather than prose so it cannot
|
|
93
93
|
// rot: a NEW bare `async function require*` added later fails this test.
|
|
94
94
|
// requireBuilder — the API gate. Guarded here (BV1.R109-follow-up).
|
|
95
|
-
// requireBuilderPage
|
|
96
|
-
// requireGovernmentPage
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
95
|
+
// requireBuilderPage \
|
|
96
|
+
// requireGovernmentPage \ all FOUR delegate to runPageGate, guarded by
|
|
97
|
+
// requireProjectCuratePage / BV1.R107 (the /people curate gate; the
|
|
98
|
+
// requireBoardVotePage / ADR 0266 /board-room vote gate)
|
|
99
|
+
// Task 1003450 removed two more — requireNonXenosPage and the R113
|
|
100
|
+
// requireGovernmentManagePage — which had both ended up dispatched from no
|
|
101
|
+
// route at all; tests/hall_page_gate_map.mjs now fails if that recurs.
|
|
101
102
|
// Anything else async and router-mounted must be added with its own try/catch.
|
|
102
103
|
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
|
|
103
104
|
const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
|
|
104
|
-
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', '
|
|
105
|
+
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage'],
|
|
105
106
|
'a new async router middleware appeared — give it a try/catch and add it here');
|
|
106
107
|
// The page gates must actually delegate (that is WHY they are safe).
|
|
107
|
-
for (const name of ['requireBuilderPage', 'requireGovernmentPage', '
|
|
108
|
+
for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage', 'requireBoardVotePage']) {
|
|
108
109
|
const fn = src.slice(src.indexOf(`async function ${name}`));
|
|
109
110
|
assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
|
|
110
111
|
}
|
|
@@ -50,14 +50,19 @@ const resolver = require('../modules/government/resolver.js');
|
|
|
50
50
|
const wandering = require('../modules/builder-settings/wandering-prefs.js');
|
|
51
51
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
52
52
|
|
|
53
|
-
const GATES = ['requireBuilderPage', '
|
|
53
|
+
const GATES = ['requireBuilderPage', 'requireGovernmentPage'];
|
|
54
54
|
const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
55
55
|
|
|
56
56
|
// ═══ the ORACLE — hand-written, from the rank tests R107 deleted ═════════════
|
|
57
57
|
// requireBuilderPage admitted any session at all → every live rank
|
|
58
|
-
// requireNonXenosPage admitted rank !== 'xenos' → thetes and up
|
|
59
58
|
// requireGovernmentPage admitted rank === 'archon' → archon only …until ADR 0157
|
|
60
59
|
//
|
|
60
|
+
// R107 migrated a THIRD gate, requireNonXenosPage (rank !== 'xenos' → thetes and
|
|
61
|
+
// up, via page.view.builder). Task 1002710 emptied it of routes and task 1003450
|
|
62
|
+
// deleted it; its rows are gone from the tables below rather than kept as an
|
|
63
|
+
// oracle for a function that no longer exists. The parity claim it carried is
|
|
64
|
+
// unaffected — it held right up to the deletion.
|
|
65
|
+
//
|
|
61
66
|
// ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
|
|
62
67
|
// the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
|
|
63
68
|
// was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
|
|
@@ -70,7 +75,6 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
|
70
75
|
// the gate matches its declared floor rather than merely "didn't change".
|
|
71
76
|
const PRE_R107_AUDIENCE = {
|
|
72
77
|
requireBuilderPage: { xenos: true, thetes: true, metic: true, archon: true },
|
|
73
|
-
requireNonXenosPage: { xenos: false, thetes: true, metic: true, archon: true },
|
|
74
78
|
requireGovernmentPage: { xenos: false, thetes: false, metic: true, archon: true },
|
|
75
79
|
};
|
|
76
80
|
// Where a SIGNED-IN builder who fails the gate is sent. Never the sign-in flow —
|
|
@@ -80,12 +84,10 @@ const PRE_R107_AUDIENCE = {
|
|
|
80
84
|
// instead of silently dumping the builder at the home page.
|
|
81
85
|
const DENIED_TO = {
|
|
82
86
|
requireBuilderPage: '/builders',
|
|
83
|
-
requireNonXenosPage: '/builders/not-ready',
|
|
84
87
|
requireGovernmentPage: '/builders',
|
|
85
88
|
};
|
|
86
89
|
const GATE_PERMISSION = {
|
|
87
90
|
requireBuilderPage: 'page.view.public',
|
|
88
|
-
requireNonXenosPage: 'page.view.builder',
|
|
89
91
|
requireGovernmentPage: 'page.view.government',
|
|
90
92
|
};
|
|
91
93
|
const deniedLoc = (gate, { unavailable = false } = {}) =>
|
|
@@ -93,7 +95,6 @@ const deniedLoc = (gate, { unavailable = false } = {}) =>
|
|
|
93
95
|
// The equivalent rank floor, for the independent cross-check below.
|
|
94
96
|
const GATE_FLOOR = {
|
|
95
97
|
requireBuilderPage: 'xenos',
|
|
96
|
-
requireNonXenosPage: 'thetes',
|
|
97
98
|
// ADR 0157: `page.view.government` floor archon→metic.
|
|
98
99
|
requireGovernmentPage: 'metic',
|
|
99
100
|
};
|
|
@@ -264,9 +265,14 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
|
|
|
264
265
|
test('B5: every permission a page gate names is a real catalog key', async () => {
|
|
265
266
|
// Not an oracle — a sanity check that the gates did not migrate onto a typo,
|
|
266
267
|
// which would silently deny everyone (fail-closed, but a bug all the same).
|
|
267
|
-
for (const key of ['page.view.public', 'page.view.
|
|
268
|
+
for (const key of ['page.view.public', 'page.view.government']) {
|
|
268
269
|
assert.ok(catalog.byKey(key), `${key} must exist in the catalog`);
|
|
269
270
|
}
|
|
271
|
+
// page.view.builder is deliberately NOT in that list any more: task 1003450
|
|
272
|
+
// deleted the only gate that named it. The atom is still seeded and attachable
|
|
273
|
+
// (catalog.js says so), so it is still a real key — it just is not a page
|
|
274
|
+
// gate's permission, and asserting it here would imply otherwise.
|
|
275
|
+
assert.ok(catalog.byKey('page.view.builder'), 'the atom survives its gate');
|
|
270
276
|
});
|
|
271
277
|
|
|
272
278
|
// ═══ PART C — the wandering CEILING (role metadata, not a permission) ════════
|
|
@@ -34,11 +34,13 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
|
|
|
34
34
|
//
|
|
35
35
|
// Audiences (the floors live in modules/government/catalog.js; the gate
|
|
36
36
|
// functions in src/bongos/auth.js):
|
|
37
|
-
// requireBuilderPage page.view.public
|
|
37
|
+
// requireBuilderPage page.view.public any signed-in builder (xenos+)
|
|
38
38
|
// requireGovernmentPage page.view.government metic+ (ADR 0157)
|
|
39
39
|
// requireProjectCuratePage project.curate metic+
|
|
40
|
-
// requireGovernmentManagePage government.manage archon
|
|
41
40
|
// requireBoardVotePage board.vote.cast xenos+ (ADR 0266)
|
|
41
|
+
// That list is now CHECKED, not just written: "every page gate is dispatched"
|
|
42
|
+
// below reads the gate table out of auth.js and fails if a gate here guards no
|
|
43
|
+
// URL. Task 1003450 added it after finding two that did not.
|
|
42
44
|
const ORACLE = {
|
|
43
45
|
// Any signed-in builder — no rank floor.
|
|
44
46
|
'/builders/work': 'requireBuilderPage',
|
|
@@ -60,10 +62,11 @@ const ORACLE = {
|
|
|
60
62
|
'/builders/studio': 'requireBuilderPage',
|
|
61
63
|
'/builders/ideas': 'requireBuilderPage',
|
|
62
64
|
'/builders/collab': 'requireBuilderPage',
|
|
63
|
-
// task 1002710: the orientation reading rooms moved DOWN to this tier from
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
65
|
+
// task 1002710: the orientation reading rooms moved DOWN to this tier from a
|
|
66
|
+
// thetes+ gate. They are the onboarding documents, so gating them above xenos
|
|
67
|
+
// turned away the exact reader they were written for. The sign-in requirement
|
|
68
|
+
// is unchanged — only the RANK floor is gone. (Task 1003450 then deleted the
|
|
69
|
+
// gate those two rows left behind; see GONE_GATES below.)
|
|
67
70
|
'/builders/primer': 'requireBuilderPage',
|
|
68
71
|
'/builders/primer.md': 'requireBuilderPage',
|
|
69
72
|
'/builders/diagrams': 'requireBuilderPage',
|
|
@@ -118,7 +121,6 @@ const ORACLE = {
|
|
|
118
121
|
'/builders/ranks': null,
|
|
119
122
|
'/builders/drachmae': null,
|
|
120
123
|
'/builders/profile': null,
|
|
121
|
-
'/builders/not-ready': null,
|
|
122
124
|
|
|
123
125
|
// NOT deliberate — recorded because it is true, and filed as idea 1000907
|
|
124
126
|
// (task 1003448). The detail pages are addressed by a second segment, and
|
|
@@ -137,6 +139,15 @@ const ORACLE = {
|
|
|
137
139
|
// from the map" is indistinguishable from the oversight this file exists to
|
|
138
140
|
// catch. GONE_PAGES below keeps the row honest if the file ever returns.
|
|
139
141
|
'/builders/my-projects': null,
|
|
142
|
+
|
|
143
|
+
// Gone too, and for a reason worth keeping in front of the next reader: this
|
|
144
|
+
// was a bespoke refusal PAGE ("not yet — ship three tasks to graduate"), the
|
|
145
|
+
// deniedTo of the thetes+ gate. Task 1002613 replaced that whole idea with a
|
|
146
|
+
// banner the shell renders from ?denied=<permission> on whatever page the
|
|
147
|
+
// redirect lands on — which names the permission actually missing instead of
|
|
148
|
+
// assuming one tier's remedy. Once task 1002710 emptied the gate, nothing
|
|
149
|
+
// linked here or redirected here; task 1003450 removed page and gate together.
|
|
150
|
+
'/builders/not-ready': null,
|
|
140
151
|
};
|
|
141
152
|
|
|
142
153
|
// ─── reconstruct the URL→gate map from the source ───────────────────────────
|
|
@@ -250,6 +261,8 @@ const GONE_PAGES = {
|
|
|
250
261
|
// redirect. tests/hall_settings_world.mjs pins the deletion itself; this row
|
|
251
262
|
// pins what the deletion means for the gate map.
|
|
252
263
|
'/builders/my-projects': 'task 1003308 — the hub owns "my projects"',
|
|
264
|
+
// Task 1003450 — the shell's ?denied= banner replaced the bespoke refusal page.
|
|
265
|
+
'/builders/not-ready': 'task 1003450 — the shell renders the refusal from ?denied=',
|
|
253
266
|
};
|
|
254
267
|
|
|
255
268
|
test('a row that is null because the page left the hall serves no page', () => {
|
|
@@ -261,12 +274,72 @@ test('a row that is null because the page left the hall serves no page', () => {
|
|
|
261
274
|
}
|
|
262
275
|
});
|
|
263
276
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
// ─── the gate table itself: nothing exported that guards nothing ────────────
|
|
278
|
+
//
|
|
279
|
+
// The previous version of this block asserted, by name, that ONE gate
|
|
280
|
+
// (requireNonXenosPage) guarded no URL — recording the dead code instead of
|
|
281
|
+
// removing it. That reads as coverage and is the opposite: it pinned the defect
|
|
282
|
+
// in place, and it could not see requireGovernmentManagePage, which had gone the
|
|
283
|
+
// same way when R16 widened /government's shell to the metic floor. Both gates
|
|
284
|
+
// are gone (task 1003450). What replaces the row is the general rule, which
|
|
285
|
+
// fails on the NEXT one without anybody having to think of its name.
|
|
286
|
+
//
|
|
287
|
+
// Why it matters beyond tidiness: an exported gate with no route is a loaded
|
|
288
|
+
// gun. It still resolves a permission and still redirects, so wiring it is one
|
|
289
|
+
// line — and the reviewer of that line sees a function that looks maintained.
|
|
290
|
+
// requireNonXenosPage would have re-imposed a rank floor the owner had
|
|
291
|
+
// deliberately removed (task 1002710), and its deniedTo pointed at a page that
|
|
292
|
+
// by then said the wrong thing.
|
|
293
|
+
const AUTH_SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/auth.js'), 'utf8');
|
|
294
|
+
|
|
295
|
+
// The keys of the PAGE_GATES table, read statically — the same posture as the
|
|
296
|
+
// dispatch parse above (importing auth.js pulls the whole app in).
|
|
297
|
+
function readPageGateNames(src) {
|
|
298
|
+
const table = /const PAGE_GATES = Object\.freeze\(\{([\s\S]*?)\n\}\);/.exec(src);
|
|
299
|
+
assert.ok(table, 'PAGE_GATES is still a `const PAGE_GATES = Object.freeze({…});` literal');
|
|
300
|
+
// Entry keys sit at exactly one indent level; `permission:` / `deniedTo:` /
|
|
301
|
+
// `alsoAdmits:` are nested deeper, so the two-space anchor keeps them out.
|
|
302
|
+
return [...table[1].matchAll(/^ {2}(require\w+):/gm)].map((m) => m[1]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
test('every page gate in the table is dispatched by at least one hall URL', () => {
|
|
306
|
+
const declared = readPageGateNames(AUTH_SRC);
|
|
307
|
+
assert.ok(declared.length >= 4, 'the gate table was found — an empty parse must not pass silently');
|
|
269
308
|
const wired = new Set(DISPATCH.map((d) => d.gate));
|
|
270
|
-
|
|
271
|
-
|
|
309
|
+
for (const gate of declared) {
|
|
310
|
+
assert.ok(wired.has(gate),
|
|
311
|
+
`${gate} is declared in PAGE_GATES and dispatched from no route. Either wire it to a URL `
|
|
312
|
+
+ '(and add that URL to the ORACLE above), or delete the gate — an exported gate that '
|
|
313
|
+
+ 'guards nothing is a rank floor waiting to be re-imposed by accident.');
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('every gate a URL dispatches is declared in the gate table', () => {
|
|
318
|
+
// The other direction: gdsAuth.<name> in serve-internal.js is resolved at
|
|
319
|
+
// runtime, so a typo or a rename is an undefined-is-not-a-function on the
|
|
320
|
+
// first request to that page rather than a boot failure.
|
|
321
|
+
const declared = new Set(readPageGateNames(AUTH_SRC));
|
|
322
|
+
for (const { gate } of DISPATCH) {
|
|
323
|
+
assert.ok(declared.has(gate), `serve-internal.js dispatches gdsAuth.${gate}, which PAGE_GATES does not declare`);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("every gate's deniedTo is a hall page that is actually served", () => {
|
|
328
|
+
// The failure this catches is the one task 1003450 cleaned up, in its most
|
|
329
|
+
// dangerous form: deleting a page that a live gate still redirects to. The
|
|
330
|
+
// refused visitor would land on a 404 with no way back and no explanation —
|
|
331
|
+
// strictly worse than the 403 the redirect exists to avoid.
|
|
332
|
+
const dests = [...AUTH_SRC.matchAll(/deniedTo:\s*'([^']+)'/g)].map((m) => m[1]);
|
|
333
|
+
assert.ok(dests.length, 'the deniedTo destinations were found');
|
|
334
|
+
for (const dest of dests) {
|
|
335
|
+
assert.ok(dest.startsWith('/builders'), `${dest} must be a hall URL`);
|
|
336
|
+
const slug = dest.replace(/^\/builders\/?/, '');
|
|
337
|
+
// The oracle spells the hall index with its trailing slash; every other row
|
|
338
|
+
// is written without one.
|
|
339
|
+
assert.equal(ORACLE[slug ? dest : '/builders/'], null,
|
|
340
|
+
`${dest} is a deniedTo, so it must be UNGATED — redirecting a refused visitor at a gated page loops`);
|
|
341
|
+
if (!slug) continue; // /builders itself is the hall index, served from index.html
|
|
342
|
+
assert.ok(fs.existsSync(path.join(HALL_PAGES_DIR, `${slug}.html`)),
|
|
343
|
+
`${dest} is a deniedTo destination but no page is served there — a refused visitor gets a 404`);
|
|
344
|
+
}
|
|
272
345
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// tests/hall_settings_world.mjs — the PANEL family in the chrome world
|
|
2
|
-
// (task 1003308, goal 1000081): settings, profile, pair
|
|
2
|
+
// (task 1003308, goal 1000081): settings, profile, pair.
|
|
3
3
|
//
|
|
4
4
|
// tests/hall_tokens.mjs pins the TIER these pages spend; this pins how they
|
|
5
5
|
// spend it. Everything here is a fact that is silent in a browser: the page
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
// node scripts/hall-preview/server.js --port 4621 --fixture-me
|
|
34
34
|
// node modules/ui-design/kit/render.js --states modules/hall-ui/public/settings.states.json \
|
|
35
35
|
// --base-in http://builders.localhost:4621 --base-out http://builders.localhost:4621
|
|
36
|
-
// (and profile / pair
|
|
36
|
+
// (and profile / pair .states.json the same way)
|
|
37
37
|
|
|
38
38
|
import { test } from 'node:test';
|
|
39
39
|
import assert from 'node:assert/strict';
|
|
@@ -46,7 +46,10 @@ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
|
46
46
|
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
47
47
|
const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// not-ready was the fourth until task 1003450 deleted the page — see
|
|
50
|
+
// tests/hall_page_gate_map.mjs (GONE_PAGES) for why, and for the row that stops
|
|
51
|
+
// it coming back ungated. The archetype is unaffected: three pages still spend it.
|
|
52
|
+
const PAGES = ['settings', 'profile', 'pair'];
|
|
50
53
|
const SHEETS = ['panel.css', 'settings.css', 'pair.css', 'profile.css'];
|
|
51
54
|
|
|
52
55
|
const panelCss = read('panel.css');
|
|
@@ -166,9 +166,12 @@ await t('the OTHER page gates are untouched — their seam is never consulted at
|
|
|
166
166
|
'and its denial is unchanged, down to the permission it names');
|
|
167
167
|
const { nexted: pub } = await runGate('requireBuilderPage', OWNER, spy);
|
|
168
168
|
assert.ok(pub, 'the public hall page still admits every signed-in builder, exactly as before');
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
// A third gate stood here — requireNonXenosPage, refusing a xenos to
|
|
170
|
+
// /builders/not-ready. Task 1003450 deleted it and that page together; the two
|
|
171
|
+
// above carry the same claim (a gate declaring no alsoAdmits never touches the
|
|
172
|
+
// seam), and the board-room gate below is the wider third case.
|
|
173
|
+
const { nexted: vote } = await runGate('requireBoardVotePage', OWNER, spy);
|
|
174
|
+
assert.ok(vote, 'board.vote.cast floors at xenos on purpose (ADR 0266) — admitted without the seam');
|
|
172
175
|
});
|
|
173
176
|
|
|
174
177
|
await t('only ONE gate declares a second door (a widening must be opted into, one gate at a time)', () => {
|
|
@@ -27,7 +27,7 @@ import os from 'node:os';
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
|
|
29
29
|
const require = createRequire(import.meta.url);
|
|
30
|
-
const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState } = require('../scripts/gds/worktree.js');
|
|
30
|
+
const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules } = require('../scripts/gds/worktree.js');
|
|
31
31
|
|
|
32
32
|
let passed = 0;
|
|
33
33
|
let failed = 0;
|
|
@@ -140,6 +140,55 @@ test('a REAL node_modules dir is left for git to refuse — we never rm it', ()
|
|
|
140
140
|
eqArr(removalPlan({ nodeModulesState: 'real-dir' }), ['git-worktree-remove']);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
// ---- shadowingNodeModules (task 1003627) ----------------------------------
|
|
144
|
+
// An inside-repo worktree gets NO junction by design — node walks up to the main
|
|
145
|
+
// checkout. But walk-up stops at the FIRST node_modules, so a stray one in
|
|
146
|
+
// between captures every inside-repo worktree at once, and `require` still
|
|
147
|
+
// SUCCEEDS from the wrong place. Two agents read that as "worktree.js add failed
|
|
148
|
+
// to make a junction" on 2026-09-04; there was no junction to make.
|
|
149
|
+
const R = 'C:/repo';
|
|
150
|
+
const WT = 'C:/repo/.claude/worktrees/task-1';
|
|
151
|
+
const at = (...present) => {
|
|
152
|
+
const set = new Set(present.map((p) => path.resolve(p)));
|
|
153
|
+
return (p) => set.has(path.resolve(p));
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
test('names a stray node_modules one level above the worktree', () => {
|
|
157
|
+
const hit = shadowingNodeModules({
|
|
158
|
+
wtPath: WT, repoRoot: R, existsFn: at('C:/repo/.claude/worktrees/node_modules'),
|
|
159
|
+
});
|
|
160
|
+
eq(path.resolve(hit), path.resolve('C:/repo/.claude/worktrees/node_modules'));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('names a stray further up too (any level BELOW the repo root shadows it)', () => {
|
|
164
|
+
const hit = shadowingNodeModules({ wtPath: WT, repoRoot: R, existsFn: at('C:/repo/.claude/node_modules') });
|
|
165
|
+
eq(path.resolve(hit), path.resolve('C:/repo/.claude/node_modules'));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('the repo root\'s OWN node_modules is not a stray — that is the target', () => {
|
|
169
|
+
// The whole point of walk-up is to reach this one; flagging it would make the
|
|
170
|
+
// warning fire on every healthy worktree, which is how a warning becomes noise.
|
|
171
|
+
eq(shadowingNodeModules({ wtPath: WT, repoRoot: R, existsFn: at('C:/repo/node_modules') }), null);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('no stray anywhere → null', () => {
|
|
175
|
+
eq(shadowingNodeModules({ wtPath: WT, repoRoot: R, existsFn: () => false }), null);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('an OUTSIDE-repo worktree is not checked — it gets a junction instead', () => {
|
|
179
|
+
eq(shadowingNodeModules({ wtPath: 'C:/tmp/wt-9', repoRoot: R, existsFn: () => true }), null);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('the nearest stray wins, matching node\'s own resolution order', () => {
|
|
183
|
+
const hit = shadowingNodeModules({
|
|
184
|
+
wtPath: WT,
|
|
185
|
+
repoRoot: R,
|
|
186
|
+
existsFn: at('C:/repo/.claude/worktrees/node_modules', 'C:/repo/.claude/node_modules'),
|
|
187
|
+
});
|
|
188
|
+
eq(path.resolve(hit), path.resolve('C:/repo/.claude/worktrees/node_modules'),
|
|
189
|
+
'node stops at the first node_modules it meets walking up');
|
|
190
|
+
});
|
|
191
|
+
|
|
143
192
|
console.log('');
|
|
144
193
|
console.log(`worktree_helper: ${passed} passed, ${failed} failed`);
|
|
145
194
|
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Not yet available — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="This page unlocks as you advance. Ship your first three tasks to open the rest of the builder hub.">
|
|
8
|
-
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
-
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
-
<!-- The PANEL archetype (task 1003308): this page is its sign-in gate shape. -->
|
|
13
|
-
<link rel="stylesheet" href="/builders/panel.css">
|
|
14
|
-
</head>
|
|
15
|
-
<body data-page="not-ready" data-page-title="Locked">
|
|
16
|
-
|
|
17
|
-
<div class="app">
|
|
18
|
-
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
19
|
-
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
20
|
-
<div class="app-body">
|
|
21
|
-
<header class="app-topbar" id="app-topbar"></header>
|
|
22
|
-
<div class="app-content">
|
|
23
|
-
<!-- The page's ONE h1 (one-header-main-h1), in the family's voice. -->
|
|
24
|
-
<div class="page-head page-head--room page-head--panel">
|
|
25
|
-
<h1 id="not-ready-h1" class="page-head__h">not yet</h1>
|
|
26
|
-
<p class="page-head__sub">This page unlocks as you advance.</p>
|
|
27
|
-
</div>
|
|
28
|
-
<main id="hall-main">
|
|
29
|
-
<section class="scroll gate" aria-labelledby="not-ready-h">
|
|
30
|
-
<h2 id="not-ready-h" class="gate__h">Not yet available</h2>
|
|
31
|
-
<p class="gate__lede">
|
|
32
|
-
Xenos builders start with a focused set of pages — ship your first
|
|
33
|
-
<strong>three tasks</strong> to graduate and open the rest of
|
|
34
|
-
{{worldName}}'s builder hub.
|
|
35
|
-
</p>
|
|
36
|
-
<div class="row-end">
|
|
37
|
-
<a class="pbtn pbtn--save" href="/ranks">See how ranks work</a>
|
|
38
|
-
<a class="pbtn pbtn--quiet" href="/">Back to home</a>
|
|
39
|
-
</div>
|
|
40
|
-
</section>
|
|
41
|
-
</main>
|
|
42
|
-
<footer class="app-foot" id="app-foot"></footer>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
48
|
-
|
|
49
|
-
<script src="/builders/shell.js"></script>
|
|
50
|
-
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
51
|
-
<script src="/builders/palette.js" defer></script>
|
|
52
|
-
</body>
|
|
53
|
-
</html>
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_": "The NOT-READY page through modules/ui-design/kit/render.js (task 1003308) — the sign-in gate shape of the panel family, static markup with no feed. Renders under the kit's own stub and under the harness alike.",
|
|
3
|
-
"page": "/not-ready",
|
|
4
|
-
"surface": "hall-ui",
|
|
5
|
-
"stub": { "prefix": "/builders/" },
|
|
6
|
-
"modeQuery": false,
|
|
7
|
-
"ignoreRequests": ["/api/(gds|bongos)/"],
|
|
8
|
-
"ignoreConsole": ["/api/(gds|bongos)/", "\b401\b", "\b404\b", "Unauthorized", "Not Found", "load failed"],
|
|
9
|
-
"states": {
|
|
10
|
-
"locked": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#not-ready-h1", "#not-ready-h"] } },
|
|
11
|
-
"out": { "auth": false, "actions": [["wait", 400]], "expect": { "visible": ["#not-ready-h1"] } }
|
|
12
|
-
},
|
|
13
|
-
"reducedMotion": { "auth": true, "actions": [["wait", 400]] }
|
|
14
|
-
}
|