@bongos/core 1.21.89 → 1.21.91
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 +47 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +37 -1
- package/docs/api-reference.md +8 -2
- package/docs/copy-inventory.md +7 -7
- package/docs/copy-registry.json +7 -7
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +266 -274
- package/modules/government/routes/government.js +4 -0
- package/modules/hall-ui/public/craft-mode.js +60 -0
- package/modules/hall-ui/public/style.css +32 -0
- package/modules/hall-ui/records/craft-mode.md +1 -1
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +55 -0
- package/modules/lifecycle/routes/moving.js +30 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/craft_mode_tallies.mjs +149 -0
- package/tests/government_board_routes.mjs +361 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// tests/government_board_routes.mjs — route-level tests for three Board Room
|
|
2
|
+
// routes that had none (task 1004655):
|
|
3
|
+
//
|
|
4
|
+
// POST /government/board/charter (the genesis charter pick)
|
|
5
|
+
// POST /government/board/items/:itemId/withdraw (a proposer takes back a sitting)
|
|
6
|
+
// GET /government/board/genesis-stages (who each founding stage waits on)
|
|
7
|
+
//
|
|
8
|
+
// The module/SQL walls are pinned elsewhere (government_charter_forms.mjs,
|
|
9
|
+
// government_board_withdraw.mjs, government_board_genesis_stage.mjs). What those
|
|
10
|
+
// cannot see is the HTTP mapping: which atom gates each route, which status and
|
|
11
|
+
// error code each refusal becomes, and that the SESSION builder (never the body)
|
|
12
|
+
// is the proposer. So this drives a real express app through the real router
|
|
13
|
+
// and the real board / board-withdraw logic, with the REAL requirePermission
|
|
14
|
+
// gate resolving a STUB government port on the seam and the db slice
|
|
15
|
+
// monkeypatched by property — the tests/government_routes.mjs pattern.
|
|
16
|
+
//
|
|
17
|
+
// The live constitution is this checkout's neutral monarchy (rank:archon,
|
|
18
|
+
// first_ratifier) — a guard test below pins that, because the charter cases
|
|
19
|
+
// depend on it.
|
|
20
|
+
//
|
|
21
|
+
// Run: node --test tests/government_board_routes.mjs
|
|
22
|
+
|
|
23
|
+
import { strict as assert } from 'node:assert';
|
|
24
|
+
import { test, after } from 'node:test';
|
|
25
|
+
import http from 'node:http';
|
|
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
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
34
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
35
|
+
const govDb = require(path.join(GOV, 'db.js'));
|
|
36
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
37
|
+
const { readCharter } = require(path.join(GOV, 'charter.js'));
|
|
38
|
+
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
39
|
+
const express = require('express');
|
|
40
|
+
|
|
41
|
+
const SESSION_ID = 42;
|
|
42
|
+
const MONARCHY = Object.freeze({
|
|
43
|
+
membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── harness ─────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
// Every db function a test stubs is recorded, then restored after the test.
|
|
49
|
+
const calls = [];
|
|
50
|
+
async function withDb(stubs, fn) {
|
|
51
|
+
const originals = {};
|
|
52
|
+
for (const [k, v] of Object.entries(stubs)) {
|
|
53
|
+
originals[k] = govDb[k];
|
|
54
|
+
govDb[k] = (...args) => { calls.push([k, ...args]); return v(...args); };
|
|
55
|
+
}
|
|
56
|
+
try { return await fn(); } finally { for (const [k, v] of Object.entries(originals)) govDb[k] = v; }
|
|
57
|
+
}
|
|
58
|
+
const called = (name) => calls.filter(([k]) => k === name);
|
|
59
|
+
|
|
60
|
+
// Fake the session (requireBuilder is captured at router build time); keep the
|
|
61
|
+
// REAL requirePermission gate, which resolves the government port per request.
|
|
62
|
+
const realRequireBuilder = api.requireBuilder;
|
|
63
|
+
api.requireBuilder = (req, res, next) => { req.builder = { id: SESSION_ID, rank: 'archon' }; next(); };
|
|
64
|
+
|
|
65
|
+
function grant(keys) {
|
|
66
|
+
seams._reset();
|
|
67
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => keys });
|
|
68
|
+
}
|
|
69
|
+
const ALL = ['board.item.open', 'board.vote.cast'];
|
|
70
|
+
grant(ALL);
|
|
71
|
+
|
|
72
|
+
const buildGovernmentRouter = require(path.join(GOV, 'routes', 'government.js'));
|
|
73
|
+
const app = express();
|
|
74
|
+
app.use(express.json());
|
|
75
|
+
app.use(attachFail);
|
|
76
|
+
app.use(buildGovernmentRouter());
|
|
77
|
+
const server = http.createServer(app);
|
|
78
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
79
|
+
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
80
|
+
|
|
81
|
+
after(() => {
|
|
82
|
+
server.close();
|
|
83
|
+
api.requireBuilder = realRequireBuilder;
|
|
84
|
+
seams._reset();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
async function call(method, p, body) {
|
|
88
|
+
calls.length = 0;
|
|
89
|
+
const res = await fetch(`${BASE}${p}`, {
|
|
90
|
+
method,
|
|
91
|
+
headers: body === undefined ? {} : { 'content-type': 'application/json' },
|
|
92
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
93
|
+
});
|
|
94
|
+
let json = null;
|
|
95
|
+
try { json = await res.json(); } catch { /* no body */ }
|
|
96
|
+
return { status: res.status, json };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The db a successful charter filing walks through: the amendment row, then
|
|
100
|
+
// its sitting under the constitution IN FORCE.
|
|
101
|
+
const filingDb = (over = {}) => ({
|
|
102
|
+
builderHoldsRankActive: async () => true,
|
|
103
|
+
getRankByKey: async () => null,
|
|
104
|
+
createAmendment: async (a) => ({ id: '5', proposed: a.proposed, proposed_by: a.proposedBy }),
|
|
105
|
+
openBoardItem: async (a) => ({
|
|
106
|
+
id: '21', subject_type: a.subjectType, subject_id: '5', opened_by: a.openedBy,
|
|
107
|
+
constitution: a.constitution, closes_at: null, closed_at: null,
|
|
108
|
+
}),
|
|
109
|
+
getActiveBuildersWithRank: async () => [],
|
|
110
|
+
...over,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('this checkout runs the neutral monarchy (the charter cases below depend on it)', () => {
|
|
114
|
+
assert.equal(readCharter(config.loadGovernmentConfig().board).form, 'monarchy');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── the rank wall ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
// Holding the OTHER board atom is not enough: charter and withdraw need
|
|
120
|
+
// board.item.open, the stage read needs board.vote.cast. One test per route, so
|
|
121
|
+
// a broken gate names its route.
|
|
122
|
+
const unreachable = {
|
|
123
|
+
listLatestGenesisStageItems: async () => { throw new Error('must not be reached'); },
|
|
124
|
+
getBoardItemForVote: async () => { throw new Error('must not be reached'); },
|
|
125
|
+
createAmendment: async () => { throw new Error('must not be reached'); },
|
|
126
|
+
};
|
|
127
|
+
for (const [method, p, body, held, missing] of [
|
|
128
|
+
['POST', '/government/board/charter', { form: 'direct-democracy' }, 'board.vote.cast', 'board.item.open'],
|
|
129
|
+
['POST', '/government/board/items/21/withdraw', {}, 'board.vote.cast', 'board.item.open'],
|
|
130
|
+
['GET', '/government/board/genesis-stages', undefined, 'board.item.open', 'board.vote.cast'],
|
|
131
|
+
]) {
|
|
132
|
+
test(`${method} ${p} refuses a builder without ${missing} (403 permission_forbidden), before the db`, async () => {
|
|
133
|
+
grant([held]);
|
|
134
|
+
try {
|
|
135
|
+
await withDb(unreachable, async () => {
|
|
136
|
+
const r = await call(method, p, body);
|
|
137
|
+
assert.equal(r.status, 403, `${method} ${p} must deny without ${missing}`);
|
|
138
|
+
assert.equal(r.json.error.code, 'permission_forbidden');
|
|
139
|
+
assert.equal(calls.length, 0, 'the refusal is before the db');
|
|
140
|
+
});
|
|
141
|
+
} finally { grant(ALL); }
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── POST /government/board/charter ──────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
test('charter: a new form is filed as an amendment by the SESSION builder, 201 with what it reads as', async () => {
|
|
148
|
+
await withDb(filingDb(), async () => {
|
|
149
|
+
// A body cannot name the proposer: the strict schema refuses the field outright.
|
|
150
|
+
const spoof = await call('POST', '/government/board/charter', { form: 'direct-democracy', proposed_by: 999 });
|
|
151
|
+
assert.equal(spoof.status, 400);
|
|
152
|
+
assert.equal(spoof.json.error.code, 'validation_failed');
|
|
153
|
+
assert.equal(called('createAmendment').length, 0);
|
|
154
|
+
|
|
155
|
+
const r = await call('POST', '/government/board/charter', { form: 'direct-democracy' });
|
|
156
|
+
assert.equal(r.status, 201);
|
|
157
|
+
assert.equal(r.json.ok, true);
|
|
158
|
+
assert.equal(r.json.reads_as.form, 'direct-democracy');
|
|
159
|
+
assert.equal(r.json.item.id, '21');
|
|
160
|
+
const [filed] = called('createAmendment');
|
|
161
|
+
assert.ok(filed, 'an amendment was filed');
|
|
162
|
+
assert.equal(filed[1].proposed.membership, 'rank:xenos+');
|
|
163
|
+
assert.equal(filed[1].proposedBy, SESSION_ID, 'the proposer is the session, never the body');
|
|
164
|
+
const [opened] = called('openBoardItem');
|
|
165
|
+
assert.equal(opened[1].openedBy, SESSION_ID);
|
|
166
|
+
assert.deepEqual(opened[1].constitution, config.loadGovernmentConfig().board,
|
|
167
|
+
'the sitting runs under the constitution IN FORCE, not the one proposed');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('charter: picking the form already in force is no_change 409 and files nothing', async () => {
|
|
172
|
+
await withDb(filingDb(), async () => {
|
|
173
|
+
const r = await call('POST', '/government/board/charter', { form: 'monarchy' });
|
|
174
|
+
assert.equal(r.status, 409);
|
|
175
|
+
assert.equal(r.json.error.code, 'no_change');
|
|
176
|
+
assert.equal(called('createAmendment').length, 0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('charter: an unreadable choice is invalid_charter 400 naming the reason, and files nothing', async () => {
|
|
181
|
+
await withDb(filingDb(), async () => {
|
|
182
|
+
for (const [body, reason] of [
|
|
183
|
+
[{ form: 'no-such-form' }, 'unknown_form'],
|
|
184
|
+
[{}, 'nothing_chosen'],
|
|
185
|
+
[{ franchise: 'nobody-knows' }, 'unknown_franchise'],
|
|
186
|
+
[{ pass_rule: 'coin_toss' }, 'unknown_pass_rule'],
|
|
187
|
+
[{ franchise: 'council' }, 'council_needs_a_rank'],
|
|
188
|
+
]) {
|
|
189
|
+
const r = await call('POST', '/government/board/charter', body);
|
|
190
|
+
assert.equal(r.status, 400, JSON.stringify(body));
|
|
191
|
+
assert.equal(r.json.error.code, 'invalid_charter', JSON.stringify(body));
|
|
192
|
+
assert.equal(r.json.error.details.reason, reason, JSON.stringify(body));
|
|
193
|
+
}
|
|
194
|
+
// A council must be a rank that exists; the refusal names the key.
|
|
195
|
+
const ghost = await call('POST', '/government/board/charter', { franchise: 'council', council_rank: 'ghosts' });
|
|
196
|
+
assert.equal(ghost.status, 400);
|
|
197
|
+
assert.equal(ghost.json.error.code, 'invalid_charter');
|
|
198
|
+
assert.equal(ghost.json.error.details.reason, 'unknown_council_rank');
|
|
199
|
+
assert.equal(ghost.json.error.details.rank_key, 'ghosts');
|
|
200
|
+
assert.equal(called('createAmendment').length, 0);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('charter: a malformed body is refused before the board is asked', async () => {
|
|
205
|
+
await withDb(filingDb(), async () => {
|
|
206
|
+
const typed = await call('POST', '/government/board/charter', { form: 7 });
|
|
207
|
+
assert.equal(typed.status, 400);
|
|
208
|
+
assert.equal(typed.json.error.code, 'validation_failed');
|
|
209
|
+
const clock = await call('POST', '/government/board/charter', { form: 'direct-democracy', window_minutes: 1.5 });
|
|
210
|
+
assert.equal(clock.status, 400);
|
|
211
|
+
assert.equal(clock.json.error.code, 'invalid_proposal');
|
|
212
|
+
assert.equal(clock.json.error.details.reason, 'bad_window_minutes');
|
|
213
|
+
assert.equal(called('createAmendment').length, 0);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('charter: a clock (window_minutes) is accepted and filed with the proposal', async () => {
|
|
218
|
+
// The handler reads window_minutes and the charter builder files it, so the
|
|
219
|
+
// strict schema must declare it — otherwise every request carrying a clock
|
|
220
|
+
// is refused as an unknown field and the clock can never be chosen here.
|
|
221
|
+
await withDb(filingDb(), async () => {
|
|
222
|
+
const r = await call('POST', '/government/board/charter', { pass_rule: 'consent', window_minutes: 90 });
|
|
223
|
+
assert.equal(r.status, 201, JSON.stringify(r.json));
|
|
224
|
+
const [filed] = called('createAmendment');
|
|
225
|
+
assert.equal(filed[1].proposed.window_minutes, 90);
|
|
226
|
+
assert.equal(filed[1].proposed.pass_rule, 'consent');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('charter: a pick that unseats the proposer is 409 proposer_would_lose_their_seat until acknowledged', async () => {
|
|
231
|
+
await withDb(filingDb({ builderHoldsRankActive: async () => false }), async () => {
|
|
232
|
+
const r = await call('POST', '/government/board/charter', { form: 'direct-democracy' });
|
|
233
|
+
assert.equal(r.status, 409);
|
|
234
|
+
assert.equal(r.json.error.code, 'proposer_would_lose_their_seat');
|
|
235
|
+
assert.equal(r.json.error.details.membership, 'rank:xenos+');
|
|
236
|
+
assert.deepEqual(r.json.error.details.retry_with, { acknowledge_self_removal: true });
|
|
237
|
+
assert.ok(r.json.error.details.what_this_means, 'the refusal carries the words the hall shows');
|
|
238
|
+
assert.equal(called('createAmendment').length, 0, 'nothing is filed on the refusal');
|
|
239
|
+
|
|
240
|
+
const ack = await call('POST', '/government/board/charter', { form: 'direct-democracy', acknowledge_self_removal: true });
|
|
241
|
+
assert.equal(ack.status, 201, 'the acknowledged pick files');
|
|
242
|
+
assert.equal(called('createAmendment').length, 1);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('charter: a db failure is a plain 500 charter_failed', async () => {
|
|
247
|
+
await withDb(filingDb({ createAmendment: async () => { throw new Error('boom'); } }), async () => {
|
|
248
|
+
const r = await call('POST', '/government/board/charter', { form: 'direct-democracy' });
|
|
249
|
+
assert.equal(r.status, 500);
|
|
250
|
+
assert.equal(r.json.error.code, 'charter_failed');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── POST /government/board/items/:itemId/withdraw ───────────────────────────
|
|
255
|
+
|
|
256
|
+
const openItem = (over = {}) => ({
|
|
257
|
+
id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: String(SESSION_ID),
|
|
258
|
+
constitution: MONARCHY, closed_at: null, expired: false, ...over,
|
|
259
|
+
});
|
|
260
|
+
const withdrawDb = (over = {}) => ({
|
|
261
|
+
getBoardItemForVote: async () => openItem(),
|
|
262
|
+
getVotesForItem: async () => [],
|
|
263
|
+
withdrawBoardItem: async ({ itemId }) => ({ ...openItem(), id: String(itemId), closed_at: 'T', outcome: 'withdrawn' }),
|
|
264
|
+
...over,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('withdraw: the proposer takes back their own unvoted sitting — 200, closed as withdrawn', async () => {
|
|
268
|
+
await withDb(withdrawDb(), async () => {
|
|
269
|
+
const r = await call('POST', '/government/board/items/21/withdraw', {});
|
|
270
|
+
assert.equal(r.status, 200);
|
|
271
|
+
assert.equal(r.json.ok, true);
|
|
272
|
+
assert.equal(r.json.item_closed, true);
|
|
273
|
+
assert.equal(r.json.outcome, 'withdrawn');
|
|
274
|
+
const [w] = called('withdrawBoardItem');
|
|
275
|
+
assert.ok(w, 'the write ran');
|
|
276
|
+
assert.equal(w[1].itemId, '21');
|
|
277
|
+
assert.equal(w[1].builderId, SESSION_ID, 'the withdrawer is the session builder');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('withdraw: someone who did not open the sitting is refused 403 not_proposer', async () => {
|
|
282
|
+
for (const openedBy of ['99', null]) {
|
|
283
|
+
await withDb(withdrawDb({ getBoardItemForVote: async () => openItem({ opened_by: openedBy }) }), async () => {
|
|
284
|
+
const r = await call('POST', '/government/board/items/21/withdraw', {});
|
|
285
|
+
assert.equal(r.status, 403, `opened_by ${openedBy}`);
|
|
286
|
+
assert.equal(r.json.error.code, 'not_proposer');
|
|
287
|
+
assert.equal(r.json.error.details.item_id, 21);
|
|
288
|
+
assert.equal(called('withdrawBoardItem').length, 0, 'nothing is written');
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('withdraw: once anyone has voted it is refused 409 votes_cast', async () => {
|
|
294
|
+
const votes = [{ voter_id: '43', direction: 'no' }];
|
|
295
|
+
await withDb(withdrawDb({ getVotesForItem: async () => votes }), async () => {
|
|
296
|
+
const r = await call('POST', '/government/board/items/21/withdraw', {});
|
|
297
|
+
assert.equal(r.status, 409);
|
|
298
|
+
assert.equal(r.json.error.code, 'votes_cast');
|
|
299
|
+
assert.equal(called('withdrawBoardItem').length, 0, 'the voters keep their say');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('withdraw: missing, closed, expired and raced sittings each get their own refusal', async () => {
|
|
304
|
+
for (const [over, status, code] of [
|
|
305
|
+
[{ getBoardItemForVote: async () => null }, 404, 'item_not_found'],
|
|
306
|
+
[{ getBoardItemForVote: async () => openItem({ closed_at: 'T', outcome: 'passed' }) }, 409, 'item_closed'],
|
|
307
|
+
[{ getBoardItemForVote: async () => openItem({ expired: true }) }, 409, 'vote_window_expired'],
|
|
308
|
+
// A vote landing between the reads and the write: the guarded write finds no row.
|
|
309
|
+
[{ withdrawBoardItem: async () => null }, 409, 'item_changed'],
|
|
310
|
+
]) {
|
|
311
|
+
await withDb(withdrawDb(over), async () => {
|
|
312
|
+
const r = await call('POST', '/government/board/items/21/withdraw', {});
|
|
313
|
+
assert.equal(r.status, status, code);
|
|
314
|
+
assert.equal(r.json.error.code, code);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('withdraw: a non-numeric item id is 400 before the db', async () => {
|
|
320
|
+
await withDb(withdrawDb(), async () => {
|
|
321
|
+
const r = await call('POST', '/government/board/items/abc/withdraw', {});
|
|
322
|
+
assert.equal(r.status, 400);
|
|
323
|
+
assert.equal(r.json.error.code, 'bad_item_id');
|
|
324
|
+
assert.equal(calls.length, 0);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ── GET /government/board/genesis-stages ────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
test('genesis-stages: five stages, and an open sitting names who it is still waiting on', async () => {
|
|
331
|
+
const sitting = {
|
|
332
|
+
id: '90', subject_type: 'genesis_stage', subject_id: '3', opened_by: String(SESSION_ID),
|
|
333
|
+
opened_at: 'T0', closes_at: null, closed_at: null, outcome: null, remaining_seconds: null, constitution: MONARCHY,
|
|
334
|
+
};
|
|
335
|
+
const passed = { ...sitting, id: '80', subject_id: '1', closed_at: 'T1', outcome: 'passed' };
|
|
336
|
+
await withDb({
|
|
337
|
+
listLatestGenesisStageItems: async () => [passed, sitting],
|
|
338
|
+
getVotesForItems: async () => [],
|
|
339
|
+
getActiveBuildersWithRank: async () => [{ builder_id: '42', github_login: 'founder', display_name: 'Founder' }],
|
|
340
|
+
}, async () => {
|
|
341
|
+
const r = await call('GET', '/government/board/genesis-stages');
|
|
342
|
+
assert.equal(r.status, 200);
|
|
343
|
+
const stages = r.json.stages;
|
|
344
|
+
assert.deepEqual(stages.map((s) => s.key), ['scope', 'strategize', 'charter', 'legalize', 'legislation']);
|
|
345
|
+
assert.equal(stages[0].state, 'passed');
|
|
346
|
+
assert.equal(stages[1].state, 'not_put');
|
|
347
|
+
assert.equal(stages[2].state, 'sitting');
|
|
348
|
+
assert.equal(stages[2].item_id, '90');
|
|
349
|
+
assert.equal(stages[2].one_vote_decides, true);
|
|
350
|
+
assert.deepEqual(stages[2].waiting_on.map((m) => m.builder_id), ['42']);
|
|
351
|
+
assert.deepEqual(called('getVotesForItems')[0][1], ['90'], 'only the open sitting is asked for votes');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('genesis-stages: a db failure is a plain 500 genesis_stage_read_failed', async () => {
|
|
356
|
+
await withDb({ listLatestGenesisStageItems: async () => { throw new Error('boom'); } }, async () => {
|
|
357
|
+
const r = await call('GET', '/government/board/genesis-stages');
|
|
358
|
+
assert.equal(r.status, 500);
|
|
359
|
+
assert.equal(r.json.error.code, 'genesis_stage_read_failed');
|
|
360
|
+
});
|
|
361
|
+
});
|