@bongos/core 1.19.185 → 1.19.187
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 +90 -85
- package/clients/bongos-client/index.d.ts +2 -2
- package/config/government.neutral.json +6 -6
- package/docs/adr/0172-editable-rank-roles-substrate-axis.md +1 -1
- package/docs/api/openapi.json +12 -12
- package/docs/api-reference.md +7 -7
- package/modules/builder-settings/wandering-prefs.js +13 -13
- package/modules/government/catalog.js +79 -79
- package/modules/government/config.js +43 -33
- package/modules/government/db.js +58 -58
- package/modules/government/protected-surfaces.js +7 -7
- package/modules/government/protected-surfaces.json +4 -4
- package/modules/government/ranks.js +17 -17
- package/modules/government/resolver.js +24 -24
- package/modules/government/routes/government.js +88 -88
- package/modules/government/routes/rank-sync.js +8 -8
- package/modules/government/session-scopes.js +7 -7
- package/modules/government/session-scopes.json +5 -5
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/{governance.css → government.css} +11 -11
- package/modules/hall-ui/public/{governance.html → government.html} +14 -14
- package/modules/hall-ui/public/{governance.js → government.js} +108 -108
- package/modules/hall-ui/public/harbor.js +2 -2
- package/modules/hall-ui/public/shell.js +19 -15
- package/modules/hall-ui/public/watch.js +1 -1
- package/modules/lifecycle/claim-eligibility.js +1 -1
- package/modules/lifecycle/db.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +138 -0
- package/scripts/hall-preview/server.js +1 -1
- package/src/bongos/auth.js +11 -11
- package/src/bongos/serve-internal.js +8 -8
- package/src/module-api.js +5 -1
- package/tests/auth_page_gate.mjs +2 -2
- package/tests/auth_resolve_failure.mjs +4 -4
- package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +39 -39
- package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
- package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
- package/tests/{governance_config.mjs → government_config.mjs} +54 -54
- package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
- package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
- package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
- package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
- package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +14 -14
- package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
- package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
- package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
- package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
- package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
- package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
- package/tests/{governance_routes.mjs → government_routes.mjs} +36 -36
- package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
- package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
- package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
- package/tests/government_vocabulary.mjs +94 -0
- package/tests/hall_nav.mjs +8 -7
- package/tests/hall_page_gate_map.mjs +9 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_resolver.mjs — the permission resolver (BV1.R95, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// DB-free: exercises the resolver's PURE core (effectivePermissions) plus the
|
|
4
4
|
// fail-closed short-circuits and the kernel-port shape. The live DB join is
|
|
@@ -45,7 +45,7 @@ test('the result is frozen — a caller cannot mutate a resolved permission set'
|
|
|
45
45
|
|
|
46
46
|
// R98 narrowed this: the role axis carries every catalog key EXCEPT the machine-
|
|
47
47
|
// principal ones, which come from an identity and never from a role. The principal
|
|
48
|
-
// axis has its own file (tests/
|
|
48
|
+
// axis has its own file (tests/government_principals.mjs).
|
|
49
49
|
test('every role-held catalog key survives resolution (the seed can grant anything defined)', () => {
|
|
50
50
|
const roleHeld = catalog.permissionKeys().filter((k) => !catalog.isPrincipalKey(k));
|
|
51
51
|
assert.deepEqual(effectivePermissions(catalog.permissionKeys()), [...roleHeld].sort());
|
|
@@ -74,10 +74,10 @@ test('the resolver holds no cache — authority is read fresh per request (ADR 0
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test('the kernel port exposes the resolver without core importing the module', () => {
|
|
77
|
-
const {
|
|
78
|
-
assert.equal(typeof
|
|
79
|
-
assert.equal(typeof
|
|
80
|
-
assert.equal(typeof
|
|
77
|
+
const { governmentPort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
78
|
+
assert.equal(typeof governmentPort.resolveBuilderPermissions, 'function');
|
|
79
|
+
assert.equal(typeof governmentPort.builderHasPermissions, 'function');
|
|
80
|
+
assert.equal(typeof governmentPort.isSystem, 'function');
|
|
81
81
|
const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
|
|
82
82
|
assert.ok(manifest.provides.includes('government'), 'module.json must declare the government port');
|
|
83
83
|
assert.ok(manifest.contributes.routes.includes('government'), 'the port registers from the route factory');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_routes.mjs — the governance CRUD API (BV1.R110, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// Two layers, both DB-free (the memory_isolation.mjs pattern):
|
|
4
4
|
//
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// ASSIGNMENT is still rank-derived (R95b); and every route denies without the
|
|
20
20
|
// `government.manage` grant.
|
|
21
21
|
//
|
|
22
|
-
// Run: node tests/
|
|
22
|
+
// Run: node tests/government_routes.mjs
|
|
23
23
|
|
|
24
24
|
import { strict as assert } from 'node:assert';
|
|
25
25
|
import { test, after } from 'node:test';
|
|
@@ -99,23 +99,23 @@ test('checkRouteRanks passes over the real tree (no governance findings)', () =>
|
|
|
99
99
|
// The db stub: every function the routes call, recording args. Reset per test.
|
|
100
100
|
const calls = [];
|
|
101
101
|
const stub = {
|
|
102
|
-
|
|
102
|
+
listRanksWithGrants: async () => [{ rank_key: 'metic', label: 'Metic', description: null, is_system: true, seeded_rank: 'metic', created_by: null, created_at: 't', permissions: ['task.create'] }],
|
|
103
103
|
listAssignments: async () => [{ builder_id: 7, github_login: 'x', display_name: 'X', rank: 'metic', rank_key: 'metic', role_label: 'Metic', is_system: true, seeded_rank: 'metic', assigned_by: null, assigned_at: 't' }],
|
|
104
104
|
// Every rank name is a seeded rank-role; anything else is a custom one. Keyed off
|
|
105
105
|
// the catalog's ladder rather than a hand-listed pair, so a rank added to
|
|
106
106
|
// RANK_ORDER is exercised here instead of quietly falling through as "custom".
|
|
107
|
-
|
|
107
|
+
getRankByKey: async (k) => (k === 'missing' ? null : {
|
|
108
108
|
rank_key: k,
|
|
109
109
|
label: k,
|
|
110
110
|
is_system: catalog.RANK_ORDER.includes(k),
|
|
111
111
|
seeded_rank: catalog.RANK_ORDER.includes(k) ? k : null,
|
|
112
112
|
}),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
createRank: async (a) => ({ rank_key: a.rankKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
|
|
114
|
+
grantRankPermission: async () => true,
|
|
115
|
+
revokeRankPermission: async () => true,
|
|
116
|
+
resetRankGrants: async ({ permissionKeys }) => permissionKeys.length,
|
|
117
|
+
assignRank: async () => true,
|
|
118
|
+
unassignRank: async () => true,
|
|
119
119
|
};
|
|
120
120
|
const REAL = {};
|
|
121
121
|
for (const k of Object.keys(stub)) {
|
|
@@ -135,11 +135,11 @@ function grant(keys) {
|
|
|
135
135
|
}
|
|
136
136
|
grant(['government.manage']);
|
|
137
137
|
|
|
138
|
-
const
|
|
138
|
+
const buildGovernmentRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
|
|
139
139
|
const app = express();
|
|
140
140
|
app.use(express.json());
|
|
141
141
|
app.use(attachFail);
|
|
142
|
-
app.use(
|
|
142
|
+
app.use(buildGovernmentRouter());
|
|
143
143
|
const server = http.createServer(app);
|
|
144
144
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
145
145
|
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
@@ -189,8 +189,8 @@ test('without government.manage EVERY route 403s (reads included)', async () =>
|
|
|
189
189
|
test('GET /government/ranks returns the role list with grants', async () => {
|
|
190
190
|
const r = await call('GET', '/government/ranks');
|
|
191
191
|
assert.equal(r.status, 200);
|
|
192
|
-
assert.equal(r.json.
|
|
193
|
-
assert.deepEqual(r.json.
|
|
192
|
+
assert.equal(r.json.ranks[0].rank_key, 'metic');
|
|
193
|
+
assert.deepEqual(r.json.ranks[0].permissions, ['task.create']);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
test('GET /government/permissions renders the catalog: grantable flags + principals, no DB call', async () => {
|
|
@@ -221,9 +221,9 @@ test('GET /government/assignments returns builder↔role rows', async () => {
|
|
|
221
221
|
test('POST /government/ranks creates a custom role, created_by = the session builder', async () => {
|
|
222
222
|
const r = await call('POST', '/government/ranks', { rank_key: 'triage-helper', label: 'Triage helper' });
|
|
223
223
|
assert.equal(r.status, 201);
|
|
224
|
-
assert.equal(r.json.
|
|
225
|
-
assert.equal(r.json.
|
|
226
|
-
const c = calls.find(([k]) => k === '
|
|
224
|
+
assert.equal(r.json.rank.rank_key, 'triage-helper');
|
|
225
|
+
assert.equal(r.json.rank.is_system, false);
|
|
226
|
+
const c = calls.find(([k]) => k === 'createRank');
|
|
227
227
|
assert.equal(c[1].createdBy, 42, 'created_by must be the session builder, never input');
|
|
228
228
|
});
|
|
229
229
|
|
|
@@ -235,14 +235,14 @@ test('POST /government/ranks validates: label required, rank_key must be a slug'
|
|
|
235
235
|
const r = await call('POST', '/government/ranks', { rank_key: bad, label: 'L' });
|
|
236
236
|
assert.equal(r.status, 400, `rank_key "${bad}" must be rejected`);
|
|
237
237
|
}
|
|
238
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
238
|
+
assert.equal(calls.filter(([k]) => k === 'createRank').length, 0, 'nothing reached the db');
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
test('POST /government/ranks maps a unique-violation to rank_exists 409', async () => {
|
|
242
|
-
const prev = stub.
|
|
243
|
-
stub.
|
|
242
|
+
const prev = stub.createRank;
|
|
243
|
+
stub.createRank = async () => { const e = new Error('dup'); e.code = '23505'; throw e; };
|
|
244
244
|
const r = await call('POST', '/government/ranks', { rank_key: 'metic-two', label: 'L' });
|
|
245
|
-
stub.
|
|
245
|
+
stub.createRank = prev;
|
|
246
246
|
assert.equal(r.status, 409);
|
|
247
247
|
assert.equal(r.json.error.code, 'rank_exists');
|
|
248
248
|
});
|
|
@@ -252,7 +252,7 @@ test('POST /government/ranks maps a unique-violation to rank_exists 409', async
|
|
|
252
252
|
test('PUT grant: an operational permission lands on a custom role', async () => {
|
|
253
253
|
const r = await call('PUT', '/government/ranks/triage-helper/permissions/idea.triage');
|
|
254
254
|
assert.equal(r.status, 200);
|
|
255
|
-
const c = calls.find(([k]) => k === '
|
|
255
|
+
const c = calls.find(([k]) => k === 'grantRankPermission');
|
|
256
256
|
assert.deepEqual(c[1], { rankKey: 'triage-helper', permissionKey: 'idea.triage' });
|
|
257
257
|
});
|
|
258
258
|
|
|
@@ -261,7 +261,7 @@ test('PUT grant: a SYSTEM permission is rejected as non-grantable — the R110 w
|
|
|
261
261
|
const r = await call('PUT', `/government/ranks/triage-helper/permissions/${sysKey}`);
|
|
262
262
|
assert.equal(r.status, 400, `${sysKey} must be refused`);
|
|
263
263
|
assert.equal(r.json.error.code, 'permission_not_grantable');
|
|
264
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
264
|
+
assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0, 'the refusal is BEFORE the db');
|
|
265
265
|
}
|
|
266
266
|
});
|
|
267
267
|
|
|
@@ -274,7 +274,7 @@ test('PUT grant: an unknown permission fails closed', async () => {
|
|
|
274
274
|
test('PUT grant: a sub-Archon rank-role is EDITABLE (ADR 0172)', async () => {
|
|
275
275
|
const r = await call('PUT', '/government/ranks/metic/permissions/idea.triage');
|
|
276
276
|
assert.equal(r.status, 200);
|
|
277
|
-
const c = calls.find(([k]) => k === '
|
|
277
|
+
const c = calls.find(([k]) => k === 'grantRankPermission');
|
|
278
278
|
assert.deepEqual(c[1], { rankKey: 'metic', permissionKey: 'idea.triage' });
|
|
279
279
|
});
|
|
280
280
|
|
|
@@ -293,7 +293,7 @@ test('PUT grant: a SUBSTRATE key is refused even on a rank-role (it would confer
|
|
|
293
293
|
assert.equal(r.json.error.code, 'permission_not_grantable');
|
|
294
294
|
assert.match(r.json.error.details.reason || '', /substrate/i,
|
|
295
295
|
'the refusal must name the substrate wall, not the custom-role one — the two are a policy choice apart');
|
|
296
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
296
|
+
assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0);
|
|
297
297
|
}
|
|
298
298
|
});
|
|
299
299
|
|
|
@@ -301,7 +301,7 @@ test('PUT grant: the ARCHON rank-role stays immutable (the lockout guard)', asyn
|
|
|
301
301
|
const r = await call('PUT', '/government/ranks/archon/permissions/idea.triage');
|
|
302
302
|
assert.equal(r.status, 403);
|
|
303
303
|
assert.equal(r.json.error.code, 'archon_rank_immutable');
|
|
304
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
304
|
+
assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0);
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
test('PUT grant: an unknown role 404s', async () => {
|
|
@@ -313,7 +313,7 @@ test('PUT grant: an unknown role 404s', async () => {
|
|
|
313
313
|
test('DELETE grant revokes without consulting the catalog (stale keys stay removable)', async () => {
|
|
314
314
|
const r = await call('DELETE', '/government/ranks/triage-helper/permissions/some.retired.key');
|
|
315
315
|
assert.equal(r.status, 200);
|
|
316
|
-
const c = calls.find(([k]) => k === '
|
|
316
|
+
const c = calls.find(([k]) => k === 'revokeRankPermission');
|
|
317
317
|
assert.deepEqual(c[1], { rankKey: 'triage-helper', permissionKey: 'some.retired.key' });
|
|
318
318
|
});
|
|
319
319
|
|
|
@@ -324,7 +324,7 @@ test('DELETE grant: revocable on a sub-Archon rank-role, refused on the Archon o
|
|
|
324
324
|
const archon = await call('DELETE', '/government/ranks/archon/permissions/idea.triage');
|
|
325
325
|
assert.equal(archon.status, 403);
|
|
326
326
|
assert.equal(archon.json.error.code, 'archon_rank_immutable');
|
|
327
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
327
|
+
assert.equal(calls.filter(([k]) => k === 'revokeRankPermission').length, 0);
|
|
328
328
|
});
|
|
329
329
|
|
|
330
330
|
// --- reset (ADR 0172) --------------------------------------------------------
|
|
@@ -333,7 +333,7 @@ test('POST reset: a rank-role goes back to its floor-derived catalog seed', asyn
|
|
|
333
333
|
const r = await call('POST', '/government/ranks/thetes/reset');
|
|
334
334
|
assert.equal(r.status, 200);
|
|
335
335
|
assert.equal(r.json.seeded_rank, 'thetes');
|
|
336
|
-
const c = calls.find(([k]) => k === '
|
|
336
|
+
const c = calls.find(([k]) => k === 'resetRankGrants');
|
|
337
337
|
assert.deepEqual([...c[1].permissionKeys].sort(), [...catalog.seedForRank('thetes')].sort());
|
|
338
338
|
});
|
|
339
339
|
|
|
@@ -341,7 +341,7 @@ test('POST reset: refused on a custom role (no catalog default to return to)', a
|
|
|
341
341
|
const r = await call('POST', '/government/ranks/triage-helper/reset');
|
|
342
342
|
assert.equal(r.status, 400);
|
|
343
343
|
assert.equal(r.json.error.code, 'rank_has_no_default');
|
|
344
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
344
|
+
assert.equal(calls.filter(([k]) => k === 'resetRankGrants').length, 0);
|
|
345
345
|
});
|
|
346
346
|
|
|
347
347
|
// --- assignments -------------------------------------------------------------
|
|
@@ -349,7 +349,7 @@ test('POST reset: refused on a custom role (no catalog default to return to)', a
|
|
|
349
349
|
test('POST /government/assignments assigns a custom role, assigned_by = the session builder', async () => {
|
|
350
350
|
const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: 'triage-helper' });
|
|
351
351
|
assert.equal(r.status, 201);
|
|
352
|
-
const c = calls.find(([k]) => k === '
|
|
352
|
+
const c = calls.find(([k]) => k === 'assignRank');
|
|
353
353
|
assert.deepEqual(c[1], { builderId: 7, rankKey: 'triage-helper', assignedBy: 42 });
|
|
354
354
|
});
|
|
355
355
|
|
|
@@ -357,14 +357,14 @@ test('POST /government/assignments refuses a seeded rank-role (rank is the write
|
|
|
357
357
|
const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: 'archon' });
|
|
358
358
|
assert.equal(r.status, 403);
|
|
359
359
|
assert.equal(r.json.error.code, 'seeded_rank_assignment_forbidden');
|
|
360
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
360
|
+
assert.equal(calls.filter(([k]) => k === 'assignRank').length, 0);
|
|
361
361
|
});
|
|
362
362
|
|
|
363
363
|
test('POST /government/assignments maps a missing builder (FK) to 404', async () => {
|
|
364
|
-
const prev = stub.
|
|
365
|
-
stub.
|
|
364
|
+
const prev = stub.assignRank;
|
|
365
|
+
stub.assignRank = async () => { const e = new Error('fk'); e.code = '23503'; throw e; };
|
|
366
366
|
const r = await call('POST', '/government/assignments', { builder_id: 999, rank_key: 'triage-helper' });
|
|
367
|
-
stub.
|
|
367
|
+
stub.assignRank = prev;
|
|
368
368
|
assert.equal(r.status, 404);
|
|
369
369
|
assert.equal(r.json.error.code, 'builder_not_found');
|
|
370
370
|
});
|
|
@@ -378,7 +378,7 @@ test('POST /government/assignments validates builder_id and rank_key', async ()
|
|
|
378
378
|
test('DELETE /government/assignments/:builderId/:rankKey unassigns a custom role', async () => {
|
|
379
379
|
const r = await call('DELETE', '/government/assignments/7/triage-helper');
|
|
380
380
|
assert.equal(r.status, 200);
|
|
381
|
-
const c = calls.find(([k]) => k === '
|
|
381
|
+
const c = calls.find(([k]) => k === 'unassignRank');
|
|
382
382
|
assert.deepEqual(c[1], { builderId: 7, rankKey: 'triage-helper' });
|
|
383
383
|
});
|
|
384
384
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_seed.mjs — the compat-seed migration drift guard (BV1.R93, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// The seed migration (governance_002_seed_and_backfill.sql) MATERIALISES the role→
|
|
4
4
|
// permission grants as SQL. This parses that file and asserts the four role arrays
|
|
5
|
-
// equal the code catalog's
|
|
5
|
+
// equal the code catalog's RANK_SEED exactly — so the SQL snapshot can never
|
|
6
6
|
// silently drift from the catalog (the "no second list that drifts" discipline). If
|
|
7
7
|
// you change catalog.js, this goes red until the migration is regenerated (a NEW
|
|
8
8
|
// migration for a NEW grant — migrations are run-once).
|
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
-
const {
|
|
18
|
+
const { RANK_SEED, RANK_ORDER } = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
19
19
|
|
|
20
20
|
const MIG_DIR = path.join(ROOT, 'modules', 'government', 'migrations');
|
|
21
21
|
const seedSql = fs.readFileSync(path.join(MIG_DIR, 'governance_002_seed_and_backfill.sql'), 'utf8');
|
|
@@ -25,7 +25,7 @@ const tablesSql = fs.readFileSync(path.join(MIG_DIR, 'governance_001_tables.sql'
|
|
|
25
25
|
// grant migration. Migrations are run-once, so a floor change in catalog.js can never
|
|
26
26
|
// be made by editing 002 — an already-migrated database would keep the old grant. It
|
|
27
27
|
// arrives as a follow-on migration instead (governance_003 = task.abandon → metic,
|
|
28
|
-
// ADR 0152), and what must equal
|
|
28
|
+
// ADR 0152), and what must equal RANK_SEED is the union of them all, not any one
|
|
29
29
|
// file. Read in filename order; every file is scanned for `SELECT '<role>', unnest(…)`
|
|
30
30
|
// blocks, so a new grant migration needs no change here.
|
|
31
31
|
// Migrations that do NOT grant. Excluded by NAME rather than by "does it
|
|
@@ -69,7 +69,7 @@ function grantBlocksForRole(role) {
|
|
|
69
69
|
return blocks;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
test('the union of every grant migration matches
|
|
72
|
+
test('the union of every grant migration matches RANK_SEED exactly (no drift)', () => {
|
|
73
73
|
for (const role of RANK_ORDER) {
|
|
74
74
|
const blocks = grantBlocksForRole(role);
|
|
75
75
|
assert.ok(blocks.length > 0, `no grant block for ${role} in any governance migration`);
|
|
@@ -78,11 +78,11 @@ test('the union of every grant migration matches RANK_ROLE_SEED exactly (no drif
|
|
|
78
78
|
}
|
|
79
79
|
const granted = applyRenames(blocks.flatMap((b) => b.keys));
|
|
80
80
|
// set-equality (order in SQL is illustrative; the grant set is what matters)
|
|
81
|
-
assert.deepEqual(new Set(granted), new Set(
|
|
82
|
-
`${role}: grant migrations drift from catalog
|
|
81
|
+
assert.deepEqual(new Set(granted), new Set(RANK_SEED[role]),
|
|
82
|
+
`${role}: grant migrations drift from catalog RANK_SEED`);
|
|
83
83
|
// no key re-granted in a LATER migration (a no-op INSERT that muddies the ledger)
|
|
84
84
|
assert.equal(granted.length, new Set(granted).size, `${role}: the same permission is granted twice across migrations`);
|
|
85
|
-
assert.equal(new Set(granted).size,
|
|
85
|
+
assert.equal(new Set(granted).size, RANK_SEED[role].length, `${role}: grant count mismatch`);
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
88
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_session_scope.mjs — session-scopes as a first-class axis
|
|
2
2
|
// (BV1.R99, ADR 0151 §3; the model behind ADR 0053). DB-free.
|
|
3
3
|
//
|
|
4
4
|
// A session-scope is capability attached to the TOKEN, not to the builder: a
|
|
@@ -75,7 +75,7 @@ const ARCHON_POWERS = [
|
|
|
75
75
|
const METIC_POWERS = ['idea.triage', 'task.create', 'task.edit', 'goal.create', 'box.source.full'];
|
|
76
76
|
|
|
77
77
|
// Stub the ONE DB read the resolver makes, so the composition is exercised
|
|
78
|
-
// end-to-end without Postgres (the tests/
|
|
78
|
+
// end-to-end without Postgres (the tests/government_ownership_scope.mjs pattern).
|
|
79
79
|
const realGetPermissionKeys = db.getPermissionKeysForBuilder;
|
|
80
80
|
async function holding(keys, fn) {
|
|
81
81
|
db.getPermissionKeysForBuilder = async () => keys;
|
|
@@ -370,7 +370,7 @@ test('G2: requireBuilder DENIES a session whose source the registry does not dec
|
|
|
370
370
|
|
|
371
371
|
test('H1: R95/R98/R100 invariants still hold — the role answer is untouched', async () => {
|
|
372
372
|
// R99 composes ON TOP of effectivePermissions; widening INSIDE it would red
|
|
373
|
-
// tests/
|
|
373
|
+
// tests/government_parity.mjs Parts A and C.
|
|
374
374
|
assert.deepEqual(effectivePermissions(['principal.bfg', 'box.manage.own']), ['box.manage.own']);
|
|
375
375
|
assert.deepEqual(effectivePermissions(['idea.triage', 'not.a.real.permission']), ['idea.triage']);
|
|
376
376
|
for (const rank of RANK_ORDER) {
|
|
@@ -419,16 +419,16 @@ test('H3: the resolve is still per-request and frozen — no memoised session au
|
|
|
419
419
|
// ═══ I — the ONE governance port carries the axis ════════════════════════════
|
|
420
420
|
|
|
421
421
|
test('I1: the session-scope axis lands on the single governance port', () => {
|
|
422
|
-
const {
|
|
422
|
+
const { governmentPort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
423
423
|
for (const name of ['resolveSessionPermissions', 'narrowToSessionScope', 'sessionScopeFor', 'listSessionScopes']) {
|
|
424
|
-
assert.equal(typeof
|
|
424
|
+
assert.equal(typeof governmentPort[name], 'function', `the port must expose ${name}`);
|
|
425
425
|
}
|
|
426
426
|
// still the same one object port the earlier slices registered
|
|
427
427
|
for (const name of ['resolveBuilderPermissions', 'resolvePrincipalPermissions', 'authorizeOwned']) {
|
|
428
|
-
assert.equal(typeof
|
|
428
|
+
assert.equal(typeof governmentPort[name], 'function');
|
|
429
429
|
}
|
|
430
|
-
assert.deepEqual([...
|
|
431
|
-
assert.equal(
|
|
432
|
-
assert.deepEqual([...
|
|
433
|
-
assert.ok(
|
|
430
|
+
assert.deepEqual([...governmentPort.sessionScopeFor('box').permissions], EXPECTED_BOX_SCOPE);
|
|
431
|
+
assert.equal(governmentPort.sessionScopeFor('ghost'), null);
|
|
432
|
+
assert.deepEqual([...governmentPort.narrowToSessionScope(seedForRank('archon'), 'box')], EXPECTED_BOX_SCOPE);
|
|
433
|
+
assert.ok(governmentPort.listSessionScopes().length >= 2);
|
|
434
434
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_vanilla_boot.mjs — the VANILLA BOOT proof (BV1.R102, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// The core/host split only holds if a FRESH instance — one that has never been
|
|
4
4
|
// branded, with no config/government.json on disk and no rows in
|
|
5
5
|
// governance_builder_roles — comes up sane, anonymous, and powerless. This file is
|
|
6
|
-
// that proof. It is deliberately scoped to what R96's tests/
|
|
6
|
+
// that proof. It is deliberately scoped to what R96's tests/government_config.mjs
|
|
7
7
|
// does NOT cover: that file pins the config CONTRACT (precedence, per-field merge,
|
|
8
8
|
// the three fail-closed strips) driven by INJECTED fixtures; this one boots the real
|
|
9
9
|
// thing off disk and asserts the three vanilla properties:
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// outcome ADR 0016 exists to rule out, and this file is what would go red.
|
|
24
24
|
//
|
|
25
25
|
// DB-free throughout: the config layer is pure + disk, and the grant side is asserted
|
|
26
|
-
// against catalog.
|
|
26
|
+
// against catalog.RANK_SEED (code) rather than a live join. Crucially the two
|
|
27
27
|
// sides read DIFFERENT sources — the seed side reads catalog.js, the config side
|
|
28
28
|
// reads the neutral pack's raw bytes — so a change to either one moves only one side
|
|
29
29
|
// and the assertion goes red.
|
|
@@ -39,7 +39,7 @@ const require = createRequire(import.meta.url);
|
|
|
39
39
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
40
40
|
const GOV = path.join(ROOT, 'modules', 'government');
|
|
41
41
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
42
|
-
const {
|
|
42
|
+
const { loadGovernmentConfig, resolveGovernmentConfig, NEUTRAL_PATH, INSTANCE_PATH } = require(path.join(GOV, 'config.js'));
|
|
43
43
|
const { effectivePermissions, resolveBuilderPermissions } = require(path.join(GOV, 'resolver.js'));
|
|
44
44
|
const fitness = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
|
|
45
45
|
const { envPrefix } = require(path.join(ROOT, 'src', 'instance-config'));
|
|
@@ -51,10 +51,10 @@ const NEUTRAL_TEXT = fs.readFileSync(NEUTRAL_PATH, 'utf8');
|
|
|
51
51
|
|
|
52
52
|
// The vanilla boot, exactly as a fresh instance performs it: real neutral pack on
|
|
53
53
|
// disk, no instance pack, no env.
|
|
54
|
-
const vanilla = () =>
|
|
54
|
+
const vanilla = () => loadGovernmentConfig({ env: NO_ENV });
|
|
55
55
|
// Everything config hands the boot, flattened — the ONLY authority a file could
|
|
56
56
|
// possibly contribute.
|
|
57
|
-
const configUnion = (cfg) => [...new Set(Object.values(cfg.
|
|
57
|
+
const configUnion = (cfg) => [...new Set(Object.values(cfg.ranks).flatMap((r) => r.permissions))].sort();
|
|
58
58
|
|
|
59
59
|
// Runs `fn(dir)` against a scratch dir, always cleaned up. Nothing under it exists
|
|
60
60
|
// until the test writes it, so `path.join(dir, 'absent.json')` is a genuine ENOENT.
|
|
@@ -68,9 +68,9 @@ const withScratchDir = (fn) => {
|
|
|
68
68
|
test('a fresh instance boots the four rank-roles with usable labels and descriptions', () => {
|
|
69
69
|
assert.equal(fs.existsSync(INSTANCE_PATH), false, 'fixture guard: this checkout IS the vanilla case (no instance pack)');
|
|
70
70
|
const out = vanilla();
|
|
71
|
-
assert.deepEqual(Object.keys(out.
|
|
71
|
+
assert.deepEqual(Object.keys(out.ranks).sort(), [...catalog.RANK_ORDER].sort());
|
|
72
72
|
for (const rank of catalog.RANK_ORDER) {
|
|
73
|
-
const role = out.
|
|
73
|
+
const role = out.ranks[rank];
|
|
74
74
|
assert.equal(role.seeded, true, `${rank} must boot as a SEEDED rank-role, not a custom one`);
|
|
75
75
|
assert.ok(role.label && role.label !== rank, `${rank} must carry a human label from the neutral pack, got "${role.label}"`);
|
|
76
76
|
assert.ok(role.description.length > 10, `${rank} must carry a usable description, got "${role.description}"`);
|
|
@@ -83,13 +83,13 @@ test('a core with NO config packs at all still boots four roles rather than none
|
|
|
83
83
|
// set must never shrink below them — a boot that dropped `archon` would leave the
|
|
84
84
|
// instance with grants no template describes.
|
|
85
85
|
withScratchDir((dir) => {
|
|
86
|
-
const out =
|
|
86
|
+
const out = loadGovernmentConfig({
|
|
87
87
|
neutralPath: path.join(dir, 'absent-neutral.json'),
|
|
88
88
|
instancePath: path.join(dir, 'absent-instance.json'),
|
|
89
89
|
env: NO_ENV,
|
|
90
90
|
});
|
|
91
|
-
assert.deepEqual(Object.keys(out.
|
|
92
|
-
for (const rank of catalog.RANK_ORDER) assert.equal(out.
|
|
91
|
+
assert.deepEqual(Object.keys(out.ranks).sort(), [...catalog.RANK_ORDER].sort());
|
|
92
|
+
for (const rank of catalog.RANK_ORDER) assert.equal(out.ranks[rank].label, rank, 'a role with no template falls back to its canonical rank name');
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
|
|
@@ -104,8 +104,8 @@ test('a malformed config pack fails LOUD — it never degrades into a silent gra
|
|
|
104
104
|
withScratchDir((dir) => {
|
|
105
105
|
const broken = path.join(dir, 'broken.json');
|
|
106
106
|
fs.writeFileSync(broken, '{ not json');
|
|
107
|
-
assert.throws(() =>
|
|
108
|
-
assert.throws(() =>
|
|
107
|
+
assert.throws(() => loadGovernmentConfig({ instancePath: broken, env: NO_ENV }), SyntaxError);
|
|
108
|
+
assert.throws(() => loadGovernmentConfig({ neutralPath: broken, env: NO_ENV }), SyntaxError);
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
|
|
@@ -126,7 +126,7 @@ test('the identity guard is not a no-op — a planted host brand in the neutral
|
|
|
126
126
|
// The mutation test, kept permanent. Both host literals fitness knows about must
|
|
127
127
|
// trip it, in the exact file this task is about.
|
|
128
128
|
for (const brand of ['Off the Boats', 'amazonprimea.com']) {
|
|
129
|
-
const poisoned = NEUTRAL_TEXT.replace('"
|
|
129
|
+
const poisoned = NEUTRAL_TEXT.replace('"ranks"', `"instance": "${brand}",\n "ranks"`);
|
|
130
130
|
const found = fitness.scanPromptIdentity('config/government.neutral.json', poisoned, '.json');
|
|
131
131
|
assert.ok(found.length >= 1, `a "${brand}" literal in the neutral pack must be caught`);
|
|
132
132
|
}
|
|
@@ -141,7 +141,7 @@ test('a vanilla boot grants ZERO permissions through config', () => {
|
|
|
141
141
|
const out = vanilla();
|
|
142
142
|
assert.deepEqual(configUnion(out), [], 'config contributed authority to a vanilla boot');
|
|
143
143
|
for (const key of catalog.systemKeys()) {
|
|
144
|
-
for (const role of Object.values(out.
|
|
144
|
+
for (const role of Object.values(out.ranks)) {
|
|
145
145
|
assert.ok(!role.permissions.includes(key), `vanilla boot handed role "${role.key}" the system permission "${key}"`);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
@@ -158,10 +158,10 @@ test('the rank-roles’ grants derive from catalog floors, never from the neutra
|
|
|
158
158
|
// (code), the file side is the pack's raw bytes. Moving either one moves one side
|
|
159
159
|
// only, so this cannot pass vacuously.
|
|
160
160
|
for (const rank of catalog.RANK_ORDER) {
|
|
161
|
-
assert.ok(catalog.
|
|
161
|
+
assert.ok(catalog.RANK_SEED[rank].length > 0, `${rank} must hold a floor-derived grant set`);
|
|
162
162
|
}
|
|
163
|
-
assert.ok(catalog.
|
|
164
|
-
assert.ok(!catalog.
|
|
163
|
+
assert.ok(catalog.RANK_SEED.archon.includes('deploy.push_main'), 'fixture guard: archon holds the seed’s system power');
|
|
164
|
+
assert.ok(!catalog.RANK_SEED.xenos.includes('deploy.push_main'), 'fixture guard: the floors are a real ladder');
|
|
165
165
|
for (const key of catalog.permissionKeys()) {
|
|
166
166
|
assert.ok(!NEUTRAL_TEXT.includes(key), `the neutral pack names the permission "${key}" — grants belong to governance_002's floors, never to a file`);
|
|
167
167
|
}
|
|
@@ -173,22 +173,22 @@ test('a POISONED neutral pack still cannot grant on a vanilla boot', () => {
|
|
|
173
173
|
// edit to it is the vanilla-boot threat. A seeded role may be relabelled and never
|
|
174
174
|
// re-granted; a custom template may hold operational keys and never a system one.
|
|
175
175
|
const poisoned = JSON.parse(NEUTRAL_TEXT);
|
|
176
|
-
for (const rank of catalog.RANK_ORDER) poisoned.
|
|
177
|
-
poisoned.
|
|
178
|
-
const out =
|
|
176
|
+
for (const rank of catalog.RANK_ORDER) poisoned.ranks[rank].permissions = [...catalog.permissionKeys()];
|
|
177
|
+
poisoned.ranks.superuser = { label: 'Superuser', permissions: [...catalog.permissionKeys()] };
|
|
178
|
+
const out = resolveGovernmentConfig({ neutral: poisoned, env: NO_ENV });
|
|
179
179
|
for (const rank of catalog.RANK_ORDER) {
|
|
180
|
-
assert.deepEqual(out.
|
|
180
|
+
assert.deepEqual(out.ranks[rank].permissions, [], `a poisoned neutral pack re-granted the seeded role "${rank}"`);
|
|
181
181
|
}
|
|
182
182
|
for (const key of catalog.systemKeys()) {
|
|
183
|
-
assert.ok(!out.
|
|
183
|
+
assert.ok(!out.ranks.superuser.permissions.includes(key), `a neutral-pack template smuggled the system permission "${key}"`);
|
|
184
184
|
}
|
|
185
|
-
assert.deepEqual(out.
|
|
185
|
+
assert.deepEqual(out.ranks.superuser.permissions, [...catalog.grantableKeys()].sort(), 'operational keys are all a template may ever propose');
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
test('the env layer cannot widen authority on a vanilla boot', () => {
|
|
189
189
|
const env = { [`${envPrefix()}_GOVERNANCE_ROLE_LABELS`]: 'archon=Owner' };
|
|
190
|
-
const out =
|
|
191
|
-
assert.equal(out.
|
|
190
|
+
const out = loadGovernmentConfig({ env });
|
|
191
|
+
assert.equal(out.ranks.archon.label, 'Owner', 'env may relabel');
|
|
192
192
|
assert.deepEqual(configUnion(out), [], 'env must never add a permission');
|
|
193
193
|
});
|
|
194
194
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// tests/government_vocabulary.mjs — the ADR 0174 vocabulary wall
|
|
2
|
+
// (goal 1000068 R07, task 1003008).
|
|
3
|
+
//
|
|
4
|
+
// The rule under test says: "role" means a CRAFT — Artist, Ideator, or Builder —
|
|
5
|
+
// and never a rank. `scripts/gds/fitness.js` checkVocabularyWall enforces it on
|
|
6
|
+
// the government surface, in the CI `unit` check.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS FILE EXISTS AT ALL. A naming rule is the easiest kind of check to
|
|
9
|
+
// ship broken: allowlist enough and it passes forever while proving nothing, and
|
|
10
|
+
// nobody notices because green is green. So the load-bearing tests here are the
|
|
11
|
+
// MUTATION tests — plant a violation, assert the build reds. A rule that cannot
|
|
12
|
+
// be made to fail is not a rule.
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const fitness = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
|
|
22
|
+
|
|
23
|
+
// ---------- the wall holds on the real tree ----------
|
|
24
|
+
|
|
25
|
+
test('the government surface is clean today', () => {
|
|
26
|
+
const r = fitness.checkVocabularyWall();
|
|
27
|
+
assert.equal(r.ok, true,
|
|
28
|
+
`the vocabulary wall is failing on the real tree:\n${r.violations.join('\n')}`);
|
|
29
|
+
assert.ok(r.note.includes('file(s) scanned'), 'the check must report what it scanned');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---------- the mutation tests: the rule can actually fail ----------
|
|
33
|
+
|
|
34
|
+
test('a planted "role" on the government surface REDS the build', () => {
|
|
35
|
+
const planted = withPlantedLine('// a custom role may hold this');
|
|
36
|
+
assert.ok(planted.some((v) => /"role"/.test(v)),
|
|
37
|
+
'a "role" on the government surface must be caught — otherwise this rule is decoration');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('a planted "governance" on the government surface REDS the build', () => {
|
|
41
|
+
const planted = withPlantedLine('// see the governance tab');
|
|
42
|
+
assert.ok(planted.some((v) => /"governance"/.test(v)),
|
|
43
|
+
'the pre-rename name must be caught — it is how the old vocabulary creeps back');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Plant a line into a real government-surface file, run the check, restore.
|
|
47
|
+
// Uses the real file rather than a fixture so the test exercises the same path
|
|
48
|
+
// resolution the CI gate does.
|
|
49
|
+
function withPlantedLine(line) {
|
|
50
|
+
const fs = require('node:fs');
|
|
51
|
+
const target = path.join(ROOT, 'modules', 'government', 'resolver.js');
|
|
52
|
+
const original = fs.readFileSync(target, 'utf8');
|
|
53
|
+
try {
|
|
54
|
+
fs.writeFileSync(target, `${original}\n${line}\n`);
|
|
55
|
+
return fitness.checkVocabularyWall().violations;
|
|
56
|
+
} finally {
|
|
57
|
+
fs.writeFileSync(target, original);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------- the allowlist is small, reasoned, and real ----------
|
|
62
|
+
|
|
63
|
+
test('every allowlist entry carries a written reason', () => {
|
|
64
|
+
for (const a of fitness.VOCAB_ALLOW) {
|
|
65
|
+
assert.ok(a.file && a.match, 'an allowlist entry needs a file and a match');
|
|
66
|
+
assert.ok(a.why && a.why.length > 60,
|
|
67
|
+
`${a.file} (${a.match}) must record WHY it is excused — an undocumented `
|
|
68
|
+
+ 'exemption is indistinguishable from giving up.');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the allowlist stays small — growth means finish the rename, not add a line', () => {
|
|
73
|
+
// Every current entry is a COMPATIBILITY surface that must name the old thing:
|
|
74
|
+
// the legacy instance-config key, the legacy env var, the 308 route alias, and
|
|
75
|
+
// the manifest sentence that states the ban itself. If this cap is in the way,
|
|
76
|
+
// the honest move is almost always to delete a compatibility shim whose window
|
|
77
|
+
// has closed — not to raise the number.
|
|
78
|
+
assert.ok(fitness.VOCAB_ALLOW.length <= 5,
|
|
79
|
+
`the allowlist has grown to ${fitness.VOCAB_ALLOW.length}. A naming rule that `
|
|
80
|
+
+ 'needs a long allowlist has stopped being evidence. Finish the rename or '
|
|
81
|
+
+ 'retire a shim instead of raising this cap.');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('every allowlist entry still matches something — no stale exemptions', () => {
|
|
85
|
+
const fs = require('node:fs');
|
|
86
|
+
for (const a of fitness.VOCAB_ALLOW) {
|
|
87
|
+
const abs = path.join(ROOT, a.file);
|
|
88
|
+
assert.ok(fs.existsSync(abs), `${a.file} is allowlisted but does not exist`);
|
|
89
|
+
const body = fs.readFileSync(abs, 'utf8').toLowerCase();
|
|
90
|
+
assert.ok(body.includes(a.match.toLowerCase()),
|
|
91
|
+
`${a.file} no longer contains "${a.match}" — the exemption outlived its cause `
|
|
92
|
+
+ 'and should be deleted, not carried.');
|
|
93
|
+
}
|
|
94
|
+
});
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -127,21 +127,22 @@ function hrefResolves(item) {
|
|
|
127
127
|
|
|
128
128
|
test('sidebar groups read by audience, in order', () => {
|
|
129
129
|
const nav = parseNav(boot().sidebarHtml);
|
|
130
|
-
// 'Archon' → 'Governance' in ADR 0157
|
|
130
|
+
// 'Archon' → 'Governance' in ADR 0157 (the group's pages opened to Metic, so a header
|
|
131
|
+
// naming the top rank was wrong), then → 'Government' by ADR 0174: a header
|
|
131
132
|
// reading "Archon" over items a Metic can open would misdescribe the ladder.
|
|
132
|
-
assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', '
|
|
133
|
+
assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Government']);
|
|
133
134
|
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work'],
|
|
134
135
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
135
136
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
136
137
|
// People (task 1002577): the /scouting directory graduated into the hall.
|
|
137
138
|
// Gated cosmetically to match its data endpoint — `project.curate`, floor METIC,
|
|
138
|
-
// so the nav entry reads '
|
|
139
|
+
// so the nav entry reads 'government' since ADR 0174 (it said 'archon', then
|
|
139
140
|
// was stricter than the wall). requireProjectCuratePage is the wall server-side.
|
|
140
141
|
assert.deepEqual(nav[2].items.map((i) => i.id), ['leaderboard', 'people'], 'Community = everyone else');
|
|
141
142
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
142
143
|
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
143
|
-
// '
|
|
144
|
-
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions', '
|
|
144
|
+
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|
|
145
|
+
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions', 'government']);
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
test('Economy lives in Docs and points at the reward-schedule page', () => {
|
|
@@ -190,11 +191,11 @@ test('Achievements has no nav entry, but #achievements still routes to Standing'
|
|
|
190
191
|
|
|
191
192
|
test('collapsible groups carry a stable storage key (collapse persistence)', () => {
|
|
192
193
|
const html = boot().sidebarHtml;
|
|
193
|
-
// 'archon' → 'governance' follows
|
|
194
|
+
// 'archon' → 'governance' → 'government' follows ADR 0157 then ADR 0174. The slug is the
|
|
194
195
|
// hall-nav-collapsed storage key, so any builder who had collapsed the old "archon"
|
|
195
196
|
// group gets the new one expanded once — cosmetic, and preferable to keeping a stale
|
|
196
197
|
// slug that no longer matches the visible label.
|
|
197
|
-
for (const g of ['you', 'community', 'docs', '
|
|
198
|
+
for (const g of ['you', 'community', 'docs', 'government']) {
|
|
198
199
|
assert.match(html, new RegExp(`data-group="${g}"`), `${g} is collapsible (hall-nav-collapsed key)`);
|
|
199
200
|
}
|
|
200
201
|
assert.ok(!/nav-group--collapsible[^>]*>(?!<button)/.test(html.split('<div class="nav-group')[1] || ''),
|