@bongos/core 1.19.183 → 1.19.185
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 +102 -92
- package/clients/bongos-client/bongos-client.global.js +19 -19
- package/clients/bongos-client/index.cjs +19 -19
- package/clients/bongos-client/index.d.ts +29 -29
- package/clients/bongos-client/index.mjs +19 -19
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +2 -2
- package/docs/api/openapi.json +117 -117
- package/docs/api-reference.md +10 -10
- package/docs/canonical-permissions.md +6 -6
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +19 -19
- package/modules/{governance → government}/catalog.js +8 -8
- package/modules/{governance → government}/config.js +6 -6
- package/modules/{governance → government}/db.js +44 -44
- package/modules/government/migrations/government_001_rename_from_governance.sql +213 -0
- package/modules/government/migrations/government_002_rename_permission_keys.sql +112 -0
- package/modules/government/module.json +22 -0
- package/modules/{governance → government}/protected-surfaces.json +0 -1
- package/modules/{governance/rank-roles.js → government/ranks.js} +18 -18
- package/modules/{governance/routes/governance.js → government/routes/government.js} +109 -84
- package/modules/{governance/routes/role-sync.js → government/routes/rank-sync.js} +7 -7
- package/modules/hall-ui/public/gate.js +1 -1
- package/modules/hall-ui/public/governance.js +38 -38
- package/modules/hall-ui/public/harbor.js +2 -2
- package/modules/hall-ui/public/shell.js +3 -3
- package/modules/hall-ui/public/watch.js +1 -1
- package/modules/lifecycle/db.js +6 -6
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +11 -1
- package/scripts/gds/migration-namespace.js +15 -3
- package/scripts/gds/publish-manifest.js +2 -3
- package/scripts/gds/seed-governance-tasks.js +3 -3
- package/src/bongos/auth.js +16 -16
- package/src/bongos/db.js +6 -6
- package/src/bongos/module-scope-map.js +7 -22
- package/src/bongos/permission-path-check.js +3 -3
- package/src/bongos/route-rank-check.js +15 -21
- package/src/bongos/serve-internal.js +2 -2
- package/src/module-api.js +7 -3
- package/tests/auth_page_gate.mjs +2 -2
- package/tests/criterion_authoring_gate.mjs +1 -1
- package/tests/governance_abuse_matrix.mjs +38 -38
- package/tests/governance_catalog.mjs +4 -4
- package/tests/governance_claim_eligibility.mjs +1 -1
- package/tests/governance_config.mjs +7 -7
- package/tests/governance_fold.mjs +4 -4
- package/tests/governance_fold_bench.mjs +2 -2
- package/tests/governance_live_proof.mjs +6 -6
- package/tests/governance_ownership_scope.mjs +2 -2
- package/tests/governance_page_gates.mjs +10 -10
- package/tests/governance_parity.mjs +3 -3
- package/tests/governance_principals.mjs +3 -3
- package/tests/governance_protected_surfaces.mjs +44 -19
- package/tests/governance_require_permission.mjs +11 -11
- package/tests/governance_resolver.mjs +4 -4
- package/tests/governance_role_sync.mjs +35 -35
- package/tests/governance_routes.mjs +98 -98
- package/tests/governance_seed.mjs +27 -4
- package/tests/governance_session_scope.mjs +5 -5
- package/tests/governance_vanilla_boot.mjs +4 -4
- package/tests/hall_page_gate_map.mjs +3 -3
- package/tests/memory_isolation.mjs +2 -2
- package/tests/module_loader.mjs +1 -1
- package/tests/onboarding_observability.mjs +2 -2
- package/tests/rank_tier_single_source.mjs +2 -2
- package/tests/route_permission_check.mjs +2 -2
- package/modules/governance/module.json +0 -11
- /package/config/{governance.neutral.json → government.neutral.json} +0 -0
- /package/modules/{governance → government}/CLAUDE.md +0 -0
- /package/modules/{governance → government}/migrations/governance_001_tables.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_002_seed_and_backfill.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_003_task_abandon_metic.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_004_criterion_create.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_005_resync_builder_rank_roles.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_006_version_create.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_007_delegate_archon_ops_to_metic.sql +0 -0
- /package/modules/{governance → government}/protected-surfaces.js +0 -0
- /package/modules/{governance → government}/resolver.js +0 -0
- /package/modules/{governance → government}/session-scopes.js +0 -0
- /package/modules/{governance → government}/session-scopes.json +0 -0
|
@@ -142,7 +142,7 @@ try {
|
|
|
142
142
|
'BEFORE: the thetes builder cannot triage (403 permission_forbidden)');
|
|
143
143
|
|
|
144
144
|
// Act 2 — an Archon creates a custom role and grants the operational permission.
|
|
145
|
-
r = await call(ARCHON_TOKEN, 'POST', '/
|
|
145
|
+
r = await call(ARCHON_TOKEN, 'POST', '/government/ranks',
|
|
146
146
|
{ role_key: ROLE_KEY, label: 'GLP demo triage role' });
|
|
147
147
|
ok(r.status === 201, 'the Archon creates a custom role (201)');
|
|
148
148
|
r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${OPERATIONAL_KEY}`);
|
|
@@ -153,13 +153,13 @@ try {
|
|
|
153
153
|
r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${SYSTEM_KEY}`);
|
|
154
154
|
ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
|
|
155
155
|
`system '${SYSTEM_KEY}' is NOT grantable, even by the Archon (400 permission_not_grantable)`);
|
|
156
|
-
r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/
|
|
156
|
+
r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/government.manage`);
|
|
157
157
|
ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
|
|
158
|
-
'the meta-escalation (
|
|
158
|
+
'the meta-escalation (government.manage) is refused the same way');
|
|
159
159
|
|
|
160
160
|
// Act 4 — assign, then the same builder performs the action: no re-login, no
|
|
161
161
|
// restart — the per-request, uncached resolve (ADR 0016) picks the role up.
|
|
162
|
-
r = await call(ARCHON_TOKEN, 'POST', '/
|
|
162
|
+
r = await call(ARCHON_TOKEN, 'POST', '/government/assignments',
|
|
163
163
|
{ builder_id: THETES, role_key: ROLE_KEY });
|
|
164
164
|
ok(r.status === 201, 'the Archon assigns the custom role to the thetes builder (201)');
|
|
165
165
|
r = await call(THETES_TOKEN, 'PATCH', `/inbox/${ideaId}`, { status: 'discarded' });
|
|
@@ -176,7 +176,7 @@ try {
|
|
|
176
176
|
// …the resolver reports it honestly (it is a defined catalog key; matrix V10
|
|
177
177
|
// proves no route consumes it via requirePermission, so it admits nothing
|
|
178
178
|
// in-app)…
|
|
179
|
-
const resolver = require('../modules/
|
|
179
|
+
const resolver = require('../modules/government/resolver.js');
|
|
180
180
|
const perms = await resolver.resolveBuilderPermissions(THETES);
|
|
181
181
|
ok(perms.includes(OPERATIONAL_KEY), 'the resolved set includes the operational grant');
|
|
182
182
|
console.log(` (resolved set ${perms.includes(SYSTEM_KEY) ? 'includes' : 'omits'} the forced '${SYSTEM_KEY}' — in-app it guards no route either way)`);
|
|
@@ -187,7 +187,7 @@ try {
|
|
|
187
187
|
// protected-path diff while the forced grant row sits in the database.
|
|
188
188
|
const { checkPermissionPaths } = require('../src/bongos/permission-path-check.js');
|
|
189
189
|
const sub = checkPermissionPaths({
|
|
190
|
-
changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/
|
|
190
|
+
changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/government/catalog.js'],
|
|
191
191
|
builderRank: 'thetes',
|
|
192
192
|
});
|
|
193
193
|
ok(sub.hardFail === true && sub.findings.length === 2,
|
|
@@ -30,7 +30,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
30
30
|
|
|
31
31
|
const require = createRequire(import.meta.url);
|
|
32
32
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
33
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
34
34
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
35
35
|
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
36
36
|
const db = require(path.join(GOV, 'db.js'));
|
|
@@ -397,7 +397,7 @@ test('G2: the role resolve is untouched — own-scoped keys still resolve, princ
|
|
|
397
397
|
});
|
|
398
398
|
|
|
399
399
|
test('G3: the kernel port carries the ownership axis on the ONE governance port', () => {
|
|
400
|
-
const { governancePort } = require(path.join(GOV, 'routes', '
|
|
400
|
+
const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
401
401
|
for (const name of ['authorizeOwned', 'authorizeOwnedOrAny', 'sameOwner', 'isOwnershipScoped', 'crossOwnerKeysFor']) {
|
|
402
402
|
assert.equal(typeof governancePort[name], 'function', `the port must expose ${name}`);
|
|
403
403
|
}
|
|
@@ -45,8 +45,8 @@ dbKernel.lookupSession = async () => {
|
|
|
45
45
|
|
|
46
46
|
const auth = require('../src/bongos/auth.js');
|
|
47
47
|
const seams = require('../src/module-seams.js');
|
|
48
|
-
const catalog = require('../modules/
|
|
49
|
-
const resolver = require('../modules/
|
|
48
|
+
const catalog = require('../modules/government/catalog.js');
|
|
49
|
+
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
|
|
|
@@ -58,7 +58,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
|
58
58
|
// requireNonXenosPage admitted rank !== 'xenos' → thetes and up
|
|
59
59
|
// requireGovernancePage admitted rank === 'archon' → archon only …until ADR 0157
|
|
60
60
|
//
|
|
61
|
-
// ADR 0157 (owner decision, 2026-08-05) lowered `page.view.
|
|
61
|
+
// ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
|
|
62
62
|
// the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
|
|
63
63
|
// was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
|
|
64
64
|
// were first written against — and was renamed once the floor moved; the audience here,
|
|
@@ -86,7 +86,7 @@ const DENIED_TO = {
|
|
|
86
86
|
const GATE_PERMISSION = {
|
|
87
87
|
requireBuilderPage: 'page.view.public',
|
|
88
88
|
requireNonXenosPage: 'page.view.builder',
|
|
89
|
-
requireGovernancePage: 'page.view.
|
|
89
|
+
requireGovernancePage: 'page.view.government',
|
|
90
90
|
};
|
|
91
91
|
const deniedLoc = (gate, { unavailable = false } = {}) =>
|
|
92
92
|
`${DENIED_TO[gate]}?denied=${GATE_PERMISSION[gate]}${unavailable ? '&reason=unavailable' : ''}`;
|
|
@@ -94,7 +94,7 @@ const deniedLoc = (gate, { unavailable = false } = {}) =>
|
|
|
94
94
|
const GATE_FLOOR = {
|
|
95
95
|
requireBuilderPage: 'xenos',
|
|
96
96
|
requireNonXenosPage: 'thetes',
|
|
97
|
-
// ADR 0157: `page.view.
|
|
97
|
+
// ADR 0157: `page.view.government` floor archon→metic.
|
|
98
98
|
requireGovernancePage: 'metic',
|
|
99
99
|
};
|
|
100
100
|
const SIGN_IN = '/api/gds/auth/web/start';
|
|
@@ -126,7 +126,7 @@ async function runGate(name, { rank = null, resolve, port = true } = {}) {
|
|
|
126
126
|
lookupThrows = false;
|
|
127
127
|
seams._reset();
|
|
128
128
|
if (port) {
|
|
129
|
-
seams.registerProvider('
|
|
129
|
+
seams.registerProvider('government', {
|
|
130
130
|
// The real seed for that rank — the rows governance_002 gives the one
|
|
131
131
|
// rank-role a builder holds. The INPUT, not the oracle.
|
|
132
132
|
resolveBuilderPermissions: resolve || (async () => catalog.seedForRank(rank)),
|
|
@@ -144,7 +144,7 @@ async function runGate(name, { rank = null, resolve, port = true } = {}) {
|
|
|
144
144
|
|
|
145
145
|
test('A0: the hand-written audience table agrees with the real threshold function', () => {
|
|
146
146
|
// A second, catalog-free oracle for the literal above: auth.rankMeetsThreshold
|
|
147
|
-
// is the production rank predicate and reads nothing from modules/
|
|
147
|
+
// is the production rank predicate and reads nothing from modules/government/.
|
|
148
148
|
for (const gate of GATES) {
|
|
149
149
|
for (const rank of LIVE_RANKS) {
|
|
150
150
|
assert.equal(
|
|
@@ -203,7 +203,7 @@ test('B1: a session-lookup error fails CLOSED (redirect to sign-in, no page)', a
|
|
|
203
203
|
for (const gate of GATES) {
|
|
204
204
|
nextSession = null;
|
|
205
205
|
seams._reset();
|
|
206
|
-
seams.registerProvider('
|
|
206
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => catalog.seedForRank('archon') });
|
|
207
207
|
lookupThrows = true;
|
|
208
208
|
const res = fakeRes();
|
|
209
209
|
let nexted = false;
|
|
@@ -244,7 +244,7 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
|
|
|
244
244
|
nextSession = { builder_id: 7, rank: 'archon' };
|
|
245
245
|
lookupThrows = false;
|
|
246
246
|
seams._reset();
|
|
247
|
-
seams.registerProvider('
|
|
247
|
+
seams.registerProvider('government', {
|
|
248
248
|
resolveBuilderPermissions: async () => { calls += 1; return keys; },
|
|
249
249
|
});
|
|
250
250
|
const req = { headers: { cookie: 'gds_session=validtoken' } };
|
|
@@ -264,7 +264,7 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
|
|
|
264
264
|
test('B5: every permission a page gate names is a real catalog key', async () => {
|
|
265
265
|
// Not an oracle — a sanity check that the gates did not migrate onto a typo,
|
|
266
266
|
// which would silently deny everyone (fail-closed, but a bug all the same).
|
|
267
|
-
for (const key of ['page.view.public', 'page.view.builder', 'page.view.
|
|
267
|
+
for (const key of ['page.view.public', 'page.view.builder', 'page.view.government']) {
|
|
268
268
|
assert.ok(catalog.byKey(key), `${key} must exist in the catalog`);
|
|
269
269
|
}
|
|
270
270
|
});
|
|
@@ -57,8 +57,8 @@ const { RANKS } = require(path.join(ROOT, 'src', 'bongos', 'db-kernel.js'));
|
|
|
57
57
|
const routeRank = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
58
58
|
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
59
59
|
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
60
|
-
const catalog = require(path.join(ROOT, 'modules', '
|
|
61
|
-
const resolver = require(path.join(ROOT, 'modules', '
|
|
60
|
+
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
61
|
+
const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
|
|
62
62
|
|
|
63
63
|
const { PERMISSIONS, RANK_ORDER, seedForRank } = catalog;
|
|
64
64
|
const FLOORED = PERMISSIONS.filter((p) => p.floor !== null);
|
|
@@ -287,7 +287,7 @@ function fakeRes() {
|
|
|
287
287
|
// not the logic — resolvedFor() is the real resolver over the real seed.
|
|
288
288
|
async function permissionAdmits(key, rank) {
|
|
289
289
|
seams._reset();
|
|
290
|
-
seams.registerProvider('
|
|
290
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => resolvedFor(rank) });
|
|
291
291
|
let nexted = false;
|
|
292
292
|
await auth.requirePermission(key)({ builder: { id: 7, rank } }, fakeRes(), () => { nexted = true; });
|
|
293
293
|
seams._reset();
|
|
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
25
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
26
26
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
27
27
|
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
28
28
|
|
|
@@ -149,7 +149,7 @@ test('resolved principal sets are frozen — a caller cannot widen one', () => {
|
|
|
149
149
|
// ── show, never grant ───────────────────────────────────────────────────────────
|
|
150
150
|
|
|
151
151
|
test('the tab surface is a read-only projection: shown, flagged non-grantable', () => {
|
|
152
|
-
const { governancePort } = require(path.join(GOV, 'routes', '
|
|
152
|
+
const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
153
153
|
assert.equal(typeof governancePort.listPrincipals, 'function');
|
|
154
154
|
assert.equal(typeof governancePort.resolvePrincipalPermissions, 'function');
|
|
155
155
|
const listed = governancePort.listPrincipals();
|
|
@@ -162,7 +162,7 @@ test('the tab surface is a read-only projection: shown, flagged non-grantable',
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
test('the governance port exposes no way to create, grant, or assign a principal', () => {
|
|
165
|
-
const { governancePort } = require(path.join(GOV, 'routes', '
|
|
165
|
+
const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
166
166
|
const mutator = /(create|grant|assign|add|set|write|provision|revoke|delete|update)/i;
|
|
167
167
|
for (const name of Object.keys(governancePort)) {
|
|
168
168
|
if (!/principal/i.test(name)) continue;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// The load-bearing test is BEHAVIOR-IDENTITY. R101 moved WHERE the truth lives
|
|
10
10
|
// (a hardcoded PROTECTED_GLOBS constant in src/bongos/permission-path-check.js →
|
|
11
|
-
// governance-owned data in modules/
|
|
11
|
+
// governance-owned data in modules/government/protected-surfaces.json). It did
|
|
12
12
|
// NOT change WHAT is protected. EXPECTED_GLOBS_BEFORE_R101 below is the literal
|
|
13
13
|
// pre-change constant, pasted verbatim; the registry must reproduce it exactly,
|
|
14
14
|
// in order.
|
|
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
25
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
26
26
|
|
|
27
27
|
const ppc = require(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'));
|
|
28
28
|
const registry = require(path.join(GOV, 'protected-surfaces.js'));
|
|
@@ -92,22 +92,34 @@ const EXPECTED_GLOBS_BEFORE_R101 = [
|
|
|
92
92
|
// Each entry is { after, glob, why }: `after` is the glob it is inserted
|
|
93
93
|
// directly below, so the expected list is reconstructed in order rather than
|
|
94
94
|
// re-pasted (a re-paste is how the two lists drift apart).
|
|
95
|
-
const ADDED_SINCE_R101 = [
|
|
95
|
+
const ADDED_SINCE_R101 = [];
|
|
96
|
+
|
|
97
|
+
// Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
|
|
98
|
+
// a removal on purpose: a rename keeps the protection and moves it, a removal
|
|
99
|
+
// ends it. The subset test below accepts a documented rename and rejects a bare
|
|
100
|
+
// disappearance, so "we renamed it" can never be used to quietly unprotect
|
|
101
|
+
// something — the replacement must actually be in the registry.
|
|
102
|
+
const RENAMED_SINCE_R101 = [
|
|
96
103
|
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
why: 'goal 1000068 /
|
|
100
|
-
+ '
|
|
101
|
-
+ '
|
|
102
|
-
+ '
|
|
103
|
-
+ '
|
|
104
|
+
from: 'modules/governance/',
|
|
105
|
+
to: 'modules/government/',
|
|
106
|
+
why: 'goal 1000068 / ADR 0174 — the authz core was renamed governance→government. '
|
|
107
|
+
+ 'The folder moved in task 1003004 and the one-release compatibility glob was '
|
|
108
|
+
+ 'dropped in R05 (task 1003006) along with the /governance/* route aliases. '
|
|
109
|
+
+ 'Protection did not lapse at any point: the new glob was registered BEFORE '
|
|
110
|
+
+ 'the folder moved (task 1003003), so both were listed across the window.',
|
|
104
111
|
},
|
|
105
112
|
];
|
|
106
113
|
|
|
107
114
|
// The list the registry must produce TODAY: the pre-R101 set with each
|
|
108
115
|
// documented addition spliced in at its declared position.
|
|
109
116
|
const EXPECTED_GLOBS = (() => {
|
|
110
|
-
|
|
117
|
+
let out = [...EXPECTED_GLOBS_BEFORE_R101];
|
|
118
|
+
for (const r of RENAMED_SINCE_R101) {
|
|
119
|
+
const at = out.indexOf(r.from);
|
|
120
|
+
assert.notEqual(at, -1, `RENAMED_SINCE_R101 renames '${r.from}', which is not in the list`);
|
|
121
|
+
out[at] = r.to;
|
|
122
|
+
}
|
|
111
123
|
for (const a of ADDED_SINCE_R101) {
|
|
112
124
|
const at = out.indexOf(a.after);
|
|
113
125
|
assert.notEqual(at, -1, `ADDED_SINCE_R101 anchors on '${a.after}', which is not in the list`);
|
|
@@ -119,15 +131,28 @@ const EXPECTED_GLOBS = (() => {
|
|
|
119
131
|
// ---------- behavior identity: the same globs, at the same floor ----------
|
|
120
132
|
|
|
121
133
|
test('nothing protected before R101 has been unprotected since', () => {
|
|
122
|
-
// The load-bearing half.
|
|
123
|
-
//
|
|
124
|
-
//
|
|
134
|
+
// The load-bearing half. A surface may be RENAMED (protection moves with it)
|
|
135
|
+
// but never simply dropped. Renames must be declared, and the declared
|
|
136
|
+
// replacement must actually be present — otherwise "it was renamed" becomes a
|
|
137
|
+
// way to unprotect a trust surface with a comment.
|
|
138
|
+
const renamedFrom = new Map(RENAMED_SINCE_R101.map((r) => [r.from, r.to]));
|
|
125
139
|
for (const glob of EXPECTED_GLOBS_BEFORE_R101) {
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
if (registry.PROTECTED_GLOBS.includes(glob)) continue;
|
|
141
|
+
const to = renamedFrom.get(glob);
|
|
142
|
+
assert.ok(to,
|
|
128
143
|
`'${glob}' was protected before R101 and is no longer in the registry — `
|
|
129
|
-
+ 'a protected surface cannot be dropped without an ADR saying why.'
|
|
130
|
-
|
|
144
|
+
+ 'a protected surface cannot be dropped without an ADR saying why. If it '
|
|
145
|
+
+ 'was renamed, declare it in RENAMED_SINCE_R101.');
|
|
146
|
+
assert.ok(registry.PROTECTED_GLOBS.includes(to),
|
|
147
|
+
`'${glob}' is declared as renamed to '${to}', but '${to}' is not in the `
|
|
148
|
+
+ 'registry — the protection went nowhere.');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('every rename since R101 carries a documented reason', () => {
|
|
153
|
+
for (const r of RENAMED_SINCE_R101) {
|
|
154
|
+
assert.ok(r.why && r.why.length > 40,
|
|
155
|
+
`${r.from} -> ${r.to} must record WHY the surface moved.`);
|
|
131
156
|
}
|
|
132
157
|
});
|
|
133
158
|
|
|
@@ -234,7 +259,7 @@ test('a known path is protected at the right floor; an unknown path is not', ()
|
|
|
234
259
|
assert.equal(ppc.floorFor('src/bongos/auth.js'), 'metic');
|
|
235
260
|
assert.equal(ppc.permissionFor('src/bongos/auth.js'), 'path.protected.modify');
|
|
236
261
|
assert.equal(registry.isProtected('migrations/999_x.sql'), true, 'subtree globs still match');
|
|
237
|
-
assert.equal(registry.floorFor('modules/
|
|
262
|
+
assert.equal(registry.floorFor('modules/government/catalog.js'), 'metic');
|
|
238
263
|
|
|
239
264
|
assert.equal(ppc.floorFor('public/game/main.js'), null);
|
|
240
265
|
assert.equal(ppc.permissionFor('docs/session-logs/2026-07-30-x.md'), null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tests/governance_require_permission.mjs — the requirePermission gate (BV1.R97, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// DB-free. The gate reaches the resolver through the `governance` KERNEL PORT
|
|
4
|
-
// (the one-way rule — core never requires modules/
|
|
4
|
+
// (the one-way rule — core never requires modules/government/), so a stub
|
|
5
5
|
// provider registered on the seam is the whole test double: no Postgres, no
|
|
6
6
|
// router, no live module. That is also the point being asserted — if auth.js ever
|
|
7
7
|
// went back to importing the module directly, these tests could not stub it.
|
|
@@ -23,7 +23,7 @@ const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
|
|
|
23
23
|
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
24
24
|
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
25
25
|
const { classify } = require(path.join(ROOT, 'src', 'bongos', 'api-errors.js'));
|
|
26
|
-
const catalog = require(path.join(ROOT, 'modules', '
|
|
26
|
+
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
27
27
|
|
|
28
28
|
// Minimal res double capturing status + body, with res.fail() installed exactly
|
|
29
29
|
// as the router installs it (mirrors tests/rank_gate.mjs).
|
|
@@ -42,7 +42,7 @@ function fakeRes() {
|
|
|
42
42
|
// resolveBuilderPermissions; pass null to leave the port UNREGISTERED.
|
|
43
43
|
async function run(gate, req, resolve) {
|
|
44
44
|
seams._reset();
|
|
45
|
-
if (resolve !== null) seams.registerProvider('
|
|
45
|
+
if (resolve !== null) seams.registerProvider('government', { resolveBuilderPermissions: resolve });
|
|
46
46
|
const res = fakeRes();
|
|
47
47
|
let nexted = false;
|
|
48
48
|
await gate(req, res, () => { nexted = true; });
|
|
@@ -63,12 +63,12 @@ test('a builder holding the permission passes the gate', async () => {
|
|
|
63
63
|
|
|
64
64
|
test('a missing grant 403s with permission_forbidden and explains the denial', async () => {
|
|
65
65
|
const { res, nexted } = await run(
|
|
66
|
-
auth.requirePermission('
|
|
66
|
+
auth.requirePermission('government.manage'), { builder: BUILDER }, holding('idea.triage'),
|
|
67
67
|
);
|
|
68
68
|
assert.equal(nexted, false, 'next() must NOT be called');
|
|
69
69
|
assert.equal(res._status, 403);
|
|
70
70
|
assert.equal(res._json.error.code, 'permission_forbidden');
|
|
71
|
-
assert.deepEqual(res._json.error.details.required, ['
|
|
71
|
+
assert.deepEqual(res._json.error.details.required, ['government.manage']);
|
|
72
72
|
assert.deepEqual(res._json.error.details.held, [], 'holds none of the required perms');
|
|
73
73
|
});
|
|
74
74
|
|
|
@@ -89,7 +89,7 @@ test('the denial names the floor rank, not just the atom (task 1002613, map P-1)
|
|
|
89
89
|
// this test moves WITH a floor change instead of pinning stale prose — the
|
|
90
90
|
// D-16/N-3 failure shape the fix exists to avoid.
|
|
91
91
|
seams._reset();
|
|
92
|
-
seams.registerProvider('
|
|
92
|
+
seams.registerProvider('government', {
|
|
93
93
|
resolveBuilderPermissions: holding('idea.triage'),
|
|
94
94
|
permissionFloor: (key) => (catalog.byKey(key) || {}).floor || null,
|
|
95
95
|
});
|
|
@@ -107,11 +107,11 @@ test('the denial names the floor rank, not just the atom (task 1002613, map P-1)
|
|
|
107
107
|
|
|
108
108
|
test('a port without permissionFloor still denies with a message; floors is null', async () => {
|
|
109
109
|
const { res } = await run(
|
|
110
|
-
auth.requirePermission('
|
|
110
|
+
auth.requirePermission('government.manage'), { builder: BUILDER }, holding('idea.triage'),
|
|
111
111
|
);
|
|
112
112
|
assert.equal(res._status, 403);
|
|
113
113
|
assert.equal(res._json.error.details.floors, null);
|
|
114
|
-
assert.ok(res._json.error.message.includes('
|
|
114
|
+
assert.ok(res._json.error.message.includes('government.manage'),
|
|
115
115
|
'the message names the missing atom even when no floor is resolvable');
|
|
116
116
|
});
|
|
117
117
|
|
|
@@ -143,7 +143,7 @@ test('an UNREGISTERED governance port denies, it does not admit', async () => {
|
|
|
143
143
|
|
|
144
144
|
test('a malformed port (no resolveBuilderPermissions) denies', async () => {
|
|
145
145
|
seams._reset();
|
|
146
|
-
seams.registerProvider('
|
|
146
|
+
seams.registerProvider('government', { isSystem: () => true });
|
|
147
147
|
const res = fakeRes();
|
|
148
148
|
let nexted = false;
|
|
149
149
|
await auth.requirePermission('idea.triage')({ builder: BUILDER }, res, () => { nexted = true; });
|
|
@@ -175,7 +175,7 @@ test('the gate resolves per request — no memoised authority (ADR 0016)', async
|
|
|
175
175
|
let calls = 0;
|
|
176
176
|
let keys = ['idea.triage'];
|
|
177
177
|
seams._reset();
|
|
178
|
-
seams.registerProvider('
|
|
178
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => { calls += 1; return keys; } });
|
|
179
179
|
const gate = auth.requirePermission('idea.triage');
|
|
180
180
|
const first = fakeRes();
|
|
181
181
|
let admitted = false;
|
|
@@ -264,7 +264,7 @@ test('over the four seeded ranks, requirePermission admits exactly who requireRa
|
|
|
264
264
|
'security.report.file',
|
|
265
265
|
];
|
|
266
266
|
const cases = [
|
|
267
|
-
['
|
|
267
|
+
['government.manage', ['archon']],
|
|
268
268
|
['task.claim.any', ['thetes', 'metic', 'archon']],
|
|
269
269
|
['cost.log', ['xenos', 'thetes', 'metic', 'archon']],
|
|
270
270
|
// The CORE route gates migrated in BV1.R104, each paired with the requireRank
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
19
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
20
20
|
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
21
21
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
22
22
|
const { effectivePermissions, resolveBuilderPermissions, builderHasPermissions } = resolver;
|
|
@@ -74,11 +74,11 @@ 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 { governancePort } = require(path.join(GOV, 'routes', '
|
|
77
|
+
const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
78
78
|
assert.equal(typeof governancePort.resolveBuilderPermissions, 'function');
|
|
79
79
|
assert.equal(typeof governancePort.builderHasPermissions, 'function');
|
|
80
80
|
assert.equal(typeof governancePort.isSystem, 'function');
|
|
81
81
|
const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
|
|
82
|
-
assert.ok(manifest.provides.includes('
|
|
83
|
-
assert.ok(manifest.contributes.routes.includes('
|
|
82
|
+
assert.ok(manifest.provides.includes('government'), 'module.json must declare the government port');
|
|
83
|
+
assert.ok(manifest.contributes.routes.includes('government'), 'the port registers from the route factory');
|
|
84
84
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// tests/governance_role_sync.mjs — rank→role assignment stays in sync (BV1.R95b, ADR 0151).
|
|
2
2
|
//
|
|
3
|
-
// DB-free. The decision the writer executes is a pure function (modules/
|
|
4
|
-
//
|
|
5
|
-
// than a re-implementation: db.
|
|
3
|
+
// DB-free. The decision the writer executes is a pure function (modules/government/
|
|
4
|
+
// ranks.js planRankSync), so these exercise PRODUCTION logic directly rather
|
|
5
|
+
// than a re-implementation: db.syncRank reads the two row sets, calls this exact
|
|
6
6
|
// function, and executes its verdict. The SQL halves — the DELETE/INSERT the writer
|
|
7
7
|
// issues and the repair migration — are asserted structurally.
|
|
8
8
|
//
|
|
@@ -21,21 +21,21 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
23
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
25
|
-
const {
|
|
24
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
25
|
+
const { planRankSync, isSeededRank } = require(path.join(GOV, 'ranks.js'));
|
|
26
26
|
|
|
27
27
|
// The seeded rank-roles exactly as governance_002 writes them.
|
|
28
28
|
const SEEDED = [
|
|
29
|
-
{
|
|
30
|
-
{
|
|
31
|
-
{
|
|
32
|
-
{
|
|
29
|
+
{ rank_key: 'xenos', seeded_rank: 'xenos', is_system: true },
|
|
30
|
+
{ rank_key: 'thetes', seeded_rank: 'thetes', is_system: true },
|
|
31
|
+
{ rank_key: 'metic', seeded_rank: 'metic', is_system: true },
|
|
32
|
+
{ rank_key: 'archon', seeded_rank: 'archon', is_system: true },
|
|
33
33
|
];
|
|
34
|
-
const
|
|
34
|
+
const seededRank_ = (rank) => ({ rank_key: rank, seeded_rank: rank, is_system: true });
|
|
35
35
|
// An Archon-authored custom role from the R110 tab: no seeded_rank, not a system role.
|
|
36
|
-
const
|
|
36
|
+
const customRank = (key) => ({ rank_key: key, seeded_rank: null, is_system: false });
|
|
37
37
|
|
|
38
|
-
const plan = (rank,
|
|
38
|
+
const plan = (rank, currentRanks) => planRankSync({ rank, currentRanks, seededRanks: SEEDED });
|
|
39
39
|
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
41
|
// registration — a builder with no roles gets the one matching their rank
|
|
@@ -60,7 +60,7 @@ test('registration: the firstAdmin bootstrap (seated straight to archon) assigns
|
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
|
|
62
62
|
test('promotion xenos→thetes: the new role is added and the old one removed', () => {
|
|
63
|
-
const p = plan('thetes', [
|
|
63
|
+
const p = plan('thetes', [seededRank_('xenos')]);
|
|
64
64
|
assert.equal(p.target, 'thetes');
|
|
65
65
|
assert.deepEqual([...p.add], ['thetes']);
|
|
66
66
|
assert.deepEqual([...p.remove], ['xenos'], 'the superseded rank-role must be removed');
|
|
@@ -69,8 +69,8 @@ test('promotion xenos→thetes: the new role is added and the old one removed',
|
|
|
69
69
|
|
|
70
70
|
test('promotion via the auto-graduation path leaves exactly one seeded role', () => {
|
|
71
71
|
// maybeAutoGraduateToThetes runs the same _applyRankChangeTx, so the same plan.
|
|
72
|
-
const p = plan('thetes', [
|
|
73
|
-
const seededHeld = p.next.filter((k) => SEEDED.some((s) => s.
|
|
72
|
+
const p = plan('thetes', [seededRank_('xenos')]);
|
|
73
|
+
const seededHeld = p.next.filter((k) => SEEDED.some((s) => s.rank_key === k));
|
|
74
74
|
assert.equal(seededHeld.length, 1);
|
|
75
75
|
});
|
|
76
76
|
|
|
@@ -79,7 +79,7 @@ test('promotion via the auto-graduation path leaves exactly one seeded role', ()
|
|
|
79
79
|
// ---------------------------------------------------------------------------
|
|
80
80
|
|
|
81
81
|
test('demotion archon→metic REMOVES the archon role (not merely adds metic)', () => {
|
|
82
|
-
const p = plan('metic', [
|
|
82
|
+
const p = plan('metic', [seededRank_('archon')]);
|
|
83
83
|
assert.deepEqual([...p.remove], ['archon'], 'the higher rank-role must be REMOVED on demotion');
|
|
84
84
|
assert.deepEqual([...p.add], ['metic']);
|
|
85
85
|
// The absence assertion is the one that matters: an additive-only sync would
|
|
@@ -89,7 +89,7 @@ test('demotion archon→metic REMOVES the archon role (not merely adds metic)',
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
test('demotion archon→xenos (a multi-rung drop) removes archon just the same', () => {
|
|
92
|
-
const p = plan('xenos', [
|
|
92
|
+
const p = plan('xenos', [seededRank_('archon')]);
|
|
93
93
|
assert.ok(p.remove.includes('archon'));
|
|
94
94
|
assert.ok(!p.next.includes('archon'));
|
|
95
95
|
assert.deepEqual([...p.next], ['xenos']);
|
|
@@ -98,7 +98,7 @@ test('demotion archon→xenos (a multi-rung drop) removes archon just the same',
|
|
|
98
98
|
test('a builder holding several stale seeded roles is reduced to exactly one', () => {
|
|
99
99
|
// Defence in depth: if drift ever left two seeded roles on one builder, the sync
|
|
100
100
|
// must not pick one and leave the other — every non-matching seeded role goes.
|
|
101
|
-
const p = plan('thetes', [
|
|
101
|
+
const p = plan('thetes', [seededRank_('archon'), seededRank_('metic'), seededRank_('xenos')]);
|
|
102
102
|
assert.deepEqual([...p.remove].sort(), ['archon', 'metic', 'xenos']);
|
|
103
103
|
assert.deepEqual([...p.next], ['thetes']);
|
|
104
104
|
});
|
|
@@ -108,17 +108,17 @@ test('a builder holding several stale seeded roles is reduced to exactly one', (
|
|
|
108
108
|
// ---------------------------------------------------------------------------
|
|
109
109
|
|
|
110
110
|
test('an Archon-assigned custom role survives a rank change', () => {
|
|
111
|
-
const p = plan('metic', [
|
|
111
|
+
const p = plan('metic', [seededRank_('archon'), customRank('release-manager')]);
|
|
112
112
|
assert.deepEqual([...p.remove], ['archon'], 'only the seeded rank-role rotates');
|
|
113
113
|
assert.ok(!p.remove.includes('release-manager'), 'a custom role must never be revoked by a rank change');
|
|
114
114
|
assert.deepEqual([...p.next], ['metic', 'release-manager']);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
test('a custom role is not mistaken for the rank-role even when its key matches a rank name', () => {
|
|
118
|
-
//
|
|
118
|
+
// rank_key is UNIQUE so this cannot happen today, but the predicate must key on
|
|
119
119
|
// is_system + seeded_rank rather than on the key's spelling.
|
|
120
|
-
const impostor = {
|
|
121
|
-
assert.equal(
|
|
120
|
+
const impostor = { rank_key: 'archon', seeded_rank: null, is_system: false };
|
|
121
|
+
assert.equal(isSeededRank(impostor), false);
|
|
122
122
|
const p = plan('metic', [impostor]);
|
|
123
123
|
assert.deepEqual([...p.remove], [], 'a non-system role is never rotated away');
|
|
124
124
|
});
|
|
@@ -127,7 +127,7 @@ test('a builder on a non-seeded legacy rank resolves to zero seeded roles', () =
|
|
|
127
127
|
// migration 023's CHECK still admits twelve reserved ranks. governance_002's INNER
|
|
128
128
|
// JOIN gives them no row BY DESIGN (ADR 0151's fail-closed divergence) — preserve
|
|
129
129
|
// it, and strip any seeded role they were carrying.
|
|
130
|
-
const p = plan('hippeis', [
|
|
130
|
+
const p = plan('hippeis', [seededRank_('archon')]);
|
|
131
131
|
assert.equal(p.target, null, 'no seeded role exists for a legacy rank — invent none');
|
|
132
132
|
assert.deepEqual([...p.add], []);
|
|
133
133
|
assert.deepEqual([...p.remove], ['archon'], 'a legacy rank must not retain a seeded rank-role');
|
|
@@ -135,7 +135,7 @@ test('a builder on a non-seeded legacy rank resolves to zero seeded roles', () =
|
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
test('a legacy-rank builder still keeps their custom roles', () => {
|
|
138
|
-
const p = plan('hippeis', [
|
|
138
|
+
const p = plan('hippeis', [seededRank_('metic'), customRank('release-manager')]);
|
|
139
139
|
assert.deepEqual([...p.next], ['release-manager']);
|
|
140
140
|
});
|
|
141
141
|
|
|
@@ -144,23 +144,23 @@ test('a legacy-rank builder still keeps their custom roles', () => {
|
|
|
144
144
|
// ---------------------------------------------------------------------------
|
|
145
145
|
|
|
146
146
|
test('the sync is idempotent — running it twice changes nothing the second time', () => {
|
|
147
|
-
const first = plan('metic', [
|
|
147
|
+
const first = plan('metic', [seededRank_('archon')]);
|
|
148
148
|
assert.ok(first.add.length + first.remove.length > 0, 'the first run must do work');
|
|
149
|
-
const after = first.next.map((key) => (SEEDED.find((s) => s.
|
|
150
|
-
const second =
|
|
149
|
+
const after = first.next.map((key) => (SEEDED.find((s) => s.rank_key === key) || customRank(key)));
|
|
150
|
+
const second = planRankSync({ rank: 'metic', currentRanks: after, seededRanks: SEEDED });
|
|
151
151
|
assert.deepEqual([...second.add], []);
|
|
152
152
|
assert.deepEqual([...second.remove], []);
|
|
153
153
|
assert.deepEqual([...second.next], [...first.next]);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
test('a builder already holding the right role produces no write at all', () => {
|
|
157
|
-
const p = plan('metic', [
|
|
157
|
+
const p = plan('metic', [seededRank_('metic'), customRank('release-manager')]);
|
|
158
158
|
assert.deepEqual([...p.add], []);
|
|
159
159
|
assert.deepEqual([...p.remove], []);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
test('the plan is frozen — a caller cannot widen it after the decision', () => {
|
|
163
|
-
const p = plan('metic', [
|
|
163
|
+
const p = plan('metic', [seededRank_('archon')]);
|
|
164
164
|
assert.ok(Object.isFrozen(p) && Object.isFrozen(p.add) && Object.isFrozen(p.remove));
|
|
165
165
|
assert.throws(() => p.add.push('archon'), TypeError);
|
|
166
166
|
});
|
|
@@ -175,15 +175,15 @@ const repairSql = fs.readFileSync(path.join(migDir, 'governance_005_resync_build
|
|
|
175
175
|
const stripSqlComments = (sql) => sql.replace(/--[^\n]*/g, '');
|
|
176
176
|
|
|
177
177
|
test('the writer DELETEs the superseded assignments, not just INSERTs the new one', () => {
|
|
178
|
-
assert.match(dbSrc, /DELETE FROM
|
|
179
|
-
'
|
|
180
|
-
assert.match(dbSrc, /INSERT INTO
|
|
178
|
+
assert.match(dbSrc, /DELETE FROM government_builder_ranks/,
|
|
179
|
+
'syncRank must delete superseded rank-roles — an additive-only writer preserves privilege retention');
|
|
180
|
+
assert.match(dbSrc, /INSERT INTO government_builder_ranks[\s\S]*ON CONFLICT \(builder_id, rank_key\) DO NOTHING/,
|
|
181
181
|
'the assignment insert must be idempotent');
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
test('the writer runs on the CALLER\'s transaction client when given one', () => {
|
|
185
|
-
assert.match(dbSrc, /async function
|
|
186
|
-
'
|
|
185
|
+
assert.match(dbSrc, /async function syncRank\(client,/,
|
|
186
|
+
'syncRank must take the caller\'s client so the rotation is atomic with the rank write');
|
|
187
187
|
assert.match(dbSrc, /const q = client \|\| pool;/,
|
|
188
188
|
'and fall back to the pool only when there is no surrounding transaction');
|
|
189
189
|
});
|
|
@@ -197,7 +197,7 @@ test('both rank-change twins rotate the role inside their transaction', () => {
|
|
|
197
197
|
assert.ok(upd !== -1, `${rel}: expected the rank UPDATE in _applyRankChangeTx`);
|
|
198
198
|
assert.ok(sync !== -1, `${rel}: the rank write must rotate the seeded rank-role`);
|
|
199
199
|
assert.ok(sync > upd, `${rel}: the rotation must follow the rank write inside the same transaction`);
|
|
200
|
-
assert.match(src, /resolveOptional\('
|
|
200
|
+
assert.match(src, /resolveOptional\('government\.rankSync'\)/,
|
|
201
201
|
`${rel}: must reach governance through the kernel port, never by importing the module`);
|
|
202
202
|
}
|
|
203
203
|
});
|