@bongos/core 1.21.4 → 1.21.5
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 +118 -63
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +35 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +389 -518
- package/docs/copy-registry.json +880 -2143
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +17 -25
- package/docs/page-readings.json +1008 -1246
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +5 -0
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +23 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +14 -0
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +41 -0
- package/src/bongos/routes/modules.js +17 -0
- package/src/module-api.js +1 -1
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +409 -277
- package/tests/wizard_six_screens.mjs +126 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// tests/governor_docket.mjs — the governor's docket (task 1004523, goal 1000125):
|
|
2
|
+
// ONE read of every matter awaiting a governor, which the Governor City screens
|
|
3
|
+
// (tasks 1004524-1004528) all draw from. modules/government/docket.js collects
|
|
4
|
+
// sources contributed to the `governor.docket` point by their owning modules.
|
|
5
|
+
//
|
|
6
|
+
// What this pins:
|
|
7
|
+
// 1. THE MAPPER — normalizeMatter's one shape, and its fail-safe defaults (an
|
|
8
|
+
// unknown state is never a false "needs you"; an unknown rule is null).
|
|
9
|
+
// 2. THE BOARD'S PER-VIEWER STATE mirrors the /me summons (pendingVotesFor):
|
|
10
|
+
// needs = member, not cast, not the author, not expired; else moving/settled.
|
|
11
|
+
// 3. RANK SCOPING — a source the viewer may not see returns NO rows and reports
|
|
12
|
+
// `not_yours`; a contribution with no recognisable gate is never run; a
|
|
13
|
+
// gate-less or malformed one cannot widen anything (fail-closed).
|
|
14
|
+
// 4. NOTHING INVENTED — a design-named source with nothing recorded is `coming`,
|
|
15
|
+
// a module that is off is `off`, a read that throws or hangs is `unavailable`.
|
|
16
|
+
// 5. EVERY CONTRIBUTED SOURCE DECLARES ITS OWN LIST ROUTE'S ATOM — read off the
|
|
17
|
+
// route source by the route-rank auditor, so the city can never show a viewer
|
|
18
|
+
// a matter the owning route would refuse them.
|
|
19
|
+
// 6. THE ROUTE — GET /government/docket gates on board.vote.cast and answers the
|
|
20
|
+
// documented shape through the real router.
|
|
21
|
+
//
|
|
22
|
+
// DB-free: db reads are monkeypatched by property; the real seam carries the
|
|
23
|
+
// contributions.
|
|
24
|
+
//
|
|
25
|
+
// Run: node tests/governor_docket.mjs
|
|
26
|
+
|
|
27
|
+
import { strict as assert } from 'node:assert';
|
|
28
|
+
import { test, after } from 'node:test';
|
|
29
|
+
import http from 'node:http';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
38
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
39
|
+
const docket = require(path.join(ROOT, 'modules', 'government', 'docket.js'));
|
|
40
|
+
const vocab = require(path.join(ROOT, 'modules', 'government', 'docket-vocab.js'));
|
|
41
|
+
const board = require(path.join(ROOT, 'modules', 'government', 'board.js'));
|
|
42
|
+
const govDb = require(path.join(ROOT, 'modules', 'government', 'db.js'));
|
|
43
|
+
const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
|
|
44
|
+
const lifecycleDocket = require(path.join(ROOT, 'modules', 'lifecycle', 'governor-docket.js'));
|
|
45
|
+
const coreUpdateRoute = require(path.join(ROOT, 'src', 'bongos', 'routes', 'core-update.js'));
|
|
46
|
+
const ideasDocket = require(path.join(ROOT, 'modules', 'ideas', 'governor-docket.js'));
|
|
47
|
+
const autonomyDocket = require(path.join(ROOT, 'modules', 'autonomy', 'governor-docket.js'));
|
|
48
|
+
const accessRequestsRoute = require(path.join(ROOT, 'modules', 'onboarding', 'routes', 'access-requests.js'));
|
|
49
|
+
const reportsRoute = require(path.join(ROOT, 'modules', 'security', 'routes', 'reports.js'));
|
|
50
|
+
const moduleRechecks = require(path.join(ROOT, 'src', 'bongos', 'module-rechecks.js'));
|
|
51
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
52
|
+
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
53
|
+
const express = require('express');
|
|
54
|
+
|
|
55
|
+
// ── stubs: the board and headcount reads (no DB) ─────────────────────────────
|
|
56
|
+
|
|
57
|
+
const REAL = {
|
|
58
|
+
boardRoomView: board.boardRoomView,
|
|
59
|
+
getActiveBuildersWithRank: govDb.getActiveBuildersWithRank,
|
|
60
|
+
countActiveBuilders: govDb.countActiveBuilders,
|
|
61
|
+
resolveBuilderPermissions: resolver.resolveBuilderPermissions,
|
|
62
|
+
};
|
|
63
|
+
let boardView = { open: [], closed: [] };
|
|
64
|
+
let members = [{ builder_id: 7, github_login: 'ana', display_name: 'Ana' }, { builder_id: 9, github_login: 'theo', display_name: 'Theo' }];
|
|
65
|
+
board.boardRoomView = async () => boardView;
|
|
66
|
+
govDb.getActiveBuildersWithRank = async () => members;
|
|
67
|
+
govDb.countActiveBuilders = async () => 12;
|
|
68
|
+
after(() => {
|
|
69
|
+
board.boardRoomView = REAL.boardRoomView;
|
|
70
|
+
govDb.getActiveBuildersWithRank = REAL.getActiveBuildersWithRank;
|
|
71
|
+
govDb.countActiveBuilders = REAL.countActiveBuilders;
|
|
72
|
+
resolver.resolveBuilderPermissions = REAL.resolveBuilderPermissions;
|
|
73
|
+
seams._reset();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const NOW = Date.parse('2026-10-01T12:00:00Z');
|
|
77
|
+
const CONSENT = { membership: 'rank:archon', pass_rule: 'consent' };
|
|
78
|
+
const item = (over = {}) => ({
|
|
79
|
+
id: '11', subject_type: 'constitutional_amendment', subject_id: '3',
|
|
80
|
+
subject: { rationale_md: '# Give Review a seat\nmore', author_id: '9' },
|
|
81
|
+
opened_at: '2026-09-29T00:00:00Z', closes_at: '2026-10-03T00:00:00Z',
|
|
82
|
+
expired: false, closed_at: null, outcome: null, constitution: CONSENT, votes: [],
|
|
83
|
+
...over,
|
|
84
|
+
});
|
|
85
|
+
const none = () => ({ sources: new Map(), readings: new Map() });
|
|
86
|
+
|
|
87
|
+
// ── 1. the mapper ────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
test('normalizeMatter: the one shape, prefixed id, ISO dates', () => {
|
|
90
|
+
const m = docket.normalizeMatter({
|
|
91
|
+
id: 5, title: 'Admit Kai', rule: 'consent', state: 'needs', opened_at: '2026-09-30T10:00:00Z',
|
|
92
|
+
proposer: { id: 3, login: 'theo' }, tally: { for: 2, against: '1', open: 2, needs: 'x' }, ref: { type: 'a', id: 5 },
|
|
93
|
+
}, { source: 'access_requests', building: 'people' });
|
|
94
|
+
assert.equal(m.id, 'access_requests:5');
|
|
95
|
+
assert.equal(m.building, 'people');
|
|
96
|
+
assert.equal(m.opened_at, '2026-09-30T10:00:00.000Z');
|
|
97
|
+
assert.equal(m.deadline, null);
|
|
98
|
+
assert.deepEqual(m.proposer, { id: '3', login: 'theo', name: null });
|
|
99
|
+
assert.deepEqual(m.tally, { for: 2, against: 1, open: 2, needs: 'x' });
|
|
100
|
+
assert.deepEqual(m.ref, { type: 'a', id: '5' });
|
|
101
|
+
assert.deepEqual(Object.keys(m).sort(), ['building', 'deadline', 'href', 'id', 'opened_at', 'proposer', 'ref', 'rule', 'short', 'source', 'state', 'tally', 'title', 'where'].sort());
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('normalizeMatter fails SAFE: unknown state → moving (never a false "needs you"), unknown rule → null', () => {
|
|
105
|
+
const m = docket.normalizeMatter({ id: 1, title: 't', state: 'urgent!!', rule: 'vibes', building: 'castle' }, { source: 's', building: 'review' });
|
|
106
|
+
assert.equal(m.state, 'moving');
|
|
107
|
+
assert.equal(m.rule, null);
|
|
108
|
+
assert.equal(m.building, 'review', 'an unknown building falls back to the source\'s own');
|
|
109
|
+
assert.equal(docket.normalizeMatter({ title: 'no id' }, { source: 's', building: 'review' }), null);
|
|
110
|
+
assert.equal(docket.normalizeMatter({ id: 1 }, { source: 's', building: 'review' }), null);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('gateAllows is fail-closed on anything it does not recognise', () => {
|
|
114
|
+
const ctx = { has: (p) => p === 'x' };
|
|
115
|
+
assert.equal(docket.gateAllows({ permission: 'x' }, ctx), true);
|
|
116
|
+
assert.equal(docket.gateAllows({ permission: 'y' }, ctx), false);
|
|
117
|
+
assert.equal(docket.gateAllows({ anyBuilder: true }, ctx), true);
|
|
118
|
+
for (const g of [null, undefined, {}, { anyBuilder: 'yes' }, { permission: '' }, 'x', { rank: 'archon' }]) {
|
|
119
|
+
assert.equal(docket.gateAllows(g, ctx), false, JSON.stringify(g));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('homeLine speaks the design\'s words', () => {
|
|
124
|
+
assert.equal(docket.homeLine({ needs: 0, moving: 0, settled: 4 }), 'nothing waits on you.');
|
|
125
|
+
assert.equal(docket.homeLine({ needs: 3, moving: 2, settled: 0 }), '3 matters wait on you · 2 moving');
|
|
126
|
+
assert.equal(docket.homeLine({ needs: 1, moving: 0, settled: 0 }), '1 matter waits on you');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── 2. the board, per viewer ─────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
test('board: a member who has not voted NEEDS to; the same item after their vote is MOVING', () => {
|
|
132
|
+
const viewer = { viewerId: '7', members };
|
|
133
|
+
assert.equal(docket.boardMatter(item(), viewer).state, 'needs');
|
|
134
|
+
const voted = item({ votes: [{ voter_id: '7', direction: 'yes', github_login: 'ana' }] });
|
|
135
|
+
const m = docket.boardMatter(voted, viewer);
|
|
136
|
+
assert.equal(m.state, 'moving');
|
|
137
|
+
assert.deepEqual(m.tally, { for: 1, against: 0, open: 1, needs: 'passes unless someone objects' });
|
|
138
|
+
assert.equal(m.rule, 'consent');
|
|
139
|
+
assert.equal(m.building, 'charter');
|
|
140
|
+
assert.equal(m.title, 'Give Review a seat');
|
|
141
|
+
assert.deepEqual(m.proposer, { id: '9', login: 'theo', name: 'Theo' });
|
|
142
|
+
assert.equal(m.deadline, '2026-10-03T00:00:00Z');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('board: the author is not summoned (the /me summons rule), a non-member sees it settled, an expired one never needs you', () => {
|
|
146
|
+
assert.equal(docket.boardMatter(item(), { viewerId: '9', members }).state, 'moving', 'author');
|
|
147
|
+
assert.equal(docket.boardMatter(item(), { viewerId: '55', members }).state, 'settled', 'not a member');
|
|
148
|
+
assert.equal(docket.boardMatter(item({ expired: true }), { viewerId: '7', members }).state, 'moving', 'expired');
|
|
149
|
+
assert.equal(docket.boardMatter(item({ closed_at: '2026-09-30T00:00:00Z', outcome: 'passed' }), { viewerId: '7', members }).state, 'settled');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('board: a genesis stage has no author to hold back, and a full idea stands at Planning', () => {
|
|
153
|
+
const stage = item({ subject_type: 'genesis_stage', subject_id: '3', subject: { label: 'Charter', author_id: '7' }, opened_by: '7' });
|
|
154
|
+
assert.equal(docket.boardMatter(stage, { viewerId: '7', members }).state, 'needs');
|
|
155
|
+
const idea = item({ subject_type: 'full_idea', subject: { title: 'A template marketplace', author_id: '9' }, constitution: { membership: 'rank:archon', pass_rule: 'unanimous' } });
|
|
156
|
+
const m = docket.boardMatter(idea, { viewerId: '7', members });
|
|
157
|
+
assert.equal(m.building, 'planning');
|
|
158
|
+
assert.equal(m.rule, 'unanimous');
|
|
159
|
+
assert.equal(m.title, 'A template marketplace');
|
|
160
|
+
assert.equal(docket.boardMatter(item({ constitution: { pass_rule: 'nonsense' } }), { viewerId: '7', members }).rule, null);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('the city and the /me summons agree on "needs you": boardMatter and pendingVotesFor over one matrix', async () => {
|
|
164
|
+
// Every combination the rule turns on: membership, cast or not, author or not,
|
|
165
|
+
// and the subject type (a genesis stage has no author to hold back). Each case
|
|
166
|
+
// is one open, unexpired item seen by viewer 7; the two readers must agree.
|
|
167
|
+
const realAwaiting = govDb.openBoardItemsAwaitingVoter;
|
|
168
|
+
const realHolds = govDb.builderHoldsRankActive;
|
|
169
|
+
try {
|
|
170
|
+
for (const member of [true, false]) {
|
|
171
|
+
for (const voted of [true, false]) {
|
|
172
|
+
for (const author of ['7', '9', null]) {
|
|
173
|
+
for (const subjectType of ['constitutional_amendment', 'full_idea', 'genesis_stage']) {
|
|
174
|
+
const label = JSON.stringify({ member, voted, author, subjectType });
|
|
175
|
+
// pendingVotesFor reads the SQL row shape (the genesis author is nulled in SQL).
|
|
176
|
+
govDb.openBoardItemsAwaitingVoter = async () => [{
|
|
177
|
+
id: '11', subject_type: subjectType, subject_id: '3', constitution: CONSENT,
|
|
178
|
+
opened_at: 't', closes_at: null,
|
|
179
|
+
author_id: subjectType === 'genesis_stage' ? null : author,
|
|
180
|
+
already_voted: voted,
|
|
181
|
+
}];
|
|
182
|
+
govDb.builderHoldsRankActive = async () => member;
|
|
183
|
+
const summons = await board.pendingVotesFor('7');
|
|
184
|
+
// boardMatter reads the room's shape.
|
|
185
|
+
const m = docket.boardMatter(item({
|
|
186
|
+
subject_type: subjectType,
|
|
187
|
+
subject: { author_id: author, title: 't', label: 'Charter', rationale_md: 'r' },
|
|
188
|
+
votes: voted ? [{ voter_id: '7', direction: 'yes' }] : [],
|
|
189
|
+
}), { viewerId: '7', members: member ? [{ builder_id: '7' }] : [] });
|
|
190
|
+
assert.equal(m.state === 'needs', summons.count === 1, label);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
govDb.openBoardItemsAwaitingVoter = realAwaiting;
|
|
197
|
+
govDb.builderHoldsRankActive = realHolds;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('awaitsVoter: the one definition, fail-closed', () => {
|
|
202
|
+
const yes = { builderId: '7', authorId: '9', alreadyVoted: false, isMember: true };
|
|
203
|
+
assert.equal(board.awaitsVoter(yes), true);
|
|
204
|
+
assert.equal(board.awaitsVoter({ ...yes, authorId: 7 }), false, 'number and string ids compare equal');
|
|
205
|
+
assert.equal(board.awaitsVoter({ ...yes, authorId: null }), true, 'no author, nobody held back');
|
|
206
|
+
assert.equal(board.awaitsVoter({ ...yes, alreadyVoted: true }), false);
|
|
207
|
+
assert.equal(board.awaitsVoter({ ...yes, isMember: false }), false);
|
|
208
|
+
assert.equal(board.awaitsVoter({ ...yes, expired: true }), false);
|
|
209
|
+
assert.equal(board.awaitsVoter({ ...yes, closed: true }), false);
|
|
210
|
+
assert.equal(board.awaitsVoter({ ...yes, builderId: '' }), false);
|
|
211
|
+
assert.equal(board.awaitsVoter(), false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ── 3 + 4. scoping, statuses, the collector ──────────────────────────────────
|
|
215
|
+
|
|
216
|
+
const VIEWER = { id: 7, rank: 'metic' };
|
|
217
|
+
const contrib = (over) => ({ building: 'release', gate: { anyBuilder: true }, read: async () => [], ...over });
|
|
218
|
+
|
|
219
|
+
test('a source the viewer may not see returns NO rows and reports not_yours — and its read never runs', async () => {
|
|
220
|
+
boardView = { open: [], closed: [] };
|
|
221
|
+
let ran = false;
|
|
222
|
+
const c = none();
|
|
223
|
+
c.sources.set('override_requests', contrib({ gate: { permission: 'override_request.decide' }, read: async () => { ran = true; return [{ id: 1, title: 'secret', state: 'needs' }]; } }));
|
|
224
|
+
const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: c, now: NOW });
|
|
225
|
+
assert.equal(ran, false);
|
|
226
|
+
assert.equal(d.matters.length, 0);
|
|
227
|
+
assert.equal(d.sources.find((s) => s.source === 'override_requests').status, 'not_yours');
|
|
228
|
+
// The same viewer holding the atom sees it.
|
|
229
|
+
const d2 = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'override_request.decide'], contributions: c, now: NOW });
|
|
230
|
+
assert.equal(d2.matters.length, 1);
|
|
231
|
+
assert.equal(d2.matters[0].id, 'override_requests:1');
|
|
232
|
+
assert.equal(d2.sources.find((s) => s.source === 'override_requests').status, 'live');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('the board itself is scoped too: without board.vote.cast it is not_yours', async () => {
|
|
236
|
+
boardView = { open: [item()], closed: [] };
|
|
237
|
+
const d = await docket.buildDocket({ viewer: VIEWER, permissions: [], contributions: none(), now: NOW });
|
|
238
|
+
assert.equal(d.sources.find((s) => s.source === 'board').status, 'not_yours');
|
|
239
|
+
assert.equal(d.matters.length, 0);
|
|
240
|
+
const d2 = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: none(), now: NOW });
|
|
241
|
+
assert.equal(d2.matters.length, 1);
|
|
242
|
+
assert.equal(d2.matters[0].id, 'board:11');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('a contribution without a recognisable gate is DROPPED, never run (registeredContributions)', () => {
|
|
246
|
+
seams._reset();
|
|
247
|
+
seams.contribute(docket.CONTRIBUTION_POINT, { source: 'blockers', building: 'planning', read: async () => [] });
|
|
248
|
+
seams.contribute(docket.CONTRIBUTION_POINT, { source: 'goal_join_requests', building: 'people', gate: { rank: 'xenos' }, read: async () => [] });
|
|
249
|
+
seams.contribute(docket.CONTRIBUTION_POINT, { source: 'removal_proposals', building: 'disputes', gate: { permission: 'task.promote' } });
|
|
250
|
+
seams.contribute(docket.CONTRIBUTION_POINT, { source: 'criteria_uat', building: 'review', gate: { permission: 'criterion.review' }, read: async () => [] });
|
|
251
|
+
const reg = docket.registeredContributions();
|
|
252
|
+
assert.deepEqual([...reg.sources.keys()], ['criteria_uat']);
|
|
253
|
+
seams._reset();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('nothing is invented: coming / off / unavailable are each reported, with no rows', async () => {
|
|
257
|
+
boardView = { open: [], closed: [] };
|
|
258
|
+
const c = none();
|
|
259
|
+
c.sources.set('blockers', contrib({ read: async () => { throw new Error('db down'); } }));
|
|
260
|
+
c.sources.set('core_update', contrib({ read: () => new Promise(() => {}) })); // never answers
|
|
261
|
+
const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: c, now: NOW, timeoutMs: 30 });
|
|
262
|
+
const by = Object.fromEntries(d.sources.map((s) => [s.source, s]));
|
|
263
|
+
assert.equal(by.blockers.status, 'unavailable');
|
|
264
|
+
assert.equal(by.core_update.status, 'unavailable');
|
|
265
|
+
assert.match(by.core_update.reason, /in time/);
|
|
266
|
+
assert.equal(by.access_requests.status, 'off', 'no contributor registered → its module is off');
|
|
267
|
+
assert.equal(by.module_rechecks.status, 'off', 'module re-checks are a real source now, never coming');
|
|
268
|
+
for (const k of ['grade_appeals', 'standing_rules', 'treasury_spending']) {
|
|
269
|
+
assert.equal(by[k].status, 'coming', k);
|
|
270
|
+
assert.ok(by[k].reason, `${k} says why`);
|
|
271
|
+
}
|
|
272
|
+
assert.equal(d.matters.length, 0);
|
|
273
|
+
// every source the design names is reported, in declared order, once
|
|
274
|
+
const keys = d.sources.map((s) => s.source);
|
|
275
|
+
assert.equal(new Set(keys).size, keys.length);
|
|
276
|
+
for (const e of docket.EXPECTED_SOURCES) assert.ok(keys.includes(e.source), e.source);
|
|
277
|
+
assert.equal(keys[0], 'board');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('the response shape: counts, line, street-ordered buildings, sorted matters, six readings, vocabulary', async () => {
|
|
281
|
+
boardView = { open: [item()], closed: [item({ id: '12', closed_at: '2026-09-30T00:00:00Z', outcome: 'passed' }), item({ id: '13', closed_at: '2026-08-01T00:00:00Z', outcome: 'passed' })] };
|
|
282
|
+
const c = none();
|
|
283
|
+
c.sources.set('blockers', contrib({ building: 'planning', read: async () => [{ id: 4, title: 'Which payment provider', state: 'moving', opened_at: '2026-09-20T00:00:00Z' }] }));
|
|
284
|
+
c.sources.set('redteam_reports', contrib({ building: 'disputes', gate: { permission: 'security.report.adjudicate' }, read: async () => [{ id: 2, title: 'Security report: sign-in limits', state: 'needs', rule: 'decide', opened_at: '2026-09-29T00:00:00Z' }] }));
|
|
285
|
+
c.readings.set('flow', { reading: 'flow', gate: { anyBuilder: true }, read: async () => ({ value: 3, figure: '3 stuck', line: 'longest wait 4 days, waiting for review', hot: true }) });
|
|
286
|
+
const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'security.report.adjudicate'], contributions: c, now: NOW });
|
|
287
|
+
|
|
288
|
+
assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'generated_at', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
|
|
289
|
+
assert.deepEqual(d.viewer, { id: '7' });
|
|
290
|
+
// board 11 (needs) + board 12 (settled, closed this week) + blocker + report; board 13 is too old
|
|
291
|
+
assert.deepEqual(d.counts, { needs: 2, moving: 1, settled: 1 });
|
|
292
|
+
assert.equal(d.line, '2 matters wait on you · 1 moving');
|
|
293
|
+
assert.deepEqual(d.matters.map((m) => m.state), ['needs', 'needs', 'moving', 'settled'], 'needs first');
|
|
294
|
+
assert.equal(d.matters[0].id, 'board:11', 'among needs, the soonest deadline first');
|
|
295
|
+
assert.deepEqual(d.buildings.map((b) => b.key), vocab.BUILDING_KEYS);
|
|
296
|
+
const charter = d.buildings.find((b) => b.key === 'charter');
|
|
297
|
+
assert.deepEqual(charter.counts, { needs: 1, moving: 0, settled: 1 });
|
|
298
|
+
assert.deepEqual(charter.matter_ids.sort(), ['board:11', 'board:12']);
|
|
299
|
+
|
|
300
|
+
const r = Object.fromEntries(d.readings.map((x) => [x.key, x]));
|
|
301
|
+
assert.deepEqual(d.readings.map((x) => x.key), ['flow', 'promises', 'money', 'people', 'fairness', 'rules']);
|
|
302
|
+
assert.equal(r.flow.status, 'live');
|
|
303
|
+
assert.equal(r.flow.figure, '3 stuck');
|
|
304
|
+
assert.equal(r.people.figure, '12 builders');
|
|
305
|
+
assert.equal(r.fairness.status, 'live');
|
|
306
|
+
assert.equal(r.fairness.value, 1, 'one open dispute the viewer can see');
|
|
307
|
+
assert.equal(r.fairness.hot, true);
|
|
308
|
+
assert.equal(r.money.status, 'off', 'no autonomy contributor in this test, never a made-up figure');
|
|
309
|
+
assert.equal(r.rules.status, 'off');
|
|
310
|
+
assert.equal(r.promises.status, 'off', 'no lifecycle contributor in this test');
|
|
311
|
+
assert.deepEqual(d.vocabulary.states, ['needs', 'moving', 'settled']);
|
|
312
|
+
assert.deepEqual(d.vocabulary.rules.map((x) => x.key), ['decide', 'first', 'consent', 'majority', 'unanimous', 'signoff', 'rule']);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('fairness is not_yours when the viewer can see no Disputes source (never a false "0 open")', async () => {
|
|
316
|
+
boardView = { open: [], closed: [] };
|
|
317
|
+
const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: none(), now: NOW });
|
|
318
|
+
assert.equal(d.readings.find((x) => x.key === 'fairness').status, 'not_yours');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── lifecycle + core mappers ─────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
test('lifecycle mappers: override, removal, UAT, join request', () => {
|
|
324
|
+
const o = lifecycleDocket.overrideMatter({ id: 3, task_id: 99, task_title: 'Refund flow', requested_by_builder_id: 9, requester_login: 'theo', requester_display_name: 'Theo', created_at: 't' }, true);
|
|
325
|
+
assert.equal(o.state, 'needs');
|
|
326
|
+
assert.equal(o.title, 'Override: Refund flow');
|
|
327
|
+
assert.equal(o.href, '/task/99');
|
|
328
|
+
assert.equal(lifecycleDocket.overrideMatter({ id: 3, task_id: 99 }, false).state, 'moving');
|
|
329
|
+
const rm = lifecycleDocket.removalMatter({ proposal_task_id: '50', title: 'Was the old search page removed?', goal_title: 'Hall', filed_at: 't' });
|
|
330
|
+
assert.equal(rm.where, 'disputes · removal · Hall');
|
|
331
|
+
const u = lifecycleDocket.uatMatter({ id: 8, criterion_md: 'Refunds work', goal_id: 4, goal_title: 'Payments', shipped_total: 3 });
|
|
332
|
+
assert.equal(u.rule, 'signoff');
|
|
333
|
+
assert.equal(u.href, '/goals#/goal/4');
|
|
334
|
+
const j = lifecycleDocket.joinRequestMatter({ id: 2, goal_id: 4, goal_title: 'Payments', builder_id: 11, github_login: 'kai', display_name: 'Kai', created_at: 't' }, false);
|
|
335
|
+
assert.equal(j.title, 'Kai asks to join Payments');
|
|
336
|
+
assert.equal(j.state, 'needs');
|
|
337
|
+
assert.equal(lifecycleDocket.joinRequestMatter({ id: 2, goal_id: 4, goal_title: 'Payments' }, true).state, 'moving');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('flow and promises readings', () => {
|
|
341
|
+
const day = 24 * 60 * 60 * 1000;
|
|
342
|
+
const f = lifecycleDocket.flowReading({ completed: [{ updated_at: new Date(NOW - 4 * day).toISOString() }, { updated_at: new Date(NOW - day).toISOString() }], confirmed: [{ updated_at: new Date(NOW).toISOString() }] }, NOW);
|
|
343
|
+
assert.deepEqual(f, { value: 3, figure: '3 stuck', line: 'longest wait 4 days, waiting for review', hot: true });
|
|
344
|
+
assert.equal(lifecycleDocket.flowReading({}, NOW).figure, 'all moving');
|
|
345
|
+
assert.equal(lifecycleDocket.promisesReading([{ satisfied: true }, { satisfied: false }, { satisfied: true }]).figure, '2 of 3 kept');
|
|
346
|
+
assert.equal(lifecycleDocket.promisesReading([]).value, null);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('core update: behind is a matter, current is none, no answer is an error (never "up to date")', () => {
|
|
350
|
+
const m = coreUpdateRoute.coreUpdateDocketMatters({ reading: 'ok', behind: true, latest: '1.21.0', running: '1.20.75' });
|
|
351
|
+
assert.equal(m.length, 1);
|
|
352
|
+
assert.equal(m[0].title, 'Take core 1.21.0 (running 1.20.75)');
|
|
353
|
+
assert.deepEqual(coreUpdateRoute.coreUpdateDocketMatters({ reading: 'ok', behind: false }), []);
|
|
354
|
+
assert.throws(() => coreUpdateRoute.coreUpdateDocketMatters({ reading: 'no_answer', behind: false }));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('blockers: needs only a viewer who may resolve one; the hold count reads in words', () => {
|
|
358
|
+
const b = { id: 4, title: 'Which payment provider', created_at: 't', blocking_task_ids: [1, 2, 3] };
|
|
359
|
+
const m = ideasDocket.blockerDocketMatter(b, true);
|
|
360
|
+
assert.equal(m.state, 'needs');
|
|
361
|
+
assert.equal(m.where, 'planning · holds 3 tasks');
|
|
362
|
+
assert.equal(m.href, '/blockers#blocker-4');
|
|
363
|
+
assert.equal(ideasDocket.blockerDocketMatter({ ...b, blocking_task_ids: [9] }, false).state, 'moving');
|
|
364
|
+
assert.equal(ideasDocket.blockerDocketMatter({ ...b, blocking_task_ids: [9] }, false).where, 'planning · holds 1 task');
|
|
365
|
+
assert.equal(ideasDocket.blockerDocketMatter({ ...b, blocking_task_ids: [] }, true).where, 'planning · waiting on a decision');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('rules at work: the routing rule and what it settled this month', () => {
|
|
369
|
+
assert.deepEqual(ideasDocket.rulesReading(45), {
|
|
370
|
+
value: { rules: 1, settled_this_month: 45 }, figure: '1 rule',
|
|
371
|
+
line: 'the landing rule settled 45 filings this month without a vote', hot: false,
|
|
372
|
+
});
|
|
373
|
+
assert.equal(ideasDocket.rulesReading(1).line, 'the landing rule settled 1 filing this month without a vote');
|
|
374
|
+
assert.equal(ideasDocket.rulesReading(0).line, 'the landing rule settled nothing this month');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('the fence: a matter only when it needs a governor, and only a fence manager is told it needs them', () => {
|
|
378
|
+
assert.deepEqual(autonomyDocket.fenceDocketMatters({ enabled: true, goals: [{ goal_id: 1 }] }, true), []);
|
|
379
|
+
const off = autonomyDocket.fenceDocketMatters({ enabled: false, paused_reason: 'payments touched', goals: [{ goal_id: 1 }], updated_by: 3 }, true);
|
|
380
|
+
assert.equal(off.length, 1);
|
|
381
|
+
assert.equal(off[0].title, 'Unattended building is switched off');
|
|
382
|
+
assert.equal(off[0].where, 'automation · the switch · payments touched');
|
|
383
|
+
assert.equal(off[0].state, 'needs');
|
|
384
|
+
assert.equal(autonomyDocket.fenceDocketMatters({ enabled: false, goals: [] }, false)[0].state, 'settled');
|
|
385
|
+
assert.equal(autonomyDocket.fenceDocketMatters({ enabled: true, goals: [] }, true)[0].title, 'Unattended building is on, but no goal is allowed');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('money: what unattended building may still spend this month, never negative, hot under a fifth', () => {
|
|
389
|
+
const m = autonomyDocket.moneyReading({ ceilingUsd: 30, spentUsd: 12.5 });
|
|
390
|
+
assert.equal(m.figure, '$17.50 left');
|
|
391
|
+
assert.equal(m.line, 'unattended building this month · $12.50 of $30 spent');
|
|
392
|
+
assert.equal(m.value.scope, 'unattended building, this month');
|
|
393
|
+
assert.equal(m.hot, false);
|
|
394
|
+
assert.equal(autonomyDocket.moneyReading({ ceilingUsd: 30, spentUsd: 27 }).hot, true);
|
|
395
|
+
assert.equal(autonomyDocket.moneyReading({ ceilingUsd: 30, spentUsd: 41 }).figure, '$0 left');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('access requests and red-team reports map to their buildings words', () => {
|
|
399
|
+
const a = accessRequestsRoute.accessRequestDocketMatter({ id: 5, github_login: 'kai', display_name: 'Kai', vouched: true, created_at: 't' });
|
|
400
|
+
assert.equal(a.title, 'Admit Kai as a builder');
|
|
401
|
+
assert.equal(a.where, 'people · waiting to join · vouched');
|
|
402
|
+
assert.equal(a.state, 'needs');
|
|
403
|
+
assert.equal(accessRequestsRoute.accessRequestDocketMatter({ id: 5, github_login: 'kai', vouched: false }).where, 'people · waiting to join');
|
|
404
|
+
const r = reportsRoute.redteamDocketMatter({ id: 2, title: 'sign-in limits', severity: 'high', status: 'repro_attempted', reporter_builder_id: 8, reported_at: 't' });
|
|
405
|
+
assert.equal(r.title, 'Security report: sign-in limits');
|
|
406
|
+
assert.equal(r.short, 'high report');
|
|
407
|
+
assert.equal(r.where, 'disputes · security report · being reproduced');
|
|
408
|
+
assert.deepEqual(r.proposer, { id: 8 });
|
|
409
|
+
assert.equal(reportsRoute.redteamDocketMatter({ id: 2, title: 'x', status: 'open' }).where, 'disputes · security report');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('module re-checks: a failed gate needs a governor who may ask; a pending part is moving; the query reads the CURRENT signal', async () => {
|
|
413
|
+
const failed = moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'image-tools', version: '1.2.0', security_failed: true, pending_parts: null, measured_at: 't' }, true);
|
|
414
|
+
assert.equal(failed.state, 'needs');
|
|
415
|
+
assert.equal(failed.title, 'image-tools 1.2.0 failed its security check');
|
|
416
|
+
assert.equal(failed.where, 'release · modules · flagged for a re-check');
|
|
417
|
+
assert.equal(moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'm', version: '1.0.0', security_failed: true }, false).state, 'moving');
|
|
418
|
+
const pending = moduleRechecks.moduleRecheckMatter({ version_id: 8, module_key: 'm', version: '1.0.0', security_failed: false, pending_parts: ['tests'] }, true);
|
|
419
|
+
assert.equal(pending.state, 'moving');
|
|
420
|
+
assert.equal(pending.where, 'release · modules · tests pending');
|
|
421
|
+
let sql = null;
|
|
422
|
+
await moduleRechecks.listModuleRechecks({ db: { query: async (q, params) => { sql = q; assert.deepEqual(params, [50]); return { rows: [] }; } } });
|
|
423
|
+
assert.match(sql, /DISTINCT ON \(s\.version_id, s\.part\)/, 'the newest signal per part, not every historical row');
|
|
424
|
+
assert.match(sql, /ORDER BY s\.version_id, s\.part, s\.measured_at DESC, s\.id DESC/);
|
|
425
|
+
assert.match(sql, /outcome = 'failed'/);
|
|
426
|
+
assert.match(sql, /outcome = 'pending'/);
|
|
427
|
+
// The DISTINCT ON walks core_265's own composite index, whose column order is the
|
|
428
|
+
// query's ORDER BY exactly — pinned so a reorder of either side fails here.
|
|
429
|
+
const mig = fs.readFileSync(path.join(ROOT, 'migrations', 'core_265_module_assessments.sql'), 'utf8');
|
|
430
|
+
assert.match(mig, /idx_module_assessment_signals_current\s+ON module_assessment_signals \(version_id, part, measured_at DESC, id DESC\)/);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('the six readings, end to end with every module contributing: each reading is live or names why not', async () => {
|
|
434
|
+
seams._reset();
|
|
435
|
+
for (const f of ['modules/ideas/routes/blockers.js', 'modules/lifecycle/routes/lifecycle.js', 'modules/autonomy/routes/autonomy.js']) require(path.join(ROOT, f))();
|
|
436
|
+
const reg = docket.registeredContributions();
|
|
437
|
+
// Stub each contributed reading's read so no DB is touched; the GATE stays real.
|
|
438
|
+
const stubbed = { sources: new Map(), readings: new Map() };
|
|
439
|
+
const answers = {
|
|
440
|
+
flow: { value: 3, figure: '3 stuck', line: 'l', hot: true },
|
|
441
|
+
promises: { value: { kept: 11, total: 17 }, figure: '11 of 17 kept', line: 'l', hot: false },
|
|
442
|
+
money: { value: {}, figure: '$17.50 left', line: 'l', hot: false },
|
|
443
|
+
rules: { value: {}, figure: '1 rule', line: 'l', hot: false },
|
|
444
|
+
};
|
|
445
|
+
for (const [k, c] of reg.readings) stubbed.readings.set(k, { ...c, read: async () => answers[k] });
|
|
446
|
+
boardView = { open: [], closed: [] };
|
|
447
|
+
const metic = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'autonomy.precheck', 'security.report.adjudicate'], contributions: stubbed, now: NOW });
|
|
448
|
+
const by = Object.fromEntries(metic.readings.map((x) => [x.key, x]));
|
|
449
|
+
for (const k of ['flow', 'promises', 'money', 'people', 'rules']) assert.equal(by[k].status, 'live', k);
|
|
450
|
+
assert.equal(by.money.figure, '$17.50 left');
|
|
451
|
+
// A viewer without the precheck atom does not see the spend ceiling.
|
|
452
|
+
const xenos = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: stubbed, now: NOW });
|
|
453
|
+
assert.equal(xenos.readings.find((x) => x.key === 'money').status, 'not_yours');
|
|
454
|
+
assert.equal(xenos.readings.find((x) => x.key === 'money').figure, null);
|
|
455
|
+
seams._reset();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// ── 5. every contributed source declares its own list route's atom ───────────
|
|
459
|
+
|
|
460
|
+
test('each contributed source gates on EXACTLY the atom its own list route requires', async () => {
|
|
461
|
+
seams._reset();
|
|
462
|
+
const factories = [
|
|
463
|
+
'modules/ideas/routes/blockers.js',
|
|
464
|
+
'modules/lifecycle/routes/lifecycle.js',
|
|
465
|
+
'modules/lifecycle/routes/gate-approvals.js',
|
|
466
|
+
'modules/onboarding/routes/access-requests.js',
|
|
467
|
+
'modules/security/routes/reports.js',
|
|
468
|
+
'modules/autonomy/routes/autonomy.js',
|
|
469
|
+
'src/bongos/routes/core-update.js',
|
|
470
|
+
'src/bongos/routes/modules.js',
|
|
471
|
+
];
|
|
472
|
+
for (const f of factories) require(path.join(ROOT, f))();
|
|
473
|
+
const reg = docket.registeredContributions();
|
|
474
|
+
const routePerm = (file, key) => {
|
|
475
|
+
const r = rrc.classifyText(fs.readFileSync(path.join(ROOT, file), 'utf8')).find((x) => x.key === key);
|
|
476
|
+
assert.ok(r, `${key} declared in ${file}`);
|
|
477
|
+
return r.perms;
|
|
478
|
+
};
|
|
479
|
+
const expect = {
|
|
480
|
+
override_requests: ['modules/lifecycle/routes/tasks.js', 'GET /override-requests'],
|
|
481
|
+
removal_proposals: ['modules/lifecycle/routes/removal-proposals.js', 'GET /removal-proposals'],
|
|
482
|
+
criteria_uat: ['modules/lifecycle/routes/done-when.js', 'GET /done-when/pending-review'],
|
|
483
|
+
gate_approvals: ['modules/lifecycle/routes/gate-approvals.js', 'GET /gate-approvals'],
|
|
484
|
+
access_requests: ['modules/onboarding/routes/access-requests.js', 'GET /access-requests'],
|
|
485
|
+
redteam_reports: ['modules/security/routes/reports.js', 'GET /security/reports'],
|
|
486
|
+
core_update: ['src/bongos/routes/core-update.js', 'GET /core-update'],
|
|
487
|
+
};
|
|
488
|
+
for (const [source, [file, key]] of Object.entries(expect)) {
|
|
489
|
+
const c = reg.sources.get(source);
|
|
490
|
+
assert.ok(c, `${source} is contributed`);
|
|
491
|
+
assert.deepEqual([c.gate.permission], routePerm(file, key), `${source} gates on ${key}'s atom`);
|
|
492
|
+
}
|
|
493
|
+
// The own-scoped / public reads say so explicitly, and nothing else is anyBuilder.
|
|
494
|
+
const anyBuilder = [...reg.sources].filter(([, c]) => c.gate.anyBuilder === true).map(([k]) => k).sort();
|
|
495
|
+
assert.deepEqual(anyBuilder, ['autobongos_fence', 'blockers', 'goal_join_requests']);
|
|
496
|
+
for (const [k, c] of reg.sources) {
|
|
497
|
+
const decl = docket.EXPECTED_SOURCES.find((e) => e.source === k);
|
|
498
|
+
assert.ok(decl, `${k} is a declared source`);
|
|
499
|
+
assert.equal(c.building, decl.building, `${k} stands where the docket says`);
|
|
500
|
+
}
|
|
501
|
+
assert.equal(reg.sources.get('module_rechecks').gate.permission, 'module.assessment.rerun',
|
|
502
|
+
'module re-checks gate on the reassess route atom (there is no assessment read route)');
|
|
503
|
+
assert.deepEqual(routePerm('src/bongos/routes/modules.js', 'POST /store/modules/:key/versions/:version/reassess'), ['module.assessment.rerun']);
|
|
504
|
+
assert.deepEqual([...reg.readings.keys()].sort(), ['flow', 'money', 'promises', 'rules']);
|
|
505
|
+
assert.equal(reg.readings.get('money').gate.permission, 'autonomy.precheck');
|
|
506
|
+
assert.deepEqual(routePerm('modules/autonomy/routes/autonomy.js', 'GET /autonomy/precheck'), ['autonomy.precheck']);
|
|
507
|
+
// Every reading the street draws has a contributor once the modules are mounted.
|
|
508
|
+
for (const r of vocab.READINGS) {
|
|
509
|
+
if (r.key === 'people' || r.key === 'fairness') continue; // answered inside government
|
|
510
|
+
assert.ok(reg.readings.has(r.key), 'reading ' + r.key + ' is contributed');
|
|
511
|
+
}
|
|
512
|
+
seams._reset();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ── 6. the route ─────────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
const realRequireBuilder = api.requireBuilder;
|
|
518
|
+
api.requireBuilder = (req, res, next) => { req.builder = { id: 7, rank: 'metic' }; next(); };
|
|
519
|
+
let held = ['board.vote.cast'];
|
|
520
|
+
function grant(keys) {
|
|
521
|
+
held = keys;
|
|
522
|
+
seams._reset();
|
|
523
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => held });
|
|
524
|
+
}
|
|
525
|
+
grant(['board.vote.cast']);
|
|
526
|
+
resolver.resolveBuilderPermissions = async () => held;
|
|
527
|
+
const buildGovernmentRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
|
|
528
|
+
const app = express();
|
|
529
|
+
app.use(express.json());
|
|
530
|
+
app.use(attachFail);
|
|
531
|
+
app.use(buildGovernmentRouter());
|
|
532
|
+
const server = http.createServer(app);
|
|
533
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
534
|
+
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
535
|
+
after(() => { server.close(); api.requireBuilder = realRequireBuilder; });
|
|
536
|
+
|
|
537
|
+
test('GET /government/docket: 403 without board.vote.cast, the documented shape with it', async () => {
|
|
538
|
+
boardView = { open: [item()], closed: [] };
|
|
539
|
+
grant([]);
|
|
540
|
+
let res = await fetch(`${BASE}/government/docket`);
|
|
541
|
+
assert.equal(res.status, 403);
|
|
542
|
+
grant(['board.vote.cast']);
|
|
543
|
+
// a hidden source contributed AFTER the router was built is still scoped
|
|
544
|
+
seams.contribute(docket.CONTRIBUTION_POINT, { source: 'access_requests', building: 'people', gate: { permission: 'access_request.review' }, read: async () => [{ id: 1, title: 'Admit Kai', state: 'needs' }] });
|
|
545
|
+
res = await fetch(`${BASE}/government/docket`);
|
|
546
|
+
assert.equal(res.status, 200);
|
|
547
|
+
const body = await res.json();
|
|
548
|
+
assert.equal(body.viewer.id, '7');
|
|
549
|
+
assert.deepEqual(body.matters.map((m) => m.id), ['board:11']);
|
|
550
|
+
assert.equal(body.sources.find((s) => s.source === 'access_requests').status, 'not_yours');
|
|
551
|
+
assert.ok(!JSON.stringify(body).includes('Admit Kai'), 'a hidden source leaks nothing');
|
|
552
|
+
});
|
package/tests/onboard.mjs
CHANGED
|
@@ -81,10 +81,16 @@ test('every form OFFERS the optional step and names what skipping costs — skil
|
|
|
81
81
|
assert.match(cli, /Press Enter to skip/, 'the terminal form offers the skip in the prompt itself');
|
|
82
82
|
assert.match(cli, /off the public map/, 'and says what a missing description means');
|
|
83
83
|
|
|
84
|
-
// the web form
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
// the web form — since task 1004504 the optional step is no panel of its own: the
|
|
85
|
+
// sentence is an optional field on "What are you making?" whose label names what
|
|
86
|
+
// leaving it empty costs, and the team shape is the review's Team size row
|
|
87
|
+
const hub = read('modules/public-landing/public/projects.html').replace(/\r\n/g, '\n');
|
|
88
|
+
const what = (hub.match(/<section class="panel" id="panel-what"[\s\S]*?\n {10}<\/section>/) || [''])[0];
|
|
89
|
+
assert.ok(what, 'the wizard ships the screen that carries the sentence');
|
|
90
|
+
assert.match(what, /<label class="wizField"><span>In a sentence <small>\(optional — puts it on the public map\)<\/small><\/span>\s*<input type="text" id="f-desc"/,
|
|
91
|
+
'the sentence is offered as optional, and its label says what skipping costs');
|
|
92
|
+
assert.match(hub, /<div class="setRow" data-row="team">[\s\S]*?id="teamCards"/, 'the team shape rides the review\'s Team size row');
|
|
93
|
+
assert.doesNotMatch(hub, /id="panel-detail"|id="skipDetail"/, 'the old About panel and its skip control are gone');
|
|
88
94
|
|
|
89
95
|
// the route that receives it
|
|
90
96
|
const route = read('modules/provisioning/routes/provisioning.js');
|
package/tests/platform_boot.mjs
CHANGED
|
@@ -147,7 +147,7 @@ test('/create redirects into the hub wizard, seed intact (task 1002763)', async
|
|
|
147
147
|
assert.equal(hub.status, 200);
|
|
148
148
|
assert.match(hub.headers['content-type'] || '', /text\/html/);
|
|
149
149
|
assert.match(hub.body, /window\.__BRANDING__=/);
|
|
150
|
-
assert.match(hub.body,
|
|
150
|
+
assert.match(hub.body, /<h1 id="gate-h">Make something\.<\/h1>/); // the wizard's sign-in screen (task 1004504)
|
|
151
151
|
// the wizard files the request against the provisioning API, nothing else
|
|
152
152
|
assert.match(hub.body, /provisioning\/instances/);
|
|
153
153
|
});
|