@bongos/core 1.21.92 → 1.21.93

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.
@@ -0,0 +1,430 @@
1
+ // tests/government_act_alone_buildings.mjs — "decide by vote" binds on EVERY
2
+ // building's own pages, not only People's (task 1004657, goal 1000111; ADR 0363
3
+ // D5 and its Consequences).
4
+ //
5
+ // What this pins:
6
+ // 1. THE GATE (src/bongos/act-alone.js, on the doorway as requireActAlone):
7
+ // as today it lets the request through; while the row names a sitting it
8
+ // refuses with People's exact shape (409 decided_by_sitting + area, rule,
9
+ // who, what_this_means, open_with); no government module → as today; an
10
+ // unreadable table → 503, fail-closed, like People.
11
+ // 2. THE RECORD (decision-rules AREAS[].acts) matches the source BOTH WAYS:
12
+ // every listed route mounts the gate for its building, and every mounted
13
+ // gate is listed. The charter says why it has no direct route.
14
+ // 3. EVERY BUILDING, THROUGH ITS REAL ROUTER, with the real permission gate
15
+ // and the real checkActAlone over a stubbed table: each listed route is
16
+ // refused while its building is a sitting and reaches its handler as today.
17
+ // 4. NOT A LONE ACT: a brake (fence off, pause), a dry run, and a passed
18
+ // matter's act (which never passes through a route) are never refused.
19
+ //
20
+ // DB-free: the government table read and every write are stubbed by property.
21
+
22
+ import { strict as assert } from 'node:assert';
23
+ import { test, after } from 'node:test';
24
+ import http from 'node:http';
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { createRequire } from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const GOV = path.join(ROOT, 'modules', 'government');
33
+
34
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
35
+ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
36
+ const authMod = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
37
+ const { requireActAlone } = require(path.join(ROOT, 'src', 'bongos', 'act-alone.js'));
38
+ const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
39
+ const dr = require(path.join(GOV, 'decision-rules.js'));
40
+ const board = require(path.join(GOV, 'board.js'));
41
+ const govDb = require(path.join(GOV, 'db.js'));
42
+ const express = require('express');
43
+
44
+ async function stub(obj, stubs, fn) {
45
+ const originals = {};
46
+ for (const [k, v] of Object.entries(stubs)) { originals[k] = obj[k]; obj[k] = v; }
47
+ try { return await fn(); } finally { for (const [k, v] of Object.entries(originals)) obj[k] = v; }
48
+ }
49
+
50
+ // ── 1. the gate itself ──────────────────────────────────────────────────────
51
+
52
+ function fakeRes() {
53
+ const res = { failed: null };
54
+ res.fail = (code, a, b) => { res.failed = { code, a, b }; return res; };
55
+ return res;
56
+ }
57
+ async function runGate(gate, req = { builder: { id: 42 }, body: {} }) {
58
+ const res = fakeRes();
59
+ let passed = false;
60
+ await gate(req, res, () => { passed = true; });
61
+ return { passed, failed: res.failed };
62
+ }
63
+ async function withPort(port, fn) {
64
+ seams._reset();
65
+ if (port) seams.registerProvider('government', port);
66
+ try { return await fn(); } finally { seams._reset(); registerMainPort(); }
67
+ }
68
+ // The port the routers below are driven through (section 3); restored after
69
+ // each gate test swaps in its own.
70
+ const ATOMS = [
71
+ 'autonomy.fence.manage', 'criterion.satisfy', 'criterion.unsatisfy', 'gate_approval.decide', 'override_request.decide',
72
+ 'core.pin.move', 'builder.budget.set', 'goal.create', 'goal.reorder', 'version.focus.set', 'project.settings.manage',
73
+ 'security.report.adjudicate', 'access_request.review', 'government.manage',
74
+ ];
75
+ function registerMainPort() {
76
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => ATOMS, checkActAlone: board.checkActAlone });
77
+ }
78
+
79
+ test('the gate: no government module, or a port without the check, is as today', async () => {
80
+ await withPort(null, async () => assert.equal((await runGate(requireActAlone('release'))).passed, true));
81
+ await withPort({ resolveBuilderPermissions: async () => [] }, async () => assert.equal((await runGate(requireActAlone('release'))).passed, true));
82
+ });
83
+
84
+ test('the gate: as today passes, asking for the named building and the live builder', async () => {
85
+ const asked = [];
86
+ await withPort({ checkActAlone: async (area, id) => { asked.push([area, id]); return { ok: true }; } }, async () => {
87
+ assert.equal((await runGate(requireActAlone('treasury'))).passed, true);
88
+ });
89
+ assert.deepEqual(asked, [['treasury', 42]]);
90
+ });
91
+
92
+ test('the gate: a sitting refuses in People\'s exact shape, naming the matter route', async () => {
93
+ const refusal = { ok: false, reason: 'decided_by_sitting', area: 'review', rule: 'majority', who: 'the council', what_this_means: 'Review is decided by the council (majority vote). Put it to them as a matter instead.', open_with: 'POST /government/board/matters' };
94
+ await withPort({ checkActAlone: async () => refusal }, async () => {
95
+ const out = await runGate(requireActAlone('review'));
96
+ assert.equal(out.passed, false);
97
+ assert.deepEqual(out.failed, { code: 'decided_by_sitting', a: 409, b: { area: 'review', rule: 'majority', who: 'the council', what_this_means: refusal.what_this_means, open_with: 'POST /government/board/matters' } });
98
+ });
99
+ });
100
+
101
+ test('the gate: an unreadable table, or an answer it does not know, refuses 503 — fail-closed like People', async () => {
102
+ await withPort({ checkActAlone: async () => { throw new Error('db down'); } }, async () => {
103
+ const out = await runGate(requireActAlone('rules'));
104
+ assert.equal(out.passed, false);
105
+ assert.equal(out.failed.code, 'decision_rule_unreadable');
106
+ assert.equal(out.failed.a.status, 503);
107
+ });
108
+ await withPort({ checkActAlone: async () => ({ ok: false, reason: 'unknown_area' }) }, async () => {
109
+ const out = await runGate(requireActAlone('nowhere'));
110
+ assert.equal(out.passed, false);
111
+ assert.equal(out.failed.code, 'decision_rule_unreadable');
112
+ });
113
+ });
114
+
115
+ test('the gate: `unless` lets a non-decision through without asking; a bad call is refused at mount', async () => {
116
+ let asked = 0;
117
+ await withPort({ checkActAlone: async () => { asked += 1; return { ok: false, reason: 'decided_by_sitting' }; } }, async () => {
118
+ const out = await runGate(requireActAlone('automation', { unless: (req) => req.body.enabled === false }), { builder: { id: 1 }, body: { enabled: false } });
119
+ assert.equal(out.passed, true);
120
+ });
121
+ assert.equal(asked, 0);
122
+ assert.throws(() => requireActAlone(''), TypeError);
123
+ assert.throws(() => requireActAlone('rules', { unless: true }), TypeError);
124
+ assert.equal(api.requireActAlone, requireActAlone, 'modules reach it through the doorway');
125
+ });
126
+
127
+ // ── 2. the record matches the source, both ways ─────────────────────────────
128
+
129
+ function routeFiles() {
130
+ const out = [];
131
+ for (const dir of [path.join(ROOT, 'src', 'bongos', 'routes'), ...fs.readdirSync(path.join(ROOT, 'modules')).map((m) => path.join(ROOT, 'modules', m, 'routes'))]) {
132
+ if (!fs.existsSync(dir)) continue;
133
+ for (const f of fs.readdirSync(dir)) if (f.endsWith('.js')) out.push(path.join(dir, f));
134
+ }
135
+ return out;
136
+ }
137
+ const SOURCES = routeFiles().map((f) => ({ file: path.relative(ROOT, f), src: fs.readFileSync(f, 'utf8') }));
138
+ const REG_RE = /router\.(get|post|put|patch|delete)\(\s*'([^']+)'/g;
139
+
140
+ // The middleware text of one registration: from `router.x('path'` to its handler.
141
+ function registration(act) {
142
+ const [method, p] = act.split(' ');
143
+ const hits = [];
144
+ for (const { file, src } of SOURCES) {
145
+ for (const m of src.matchAll(REG_RE)) {
146
+ if (m[1].toUpperCase() !== method || m[2] !== p) continue;
147
+ const rest = src.slice(m.index);
148
+ const end = Math.min(...['(req, res', 'Handler('].map((s) => { const i = rest.indexOf(s); return i === -1 ? Infinity : i; }));
149
+ hits.push({ file, head: rest.slice(0, end), body: rest.slice(0, 3000) });
150
+ }
151
+ }
152
+ return hits;
153
+ }
154
+
155
+ test('the record: every building but the charter has direct routes; the charter says why it has none', () => {
156
+ for (const a of dr.AREAS) {
157
+ if (a.key === 'charter') {
158
+ assert.equal(a.acts.length, 0);
159
+ assert.match(a.no_direct_route, /amend/);
160
+ assert.equal(dr.resolveAll([]).find((r) => r.area === 'charter').no_direct_route, a.no_direct_route, 'the record rides the table view');
161
+ } else {
162
+ assert.ok(a.acts.length > 0, `${a.key} lists its direct routes`);
163
+ }
164
+ }
165
+ });
166
+
167
+ test('the record → the source: every listed route mounts its own building\'s gate', () => {
168
+ for (const a of dr.AREAS) {
169
+ for (const act of a.acts) {
170
+ const hits = registration(act);
171
+ assert.equal(hits.length, 1, `${act} is registered exactly once (${hits.map((h) => h.file)})`);
172
+ const { head, body } = hits[0];
173
+ const mounted = head.includes(`requireActAlone('${a.key}'`);
174
+ const inModule = a.key === 'people' && /peopleMayActAlone\(req, res\)/.test(body);
175
+ assert.ok(mounted || inModule, `${act} (${hits[0].file}) defers to the ${a.key} row`);
176
+ if (mounted) assert.ok(head.indexOf('requireActAlone') > head.search(/requirePermission|requireOwnProjectOrPin/), `${act}: the permission gate stays first`);
177
+ }
178
+ }
179
+ });
180
+
181
+ test('the source → the record: every mounted gate is listed under the building it names', () => {
182
+ let seen = 0;
183
+ for (const { file, src } of SOURCES) {
184
+ for (const m of src.matchAll(/requireActAlone\('([a-z]+)'/g)) {
185
+ const before = [...src.slice(0, m.index).matchAll(REG_RE)].pop();
186
+ assert.ok(before, `${file}: requireActAlone outside a route registration`);
187
+ const act = `${before[1].toUpperCase()} ${before[2]}`;
188
+ const area = dr.areaByKey(m[1]);
189
+ assert.ok(area, `${file}: requireActAlone('${m[1]}') names no building`);
190
+ assert.ok(area.acts.includes(act), `${act} mounts the ${m[1]} gate but is not in decision-rules AREAS.${m[1]}.acts`);
191
+ seen += 1;
192
+ }
193
+ }
194
+ const listed = dr.AREAS.reduce((n, a) => n + a.acts.length, 0) - 2; // People's two assignments ask in-module
195
+ assert.equal(seen, listed, 'one mounted gate per listed route');
196
+ });
197
+
198
+ // ── 3. every building, through its real router ──────────────────────────────
199
+
200
+ const fakeBuilder = (req, res, next) => { req.builder = { id: 42, rank: 'archon', github_login: 'someone' }; next(); };
201
+ const realApiRequireBuilder = api.requireBuilder;
202
+ const realAuthRequireBuilder = authMod.requireBuilder;
203
+ api.requireBuilder = fakeBuilder;
204
+ authMod.requireBuilder = fakeBuilder;
205
+ seams._reset();
206
+ registerMainPort();
207
+
208
+ const app = express();
209
+ app.use(express.json());
210
+ app.use(attachFail);
211
+ for (const rel of [
212
+ 'modules/autonomy/routes/autonomy.js',
213
+ 'modules/lifecycle/routes/done-when.js',
214
+ 'modules/lifecycle/routes/gate-approvals.js',
215
+ 'modules/lifecycle/routes/tasks.js',
216
+ 'modules/lifecycle/routes/goals.js',
217
+ 'modules/lifecycle/routes/versions.js',
218
+ 'modules/lifecycle/routes/project-settings.js',
219
+ 'modules/render-deploy/routes/act.js',
220
+ 'modules/npm-release/routes/release.js',
221
+ 'modules/provisioning/routes/core-upgrade.js',
222
+ 'src/bongos/routes/builders.js',
223
+ 'modules/security/routes/reports.js',
224
+ 'modules/onboarding/routes/access-requests.js',
225
+ 'modules/government/routes/government.js',
226
+ ]) app.use(require(path.join(ROOT, rel))());
227
+ const server = http.createServer(app);
228
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
229
+ const BASE = `http://127.0.0.1:${server.address().port}`;
230
+ after(() => { server.close(); api.requireBuilder = realApiRequireBuilder; authMod.requireBuilder = realAuthRequireBuilder; seams._reset(); });
231
+
232
+ async function call(method, p, body) {
233
+ const res = await fetch(`${BASE}${p}`, { method, headers: body === undefined ? {} : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
234
+ let json = null; try { json = await res.json(); } catch { /* none */ }
235
+ return { status: res.status, json };
236
+ }
237
+
238
+ // Each listed route as a request that, AS TODAY, stops in its handler's own
239
+ // first check (a bad id or body → 4xx) before any database write — so reaching
240
+ // the handler is visible and nothing is written either way.
241
+ const BAD = 'x';
242
+ const REQUESTS = {
243
+ 'PATCH /project-settings/rot-days': ['PATCH', '/project-settings/rot-days', { days: 'soon' }],
244
+ 'PATCH /project-settings/ship-mode': ['PATCH', '/project-settings/ship-mode', { mode: 'sometimes' }],
245
+ 'PATCH /work-categories/:id': ['PATCH', `/work-categories/${BAD}`, {}],
246
+ 'POST /security/reports/:id/confirm': ['POST', `/security/reports/${BAD}/confirm`, {}],
247
+ 'POST /security/reports/:id/dispute': ['POST', `/security/reports/${BAD}/dispute`, {}],
248
+ 'POST /security/reports/:id/fix': ['POST', `/security/reports/${BAD}/fix`, {}],
249
+ // People's own two ask after their validation (task 1004529), so they carry a
250
+ // valid custom rank; the writes are stubbed (PEOPLE_DB) and counted.
251
+ 'POST /government/assignments': ['POST', '/government/assignments', { builder_id: 7, rank_key: 'tester' }],
252
+ 'DELETE /government/assignments/:builderId/:rankKey': ['DELETE', '/government/assignments/7/tester'],
253
+ 'PATCH /access-requests/:id': ['PATCH', `/access-requests/${BAD}`, { status: 'invited' }],
254
+ 'POST /access-requests/invite': ['POST', '/access-requests/invite', {}],
255
+ 'POST /goals': ['POST', '/goals', {}],
256
+ 'POST /goals/reorder': ['POST', '/goals/reorder', {}],
257
+ 'POST /work-categories': ['POST', '/work-categories', {}],
258
+ 'PUT /versions/focus': ['PUT', '/versions/focus', { version_id: ' ' }],
259
+ 'POST /autonomy/fence': ['POST', '/autonomy/fence', { enabled: 'yes' }],
260
+ 'POST /autonomy/fence/goals': ['POST', '/autonomy/fence/goals', { goal_id: 0 }],
261
+ 'DELETE /autonomy/fence/goals/:goalId': ['DELETE', `/autonomy/fence/goals/${BAD}`],
262
+ 'POST /autonomy/fence/priority': ['POST', '/autonomy/fence/priority', {}],
263
+ 'POST /autonomy/builders/:builderId/pause': ['POST', `/autonomy/builders/${BAD}/pause`, { paused: false }],
264
+ 'POST /done-when/:criterionId/satisfy': ['POST', `/done-when/${BAD}/satisfy`, {}],
265
+ 'POST /done-when/:criterionId/unsatisfy': ['POST', `/done-when/${BAD}/unsatisfy`, {}],
266
+ 'POST /gate-approvals/:pr/approve': ['POST', `/gate-approvals/${BAD}/approve`, {}],
267
+ 'POST /override-requests/:id/decide': ['POST', `/override-requests/${BAD}/decide`, { decision: 'approve' }],
268
+ 'POST /provisioning/instances/:id/core-upgrade': ['POST', `/provisioning/instances/${BAD}/core-upgrade`, { to: '1.2.3' }],
269
+ 'POST /provisioning/instances/:id/update-channel': ['POST', `/provisioning/instances/${BAD}/update-channel`, { channel: 'nightly' }],
270
+ 'POST /render-deploy/act': ['POST', '/render-deploy/act', { action: 'launch' }],
271
+ 'POST /npm-release/release': ['POST', '/npm-release/release', { version: 'v-next' }],
272
+ 'PATCH /builders/:id/budget': ['PATCH', `/builders/${BAD}/budget`, { monthly_budget_usd: 5 }],
273
+ };
274
+
275
+ test('every listed route has a request in this test (a new act cannot go unexercised)', () => {
276
+ const listed = dr.AREAS.flatMap((a) => a.acts).sort();
277
+ assert.deepEqual(Object.keys(REQUESTS).sort(), listed);
278
+ });
279
+
280
+ const sitting = (area, rule = 'majority') => ({ area, decided_by: 'rank:council', rule, who: null, set_by_item_id: '30' });
281
+
282
+ let peopleWrites = 0;
283
+ const PEOPLE_DB = {
284
+ getRankByKey: async (k) => ({ rank_key: k, is_system: false, seeded_rank: null }),
285
+ assignRank: async () => { peopleWrites += 1; return true; },
286
+ unassignRank: async () => { peopleWrites += 1; return true; },
287
+ };
288
+
289
+ for (const a of dr.AREAS.filter((x) => x.acts.length)) {
290
+ test(`${a.name}: while a vote decides it, every direct route refuses one builder acting alone; as today each reaches its handler`, async () => {
291
+ peopleWrites = 0;
292
+ // A sitting at THIS building only: the refusal is the building's own row.
293
+ await stub(govDb, { ...PEOPLE_DB, listDecisionRules: async () => [sitting(a.key)], builderHoldsRankActive: async () => true }, async () => {
294
+ for (const act of a.acts) {
295
+ const [method, p, body] = REQUESTS[act];
296
+ const r = await call(method, p, body);
297
+ assert.equal(r.status, 409, `${act} under a vote → 409 (got ${r.status} ${JSON.stringify(r.json)})`);
298
+ assert.equal(r.json.error.code, 'decided_by_sitting', act);
299
+ const d = r.json.error.details || r.json.error;
300
+ assert.equal(d.area, a.key, `${act} names its own building`);
301
+ assert.equal(d.open_with, 'POST /government/board/matters', `${act} points at the sitting`);
302
+ }
303
+ });
304
+ assert.equal(peopleWrites, 0, 'nothing was written under the vote');
305
+ // Another building's sitting says nothing here, and with no row at all the
306
+ // route is exactly today's: its own handler answers (its own first check,
307
+ // or — for People's stubbed assignments — the write itself).
308
+ for (const rows of [[], [sitting(a.key === 'rules' ? 'planning' : 'rules')]]) {
309
+ await stub(govDb, { ...PEOPLE_DB, listDecisionRules: async () => rows, builderHoldsRankActive: async () => true }, async () => {
310
+ for (const act of a.acts) {
311
+ const [method, p, body] = REQUESTS[act];
312
+ const r = await call(method, p, body);
313
+ assert.notEqual(r.json && r.json.error && r.json.error.code, 'decided_by_sitting', `${act} as today`);
314
+ const ok = act.includes('/government/assignments') ? r.status < 300 : (r.status >= 400 && r.status < 500 && r.status !== 403);
315
+ assert.ok(ok, `${act} as today reaches its handler (got ${r.status} ${JSON.stringify(r.json)})`);
316
+ }
317
+ });
318
+ }
319
+ });
320
+ }
321
+
322
+ test('decide alone: a member the row seats may still act alone; one it does not seat may not', async () => {
323
+ const [method, p, body] = REQUESTS['PATCH /builders/:id/budget'];
324
+ await stub(govDb, { listDecisionRules: async () => [sitting('treasury', 'decide')], builderHoldsRankActive: async () => true }, async () => {
325
+ assert.equal((await call(method, p, body)).status, 400, 'seated: through to the handler (bad id)');
326
+ });
327
+ await stub(govDb, { listDecisionRules: async () => [sitting('treasury', 'decide')], builderHoldsRankActive: async () => false }, async () => {
328
+ assert.equal((await call(method, p, body)).status, 409, 'not seated: refused');
329
+ });
330
+ });
331
+
332
+ test('the check is cheap on the hot path: one table read, and no config-file read for any building\'s route', async () => {
333
+ let reads = 0;
334
+ let queries = 0;
335
+ const realRead = fs.readFileSync;
336
+ fs.readFileSync = (...args) => { reads += 1; return realRead(...args); };
337
+ try {
338
+ await stub(govDb, { listDecisionRules: async () => { queries += 1; return [sitting('review')]; }, builderHoldsRankActive: async () => true }, async () => {
339
+ for (const area of dr.AREA_KEYS.filter((k) => k !== 'charter')) await board.checkActAlone(area, '42');
340
+ });
341
+ } finally {
342
+ fs.readFileSync = realRead;
343
+ }
344
+ assert.equal(reads, 0, 'no synchronous file read per request (config.js is boot-only)');
345
+ assert.equal(queries, dr.AREA_KEYS.length - 1, 'one table read per check');
346
+ await stub(govDb, { listDecisionRules: async () => [sitting('review')] }, async () => {
347
+ assert.equal((await board.checkActAlone('review', '42')).ok, false, 'and the answer is unchanged');
348
+ assert.equal((await board.checkActAlone('rules', '42')).ok, true);
349
+ assert.equal((await board.checkActAlone('nowhere', '42')).reason, 'unknown_area');
350
+ });
351
+ });
352
+
353
+ test('an unreadable table refuses a building\'s route 503 — never waved through', async () => {
354
+ await stub(govDb, { listDecisionRules: async () => { throw new Error('db down'); } }, async () => {
355
+ const [method, p, body] = REQUESTS['POST /goals'];
356
+ const r = await call(method, p, body);
357
+ assert.equal(r.status, 503);
358
+ assert.equal(r.json.error.code, 'decision_rule_unreadable');
359
+ });
360
+ });
361
+
362
+ // ── 4. not a lone act ───────────────────────────────────────────────────────
363
+
364
+ const autoDb = require(path.join(ROOT, 'modules', 'autonomy', 'db.js'));
365
+
366
+ test('the brake is never refused: the fence switched OFF and one runner paused, while a vote decides Automation', async () => {
367
+ const wrote = [];
368
+ await stub(autoDb, {
369
+ setFenceEnabled: async (a) => { wrote.push(['fence', a.enabled]); return { enabled: a.enabled }; },
370
+ setBuilderPause: async (a) => { wrote.push(['pause', a.paused]); return { builder_id: a.builderId, paused: a.paused }; },
371
+ }, async () => {
372
+ await stub(govDb, { listDecisionRules: async () => [sitting('automation', 'unanimous')], builderHoldsRankActive: async () => true }, async () => {
373
+ assert.equal((await call('POST', '/autonomy/fence', { enabled: false, reason: 'stop' })).status, 200);
374
+ assert.equal((await call('POST', '/autonomy/builders/7/pause', { paused: true })).status, 200);
375
+ assert.equal((await call('POST', '/autonomy/fence', { enabled: true })).status, 409, 'switching it ON is a decision');
376
+ assert.equal((await call('POST', '/autonomy/builders/7/pause', { paused: false })).status, 409, 'resuming is a decision');
377
+ });
378
+ });
379
+ assert.deepEqual(wrote, [['fence', false], ['pause', true]]);
380
+ });
381
+
382
+ test('a dry run of the pin move only reads, so it is not refused under a vote', async () => {
383
+ await stub(govDb, { listDecisionRules: async () => [sitting('release')], builderHoldsRankActive: async () => true }, async () => {
384
+ const r = await call('POST', `/provisioning/instances/${BAD}/core-upgrade`, { to: '1.2.3', dry_run: true });
385
+ assert.notEqual(r.json && r.json.error && r.json.error.code, 'decided_by_sitting');
386
+ assert.equal((await call('POST', `/provisioning/instances/${BAD}/core-upgrade`, { to: '1.2.3' })).status, 409, 'the real move is');
387
+ });
388
+ });
389
+
390
+ test('a PASSED matter\'s act is the sitting itself: it runs while the gate would refuse every lone act', async () => {
391
+ const decisionBoard = require(path.join(GOV, 'decision-board.js'));
392
+ require(path.join(ROOT, 'modules', 'autonomy', 'matter-acts.js')).contributeAutomationMatterActs();
393
+ const wrote = [];
394
+ await stub(autoDb, { setFenceEnabled: async (a) => { wrote.push(a.enabled); return { enabled: a.enabled }; } }, async () => {
395
+ await stub(govDb, {
396
+ listDecisionRules: async () => dr.AREA_KEYS.filter((k) => k !== 'charter').map((k) => sitting(k, 'unanimous')),
397
+ builderHoldsRankActive: async () => true,
398
+ getMatter: async () => ({ id: '8', area: 'automation', title: 'Switch it on', action: { type: 'switch_on' }, proposed_by: '7' }),
399
+ }, async () => {
400
+ // the direct route is refused …
401
+ assert.equal((await call('POST', '/autonomy/fence', { enabled: true })).status, 409);
402
+ // … and the passed matter still acts, through the owning module's act
403
+ const out = await decisionBoard.actOnPassedMatter({ id: '40', subject_type: 'automation_matter', subject_id: '8', outcome: 'passed' });
404
+ assert.equal(out.result.done, true, JSON.stringify(out.result));
405
+ });
406
+ });
407
+ assert.deepEqual(wrote, [true], 'only the passed matter switched it');
408
+ });
409
+
410
+ test('a PASSED Treasury matter sets the limit (core\'s act) while the budget route refuses a lone act', async () => {
411
+ const decisionBoard = require(path.join(GOV, 'decision-board.js'));
412
+ require(path.join(ROOT, 'src', 'bongos', 'routes', 'builders.js'))(); // contributes the Treasury act, as at boot
413
+ const set = [];
414
+ seams.registerProvider('reward', { setBuilderMonthlyBudget: async (id, usd) => { set.push([id, usd]); return { id, monthly_budget_usd: usd }; } });
415
+ try {
416
+ await stub(govDb, {
417
+ listDecisionRules: async () => [sitting('treasury', 'unanimous')],
418
+ builderHoldsRankActive: async () => true,
419
+ getMatter: async () => ({ id: '9', area: 'treasury', title: 'Lu gets $50 a month', action: { type: 'set_budget', builder_id: 7, monthly_usd: 50 }, proposed_by: '7' }),
420
+ }, async () => {
421
+ assert.equal((await call('PATCH', '/builders/7/budget', { monthly_budget_usd: 50 })).status, 409, 'the lone act is refused');
422
+ const out = await decisionBoard.actOnPassedMatter({ id: '41', subject_type: 'treasury_matter', subject_id: '9', outcome: 'passed' });
423
+ assert.equal(out.result.done, true, JSON.stringify(out.result));
424
+ });
425
+ } finally {
426
+ seams._reset();
427
+ registerMainPort();
428
+ }
429
+ assert.deepEqual(set, [[7, 50]], 'only the passed matter wrote the limit');
430
+ });
@@ -22,6 +22,7 @@ const PUBLISHED_SURFACE = [
22
22
  'CORE_VERSION',
23
23
  'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
24
24
  'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
25
+ 'requireActAlone', // added by task 1004657 (ADR 0363 D5): a building's own page asks its decision-rules row before one builder acts alone. On the doorway because eight buildings' pages live in six modules and core, and each must refuse the same way People does
25
26
  'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
26
27
  // added by task 1003575 (ADR 0241 §3): the instance's PROJECT OWNER — its first
27
28
  // admitted human builder. Kernel identity like getBuilderById beside it, and
@@ -180,7 +180,8 @@ test('the route is gated like the deploy page (core.pin.move), never by rank', (
180
180
  const router = buildRouter();
181
181
  const layer = router.stack.find((l) => l.route && l.route.path === '/npm-release/release');
182
182
  assert.ok(layer.route.methods.post);
183
- assert.equal(layer.route.stack.length, 3, 'requireBuilder, the permission gate, then the handler');
183
+ assert.equal(layer.route.stack.length, 4, 'requireBuilder, the permission gate, Release\'s decision rule (task 1004657), then the handler');
184
+ assert.equal(layer.route.stack[2].handle.name, 'actAloneGate');
184
185
  const src = fs.readFileSync(new URL('../modules/npm-release/routes/release.js', import.meta.url), 'utf8');
185
186
  assert.match(src, /api\.requireBuilder,\s*\n\s*api\.requirePermission\('core\.pin\.move'\)/);
186
187
  assert.doesNotMatch(src, /api\.requireRank\(/);
@@ -114,7 +114,8 @@ test('GET /project-settings/ship-mode is open to any signed-in builder, not gate
114
114
  const names = route('get', '/project-settings/ship-mode').stack.map((l) => l.name);
115
115
  assert.equal(names.length, 2, 'requireBuilder + the handler, and no permission gate');
116
116
  const write = route('patch', '/project-settings/ship-mode').stack;
117
- assert.equal(write.length, 3, 'the write carries requireBuilder + the settings permission + the handler');
117
+ assert.equal(write.length, 4, 'the write carries requireBuilder + the settings permission + Rules\' decision rule (task 1004657) + the handler');
118
+ assert.equal(write[2].handle.name, 'actAloneGate');
118
119
  } finally { restore(); }
119
120
  });
120
121