@bongos/core 1.19.251 → 1.19.253
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 +45 -30
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0179-board-membership-predicate-grammar.md +113 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +46 -44
- package/docs/copy-registry.json +64 -46
- package/modules/government/board-membership.js +20 -20
- package/modules/government/board.js +63 -4
- package/modules/government/config.js +14 -8
- package/modules/government/db.js +18 -7
- package/modules/government/membership-predicate.js +115 -0
- package/modules/government/routes/government.js +19 -0
- package/modules/hall-ui/public/government.js +88 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +5 -1
- package/tests/government_board_amendment.mjs +87 -1
- package/tests/government_board_membership.mjs +101 -10
- package/tests/government_board_membership_db.mjs +150 -0
- package/tests/government_board_vote.mjs +5 -3
- package/tests/government_charter_library.mjs +107 -1
- package/tests/government_constitution_view.mjs +53 -0
- package/tests/rank_tier_single_source.mjs +9 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// tests/government_board_membership_db.mjs — the board franchise against a REAL
|
|
2
|
+
// Postgres (task 1003094).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS BESIDE THE DB-FREE government_board_membership.mjs. That file
|
|
5
|
+
// covers the pure parser and the resolvers against a stubbed db, which is where
|
|
6
|
+
// the grammar lives. It cannot cover the half this task actually changed: the
|
|
7
|
+
// SQL. `WHERE rank_key = $1` became `WHERE rank_key = ANY($1)` plus a DISTINCT,
|
|
8
|
+
// and a FRANCHISE bug is precisely the kind a stub agrees with and the database
|
|
9
|
+
// does not — the stub returns whatever the test author imagined the query means.
|
|
10
|
+
//
|
|
11
|
+
// What only real SQL can answer:
|
|
12
|
+
// * does `rank:metic+` genuinely seat the Archon (the regression that cost an
|
|
13
|
+
// owner their seat on their own constitution);
|
|
14
|
+
// * do the two STRUCTURAL filters survive the rewrite — a deactivated builder
|
|
15
|
+
// and a machine principal must still never govern, because a widened
|
|
16
|
+
// franchise that also admits a bot is a worse bug than the one being fixed;
|
|
17
|
+
// * does DISTINCT actually hold when a builder matches through two keys — a
|
|
18
|
+
// double-counted member silently raises the bar a consent vote must clear.
|
|
19
|
+
//
|
|
20
|
+
// It SELF-SKIPS (exit 0) with no reachable Postgres, and is listed in
|
|
21
|
+
// run-unit-tests.js's INTEGRATION set so the DB-free lane never runs it.
|
|
22
|
+
//
|
|
23
|
+
// Run: DATABASE_URL=postgres://... node tests/government_board_membership_db.mjs
|
|
24
|
+
//
|
|
25
|
+
// It creates its own throwaway `builders` / `government_builder_ranks` rows at
|
|
26
|
+
// ids far above any real row, and deletes them at the end.
|
|
27
|
+
|
|
28
|
+
import pg from 'pg';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await pool.query('SELECT 1');
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(`government_board_membership_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
|
|
38
|
+
await pool.end().catch(() => {});
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
let passed = 0, failed = 0;
|
|
42
|
+
async function t(name, fn) {
|
|
43
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
44
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
45
|
+
}
|
|
46
|
+
const assert = (await import('node:assert')).strict;
|
|
47
|
+
|
|
48
|
+
// Minimal stand-ins ONLY when the real tables are absent (throwaway container);
|
|
49
|
+
// against a migrated database these are no-ops and the real tables are used.
|
|
50
|
+
await pool.query(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS builders (
|
|
52
|
+
id bigserial PRIMARY KEY, github_login text, display_name text,
|
|
53
|
+
status text DEFAULT 'active', system_role text);
|
|
54
|
+
CREATE TABLE IF NOT EXISTS government_builder_ranks (
|
|
55
|
+
builder_id bigint, rank_key text);
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
// Ids far above any real row, and every login prefixed, so cleanup is exact and
|
|
59
|
+
// this can never touch a real builder or a real rank assignment.
|
|
60
|
+
const B = 990000000;
|
|
61
|
+
const ID = { xenos: B + 1, thetes: B + 2, metic: B + 3, archon: B + 4, gone: B + 5, bot: B + 6, ops: B + 7 };
|
|
62
|
+
const cleanup = async () => {
|
|
63
|
+
await pool.query('DELETE FROM government_builder_ranks WHERE builder_id >= $1', [B]);
|
|
64
|
+
await pool.query('DELETE FROM builders WHERE id >= $1', [B]);
|
|
65
|
+
};
|
|
66
|
+
await cleanup();
|
|
67
|
+
await pool.query(`INSERT INTO builders (id, github_login, display_name, status, system_role) VALUES
|
|
68
|
+
($1,'gbmdb-x','Xenos','active',NULL),($2,'gbmdb-t','Thetes','active',NULL),
|
|
69
|
+
($3,'gbmdb-m','Metic','active',NULL),($4,'gbmdb-a','Archon','active',NULL),
|
|
70
|
+
($5,'gbmdb-gone','Deactivated Archon','inactive',NULL),
|
|
71
|
+
($6,'gbmdb-bot','A Machine','active','bfg'),
|
|
72
|
+
($7,'gbmdb-ops','Custom Ops','active',NULL)`,
|
|
73
|
+
[ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
|
|
74
|
+
await pool.query(`INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES
|
|
75
|
+
($1,'xenos'),($2,'thetes'),($3,'metic'),($4,'archon'),($5,'archon'),($6,'archon'),($7,'gbmdb-ops-rank')`,
|
|
76
|
+
[ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
|
|
77
|
+
|
|
78
|
+
const api = require(process.cwd() + '/src/module-api.js');
|
|
79
|
+
api.pool.query = (...a) => pool.query(...a);
|
|
80
|
+
const { resolveBoardMembers, isBoardMember } = require(process.cwd() + '/modules/government/board-membership.js');
|
|
81
|
+
|
|
82
|
+
const board = (membership) => ({ membership, pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true });
|
|
83
|
+
const logins = async (m) => (await resolveBoardMembers(board(m)))
|
|
84
|
+
.filter((r) => Number(r.builder_id) >= B) // ignore any real rows on a migrated db
|
|
85
|
+
.map((r) => r.github_login.replace('gbmdb-', ''));
|
|
86
|
+
|
|
87
|
+
console.log('the predicate against real SQL');
|
|
88
|
+
|
|
89
|
+
await t('bare rank:archon is unchanged — exactly archons', async () => {
|
|
90
|
+
assert.deepEqual(await logins('rank:archon'), ['a']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await t('THE BUG: bare rank:metic seats metics ONLY (still true, deliberately)', async () => {
|
|
94
|
+
assert.deepEqual(await logins('rank:metic'), ['m']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await t('THE FIX: rank:metic+ seats metic AND archon', async () => {
|
|
98
|
+
assert.deepEqual(await logins('rank:metic+'), ['m', 'a']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await t('rank:xenos+ is how you say everyone', async () => {
|
|
102
|
+
assert.deepEqual(await logins('rank:xenos+'), ['x', 't', 'm', 'a']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await t('bare rank:xenos really does seat NEWCOMERS ONLY — the library\'s lie', async () => {
|
|
106
|
+
assert.deepEqual(await logins('rank:xenos'), ['x']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await t('a comma union works, and composes with the ladder', async () => {
|
|
110
|
+
assert.deepEqual(await logins('rank:metic,archon'), ['m', 'a']);
|
|
111
|
+
assert.deepEqual(await logins('rank:metic+,gbmdb-ops-rank'), ['m', 'a', 'ops']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await t('the deactivated and the machine are STILL excluded under the widened form', async () => {
|
|
115
|
+
// The two structural filters must survive the ANY() rewrite — a widened
|
|
116
|
+
// franchise that also admits a bot or a departed builder is a worse bug.
|
|
117
|
+
const l = await logins('rank:xenos+');
|
|
118
|
+
assert.ok(!l.includes('gone'), 'a deactivated builder never governs');
|
|
119
|
+
assert.ok(!l.includes('bot'), 'a machine principal never governs');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await t('no duplicate rows when a builder matches through two keys', async () => {
|
|
123
|
+
await pool.query('INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES ($1, $2)', [ID.archon, 'gbmdb-ops-rank']);
|
|
124
|
+
try {
|
|
125
|
+
const l = await logins('rank:archon,gbmdb-ops-rank');
|
|
126
|
+
assert.deepEqual(l.filter((x) => x === 'a').length, 1,
|
|
127
|
+
'DISTINCT holds — a double-counted member silently raises the consent bar');
|
|
128
|
+
} finally {
|
|
129
|
+
await pool.query("DELETE FROM government_builder_ranks WHERE builder_id=$1 AND rank_key='gbmdb-ops-rank'", [ID.archon]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await t('isBoardMember agrees with resolveBoardMembers on every form', async () => {
|
|
134
|
+
assert.equal(await isBoardMember(ID.archon, board('rank:metic+')), true, 'THE REGRESSION: the Archon keeps their seat');
|
|
135
|
+
assert.equal(await isBoardMember(ID.archon, board('rank:metic')), false, 'and the bare form still excludes them');
|
|
136
|
+
assert.equal(await isBoardMember(ID.metic, board('rank:metic+')), true);
|
|
137
|
+
assert.equal(await isBoardMember(ID.xenos, board('rank:xenos+')), true);
|
|
138
|
+
assert.equal(await isBoardMember(ID.gone, board('rank:xenos+')), false, 'deactivated');
|
|
139
|
+
assert.equal(await isBoardMember(ID.bot, board('rank:xenos+')), false, 'machine');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await t('an unparseable predicate still seats nobody against a real db', async () => {
|
|
143
|
+
assert.deepEqual(await logins('rank:ops+'), [], '+ on a non-ladder rank fails closed');
|
|
144
|
+
assert.deepEqual(await logins('nonsense'), []);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await cleanup();
|
|
148
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
149
|
+
await pool.end();
|
|
150
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -84,8 +84,10 @@ test('the happy path: a member yes is recorded fresh, with its +1 karma', async
|
|
|
84
84
|
let insertArgs = null;
|
|
85
85
|
await withDb({
|
|
86
86
|
getBoardItemForVote: async () => openItem(),
|
|
87
|
-
builderHoldsRankActive: async (builderId,
|
|
88
|
-
|
|
87
|
+
builderHoldsRankActive: async (builderId, rankKeys) =>
|
|
88
|
+
// The db read takes a SET since task 1003094; the REAL membership parse
|
|
89
|
+
// resolved 'rank:archon' from the snapshot to the one-element set ['archon'].
|
|
90
|
+
builderId === '42' && rankKeys.includes('archon'),
|
|
89
91
|
getBoardItemAuthor: async () => '7', // someone else authored the idea
|
|
90
92
|
insertBoardVote: async (args) => { insertArgs = args; return { id: '1', ...args }; },
|
|
91
93
|
insertVoteKarma: async (args) => { karma.push(args); },
|
|
@@ -105,7 +107,7 @@ test('membership is the SNAPSHOT\'s rule: a rank the snapshot does not name is r
|
|
|
105
107
|
// they hold A rank — just not the snapshotted one.
|
|
106
108
|
await withDb({
|
|
107
109
|
getBoardItemForVote: async () => openItem(),
|
|
108
|
-
builderHoldsRankActive: async (builderId,
|
|
110
|
+
builderHoldsRankActive: async (builderId, rankKeys) => rankKeys.includes('metic'), // holds metic, not archon
|
|
109
111
|
getBoardItemAuthor: async () => { throw new Error('must not be reached'); },
|
|
110
112
|
insertBoardVote: async () => { throw new Error('must not be reached'); },
|
|
111
113
|
}, async () => {
|
|
@@ -67,7 +67,12 @@ test('the library presents all seven structures with honest selectable marks (cl
|
|
|
67
67
|
assert.equal(selectableTrue, 4, 'monarchy, timocratic republic, direct democracy, foundation — the config-expressible four');
|
|
68
68
|
const trueBlocks = lib.split('selectable: true').slice(1);
|
|
69
69
|
for (const b of trueBlocks) {
|
|
70
|
-
|
|
70
|
+
// Window widened from 200 (task 1003095): a selectable entry now also carries
|
|
71
|
+
// `predicate` + `seatsRanks` ahead of `how`, which the test below checks
|
|
72
|
+
// against the real parser. Cut to the next entry rather than a magic length,
|
|
73
|
+
// so adding another field cannot silently push `how` out of view again.
|
|
74
|
+
const entry = b.split('{ key:')[0];
|
|
75
|
+
assert.ok(entry.includes('how:'), 'a selectable structure names its configuration');
|
|
71
76
|
}
|
|
72
77
|
assert.match(client, /recommended at your headcount/, 'the one live datum is rendered');
|
|
73
78
|
assert.match(client, /c\.key === recommendedKey/, "the pill is driven by the SERVER's recommendation, never re-derived client-side");
|
|
@@ -75,3 +80,104 @@ test('the library presents all seven structures with honest selectable marks (cl
|
|
|
75
80
|
assert.ok(client.includes(`name: '${t}'`), `trajectory ${t} present`);
|
|
76
81
|
}
|
|
77
82
|
});
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
// ── R26's ask, made mechanical (task 1003095) ───────────────────────────────
|
|
86
|
+
//
|
|
87
|
+
// The library is the ONLY place in the product that tells an owner WHAT TO TYPE.
|
|
88
|
+
// That makes a wrong `how` line worse than no library: an owner who followed
|
|
89
|
+
// `membership rank:xenos (everyone)` under the old exact-match grammar handed
|
|
90
|
+
// the board to newcomers and locked themselves out. R26's done-when asks for
|
|
91
|
+
// "selectable-today is honestly marked" — prose cannot be honest by assertion,
|
|
92
|
+
// so this checks every selectable charter's predicate against the REAL parser.
|
|
93
|
+
//
|
|
94
|
+
// Deliberately NOT a copy of the intent: it imports
|
|
95
|
+
// modules/government/membership-predicate.js, the same module the resolver and
|
|
96
|
+
// the config sanitizer use. If the grammar moves underneath this page again,
|
|
97
|
+
// this test reds instead of the page quietly lying.
|
|
98
|
+
|
|
99
|
+
function charterEntries() {
|
|
100
|
+
const client = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/government.js'), 'utf8');
|
|
101
|
+
const lib = client.slice(client.indexOf('const CHARTERS'), client.indexOf('const TRAJECTORIES'));
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const block of lib.split('{ key:').slice(1)) {
|
|
104
|
+
const key = /^\s*'([a-z-]+)'/.exec(block);
|
|
105
|
+
if (!key) continue;
|
|
106
|
+
const selectable = /selectable: (true|false)/.exec(block);
|
|
107
|
+
const predicate = /predicate: (?:'([^']*)'|null)/.exec(block);
|
|
108
|
+
const seats = /seatsRanks: (?:\[([^\]]*)\]|null)/.exec(block);
|
|
109
|
+
const how = /how: '([^']*)'/.exec(block) || /how: "([^"]*)"/.exec(block);
|
|
110
|
+
out.push({
|
|
111
|
+
key: key[1],
|
|
112
|
+
selectable: selectable ? selectable[1] === 'true' : null,
|
|
113
|
+
predicate: predicate ? (predicate[1] ?? null) : undefined,
|
|
114
|
+
seatsRanks: seats ? (seats[1] ? seats[1].split(',').map((x) => x.trim().replace(/'/g, '')) : null) : undefined,
|
|
115
|
+
how: how ? how[1] : null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
test('every SELECTABLE charter names a predicate the real resolver would actually seat as described', () => {
|
|
122
|
+
const { parseMembershipPredicate } = require(path.join(GOV, 'membership-predicate.js'));
|
|
123
|
+
const entries = charterEntries();
|
|
124
|
+
assert.equal(entries.length, 7, 'sanity: parsed all seven structures');
|
|
125
|
+
|
|
126
|
+
for (const c of entries.filter((e) => e.selectable)) {
|
|
127
|
+
assert.notEqual(c.predicate, undefined, `${c.key}: a selectable charter must declare a predicate field (null = owner-chosen)`);
|
|
128
|
+
assert.notEqual(c.seatsRanks, undefined, `${c.key}: a selectable charter must declare seatsRanks`);
|
|
129
|
+
|
|
130
|
+
if (c.predicate === null) {
|
|
131
|
+
// Owner-chosen (Foundation's custom trustee rank). Honest rather than
|
|
132
|
+
// unchecked: it must SAY the predicate is theirs to name.
|
|
133
|
+
assert.equal(c.seatsRanks, null, `${c.key}: an owner-chosen predicate cannot claim a fixed seat list`);
|
|
134
|
+
assert.match(c.how, /custom/i, `${c.key}: must say the rank is one the owner creates`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const parsed = parseMembershipPredicate(c.predicate);
|
|
139
|
+
assert.ok(parsed, `${c.key}: "${c.predicate}" does not parse — this page would tell an owner to type something that seats NOBODY`);
|
|
140
|
+
assert.deepEqual([...parsed.rankKeys], c.seatsRanks,
|
|
141
|
+
`${c.key}: "${c.predicate}" seats ${JSON.stringify([...parsed.rankKeys])}, but the library claims ${JSON.stringify(c.seatsRanks)}`);
|
|
142
|
+
assert.ok(c.how.includes(c.predicate),
|
|
143
|
+
`${c.key}: the how-line must contain the exact predicate an owner types (expected "${c.predicate}" in "${c.how}")`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('THE REGRESSION: the two lines that were untrue now seat what they say', () => {
|
|
148
|
+
const { parseMembershipPredicate } = require(path.join(GOV, 'membership-predicate.js'));
|
|
149
|
+
const by = Object.fromEntries(charterEntries().map((c) => [c.key, c]));
|
|
150
|
+
|
|
151
|
+
// Direct Democracy said "rank:xenos (everyone)" and seated NEWCOMERS ONLY.
|
|
152
|
+
const dd = by['direct-democracy'];
|
|
153
|
+
assert.equal(dd.predicate, 'rank:xenos+');
|
|
154
|
+
assert.deepEqual([...parseMembershipPredicate(dd.predicate).rankKeys], ['xenos', 'thetes', 'metic', 'archon'],
|
|
155
|
+
'every builder, which is what "everyone" has to mean');
|
|
156
|
+
assert.ok(!/rank:xenos[^+]/.test(dd.how), 'the how-line must not show the bare form that caused the bug');
|
|
157
|
+
|
|
158
|
+
// Timocratic Republic was true as far as it went and silent about Archons.
|
|
159
|
+
const tr = by['timocratic-republic'];
|
|
160
|
+
assert.equal(tr.predicate, 'rank:metic+');
|
|
161
|
+
assert.deepEqual([...parseMembershipPredicate(tr.predicate).rankKeys], ['metic', 'archon']);
|
|
162
|
+
assert.match(tr.how, /Archons included/i, 'the omission that made it quietly surprising is now stated');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('the recommendation names a charter the config can actually express', () => {
|
|
166
|
+
// The library recommended "a consent circle of all trusted members" at
|
|
167
|
+
// headcount 3–8 — a phrase with no predicate behind it under exact-match. The
|
|
168
|
+
// recommendation must point at a charter this page can tell an owner to type.
|
|
169
|
+
const by = Object.fromEntries(charterEntries().map((c) => [c.key, c]));
|
|
170
|
+
for (const n of [1, 3, 8, 9, 50, 150]) {
|
|
171
|
+
const rec = recommendedCharter(n);
|
|
172
|
+
const charter = by[rec.key];
|
|
173
|
+
assert.ok(charter, `headcount ${n} recommends "${rec.key}", which is not in the library`);
|
|
174
|
+
if (charter.selectable) {
|
|
175
|
+
assert.ok(charter.predicate !== undefined,
|
|
176
|
+
`headcount ${n} recommends selectable "${rec.key}" — it must carry a checkable predicate`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const circle = recommendedCharter(3);
|
|
180
|
+
assert.equal(circle.key, 'direct-democracy');
|
|
181
|
+
assert.match(circle.why, /rank:xenos\+/,
|
|
182
|
+
'the why must name the predicate that delivers it, not a phrase with nothing behind it');
|
|
183
|
+
});
|
|
@@ -86,3 +86,56 @@ test('the read rides the vote atom, and the client renders the SERVER value (sou
|
|
|
86
86
|
assert.ok(!/'first_ratifier'[^]*hardcod/i.test(client), 'sanity');
|
|
87
87
|
assert.match(client, /loadConstitution\(\)/, 'the panel loads for everyone the page admits');
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
// ── the hall's membership prose mirrors the server parser (task 1003094) ─────
|
|
91
|
+
//
|
|
92
|
+
// Browser code cannot import the server module (the established pattern here —
|
|
93
|
+
// the five Full Idea section labels are mirrored the same way), so the hall
|
|
94
|
+
// re-implements the predicate grammar to render it in plain language. Two copies
|
|
95
|
+
// of a grammar is exactly the drift that produced the bug this task fixes, so
|
|
96
|
+
// they are held equal here over every form.
|
|
97
|
+
//
|
|
98
|
+
// This matters more than a normal mirror: the Constitution panel is where an
|
|
99
|
+
// owner READS who governs before deciding whether to change it. A panel that
|
|
100
|
+
// misdescribes the franchise is the same failure as a resolver that mis-seats
|
|
101
|
+
// it, one layer up.
|
|
102
|
+
test('the hall renders every predicate form the server actually resolves', () => {
|
|
103
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/government.js'), 'utf8');
|
|
104
|
+
|
|
105
|
+
// Lift the browser mirror out of the IIFE and run it.
|
|
106
|
+
const ladder = /const STANDARD_LADDER = (\[[^\]]*\]);/.exec(src);
|
|
107
|
+
assert.ok(ladder, 'the hall must declare the standard ladder it expands `+` against');
|
|
108
|
+
const fn = /function membershipRankKeys\(predicate\) \{[\s\S]*?\n \}/.exec(src);
|
|
109
|
+
assert.ok(fn, 'membershipRankKeys must be findable — this test lifts it');
|
|
110
|
+
// eslint-disable-next-line no-new-func
|
|
111
|
+
const browserKeys = new Function(`const STANDARD_LADDER = ${ladder[1]}; ${fn[0]}; return membershipRankKeys;`)();
|
|
112
|
+
|
|
113
|
+
const { parseMembershipPredicate } = require(path.join(ROOT, 'modules/government/membership-predicate.js'));
|
|
114
|
+
|
|
115
|
+
const forms = [
|
|
116
|
+
'rank:archon', 'rank:metic', 'rank:xenos', 'rank:custom-rank',
|
|
117
|
+
'rank:metic+', 'rank:xenos+', 'rank:archon+',
|
|
118
|
+
'rank:metic,archon', 'rank:metic+,ops', 'rank:archon,archon',
|
|
119
|
+
'rank:custom+', 'rank:', 'rank:metic++', 'rank:+metic', 'nonsense', '',
|
|
120
|
+
'rank:archon,', 'rank:,archon', 'rank:archon,,metic',
|
|
121
|
+
];
|
|
122
|
+
for (const f of forms) {
|
|
123
|
+
const server = parseMembershipPredicate(f);
|
|
124
|
+
const browser = browserKeys(f);
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
browser === null ? null : browser,
|
|
127
|
+
server === null ? null : [...server.rankKeys],
|
|
128
|
+
`the hall and the server disagree about ${JSON.stringify(f)}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('the ladder the hall expands against IS the government ladder', () => {
|
|
134
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/government.js'), 'utf8');
|
|
135
|
+
const { RANK_ORDER } = require(path.join(ROOT, 'modules/government/catalog.js'));
|
|
136
|
+
const m = /const STANDARD_LADDER = \[([^\]]*)\]/.exec(src);
|
|
137
|
+
assert.ok(m);
|
|
138
|
+
const hall = m[1].split(',').map((x) => x.trim().replace(/['"]/g, '')).filter(Boolean);
|
|
139
|
+
assert.deepEqual(hall, [...RANK_ORDER],
|
|
140
|
+
'a hall ladder that drifts from catalog.RANK_ORDER renders a franchise nobody has');
|
|
141
|
+
});
|
|
@@ -107,6 +107,15 @@ const STATIC_LADDER_FILES = [
|
|
|
107
107
|
'modules/hall-ui/public/ranks.js',
|
|
108
108
|
'modules/hall-ui/public/profile.js',
|
|
109
109
|
'modules/hall-ui/public/watch.js',
|
|
110
|
+
// task 1003094: the Constitution panel expands the `rank:<key>+` membership
|
|
111
|
+
// form ("that rank and above") to render who governs in plain language, so it
|
|
112
|
+
// needs the ladder to expand against. Browser code cannot require the server
|
|
113
|
+
// module — the established mirror pattern here — and listing it in this array
|
|
114
|
+
// is what makes the copy provable rather than merely allowed. A second,
|
|
115
|
+
// government-specific pin lives in tests/government_constitution_view.mjs,
|
|
116
|
+
// which holds it against catalog.RANK_ORDER and holds the whole browser parser
|
|
117
|
+
// equal to the server one.
|
|
118
|
+
'modules/hall-ui/public/government.js',
|
|
110
119
|
];
|
|
111
120
|
test('static ladder copies carry the exact canonical ladder', () => {
|
|
112
121
|
for (const rel of STATIC_LADDER_FILES) {
|