@bongos/core 1.19.223 → 1.19.225
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 +43 -28
- 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 +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +82 -2
- package/docs/api-reference.md +3 -2
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/government/board.js +211 -0
- package/modules/government/db.js +125 -0
- package/modules/government/routes/government.js +54 -0
- package/modules/hall-ui/public/government.html +28 -2
- package/modules/hall-ui/public/government.js +40 -4
- package/modules/hall-ui/public/palette.js +10 -4
- package/modules/ideas/module.json +2 -1
- package/modules/ideas/routes/inbox.js +27 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/route-rank-check.js +4 -0
- package/src/module-api.js +5 -1
- package/tests/government_board_vote.mjs +247 -0
- package/tests/government_board_window.mjs +144 -0
- package/tests/government_routes.mjs +26 -5
- package/tests/hall_palette.mjs +10 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// tests/government_board_vote.mjs — cast a vote: open ballot, anti-self, live
|
|
2
|
+
// re-check, karma (BV1.R08 of goal 1000069, ADR 0175 §§4/6/9).
|
|
3
|
+
//
|
|
4
|
+
// DB-free (db monkeypatched, the sibling board tests' idiom) — and deliberately
|
|
5
|
+
// NOT membership-stubbed: castVote runs the REAL board-membership parse against
|
|
6
|
+
// the item's snapshotted constitution, with only the db row lookups stubbed, so
|
|
7
|
+
// the "who may vote is the SNAPSHOT's rule" property is exercised rather than
|
|
8
|
+
// assumed. The invariants:
|
|
9
|
+
//
|
|
10
|
+
// 1. THE SECTION KEYS ARE ONE LIST IN THREE PLACES — board.js's validation,
|
|
11
|
+
// government_003's CHECK, and the ideas template declaration. A module may
|
|
12
|
+
// not require a sibling (ADR 0083), so they are held equal HERE.
|
|
13
|
+
// 2. ANTI-SELF IS SERVER-SIDE AND UNCONDITIONAL — the bright line that keeps
|
|
14
|
+
// ratification from being ceremonial.
|
|
15
|
+
// 3. MEMBERSHIP IS RE-CHECKED LIVE, AGAINST THE SNAPSHOT — a demotion between
|
|
16
|
+
// open and vote refuses the vote; an amendment mid-sitting cannot change
|
|
17
|
+
// who votes on an already-open item.
|
|
18
|
+
// 4. IDEMPOTENT, AND KARMA RIDES ONLY THE FRESH INSERT — a double-cast is one
|
|
19
|
+
// vote and one +1, never two.
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { test } from 'node:test';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
30
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
31
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
32
|
+
const { fieldOrder, templateFor } = require(path.join(ROOT, 'modules', 'ideas', 'templates.js'));
|
|
33
|
+
|
|
34
|
+
const { FULL_IDEA_SECTION_KEYS, VOTE_KARMA_DELTA, AUTHOR_RATIFY_KARMA_DELTA, awardAuthorRatifyKarma, validateVote, castVote } = board;
|
|
35
|
+
|
|
36
|
+
// ── invariant 1: one list, three places ─────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
test('the objection section keys ARE the Full Idea template fields (no second list that drifts)', () => {
|
|
39
|
+
assert.deepEqual([...FULL_IDEA_SECTION_KEYS], fieldOrder(templateFor('base-v1')),
|
|
40
|
+
'board.js must flag against the same five sections the template declares');
|
|
41
|
+
const migration = fs.readFileSync(
|
|
42
|
+
path.join(GOV, 'migrations', 'government_003_board_tables.sql'), 'utf8');
|
|
43
|
+
for (const key of FULL_IDEA_SECTION_KEYS) {
|
|
44
|
+
assert.ok(migration.includes(`'${key}'`), `the government_003 CHECK must carry '${key}'`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── the pure payload validation ──────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
test('a yes carries nothing; a no carries a reason AND exactly one section', () => {
|
|
51
|
+
assert.deepEqual(validateVote({ direction: 'yes' }),
|
|
52
|
+
{ ok: true, direction: 'yes', reasonMd: null, sectionKey: null });
|
|
53
|
+
assert.equal(validateVote({ direction: 'yes', reasonMd: 'but…' }).reason, 'yes_carries_nothing');
|
|
54
|
+
assert.equal(validateVote({ direction: 'yes', sectionKey: 'fit' }).reason, 'yes_carries_nothing');
|
|
55
|
+
|
|
56
|
+
const no = validateVote({ direction: 'no', reasonMd: ' needs a real fit story ', sectionKey: 'fit' });
|
|
57
|
+
assert.deepEqual(no, { ok: true, direction: 'no', reasonMd: 'needs a real fit story', sectionKey: 'fit' });
|
|
58
|
+
assert.equal(validateVote({ direction: 'no', sectionKey: 'fit' }).reason, 'objection_needs_reason');
|
|
59
|
+
assert.equal(validateVote({ direction: 'no', reasonMd: ' ', sectionKey: 'fit' }).reason, 'objection_needs_reason');
|
|
60
|
+
assert.equal(validateVote({ direction: 'no', reasonMd: 'x' }).reason, 'objection_needs_section');
|
|
61
|
+
assert.equal(validateVote({ direction: 'no', reasonMd: 'x', sectionKey: 'vibes' }).reason, 'objection_needs_section');
|
|
62
|
+
for (const bad of [undefined, null, 'YES', 'abstain', 1]) {
|
|
63
|
+
assert.equal(validateVote({ direction: bad }).reason, 'bad_direction', `${String(bad)} is not a ballot`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── castVote against a stubbed db (membership logic REAL) ───────────────────
|
|
68
|
+
|
|
69
|
+
const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
70
|
+
const openItem = (over = {}) => ({
|
|
71
|
+
id: '11', subject_type: 'full_idea', subject_id: '7', opened_by: null,
|
|
72
|
+
closed_at: null, expired: false, constitution: MONARCHY, ...over,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
async function withDb(stubs, fn) {
|
|
76
|
+
const originals = {};
|
|
77
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
78
|
+
try { return await fn(); }
|
|
79
|
+
finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test('the happy path: a member yes is recorded fresh, with its +1 karma', async () => {
|
|
83
|
+
const karma = [];
|
|
84
|
+
let insertArgs = null;
|
|
85
|
+
await withDb({
|
|
86
|
+
getBoardItemForVote: async () => openItem(),
|
|
87
|
+
builderHoldsRankActive: async (builderId, rankKey) =>
|
|
88
|
+
builderId === '42' && rankKey === 'archon', // the REAL membership parse chose 'archon' from the snapshot
|
|
89
|
+
getBoardItemAuthor: async () => '7', // someone else authored the idea
|
|
90
|
+
insertBoardVote: async (args) => { insertArgs = args; return { id: '1', ...args }; },
|
|
91
|
+
insertVoteKarma: async (args) => { karma.push(args); },
|
|
92
|
+
}, async () => {
|
|
93
|
+
const out = await castVote({ itemId: 11, voterId: '42', direction: 'yes' });
|
|
94
|
+
assert.equal(out.ok, true);
|
|
95
|
+
assert.equal(out.fresh, true);
|
|
96
|
+
assert.equal(out.karma_delta, VOTE_KARMA_DELTA);
|
|
97
|
+
});
|
|
98
|
+
assert.deepEqual(insertArgs, { itemId: 11, voterId: '42', direction: 'yes', reasonMd: null, sectionKey: null });
|
|
99
|
+
assert.deepEqual(karma, [{ voterId: '42', itemId: 11, delta: VOTE_KARMA_DELTA }]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('membership is the SNAPSHOT\'s rule: a rank the snapshot does not name is refused', async () => {
|
|
103
|
+
// The live config could say anything; the item was opened under monarchy, so
|
|
104
|
+
// only an archon-holder votes on it. A metic-holder is refused even though
|
|
105
|
+
// they hold A rank — just not the snapshotted one.
|
|
106
|
+
await withDb({
|
|
107
|
+
getBoardItemForVote: async () => openItem(),
|
|
108
|
+
builderHoldsRankActive: async (builderId, rankKey) => rankKey === 'metic', // holds metic, not archon
|
|
109
|
+
getBoardItemAuthor: async () => { throw new Error('must not be reached'); },
|
|
110
|
+
insertBoardVote: async () => { throw new Error('must not be reached'); },
|
|
111
|
+
}, async () => {
|
|
112
|
+
const out = await castVote({ itemId: 11, voterId: '42', direction: 'yes' });
|
|
113
|
+
assert.deepEqual(out, { ok: false, reason: 'not_board_member' });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('the demotion between open and vote refuses the vote — live re-check, uncached', async () => {
|
|
118
|
+
let demoted = false;
|
|
119
|
+
await withDb({
|
|
120
|
+
getBoardItemForVote: async () => openItem(),
|
|
121
|
+
builderHoldsRankActive: async () => !demoted,
|
|
122
|
+
getBoardItemAuthor: async () => '7',
|
|
123
|
+
insertBoardVote: async (args) => ({ id: '1', ...args }),
|
|
124
|
+
insertVoteKarma: async () => {},
|
|
125
|
+
}, async () => {
|
|
126
|
+
assert.equal((await castVote({ itemId: 11, voterId: '42', direction: 'yes' })).ok, true);
|
|
127
|
+
demoted = true;
|
|
128
|
+
assert.deepEqual(await castVote({ itemId: 11, voterId: '42', direction: 'yes' }),
|
|
129
|
+
{ ok: false, reason: 'not_board_member' }, 'off the board on the very next request');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('anti-self: the author cannot vote on their own item — number/string ids compare equal', async () => {
|
|
134
|
+
await withDb({
|
|
135
|
+
getBoardItemForVote: async () => openItem(),
|
|
136
|
+
builderHoldsRankActive: async () => true,
|
|
137
|
+
getBoardItemAuthor: async () => 42, // Postgres hands back a number; the voter carries a string
|
|
138
|
+
insertBoardVote: async () => { throw new Error('must not be reached'); },
|
|
139
|
+
}, async () => {
|
|
140
|
+
const out = await castVote({ itemId: 11, voterId: '42', direction: 'yes' });
|
|
141
|
+
assert.deepEqual(out, { ok: false, reason: 'anti_self_vote' });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('closed and expired items refuse a vote, each with its own name', async () => {
|
|
146
|
+
await withDb({
|
|
147
|
+
getBoardItemForVote: async () => openItem({ closed_at: 't', outcome: 'passed' }),
|
|
148
|
+
}, async () => {
|
|
149
|
+
assert.deepEqual(await castVote({ itemId: 11, voterId: '42', direction: 'yes' }),
|
|
150
|
+
{ ok: false, reason: 'item_closed' });
|
|
151
|
+
});
|
|
152
|
+
await withDb({
|
|
153
|
+
getBoardItemForVote: async () => openItem({ expired: true }),
|
|
154
|
+
}, async () => {
|
|
155
|
+
// The gap between expiry and the R11 sweep: a vote after time never lands.
|
|
156
|
+
assert.deepEqual(await castVote({ itemId: 11, voterId: '42', direction: 'yes' }),
|
|
157
|
+
{ ok: false, reason: 'vote_window_expired' });
|
|
158
|
+
});
|
|
159
|
+
await withDb({ getBoardItemForVote: async () => null }, async () => {
|
|
160
|
+
assert.deepEqual(await castVote({ itemId: 999, voterId: '42', direction: 'yes' }),
|
|
161
|
+
{ ok: false, reason: 'item_not_found' });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('idempotent: a double-cast is ONE vote and ONE karma line', async () => {
|
|
166
|
+
const karma = [];
|
|
167
|
+
const standing = { id: '1', item_id: '11', voter_id: '42', direction: 'yes' };
|
|
168
|
+
let hasVote = false;
|
|
169
|
+
await withDb({
|
|
170
|
+
getBoardItemForVote: async () => openItem(),
|
|
171
|
+
builderHoldsRankActive: async () => true,
|
|
172
|
+
getBoardItemAuthor: async () => '7',
|
|
173
|
+
insertBoardVote: async (args) => (hasVote ? null : (hasVote = true, { ...standing, ...args })),
|
|
174
|
+
getBoardVote: async () => standing,
|
|
175
|
+
insertVoteKarma: async (args) => { karma.push(args); },
|
|
176
|
+
}, async () => {
|
|
177
|
+
const first = await castVote({ itemId: 11, voterId: '42', direction: 'yes' });
|
|
178
|
+
const second = await castVote({ itemId: 11, voterId: '42', direction: 'yes' });
|
|
179
|
+
assert.equal(first.fresh, true);
|
|
180
|
+
assert.equal(second.fresh, false);
|
|
181
|
+
assert.deepEqual(second.vote, standing, 'the re-cast returns the standing vote');
|
|
182
|
+
assert.equal(karma.length, 1, 'karma rode the fresh insert only');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('an invalid payload is refused before ANY db read', async () => {
|
|
187
|
+
await withDb({
|
|
188
|
+
getBoardItemForVote: async () => { throw new Error('must not be reached'); },
|
|
189
|
+
}, async () => {
|
|
190
|
+
assert.equal((await castVote({ itemId: 11, voterId: '42', direction: 'no', reasonMd: 'x' })).reason,
|
|
191
|
+
'objection_needs_section');
|
|
192
|
+
assert.equal((await castVote({ itemId: 'nope', voterId: '42', direction: 'yes' })).reason,
|
|
193
|
+
'item_not_found');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ── the structural pins ──────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
test('the vote INSERT targets UNIQUE(item_id, voter_id) — idempotency is the schema', () => {
|
|
200
|
+
const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
201
|
+
const block = src.slice(src.indexOf('async function insertBoardVote'));
|
|
202
|
+
assert.match(block, /ON CONFLICT \(item_id, voter_id\) DO NOTHING/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('the route pin exists and names the XENOS-floored atom (the R04-carried pin)', () => {
|
|
206
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
207
|
+
assert.equal(rrc.MODULE_EXPECTED_RANKS.government['POST /government/board/items/:itemId/votes'],
|
|
208
|
+
`${rrc.PERM_PIN_PREFIX}board.vote.cast`,
|
|
209
|
+
'the coarse gate is board.vote.cast; membership is the in-handler wall');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ── author karma on ratification (declared here, fired by R10's close) ───────
|
|
213
|
+
|
|
214
|
+
test('the author-ratification karma mirrors KARMA_RATIFY_DELTA (+3, not the stale +5)', () => {
|
|
215
|
+
const { KARMA_RATIFY_DELTA } = require(path.join(ROOT, 'modules', 'ideas', 'inbox.js'));
|
|
216
|
+
assert.equal(AUTHOR_RATIFY_KARMA_DELTA, KARMA_RATIFY_DELTA,
|
|
217
|
+
'board ratification pays what criterion-proposal ratification pays — one number, held equal here');
|
|
218
|
+
assert.equal(AUTHOR_RATIFY_KARMA_DELTA, 3, 'V3.R28 tuned this down from +5; do not copy the stale docs');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('awardAuthorRatifyKarma pays once per item, and nobody when there is no author', async () => {
|
|
222
|
+
const paid = [];
|
|
223
|
+
await withDb({
|
|
224
|
+
getBoardItemAuthor: async () => '7',
|
|
225
|
+
insertAuthorRatifyKarma: async (args) => { paid.push(args); return paid.length === 1; }, // second call: reason exists
|
|
226
|
+
}, async () => {
|
|
227
|
+
const first = await awardAuthorRatifyKarma({ id: '11', subject_type: 'full_idea', subject_id: '7' });
|
|
228
|
+
const second = await awardAuthorRatifyKarma({ id: '11', subject_type: 'full_idea', subject_id: '7' });
|
|
229
|
+
assert.deepEqual(first, { paid: true, karma_delta: AUTHOR_RATIFY_KARMA_DELTA });
|
|
230
|
+
assert.deepEqual(second, { paid: false, reason: 'already_paid' }, 'a re-driven close never double-pays');
|
|
231
|
+
});
|
|
232
|
+
assert.deepEqual(paid[0], { authorId: '7', itemId: '11', delta: AUTHOR_RATIFY_KARMA_DELTA });
|
|
233
|
+
|
|
234
|
+
await withDb({
|
|
235
|
+
getBoardItemAuthor: async () => null,
|
|
236
|
+
insertAuthorRatifyKarma: async () => { throw new Error('must not be reached'); },
|
|
237
|
+
}, async () => {
|
|
238
|
+
assert.deepEqual(await awardAuthorRatifyKarma({ id: '12' }), { paid: false, reason: 'no_author' });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('the ratify-karma INSERT is one atomic WHERE-NOT-EXISTS statement (idempotency in the SQL)', () => {
|
|
243
|
+
const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
244
|
+
const block = src.slice(src.indexOf('async function insertAuthorRatifyKarma'), src.indexOf('async function insertVoteKarma'));
|
|
245
|
+
assert.match(block, /WHERE NOT EXISTS/, 'the predicate is the guard — karma_log has no unique constraint');
|
|
246
|
+
assert.match(block, /board\.item\.ratified:/, 'the per-item reason key is the idempotency key');
|
|
247
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// tests/government_board_window.mjs — a window opens automatically when a Full
|
|
2
|
+
// Idea passes the grade (BV1.R07 of goal 1000069, ADR 0175 §5).
|
|
3
|
+
//
|
|
4
|
+
// DB-free: the pure bar gate is exercised directly; the open path runs against
|
|
5
|
+
// a monkeypatched db module (the government_board_membership.mjs idiom). The
|
|
6
|
+
// invariants that matter:
|
|
7
|
+
//
|
|
8
|
+
// 1. THE BAR IS THE SAME ONE THE CREDIT LANE USES — ADR 0175 §5 pins the
|
|
9
|
+
// window gate to economy's IDEA_FULL_PASS_BAR. board.js may not import
|
|
10
|
+
// economy (ADR 0083), so the two constants are held equal HERE — the
|
|
11
|
+
// no-second-list-that-drifts discipline, enforced by test.
|
|
12
|
+
// 2. A SUB-BAR IDEA NEVER REACHES THE BOARD — refused inside board.js, not
|
|
13
|
+
// by trusting the caller, and without touching the db.
|
|
14
|
+
// 3. THE CONSTITUTION IS SNAPSHOTTED — the open passes R05's sanitized board
|
|
15
|
+
// block, so an amendment cannot rewrite how an already-open item closes.
|
|
16
|
+
// 4. THE IDEMPOTENCY IS STRUCTURAL — the INSERT targets government_003's
|
|
17
|
+
// partial unique index; a losing racer gets the existing open item back.
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
28
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
29
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
30
|
+
const { BOARD_DEFAULTS } = require(path.join(GOV, 'config.js'));
|
|
31
|
+
const credits = require(path.join(ROOT, 'modules', 'economy', 'credits.js'));
|
|
32
|
+
|
|
33
|
+
const { FULL_IDEA_PASS_BAR, ideaPassesBar, openWindowForFullIdea } = board;
|
|
34
|
+
|
|
35
|
+
// ── invariant 1: one bar, two modules, held equal by this line ──────────────
|
|
36
|
+
|
|
37
|
+
test('the window bar IS the credit-lane bar (ADR 0175 §5 — no second list that drifts)', () => {
|
|
38
|
+
assert.equal(FULL_IDEA_PASS_BAR, credits.IDEA_FULL_PASS_BAR,
|
|
39
|
+
'board.js and economy/credits.js must gate on the SAME completeness bar; '
|
|
40
|
+
+ 'if the bar becomes policy, move BOTH reads to one config surface in the same change');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── invariant 2: the pure gate, fail-closed ─────────────────────────────────
|
|
44
|
+
|
|
45
|
+
test('ideaPassesBar: the bar passes at exactly 60, and every unscorable shape fails closed', () => {
|
|
46
|
+
assert.equal(ideaPassesBar(FULL_IDEA_PASS_BAR), true, 'the bar itself passes (>=)');
|
|
47
|
+
assert.equal(ideaPassesBar(100), true);
|
|
48
|
+
assert.equal(ideaPassesBar(FULL_IDEA_PASS_BAR - 1), false);
|
|
49
|
+
for (const bad of [null, undefined, NaN, 'sixty', '', {}, [], -1, Infinity]) {
|
|
50
|
+
assert.equal(ideaPassesBar(bad), Number.isFinite(Number(bad)) && Number(bad) >= FULL_IDEA_PASS_BAR ? true : false,
|
|
51
|
+
`${String(bad)} must not reach the board`);
|
|
52
|
+
}
|
|
53
|
+
// "not scored" and "could not be scored" look the same (R142) — both stay off the board.
|
|
54
|
+
assert.equal(ideaPassesBar(null), false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ── the open path, against a stubbed db ─────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
async function withDb(stubs, fn) {
|
|
60
|
+
const originals = {};
|
|
61
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
62
|
+
try { return await fn(); }
|
|
63
|
+
finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test('a sub-bar idea is refused WITHOUT touching the db', async () => {
|
|
67
|
+
await withDb({
|
|
68
|
+
openBoardItem: async () => { throw new Error('must not be called'); },
|
|
69
|
+
getOpenBoardItem: async () => { throw new Error('must not be called'); },
|
|
70
|
+
}, async () => {
|
|
71
|
+
assert.deepEqual(await openWindowForFullIdea({ ideaId: 7, completenessScore: 59 }),
|
|
72
|
+
{ opened: false, reason: 'below_bar' });
|
|
73
|
+
assert.deepEqual(await openWindowForFullIdea({ ideaId: 7, completenessScore: null }),
|
|
74
|
+
{ opened: false, reason: 'below_bar' });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('an unidentifiable idea id is refused before the bar is even consulted', async () => {
|
|
79
|
+
await withDb({ openBoardItem: async () => { throw new Error('must not be called'); } }, async () => {
|
|
80
|
+
for (const bad of [undefined, null, 0, -3, 'seven', NaN]) {
|
|
81
|
+
assert.deepEqual(await openWindowForFullIdea({ ideaId: bad, completenessScore: 100 }),
|
|
82
|
+
{ opened: false, reason: 'bad_subject' }, `${String(bad)} names no subject`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a passing idea opens a window carrying the SANITIZED constitution snapshot', async () => {
|
|
88
|
+
let got = null;
|
|
89
|
+
await withDb({
|
|
90
|
+
openBoardItem: async (args) => { got = args; return { id: '11', subject_type: 'full_idea', subject_id: '7', closes_at: null }; },
|
|
91
|
+
}, async () => {
|
|
92
|
+
const out = await openWindowForFullIdea({ ideaId: 7, completenessScore: 82 });
|
|
93
|
+
assert.equal(out.opened, true);
|
|
94
|
+
assert.equal(out.item.id, '11');
|
|
95
|
+
});
|
|
96
|
+
assert.equal(got.subjectType, 'full_idea');
|
|
97
|
+
assert.equal(got.subjectId, 7);
|
|
98
|
+
assert.equal(got.openedBy, null, 'the grade opened this, nobody did (government_003)');
|
|
99
|
+
// This checkout has no config/government.json, so the snapshot is the neutral
|
|
100
|
+
// starter's board block — which R05 pins equal to BOARD_DEFAULTS (monarchy).
|
|
101
|
+
assert.deepEqual(got.constitution, BOARD_DEFAULTS,
|
|
102
|
+
'the snapshot is R05\'s sanitized board block, not raw file contents');
|
|
103
|
+
assert.equal(got.windowMinutes, BOARD_DEFAULTS.window_minutes,
|
|
104
|
+
'monarchy has no clock — closes_at stays NULL until a project configures a sitting');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('a lost race returns the EXISTING open item, opened:false', async () => {
|
|
108
|
+
const existing = { id: '4', subject_type: 'full_idea', subject_id: '7', closed_at: null };
|
|
109
|
+
await withDb({
|
|
110
|
+
openBoardItem: async () => null, // the partial-unique-index conflict path
|
|
111
|
+
getOpenBoardItem: async (type, id) => {
|
|
112
|
+
assert.equal(type, 'full_idea'); assert.equal(id, 7);
|
|
113
|
+
return existing;
|
|
114
|
+
},
|
|
115
|
+
}, async () => {
|
|
116
|
+
const out = await openWindowForFullIdea({ ideaId: 7, completenessScore: 82 });
|
|
117
|
+
assert.deepEqual(out, { opened: false, reason: 'already_open', item: existing });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── the structural idempotency + clock, pinned as SQL source ────────────────
|
|
122
|
+
|
|
123
|
+
test('the INSERT targets the partial unique index and the DATABASE clock', () => {
|
|
124
|
+
const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
125
|
+
const start = src.indexOf('async function openBoardItem');
|
|
126
|
+
assert.ok(start > -1);
|
|
127
|
+
const block = src.slice(start, src.indexOf('async function getOpenBoardItem'));
|
|
128
|
+
assert.match(block, /ON CONFLICT \(subject_type, subject_id\) WHERE closed_at IS NULL DO NOTHING/,
|
|
129
|
+
'idempotency must be the partial unique index, not a SELECT-then-INSERT race');
|
|
130
|
+
assert.match(block, /now\(\) \+ make_interval/,
|
|
131
|
+
'closes_at comes from the database clock — absolute, so a config change never moves an open deadline');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── the port carries it ──────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
test('the government port exposes openBoardWindowForIdea for the ideas capture path', () => {
|
|
137
|
+
const routes = require(path.join(GOV, 'routes', 'government.js'));
|
|
138
|
+
routes(); // registration is hasProvider-guarded — safe to call again
|
|
139
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
140
|
+
const port = api.resolveOptional('government');
|
|
141
|
+
assert.equal(typeof port.openBoardWindowForIdea, 'function');
|
|
142
|
+
assert.equal(port.openBoardWindowForIdea, board.openWindowForFullIdea,
|
|
143
|
+
'the port method IS board.js\'s — no wrapper that could drift the bar gate');
|
|
144
|
+
});
|
|
@@ -64,12 +64,34 @@ test('the routes file declares the full R110 surface', () => {
|
|
|
64
64
|
for (const key of wanted) assert.ok(declared.has(key), `missing route ${key}`);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
test('every /governance route gates on
|
|
67
|
+
test('every /governance route gates via REAL middleware on exactly its PINNED atom', () => {
|
|
68
|
+
// Until R08 every route here gated on government.manage and this test said so
|
|
69
|
+
// literally. The Board Room's vote route (R08 of goal 1000069) gates on the
|
|
70
|
+
// deliberately XENOS-floored board.vote.cast — the atom is the coarse gate,
|
|
71
|
+
// membership is the in-handler wall — so the invariant generalizes: each
|
|
72
|
+
// route's middleware must declare EXACTLY the atom its pin promises, and the
|
|
73
|
+
// rank each classifies to must be that atom's real catalog floor. This is
|
|
74
|
+
// STRICTER than the old literal: a new route cannot sneak in on a different
|
|
75
|
+
// atom without also changing the pin table, which is the reviewed surface.
|
|
68
76
|
assert.ok(classified.length > 0, 'no /government routes found to classify');
|
|
77
|
+
const pins = rrc.MODULE_EXPECTED_RANKS.government;
|
|
69
78
|
for (const r of classified) {
|
|
70
79
|
assert.equal(r.via, 'middleware', `${r.key}: gate must be middleware, not an annotation`);
|
|
71
|
-
|
|
72
|
-
assert.
|
|
80
|
+
const pinned = pins[r.key];
|
|
81
|
+
assert.ok(pinned && pinned.startsWith(rrc.PERM_PIN_PREFIX), `${r.key}: must carry a perm pin`);
|
|
82
|
+
const atom = pinned.slice(rrc.PERM_PIN_PREFIX.length);
|
|
83
|
+
assert.deepEqual(r.perms, [atom], `${r.key}: must gate on exactly its pinned atom (${atom})`);
|
|
84
|
+
// The auditor speaks rank LABELS, not raw floors (route-rank-check.js
|
|
85
|
+
// FLOOR_LABEL): both sub-Metic floors collapse to 'any-builder' — a label
|
|
86
|
+
// that may under-report strictness, never over-report it.
|
|
87
|
+
const FLOOR_LABEL = { xenos: 'any-builder', thetes: 'any-builder', metic: 'metic+archon', archon: 'archon' };
|
|
88
|
+
const floor = (catalog.byKey(atom) || {}).floor;
|
|
89
|
+
assert.equal(r.rank, FLOOR_LABEL[floor], `${r.key}: classifies to ${atom}'s floor label (${FLOOR_LABEL[floor]})`);
|
|
90
|
+
}
|
|
91
|
+
// The admin surface's own rule survives the generalization, stated directly:
|
|
92
|
+
// everything that is not a board route still gates on government.manage.
|
|
93
|
+
for (const r of classified.filter((x) => !x.key.includes('/board/'))) {
|
|
94
|
+
assert.deepEqual(r.perms, ['government.manage'], `${r.key}: the admin surface gates on the manage atom`);
|
|
73
95
|
}
|
|
74
96
|
});
|
|
75
97
|
|
|
@@ -78,8 +100,7 @@ test('every declared /governance route is pinned, and every pin exists (both dir
|
|
|
78
100
|
assert.ok(pins, 'MODULE_EXPECTED_RANKS has no government block');
|
|
79
101
|
const declared = new Set(classified.map((r) => r.key));
|
|
80
102
|
for (const key of declared) {
|
|
81
|
-
assert.
|
|
82
|
-
`${key} must be pinned to perm:government.manage`);
|
|
103
|
+
assert.ok(pins[key], `${key} must be pinned (atom equality is asserted above)`);
|
|
83
104
|
}
|
|
84
105
|
for (const key of Object.keys(pins)) {
|
|
85
106
|
assert.ok(declared.has(key), `pin ${key} names a route the file no longer declares`);
|
package/tests/hall_palette.mjs
CHANGED
|
@@ -111,6 +111,16 @@ test('Tasks tab anchors exist in work.js makeTabs', () => {
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
test('Government section anchors exist in government.js makeTabs (the R15 three-section IA)', () => {
|
|
115
|
+
const ids = [...read('government.js').matchAll(/\{ id: '([a-z-]+)', label: '[^']*', panel:/g)].map((m) => m[1]);
|
|
116
|
+
assert.ok(ids.length === 3, `sanity: found the three government tabs (${ids})`);
|
|
117
|
+
const declared = declaredHrefs((x) => x.startsWith('/government#'));
|
|
118
|
+
assert.ok(declared.length === 3, 'palette declares the three Government sections');
|
|
119
|
+
for (const h of declared) {
|
|
120
|
+
assert.ok(ids.includes(h.split('#')[1]), `${h} must be a real government.js tab id`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
114
124
|
test('Settings panel anchors exist in settings.js RAIL_GROUPS', () => {
|
|
115
125
|
// RAIL_GROUPS is derived: RAIL_ITEMS.map(i => i.id).concat('extensions').
|
|
116
126
|
const src = read('settings.js');
|