@bongos/core 1.24.71 → 1.24.73
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 +57 -37
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +36 -2
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +4 -4
- package/docs/copy-registry.json +4 -4
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +84 -84
- package/modules/government/board.js +19 -7
- package/modules/government/config.js +21 -9
- package/modules/government/constitution.js +224 -0
- package/modules/government/db.js +28 -0
- package/modules/government/migrations/government_037_constitution.sql +33 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/constitution-restore.js +45 -0
- package/modules/government/routes/government.js +7 -7
- package/modules/hall-ui/public/board-room.js +7 -3
- package/modules/hall-ui/public/street-card.js +2 -0
- package/modules/hall-ui/public/style.css +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/government-walk.js +7 -0
- package/src/bongos/route-rank-check.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/genesis_government.mjs +11 -0
- package/tests/government_board_amendment.mjs +10 -0
- package/tests/government_charter_forms.mjs +10 -0
- package/tests/government_constitution_durable.mjs +304 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_tokens.mjs +19 -0
|
@@ -76,13 +76,23 @@ async function underMonarchy(fn) {
|
|
|
76
76
|
closeBoardItem: async ({ outcome }) => ({ id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: FOUNDER, closed_at: 'T', outcome }),
|
|
77
77
|
insertAuthorRatifyKarma: async () => true,
|
|
78
78
|
getAmendment: async () => ({ id: '5', proposed: seen.filed.proposed }),
|
|
79
|
+
// The durable row (task 1005140) — written first, read back after.
|
|
80
|
+
saveConstitutionRow: async (args) => { seen.row = { board: args.board }; return seen.row; },
|
|
81
|
+
getConstitutionRow: async () => seen.row || null,
|
|
79
82
|
};
|
|
80
83
|
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
81
84
|
const originalApply = config.applyBoardAmendment;
|
|
82
85
|
config.applyBoardAmendment = (proposed) => { seen.applied.push(proposed); return proposed; };
|
|
86
|
+
// The read-back after the apply (task 1005140) sees what was applied.
|
|
87
|
+
const originalLoad = config.loadGovernmentConfig;
|
|
88
|
+
config.loadGovernmentConfig = (opts = {}) => {
|
|
89
|
+
const out = originalLoad(opts);
|
|
90
|
+
return seen.applied.length ? { ...out, board: config.sanitizeBoard(seen.applied[seen.applied.length - 1]) } : out;
|
|
91
|
+
};
|
|
83
92
|
try { await fn(seen); } finally {
|
|
84
93
|
for (const [k, v] of Object.entries(originals)) db[k] = v;
|
|
85
94
|
config.applyBoardAmendment = originalApply;
|
|
95
|
+
config.loadGovernmentConfig = originalLoad;
|
|
86
96
|
}
|
|
87
97
|
return seen;
|
|
88
98
|
}
|
|
@@ -111,6 +121,7 @@ for (const form of ['timocratic-republic', 'direct-democracy']) {
|
|
|
111
121
|
assert.equal((await board.maybeCloseAfterVote(out.charter.item_id)).outcome, 'passed');
|
|
112
122
|
});
|
|
113
123
|
assert.equal(seen.applied.length, 1, 'the ratified constitution is applied once');
|
|
124
|
+
assert.equal(charter.readCharter(seen.row.board).form, form, 'and recorded in the durable row');
|
|
114
125
|
assert.equal(charter.readCharter(seen.applied[0]).form, form, 'the constitution in force is the government the founder picked');
|
|
115
126
|
assert.match(seen.filed.rationaleMd, /^Charter: /, 'the record says in words what was chosen');
|
|
116
127
|
});
|
|
@@ -207,10 +207,14 @@ test('the monarchy act end to end: the sole Archon ratifies their OWN amendment
|
|
|
207
207
|
// applyBoardAmendment by property lookup at call time for exactly this.
|
|
208
208
|
const origApply = config.applyBoardAmendment;
|
|
209
209
|
config.applyBoardAmendment = (proposed) => origApply(proposed, { instancePath: tmp });
|
|
210
|
+
// ...and the read-back after it (task 1005140) reads the same temp file.
|
|
211
|
+
const origLoad = config.loadGovernmentConfig;
|
|
212
|
+
config.loadGovernmentConfig = (opts = {}) => origLoad({ ...opts, instancePath: tmp });
|
|
210
213
|
try {
|
|
211
214
|
const karma = [];
|
|
212
215
|
let votes = [];
|
|
213
216
|
let authorAsked = 0;
|
|
217
|
+
let savedRow = null;
|
|
214
218
|
await withDb({
|
|
215
219
|
getBoardItemForVote: async () => amendmentItem(),
|
|
216
220
|
builderHoldsRankActive: async (builderId, rankKeys) => builderId === '42' && rankKeys.includes('archon'),
|
|
@@ -228,6 +232,9 @@ test('the monarchy act end to end: the sole Archon ratifies their OWN amendment
|
|
|
228
232
|
// reached ONLY by the post-close ratify karma, never by the vote's
|
|
229
233
|
// anti-self (the carve-out) — the counter proves the order.
|
|
230
234
|
getBoardItemAuthor: async () => { authorAsked += 1; return '42'; },
|
|
235
|
+
// The durable row (task 1005140) — written first, read back after.
|
|
236
|
+
saveConstitutionRow: async (args) => { savedRow = { board: args.board, source: args.source }; return savedRow; },
|
|
237
|
+
getConstitutionRow: async () => savedRow,
|
|
231
238
|
}, async () => {
|
|
232
239
|
// The PROPOSER casts on their own amendment — legal (§7) — and it closes
|
|
233
240
|
// immediately under first_ratifier.
|
|
@@ -240,13 +247,16 @@ test('the monarchy act end to end: the sole Archon ratifies their OWN amendment
|
|
|
240
247
|
assert.ok(closed.applied_constitution, 'the amendment was applied on close');
|
|
241
248
|
assert.equal(closed.applied_constitution.pass_rule, 'consent', 'the project now runs consent');
|
|
242
249
|
assert.equal(closed.applied_constitution.window_minutes, 90);
|
|
250
|
+
assert.equal(closed.constitution_not_applied, undefined, 'read back: it is in force');
|
|
243
251
|
assert.ok(authorAsked >= 1, 'the ratify karma DID resolve the author, after the close');
|
|
244
252
|
});
|
|
245
253
|
assert.equal(karma.length, 1, 'the proposer earned the ratification karma (+3, same act as an idea ratify)');
|
|
254
|
+
assert.equal(savedRow && savedRow.board.pass_rule, 'consent', 'the durable row carries it too, so a restart cannot lose it');
|
|
246
255
|
const onDisk = JSON.parse(fs.readFileSync(tmp, 'utf8'));
|
|
247
256
|
assert.equal(onDisk.board.pass_rule, 'consent', 'the TEMP instance pack carries the new constitution');
|
|
248
257
|
} finally {
|
|
249
258
|
config.applyBoardAmendment = origApply;
|
|
259
|
+
config.loadGovernmentConfig = origLoad;
|
|
250
260
|
try { fs.unlinkSync(tmp); } catch (_) { /* not created */ }
|
|
251
261
|
}
|
|
252
262
|
});
|
|
@@ -200,7 +200,11 @@ test('the founder picks Direct Democracy and ratifies it: the amendment path wri
|
|
|
200
200
|
const originalApply = config.applyBoardAmendment;
|
|
201
201
|
const applied = [];
|
|
202
202
|
config.applyBoardAmendment = (proposed) => { applied.push(proposed); fs.writeFileSync(target, JSON.stringify({ board: proposed })); return proposed; };
|
|
203
|
+
// The read-back after the apply (task 1005140) reads the same temp file.
|
|
204
|
+
const originalLoad = config.loadGovernmentConfig;
|
|
205
|
+
config.loadGovernmentConfig = (opts = {}) => originalLoad({ ...opts, instancePath: target });
|
|
203
206
|
let filed = null;
|
|
207
|
+
let savedRow = null;
|
|
204
208
|
const votes = [];
|
|
205
209
|
try {
|
|
206
210
|
await withDb({
|
|
@@ -216,6 +220,9 @@ test('the founder picks Direct Democracy and ratifies it: the amendment path wri
|
|
|
216
220
|
closeBoardItem: async ({ outcome }) => ({ id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: '42', closed_at: 'T', outcome }),
|
|
217
221
|
insertAuthorRatifyKarma: async () => true,
|
|
218
222
|
getAmendment: async () => ({ id: '5', proposed: filed.proposed }),
|
|
223
|
+
// The durable row (task 1005140) — written first, read back after.
|
|
224
|
+
saveConstitutionRow: async (args) => { savedRow = { board: args.board }; return savedRow; },
|
|
225
|
+
getConstitutionRow: async () => savedRow,
|
|
219
226
|
}, async () => {
|
|
220
227
|
const out = await board.proposeCharter({ form: 'direct-democracy', proposedBy: '42' });
|
|
221
228
|
assert.equal(out.ok, true);
|
|
@@ -227,11 +234,14 @@ test('the founder picks Direct Democracy and ratifies it: the amendment path wri
|
|
|
227
234
|
await board.castVote({ itemId: '21', voterId: '42', direction: 'yes' });
|
|
228
235
|
const closed = await board.maybeCloseAfterVote('21');
|
|
229
236
|
assert.equal(closed.outcome, 'passed');
|
|
237
|
+
assert.equal(closed.constitution_not_applied, undefined, 'read back: it is in force');
|
|
230
238
|
});
|
|
231
239
|
assert.deepEqual(applied, [filed.proposed], 'the ratified text is exactly what was filed');
|
|
232
240
|
assert.equal(JSON.parse(fs.readFileSync(target, 'utf8')).board.membership, 'rank:xenos+');
|
|
241
|
+
assert.equal(savedRow && savedRow.board.membership, 'rank:xenos+', 'the durable row carries it too');
|
|
233
242
|
} finally {
|
|
234
243
|
config.applyBoardAmendment = originalApply;
|
|
244
|
+
config.loadGovernmentConfig = originalLoad;
|
|
235
245
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
236
246
|
}
|
|
237
247
|
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// tests/government_constitution_durable.mjs — a ratified constitution survives a
|
|
2
|
+
// restart (task 1005140).
|
|
3
|
+
//
|
|
4
|
+
// Found live on toast, 2026-11-07: the board ratified a Timocratic Republic, the
|
|
5
|
+
// amendment was written to <instance root>/config/government.json, and the next
|
|
6
|
+
// container start (deploy, restart, or an idle wake on a free plan) erased the
|
|
7
|
+
// file. The project went back to the day-one monarchy without an error.
|
|
8
|
+
//
|
|
9
|
+
// DB-free: the constitution row lives in an in-memory stand-in for db.js's two
|
|
10
|
+
// functions; the instance pack is a REAL temp file, and "a restart" is deleting
|
|
11
|
+
// it. The invariants:
|
|
12
|
+
//
|
|
13
|
+
// 1. A restart does not lose a ratified constitution — the row restores it.
|
|
14
|
+
// 2. A file with no row is ADOPTED into the row (nobody loses one on upgrade).
|
|
15
|
+
// 3. The row wins over a stale file.
|
|
16
|
+
// 4. The row is written BEFORE the file, and a write that did not take is
|
|
17
|
+
// REPORTED at the close (constitution_not_applied), never absorbed.
|
|
18
|
+
// 5. The operator's re-apply only ever re-applies a PASSED constitutional
|
|
19
|
+
// amendment — it cannot invent a block.
|
|
20
|
+
// 6. The restore runs at boot, immediately.
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
32
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
33
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
34
|
+
const constitution = require(path.join(GOV, 'constitution.js'));
|
|
35
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
36
|
+
|
|
37
|
+
const MONARCHY = Object.freeze({ membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true });
|
|
38
|
+
const TIMOCRACY = Object.freeze({ membership: 'rank:metic+', pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true });
|
|
39
|
+
const quiet = { log: () => {}, error: () => {} };
|
|
40
|
+
|
|
41
|
+
let seq = 0;
|
|
42
|
+
const tmpPack = () => path.join(os.tmpdir(), `gov-durable-${process.pid}-${Date.now()}-${seq++}`, 'config', 'government.json');
|
|
43
|
+
|
|
44
|
+
// One harness: an in-memory row, and the config's instance path redirected to
|
|
45
|
+
// a temp file for both the write and the read — what a real process does with
|
|
46
|
+
// its own INSTANCE_PATH.
|
|
47
|
+
async function withInstance(fn, { row = null, saveFails = false, getFails = null, history } = {}) {
|
|
48
|
+
const pack = tmpPack();
|
|
49
|
+
const store = { row, saves: [] };
|
|
50
|
+
const orig = {
|
|
51
|
+
get: db.getConstitutionRow, save: db.saveConstitutionRow,
|
|
52
|
+
apply: config.applyBoardAmendment, load: config.loadGovernmentConfig,
|
|
53
|
+
list: db.listAmendmentHistory,
|
|
54
|
+
};
|
|
55
|
+
if (history !== undefined) db.listAmendmentHistory = async () => history;
|
|
56
|
+
db.getConstitutionRow = async () => { if (getFails) throw getFails; return store.row; };
|
|
57
|
+
db.saveConstitutionRow = async (args) => {
|
|
58
|
+
if (saveFails) throw new Error('connection refused');
|
|
59
|
+
store.saves.push(args);
|
|
60
|
+
store.row = { board: args.board, source: args.source, amendment_item_id: args.itemId };
|
|
61
|
+
return store.row;
|
|
62
|
+
};
|
|
63
|
+
config.applyBoardAmendment = (proposed, opts = {}) => orig.apply(proposed, { instancePath: opts.instancePath || pack });
|
|
64
|
+
config.loadGovernmentConfig = (opts = {}) => orig.load({ ...opts, instancePath: opts.instancePath || pack });
|
|
65
|
+
try { return await fn({ pack, store, live: () => orig.load({ instancePath: pack }).board }); }
|
|
66
|
+
finally {
|
|
67
|
+
db.getConstitutionRow = orig.get; db.saveConstitutionRow = orig.save; db.listAmendmentHistory = orig.list;
|
|
68
|
+
config.applyBoardAmendment = orig.apply; config.loadGovernmentConfig = orig.load;
|
|
69
|
+
fs.rmSync(path.dirname(path.dirname(pack)), { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const restart = (pack) => fs.rmSync(pack, { force: true });
|
|
74
|
+
|
|
75
|
+
// ── 1. the bug itself ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
test('a ratified constitution survives a container restart (the toast bug)', async () => {
|
|
78
|
+
await withInstance(async ({ pack, store, live }) => {
|
|
79
|
+
const out = await constitution.recordRatified(TIMOCRACY, { itemId: '7' });
|
|
80
|
+
assert.equal(out.failure, null, `the ratified amendment took: ${JSON.stringify(out.failure)}`);
|
|
81
|
+
assert.deepEqual({ ...live() }, TIMOCRACY, 'in force right after the close');
|
|
82
|
+
assert.deepEqual(store.row.board, TIMOCRACY, 'the durable row holds it');
|
|
83
|
+
assert.equal(store.row.source, 'amendment');
|
|
84
|
+
|
|
85
|
+
restart(pack); // the container's filesystem is wiped
|
|
86
|
+
assert.deepEqual({ ...live() }, MONARCHY, 'fixture: with the file gone the process alone falls back to monarchy');
|
|
87
|
+
|
|
88
|
+
const r = await constitution.reconcile({ instancePath: pack, ...quiet });
|
|
89
|
+
assert.equal(r.action, 'restored');
|
|
90
|
+
assert.deepEqual({ ...live() }, TIMOCRACY, 'after the boot restore the board\'s decision is in force again');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── 2 + 3. adopt on first read; the row wins ─────────────────────────────────
|
|
95
|
+
|
|
96
|
+
test('a file with no row is adopted into the database and the log says so', async () => {
|
|
97
|
+
await withInstance(async ({ pack, store }) => {
|
|
98
|
+
fs.mkdirSync(path.dirname(pack), { recursive: true });
|
|
99
|
+
fs.writeFileSync(pack, JSON.stringify({ ranks: { metic: { label: 'Trusted' } }, board: TIMOCRACY }));
|
|
100
|
+
const lines = [];
|
|
101
|
+
const r = await constitution.reconcile({ instancePath: pack, log: (m) => lines.push(m), error: () => {} });
|
|
102
|
+
assert.equal(r.action, 'adopted');
|
|
103
|
+
assert.deepEqual(store.row.board, TIMOCRACY);
|
|
104
|
+
assert.equal(store.row.source, 'adopted_file');
|
|
105
|
+
assert.ok(lines.some((l) => /adopted/.test(l)), 'the adoption is logged');
|
|
106
|
+
const again = await constitution.reconcile({ instancePath: pack, ...quiet });
|
|
107
|
+
assert.equal(again.action, 'in_step', 'a second pass changes nothing');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('no row and no file is the vanilla instance: nothing is written', async () => {
|
|
112
|
+
await withInstance(async ({ pack, store }) => {
|
|
113
|
+
const r = await constitution.reconcile({ instancePath: pack, ...quiet });
|
|
114
|
+
assert.equal(r.action, 'none');
|
|
115
|
+
assert.equal(store.saves.length, 0);
|
|
116
|
+
assert.equal(fs.existsSync(pack), false, 'no file is invented for a vanilla instance');
|
|
117
|
+
}, { history: [] });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('the toast case: no row, no file, a passed amendment — the first boot puts it back in force (part C)', async () => {
|
|
121
|
+
const history = [
|
|
122
|
+
{ item_id: 30, amendment_id: 9, outcome: 'returned', proposed: MONARCHY, kind: 'constitution' },
|
|
123
|
+
{ item_id: 21, amendment_id: 1, outcome: 'passed', proposed: TIMOCRACY, kind: 'constitution' },
|
|
124
|
+
];
|
|
125
|
+
await withInstance(async ({ pack, store, live }) => {
|
|
126
|
+
assert.deepEqual({ ...live() }, MONARCHY, 'fixture: the erased instance answers the day-one monarchy');
|
|
127
|
+
const lines = [];
|
|
128
|
+
const r = await constitution.reconcile({ instancePath: pack, log: (m) => lines.push(m), error: () => {} });
|
|
129
|
+
assert.equal(r.action, 'recovered', JSON.stringify(r));
|
|
130
|
+
assert.equal(r.item_id, '21', 'the PASSED amendment, never the newer returned one');
|
|
131
|
+
assert.deepEqual({ ...live() }, TIMOCRACY, 'the board decision is in force again');
|
|
132
|
+
assert.deepEqual(store.row.board, TIMOCRACY, 'and durable, so the next restart keeps it');
|
|
133
|
+
assert.ok(lines.some((l) => /put back passed amendment 1/.test(l)), 'the log says so');
|
|
134
|
+
assert.equal((await constitution.reconcile({ instancePath: pack, ...quiet })).action, 'in_step');
|
|
135
|
+
}, { history });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('the row wins over a stale file, and the file keeps its other keys', async () => {
|
|
139
|
+
await withInstance(async ({ pack, live }) => {
|
|
140
|
+
fs.mkdirSync(path.dirname(pack), { recursive: true });
|
|
141
|
+
fs.writeFileSync(pack, JSON.stringify({ ranks: { metic: { label: 'Trusted' } }, board: MONARCHY }));
|
|
142
|
+
const r = await constitution.reconcile({ instancePath: pack, ...quiet });
|
|
143
|
+
assert.equal(r.action, 'restored');
|
|
144
|
+
assert.deepEqual({ ...live() }, TIMOCRACY);
|
|
145
|
+
assert.equal(JSON.parse(fs.readFileSync(pack, 'utf8')).ranks.metic.label, 'Trusted', 'rank templates survive the restore');
|
|
146
|
+
}, { row: { board: TIMOCRACY, source: 'amendment' } });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('an unmigrated database (no table) and an unreadable one never throw and never touch the file', async () => {
|
|
150
|
+
const missing = Object.assign(new Error('relation "government_constitution" does not exist'), { code: '42P01' });
|
|
151
|
+
await withInstance(async ({ pack }) => {
|
|
152
|
+
assert.equal((await constitution.reconcile({ instancePath: pack, ...quiet })).action, 'no_table');
|
|
153
|
+
assert.equal(fs.existsSync(pack), false);
|
|
154
|
+
}, { getFails: missing });
|
|
155
|
+
await withInstance(async ({ pack }) => {
|
|
156
|
+
assert.equal((await constitution.reconcile({ instancePath: pack, ...quiet })).action, 'read_failed');
|
|
157
|
+
}, { getFails: new Error('timeout') });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── 4. row first, and a write that did not take is reported ──────────────────
|
|
161
|
+
|
|
162
|
+
test('the row is written before the file: a failed save leaves the old rule and SAYS so', async () => {
|
|
163
|
+
await withInstance(async ({ pack, live }) => {
|
|
164
|
+
const out = await constitution.recordRatified(TIMOCRACY, { itemId: '7' });
|
|
165
|
+
assert.equal(out.applied, null);
|
|
166
|
+
assert.ok(out.failure, 'reported, not absorbed');
|
|
167
|
+
assert.equal(out.failure.problems[0].where, 'database');
|
|
168
|
+
assert.equal(fs.existsSync(pack), false, 'nothing was put in force that would vanish at the next restart');
|
|
169
|
+
assert.equal(out.failure.problems[0].reason, 'save_failed');
|
|
170
|
+
assert.doesNotMatch(JSON.stringify(out.failure), /connection refused/, 'a voter sees a reason word, never the driver message');
|
|
171
|
+
assert.deepEqual({ ...live() }, MONARCHY);
|
|
172
|
+
}, { saveFails: true });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('a file write that silently did not take is caught by the read-back', async () => {
|
|
176
|
+
await withInstance(async ({ store }) => {
|
|
177
|
+
config.applyBoardAmendment = (proposed) => ({ ...proposed }); // claims success, writes nothing
|
|
178
|
+
const out = await constitution.recordRatified(TIMOCRACY, { itemId: '7' });
|
|
179
|
+
assert.ok(out.failure, 'the verify caught it');
|
|
180
|
+
assert.deepEqual(out.failure.problems.map((p) => p.where), ['in_force']);
|
|
181
|
+
assert.deepEqual(out.failure.problems[0].fields, ['membership', 'pass_rule']);
|
|
182
|
+
assert.deepEqual(store.row.board, TIMOCRACY, 'the durable row still holds the decision, so a restore can finish the job');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('closeItem hands a constitution that did not take back to the caller (and the vote route forwards it)', async () => {
|
|
187
|
+
const item = { id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: '42', closed_at: null, expired: false, constitution: MONARCHY };
|
|
188
|
+
const stubs = {
|
|
189
|
+
getVotesForItem: async () => [{ id: '1', voter_id: '42', direction: 'yes' }],
|
|
190
|
+
closeBoardItem: async ({ outcome }) => ({ ...item, closed_at: 'T', outcome }),
|
|
191
|
+
getAmendment: async () => ({ id: '5', proposed: TIMOCRACY, kind: 'constitution' }),
|
|
192
|
+
insertAuthorRatifyKarma: async () => true,
|
|
193
|
+
getBoardItemAuthor: async () => '42',
|
|
194
|
+
};
|
|
195
|
+
const orig = {};
|
|
196
|
+
for (const [k, v] of Object.entries(stubs)) { orig[k] = db[k]; db[k] = v; }
|
|
197
|
+
try {
|
|
198
|
+
await withInstance(async () => {
|
|
199
|
+
const closed = await board.closeItem(item, { cause: 'vote' });
|
|
200
|
+
assert.equal(closed.outcome, 'passed');
|
|
201
|
+
assert.ok(closed.constitution_not_applied, 'the close itself reports it');
|
|
202
|
+
assert.equal(closed.constitution_not_applied.problems[0].where, 'database');
|
|
203
|
+
}, { saveFails: true });
|
|
204
|
+
await withInstance(async ({ live }) => {
|
|
205
|
+
const closed = await board.closeItem(item, { cause: 'vote' });
|
|
206
|
+
assert.equal(closed.constitution_not_applied, undefined, 'a clean apply reports nothing');
|
|
207
|
+
assert.deepEqual({ ...closed.applied_constitution }, TIMOCRACY);
|
|
208
|
+
assert.deepEqual({ ...live() }, TIMOCRACY);
|
|
209
|
+
});
|
|
210
|
+
} finally { for (const [k, v] of Object.entries(orig)) db[k] = v; }
|
|
211
|
+
|
|
212
|
+
const routeSrc = fs.readFileSync(path.join(GOV, 'routes', 'government.js'), 'utf8');
|
|
213
|
+
assert.match(routeSrc, /closure && closure\.constitution_not_applied \? \{ constitution_not_applied:/,
|
|
214
|
+
'the vote route forwards it to the page the voter is standing on');
|
|
215
|
+
for (const page of ['board-room.js', 'street-card.js']) {
|
|
216
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', page), 'utf8');
|
|
217
|
+
assert.match(src, /constitution_not_applied/, `${page} says so instead of "in force"`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── 5. the operator's re-apply ───────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
test('re-apply puts the newest PASSED constitutional amendment back, skipping returned ones and laws', async () => {
|
|
224
|
+
const history = [
|
|
225
|
+
{ item_id: 30, amendment_id: 12, outcome: 'returned', proposed: MONARCHY, kind: 'constitution' },
|
|
226
|
+
{ item_id: 29, amendment_id: 11, outcome: 'passed', proposed: { enact: [] }, kind: 'laws' },
|
|
227
|
+
{ item_id: 21, amendment_id: 1, outcome: 'passed', proposed: TIMOCRACY, kind: 'constitution' },
|
|
228
|
+
];
|
|
229
|
+
const origList = db.listAmendmentHistory;
|
|
230
|
+
db.listAmendmentHistory = async () => history;
|
|
231
|
+
try {
|
|
232
|
+
await withInstance(async ({ store, live }) => {
|
|
233
|
+
const out = await constitution.reapplyNewestPassed({ updatedBy: '3' });
|
|
234
|
+
assert.equal(out.ok, true, JSON.stringify(out));
|
|
235
|
+
assert.equal(out.item_id, '21');
|
|
236
|
+
assert.deepEqual({ ...live() }, TIMOCRACY);
|
|
237
|
+
assert.equal(store.row.source, 'reapply');
|
|
238
|
+
assert.equal(store.saves[0].updatedBy, '3');
|
|
239
|
+
});
|
|
240
|
+
db.listAmendmentHistory = async () => history.slice(0, 2);
|
|
241
|
+
await withInstance(async ({ store }) => {
|
|
242
|
+
const out = await constitution.reapplyNewestPassed({});
|
|
243
|
+
assert.deepEqual(out, { ok: false, reason: 'no_passed_amendment' });
|
|
244
|
+
assert.equal(store.saves.length, 0, 'it never invents a constitution');
|
|
245
|
+
});
|
|
246
|
+
} finally { db.listAmendmentHistory = origList; }
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('the re-apply route is pinned to the owner atom and takes no block from the request', () => {
|
|
250
|
+
const { GOVERNMENT_PINS } = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
251
|
+
const pins = GOVERNMENT_PINS || {};
|
|
252
|
+
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'), 'utf8');
|
|
253
|
+
assert.match(src, /'POST \/government\/constitution\/reapply': 'perm:government\.manage'/);
|
|
254
|
+
if (Object.keys(pins).length) assert.equal(pins['POST /government/constitution/reapply'], 'perm:government.manage');
|
|
255
|
+
const routeSrc = fs.readFileSync(path.join(GOV, 'routes', 'government.js'), 'utf8');
|
|
256
|
+
assert.match(routeSrc, /router\.post\('\/government\/constitution\/reapply', api\.requireBuilder, api\.requirePermission\('government\.manage'\), constitution\.reapplyRoute\(log\)\)/,
|
|
257
|
+
'the route exists, gated on the owner atom');
|
|
258
|
+
assert.doesNotMatch(constitution.reapplyRoute.toString(), /req\.body/, 'the block comes from a passed amendment, never the request');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('the re-apply handler answers 404 with nothing passed and 200 with the board it put back', async () => {
|
|
262
|
+
const call = async () => {
|
|
263
|
+
const res = { code: 200, body: null,
|
|
264
|
+
status(c) { this.code = c; return this; }, json(b) { this.body = b; return this; },
|
|
265
|
+
fail(code, status, extra) { this.code = typeof status === 'number' ? status : status.status; this.body = { code, ...extra }; return this; } };
|
|
266
|
+
await constitution.reapplyRoute({ error: () => {} })({ builder: { id: '3' }, body: { board: MONARCHY } }, res);
|
|
267
|
+
return res;
|
|
268
|
+
};
|
|
269
|
+
const origList = db.listAmendmentHistory;
|
|
270
|
+
try {
|
|
271
|
+
db.listAmendmentHistory = async () => [];
|
|
272
|
+
await withInstance(async () => {
|
|
273
|
+
const res = await call();
|
|
274
|
+
assert.equal(res.code, 404);
|
|
275
|
+
assert.equal(res.body.code, 'no_passed_amendment');
|
|
276
|
+
});
|
|
277
|
+
db.listAmendmentHistory = async () => [{ item_id: 21, amendment_id: 1, outcome: 'passed', proposed: TIMOCRACY, kind: 'constitution' }];
|
|
278
|
+
await withInstance(async ({ live }) => {
|
|
279
|
+
const res = await call();
|
|
280
|
+
assert.equal(res.code, 200);
|
|
281
|
+
assert.deepEqual({ ...res.body.board }, TIMOCRACY, 'the passed amendment, not the request body');
|
|
282
|
+
assert.deepEqual({ ...live() }, TIMOCRACY);
|
|
283
|
+
});
|
|
284
|
+
} finally { db.listAmendmentHistory = origList; }
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ── 6. at boot ───────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
test('the restore runs the moment the poller starts, not after a delay', () => {
|
|
290
|
+
const poller = require(path.join(GOV, 'pollers', 'constitution-restore.js'));
|
|
291
|
+
let calls = 0;
|
|
292
|
+
poller.start({ reconcileFn: async () => { calls += 1; return { action: 'in_step' }; } });
|
|
293
|
+
try {
|
|
294
|
+
return new Promise((resolve) => setImmediate(() => { assert.equal(calls, 1); resolve(); }));
|
|
295
|
+
} finally { poller.stop(); }
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('the module declares the poller and the migration creates the one-row table', () => {
|
|
299
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
|
|
300
|
+
assert.ok(manifest.contributes.pollers.includes('constitution-restore'));
|
|
301
|
+
const sql = fs.readFileSync(path.join(GOV, 'migrations', 'government_037_constitution.sql'), 'utf8');
|
|
302
|
+
assert.match(sql, /CREATE TABLE IF NOT EXISTS government_constitution/);
|
|
303
|
+
assert.match(sql, /CHECK \(id = 1\)/, 'one row, by construction');
|
|
304
|
+
});
|
|
@@ -53,6 +53,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
53
53
|
'government_031_treasury.sql', // the treasury: a pot per goal and the loans a dry pot asks the reserve for (task 1004712)
|
|
54
54
|
'government_032_ask_flags.sql', // the reword list: matters a reader flagged as unclear (owner 2026-10-23)
|
|
55
55
|
'government_036_access_holders.sql', // who holds the keys outside Bongos + the access:<key> ask seat (task 1005000)
|
|
56
|
+
'government_037_constitution.sql', // the constitution in force as a durable row (task 1005140)
|
|
56
57
|
]);
|
|
57
58
|
|
|
58
59
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
package/tests/hall_tokens.mjs
CHANGED
|
@@ -406,3 +406,22 @@ test('the reduced-motion kill switch is present and closes the token layer', ()
|
|
|
406
406
|
assert.equal(src.split('@media (prefers-reduced-motion: reduce)').length - 1, 1,
|
|
407
407
|
'exactly one kill switch — a second copy is a place for the rule to rot');
|
|
408
408
|
});
|
|
409
|
+
|
|
410
|
+
test('ink set on an --accent fill is readable on it in both modes', () => {
|
|
411
|
+
// Task 1005103: .genesis__step.is-now .genesis__step-n painted --bg on the
|
|
412
|
+
// accent (light ~2.2:1). Any rule that fills with var(--accent) and names its
|
|
413
|
+
// own ink token must measure that ink against the fill, in both modes.
|
|
414
|
+
const fails = [];
|
|
415
|
+
const plain = src.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
416
|
+
for (const m of plain.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
417
|
+
const body = m[2];
|
|
418
|
+
if (!/background(?:-color)?\s*:\s*var\(--accent\)\s*[;}]?/.test(body)) continue;
|
|
419
|
+
const ink = body.match(/(?:^|[;\s])color\s*:\s*var\((--[\w-]+)\)/);
|
|
420
|
+
if (!ink) continue;
|
|
421
|
+
for (const mode of ['light', 'dark']) {
|
|
422
|
+
const r = ratio(ink[1], '--accent', mode);
|
|
423
|
+
if (r < 4.5) fails.push(`${mode} ${m[1].trim()}: ${ink[1]} on --accent = ${r.toFixed(2)}:1`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
assert.deepEqual(fails, [], 'ink on an accent fill needs 4.5:1');
|
|
427
|
+
});
|