@bongos/core 1.21.89 → 1.21.90
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 +21 -16
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +1 -0
- package/docs/api-reference.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/government/routes/government.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/government_board_routes.mjs +361 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.90",
|
|
6
|
+
"core_contract": "1.21.90",
|
|
7
|
+
"source_commit": "2dd5be5837e93b2d103d299c09f9337f17c8797b",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-09T05:
|
|
9
|
+
"built_at": "2026-10-09T05:58:47.014Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 590,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3003,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3622,
|
|
20
|
+
"tree_sha256": "5b43a0f4fea472ff637381cb9e1b3e09954adaaeca4030de5e80e8a5ff0d4965",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -332,7 +332,7 @@
|
|
|
332
332
|
{
|
|
333
333
|
"path": "clients/bongos-client/index.d.ts",
|
|
334
334
|
"mode": "0000644",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "963ca486acac3ab8f532e729adc5be1823c67be1f46f6fbd6364386066517ebd"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -2317,12 +2317,12 @@
|
|
|
2317
2317
|
{
|
|
2318
2318
|
"path": "docs/api-reference.md",
|
|
2319
2319
|
"mode": "0000644",
|
|
2320
|
-
"sha256": "
|
|
2320
|
+
"sha256": "d31cb1d2be0b2cfaba9cde0815fd2656ffd0db7f1ce1a1d9295926537501d679"
|
|
2321
2321
|
},
|
|
2322
2322
|
{
|
|
2323
2323
|
"path": "docs/api/openapi.json",
|
|
2324
2324
|
"mode": "0000644",
|
|
2325
|
-
"sha256": "
|
|
2325
|
+
"sha256": "fdc092f72bf761431cef7279a5aef6013acc31b3fa0e9191e5583186df73a3c4"
|
|
2326
2326
|
},
|
|
2327
2327
|
{
|
|
2328
2328
|
"path": "docs/architecture.md",
|
|
@@ -2862,7 +2862,7 @@
|
|
|
2862
2862
|
{
|
|
2863
2863
|
"path": "docs/module-api-changelog.md",
|
|
2864
2864
|
"mode": "0000644",
|
|
2865
|
-
"sha256": "
|
|
2865
|
+
"sha256": "0a5d631b97f6fea01a2941927406f27e2ea58ed9914bacbfd60effbaf920a5ad"
|
|
2866
2866
|
},
|
|
2867
2867
|
{
|
|
2868
2868
|
"path": "docs/modules-contract.md",
|
|
@@ -5157,7 +5157,7 @@
|
|
|
5157
5157
|
{
|
|
5158
5158
|
"path": "modules/government/routes/government.js",
|
|
5159
5159
|
"mode": "0000644",
|
|
5160
|
-
"sha256": "
|
|
5160
|
+
"sha256": "a77bd940b28aba83d1a567cfe6a0fc984688fa2b2b443d5ca80e79e02e55d95f"
|
|
5161
5161
|
},
|
|
5162
5162
|
{
|
|
5163
5163
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -10097,12 +10097,12 @@
|
|
|
10097
10097
|
{
|
|
10098
10098
|
"path": "package-lock.json",
|
|
10099
10099
|
"mode": "0000644",
|
|
10100
|
-
"sha256": "
|
|
10100
|
+
"sha256": "9c11125be8e792dab7ec2cf20dc39ebd25bc7e9a5d6a3c9ed0638dd65a6c2a5c"
|
|
10101
10101
|
},
|
|
10102
10102
|
{
|
|
10103
10103
|
"path": "package.json",
|
|
10104
10104
|
"mode": "0000644",
|
|
10105
|
-
"sha256": "
|
|
10105
|
+
"sha256": "9cae3f7cea2893dd528990e19282d6a416886584bd210a3b51a1086796b9419d"
|
|
10106
10106
|
},
|
|
10107
10107
|
{
|
|
10108
10108
|
"path": "public-docs/index.html",
|
|
@@ -10122,7 +10122,7 @@
|
|
|
10122
10122
|
{
|
|
10123
10123
|
"path": "release-notes.json",
|
|
10124
10124
|
"mode": "0000644",
|
|
10125
|
-
"sha256": "
|
|
10125
|
+
"sha256": "8864e076c0c535fb2bd5fee5149905b4c36ca41e8f1de18d702b7c82267cfc01"
|
|
10126
10126
|
},
|
|
10127
10127
|
{
|
|
10128
10128
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -12382,7 +12382,7 @@
|
|
|
12382
12382
|
{
|
|
12383
12383
|
"path": "src/module-api.js",
|
|
12384
12384
|
"mode": "0000644",
|
|
12385
|
-
"sha256": "
|
|
12385
|
+
"sha256": "a5a148f77f78c978b7526e75795fb74dfd5cf42fa10a77aa75b7c2f977fbff76"
|
|
12386
12386
|
},
|
|
12387
12387
|
{
|
|
12388
12388
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14314,6 +14314,11 @@
|
|
|
14314
14314
|
"mode": "0000644",
|
|
14315
14315
|
"sha256": "cf7e31016f76de229e3fb1bf68558fec10011aef041e2acc763fd53bcff24061"
|
|
14316
14316
|
},
|
|
14317
|
+
{
|
|
14318
|
+
"path": "tests/government_board_routes.mjs",
|
|
14319
|
+
"mode": "0000644",
|
|
14320
|
+
"sha256": "86ab4c52a0b14c4d44f3bcfb48d80461bfbe7ca57ad9eb6da4f6ffa3ba130729"
|
|
14321
|
+
},
|
|
14317
14322
|
{
|
|
14318
14323
|
"path": "tests/government_board_summons.mjs",
|
|
14319
14324
|
"mode": "0000644",
|
|
@@ -363,7 +363,7 @@ export interface PostGoalsReorderResponse { ok: boolean; goals: unknown }
|
|
|
363
363
|
export interface PostGoalsRequest { version_id: string; title: string; subtitle?: string; description?: string; scope_modules?: string[]; succeeds_goal_id?: number; category_id?: number; admit_reason?: string; spec_task?: boolean }
|
|
364
364
|
export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown; category_advisory: unknown; admitted: unknown; spec_task: unknown }
|
|
365
365
|
export interface PostGovernmentAssignmentsRequest { builder_id: number; rank_key: string }
|
|
366
|
-
export interface PostGovernmentBoardCharterRequest { form?: string; franchise?: string; council_rank?: string; pass_rule?: string; rationale_md?: string; acknowledge_self_removal?: boolean }
|
|
366
|
+
export interface PostGovernmentBoardCharterRequest { form?: string; franchise?: string; council_rank?: string; pass_rule?: string; window_minutes?: unknown; rationale_md?: string; acknowledge_self_removal?: boolean }
|
|
367
367
|
export interface PostGovernmentBoardCharterResponse { ok: boolean; amendment: unknown; item: unknown; reads_as: unknown }
|
|
368
368
|
export interface PostGovernmentBoardDecisionRulesRequest { rules: unknown[]; rationale_md?: string }
|
|
369
369
|
export interface PostGovernmentBoardDecisionRulesResponse { ok: boolean; amendment: unknown; item: unknown; rules: unknown }
|
package/docs/api/openapi.json
CHANGED
|
@@ -30306,6 +30306,7 @@
|
|
|
30306
30306
|
"type": "string",
|
|
30307
30307
|
"maxLength": 32
|
|
30308
30308
|
},
|
|
30309
|
+
"window_minutes": {},
|
|
30309
30310
|
"rationale_md": {
|
|
30310
30311
|
"type": "string",
|
|
30311
30312
|
"maxLength": 5000
|
package/docs/api-reference.md
CHANGED
|
@@ -348,7 +348,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
348
348
|
| GET | `/api/bongos/government/assignments` | `archon` | — | government.manage — every builder↔rank assignment row. |
|
|
349
349
|
| POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM rank to a builder. |
|
|
350
350
|
| DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-rank assignment. |
|
|
351
|
-
| POST | `/api/bongos/government/board/charter` | `metic+archon` | `form`, `franchise`, `council_rank`, `pass_rule`, `rationale_md`, `acknowledge_self_removal` | board.item.open — the genesis charter + legislation choice (task 1004427, spec D10): a form of government and/or its two live settings (f… |
|
|
351
|
+
| POST | `/api/bongos/government/board/charter` | `metic+archon` | `form`, `franchise`, `council_rank`, `pass_rule`, `window_minutes`, `rationale_md`, `acknowledge_self_removal` | board.item.open — the genesis charter + legislation choice (task 1004427, spec D10): a form of government and/or its two live settings (f… |
|
|
352
352
|
| POST | `/api/bongos/government/board/decision-rules` | `metic+archon` | `rules`, `rationale_md` | board.item.open — propose a change to WHO DECIDES WHAT (task 1004529): one or more buildings' rows of the decision-rules table, filed as … |
|
|
353
353
|
| GET | `/api/bongos/government/board/genesis-stages` | `any-builder` | — | board.vote.cast — the founding band's read (task 1004429): each genesis stage's latest sitting, and who it is still waiting on under the … |
|
|
354
354
|
| GET | `/api/bongos/government/board/items` | `any-builder` | — | board.vote.cast — the Board Room view: open items with their clock, the OPEN BALLOT, the closed history (R16 of goal 1000069). |
|
|
@@ -3010,5 +3010,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3010
3010
|
landed since 1.21.87 with no explicit bump. run 37888371878. (task 1002620)
|
|
3011
3011
|
1.21.89 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3012
3012
|
landed since 1.21.88 with no explicit bump. run 37889772259. (task 1002620)
|
|
3013
|
+
1.21.90 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3014
|
+
landed since 1.21.89 with no explicit bump. run 37891104032. (task 1002620)
|
|
3013
3015
|
---------------------------------------------------------------------------
|
|
3014
3016
|
```
|
|
@@ -750,6 +750,10 @@ module.exports = function buildGovernmentRouter() {
|
|
|
750
750
|
franchise: { type: 'string', maxLength: 32 },
|
|
751
751
|
council_rank: { type: 'string', maxLength: RANK_KEY_MAX },
|
|
752
752
|
pass_rule: { type: 'string', maxLength: 32 },
|
|
753
|
+
// Declared with no type rule: null (no clock) or a whole number, which the
|
|
754
|
+
// check just below enforces. Left out, the strict schema refused every
|
|
755
|
+
// request carrying a clock as an unknown field (found by task 1004655).
|
|
756
|
+
window_minutes: {},
|
|
753
757
|
rationale_md: { type: 'string', maxLength: 5000 },
|
|
754
758
|
acknowledge_self_removal: { type: 'boolean' },
|
|
755
759
|
})) return;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.90",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.90",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.90",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -9364,5 +9364,11 @@
|
|
|
9364
9364
|
"id": "1004656",
|
|
9365
9365
|
"text": "A new project now really runs under the government its founder picks. Before, the choice made while creating a project was stored and then ignored, so every project stayed under one-person rule. Now picking a government files"
|
|
9366
9366
|
}
|
|
9367
|
+
],
|
|
9368
|
+
"1.21.90": [
|
|
9369
|
+
{
|
|
9370
|
+
"id": "1004655",
|
|
9371
|
+
"text": "Three government actions now have tests that check them the way the site really answers: choosing a form of government, taking back your own proposal before anyone votes, and seeing who each founding stage is still waiting on."
|
|
9372
|
+
}
|
|
9367
9373
|
]
|
|
9368
9374
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.90'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -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
|
+
});
|