@bongos/core 1.21.34 → 1.21.35
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 +35 -35
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +7 -1
- package/docs/page-readings.json +308 -249
- package/modules/autonomy/governor-docket.js +68 -1
- package/modules/government/docket-floors.js +20 -0
- package/modules/government/docket-vocab.js +14 -0
- package/modules/government/docket.js +103 -2
- package/modules/government/routes/government.js +13 -0
- package/modules/hall-ui/public/street-card.js +97 -1
- package/modules/hall-ui/public/street-desk.js +108 -7
- package/modules/hall-ui/public/street-inside.js +6 -6
- package/modules/hall-ui/public/street.css +1 -0
- package/modules/hall-ui/public/street.js +24 -5
- package/modules/hall-ui/public/street.states.json +231 -6
- package/modules/lifecycle/governor-docket.js +24 -2
- package/modules/lifecycle/routes/project-settings.js +24 -0
- package/modules/onboarding/routes/access-requests.js +7 -0
- package/modules/provisioning/core-upgrade.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +36 -1
- package/modules/security/routes/reports.js +23 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/routes/backup.js +26 -0
- package/src/bongos/routes/builders.js +40 -0
- package/src/bongos/routes/modules.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +28 -1
- package/tests/hall_matter_card.mjs +11 -2
- package/tests/hall_street_desk.mjs +229 -0
|
@@ -242,7 +242,12 @@ function accessRequestDocketMatter(r) {
|
|
|
242
242
|
// The governor's docket: a pending request to join stands at People, shown only
|
|
243
243
|
// to a viewer holding `access_request.review` (this queue's own atom, because it
|
|
244
244
|
// exposes would-be builders' usernames) and needing that same viewer's act.
|
|
245
|
+
// The People desk's invite row (task 1004559), under the invite route's own atom:
|
|
246
|
+
// one GitHub name typed in place, sent to POST /access-requests/invite.
|
|
247
|
+
const INVITE_DESK_ROW = Object.freeze({ id: 'invite', name: 'Invite someone', fact: 'by their GitHub name', tool: true, ref: { type: 'invite', id: 'new' }, acts: [{ key: 'invite', field: 'github_login' }] });
|
|
248
|
+
|
|
245
249
|
function contributeAccessRequestsToDocket() {
|
|
250
|
+
api.contribute('governor.docket', { desk: 'people.invite', gate: { permission: 'access_request.review' }, read: async () => [INVITE_DESK_ROW] });
|
|
246
251
|
api.contribute('governor.docket', {
|
|
247
252
|
source: 'access_requests',
|
|
248
253
|
building: 'people',
|
|
@@ -651,3 +656,5 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
651
656
|
};
|
|
652
657
|
|
|
653
658
|
module.exports.accessRequestDocketMatter = accessRequestDocketMatter;
|
|
659
|
+
|
|
660
|
+
module.exports.INVITE_DESK_ROW = INVITE_DESK_ROW;
|
|
@@ -545,5 +545,5 @@ function makeUpdateChannelHandler({ provisioning, pool, parseId, validateOrRespo
|
|
|
545
545
|
};
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
module.exports = { UPDATE_CHANNELS, DEFAULT_UPDATE_CHANNEL, updateChannelOf, selfUpdateChannel, makeUpdateChannelHandler, CORE_UPGRADE_FROM, lastCoreMove, lastMoveSuperseded, parseRepoNote, preflightForOffer, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
|
|
548
|
+
module.exports = { loadPreview, UPDATE_CHANNELS, DEFAULT_UPDATE_CHANNEL, updateChannelOf, selfUpdateChannel, makeUpdateChannelHandler, CORE_UPGRADE_FROM, lastCoreMove, lastMoveSuperseded, parseRepoNote, preflightForOffer, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
|
|
549
549
|
|
|
@@ -25,12 +25,45 @@
|
|
|
25
25
|
const express = require('express');
|
|
26
26
|
const api = require('../../../src/module-api');
|
|
27
27
|
const provisioning = require('../provisioning');
|
|
28
|
-
const { makeCoreUpgradeHandler, makeCoreUpgradePreviewHandler, makeControlPlaneHandler, makeUpdateChannelHandler } = require('../core-upgrade');
|
|
28
|
+
const { makeCoreUpgradeHandler, makeCoreUpgradePreviewHandler, makeControlPlaneHandler, makeUpdateChannelHandler, loadPreview } = require('../core-upgrade');
|
|
29
29
|
const { requireOwnProjectOrPin } = require('../core-upgrade-authz');
|
|
30
30
|
|
|
31
31
|
const log = api.logger('provisioning');
|
|
32
32
|
|
|
33
|
+
// The Release desk's core row (task 1004559): the platform's own move, in two
|
|
34
|
+
// steps on one row. Until a dry run has PASSED for the very version on offer the
|
|
35
|
+
// row offers only 'check' (POST …/core-upgrade with dry_run); 'take' (the real
|
|
36
|
+
// move) is handed only after that pass, so no desk can move the pin without the
|
|
37
|
+
// check. The chip carries the version, which the route requires as its own
|
|
38
|
+
// confirmation. A run in progress offers nothing (the row then steps aside for
|
|
39
|
+
// the plain core-update matter). Pure; exported for tests.
|
|
40
|
+
function coreDeskRows(preview) {
|
|
41
|
+
const p = preview || {};
|
|
42
|
+
const to = p.recommended_version;
|
|
43
|
+
if (!p.behind || !to || p.instance_id == null) return [];
|
|
44
|
+
if (p.open_intent && (p.open_intent.state === 'pending' || p.open_intent.state === 'running')) return [];
|
|
45
|
+
const pf = p.preflight && p.preflight.target === to ? p.preflight : null;
|
|
46
|
+
const version = [{ key: String(to), label: String(to) }];
|
|
47
|
+
const base = { id: p.instance_id, name: `Core ${to}`, over: 'core_update', opened_at: p.observed_at || null, ref: { type: 'core_pin', id: p.instance_id } };
|
|
48
|
+
if (pf && pf.ok) return [{ ...base, fact: 'checked · nothing blocks', facts: [`running ${p.served_version || 'unknown'}`], acts: [{ key: 'take', options: version }, { key: 'check', options: version }] }];
|
|
49
|
+
if (pf) return [{ ...base, fact: 'the check refused it', facts: [String(pf.error || 'no reason given'), `running ${p.served_version || 'unknown'}`], acts: [{ key: 'check', options: version }] }];
|
|
50
|
+
return [{ ...base, fact: 'not checked yet', facts: [`running ${p.served_version || 'unknown'}`], acts: [{ key: 'check', options: version }] }];
|
|
51
|
+
}
|
|
52
|
+
function contributeCoreDesk() {
|
|
53
|
+
api.contribute('governor.docket', {
|
|
54
|
+
desk: 'release.core',
|
|
55
|
+
// the control-plane read's own atom (GET /provisioning/control-plane)
|
|
56
|
+
gate: { permission: 'core.pin.move' },
|
|
57
|
+
read: async () => {
|
|
58
|
+
const { rows } = await api.pool.query("SELECT * FROM provisioning_instances WHERE hosting_shape = 'control-plane' LIMIT 1");
|
|
59
|
+
if (!rows.length) return [];
|
|
60
|
+
return coreDeskRows(await loadPreview(api.pool, provisioning, rows[0]));
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
module.exports = function buildCoreUpgradeRouter() {
|
|
66
|
+
contributeCoreDesk();
|
|
34
67
|
const router = express.Router();
|
|
35
68
|
|
|
36
69
|
// POST /provisioning/instances/:id/core-upgrade — move the core pin.
|
|
@@ -119,3 +152,5 @@ module.exports = function buildCoreUpgradeRouter() {
|
|
|
119
152
|
|
|
120
153
|
return router;
|
|
121
154
|
};
|
|
155
|
+
|
|
156
|
+
module.exports.coreDeskRows = coreDeskRows;
|
|
@@ -56,6 +56,23 @@ function redteamDocketMatter(r) {
|
|
|
56
56
|
tally: null,
|
|
57
57
|
href: '/watch',
|
|
58
58
|
ref: { type: 'vulnerability_report', id: r.id },
|
|
59
|
+
// The floor desk's acts (task 1004559): the docket only lists this for a
|
|
60
|
+
// holder of the adjudicate atom, which both routes check.
|
|
61
|
+
acts: [{ key: 'confirm' }, { key: 'dispute' }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A confirmed report still waiting for its fix, as a Disputes desk row (task
|
|
66
|
+
// 1004559): one act, POST /security/reports/:id/fix. Pure; exported for tests.
|
|
67
|
+
function confirmedDeskRow(r) {
|
|
68
|
+
return {
|
|
69
|
+
id: r.id,
|
|
70
|
+
name: r.title,
|
|
71
|
+
fact: r.severity ? `${r.severity} · confirmed` : 'confirmed',
|
|
72
|
+
facts: ['confirmed, not fixed yet'],
|
|
73
|
+
opened_at: r.confirmed_at || r.reported_at || null,
|
|
74
|
+
ref: { type: 'vulnerability_report', id: r.id },
|
|
75
|
+
acts: [{ key: 'fix' }],
|
|
59
76
|
};
|
|
60
77
|
}
|
|
61
78
|
|
|
@@ -69,6 +86,11 @@ function contributeReportsToDocket() {
|
|
|
69
86
|
return lists.flat().map(redteamDocketMatter);
|
|
70
87
|
},
|
|
71
88
|
});
|
|
89
|
+
api.contribute('governor.docket', {
|
|
90
|
+
desk: 'disputes.confirmed',
|
|
91
|
+
gate: { permission: 'security.report.adjudicate' },
|
|
92
|
+
read: async () => (await security.listVulnerabilityReports({ scope: 'all', status: 'confirmed', limit: 40 })).map(confirmedDeskRow),
|
|
93
|
+
});
|
|
72
94
|
// The Disputes record (task 1004527): every report that was ruled on, dated by
|
|
73
95
|
// its ruling (confirmed or fixed). Same atom as the list route it reads.
|
|
74
96
|
api.contribute('governor.docket', {
|
|
@@ -318,3 +340,4 @@ module.exports = function buildSecurityReportsRouter() {
|
|
|
318
340
|
|
|
319
341
|
module.exports.redteamDocketMatter = redteamDocketMatter;
|
|
320
342
|
module.exports.rulingHistory = rulingHistory;
|
|
343
|
+
module.exports.confirmedDeskRow = confirmedDeskRow;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.35",
|
|
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.35",
|
|
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.35",
|
|
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
|
@@ -8964,5 +8964,11 @@
|
|
|
8964
8964
|
"id": "1004558",
|
|
8965
8965
|
"text": "Inside every building in the governor city, the floor you pick is now a desk you can act from, built from the new floor desks design. It shows the floor's name and how many things wait on you, up to three short numbers, and on"
|
|
8966
8966
|
}
|
|
8967
|
+
],
|
|
8968
|
+
"1.21.35": [
|
|
8969
|
+
{
|
|
8970
|
+
"id": "1004559",
|
|
8971
|
+
"text": "Governing now happens inside the buildings. On a floor's desk you can set a builder's tier, deactivate them, invite someone by GitHub name, give or take back a custom rank; confirm, dispute or mark fixed a security report and"
|
|
8972
|
+
}
|
|
8967
8973
|
]
|
|
8968
8974
|
}
|
|
@@ -24,6 +24,7 @@ const auth = require('../auth');
|
|
|
24
24
|
const { branding } = require('../../branding');
|
|
25
25
|
const { resolveEnv } = require('../../instance-config');
|
|
26
26
|
const { asyncHandler } = require('./_helpers');
|
|
27
|
+
const seams = require('../../module-seams');
|
|
27
28
|
// task 1003208: structured logging — was console.*. A core file logs through
|
|
28
29
|
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
29
30
|
const log = require('../logger').logger.child({ src: 'backup' });
|
|
@@ -76,6 +77,20 @@ function isStaleDump(stamp, nowMs = Date.now(), staleAfterHours = STALE_AFTER_HO
|
|
|
76
77
|
return age > staleAfterHours;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// The newest dump's age for the Treasury desk (task 1004559), read WITHOUT
|
|
81
|
+
// blocking: the docket is read on every street visit, so this is one async
|
|
82
|
+
// directory listing and the age from the filename (no stat), never latestDump()'s
|
|
83
|
+
// synchronous reads. Same filename rule and staleness rule as latestDump().
|
|
84
|
+
async function newestDumpAge(nowMs = Date.now()) {
|
|
85
|
+
let files;
|
|
86
|
+
try { files = await fs.promises.readdir(BACKUP_DIR); } catch { return null; }
|
|
87
|
+
const dumps = files.filter((f) => f.startsWith(`${DB_NAME}-`) && f.endsWith('.sql.gz')).sort();
|
|
88
|
+
if (!dumps.length) return null;
|
|
89
|
+
const ts = dumps[dumps.length - 1].replace(`${DB_NAME}-`, '').replace('.sql.gz', '');
|
|
90
|
+
const age = dumpAgeHours(ts, nowMs);
|
|
91
|
+
return { age_hours: age === null ? null : Math.round(age * 10) / 10, stale: isStaleDump(ts, nowMs) };
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
function latestDump(nowMs = Date.now()) {
|
|
80
95
|
let files;
|
|
81
96
|
try {
|
|
@@ -107,7 +122,16 @@ function latestDump(nowMs = Date.now()) {
|
|
|
107
122
|
};
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
// The Treasury desk's backup row (task 1004559), under the backup routes' own
|
|
126
|
+
// atom: one tap runs POST /backup/trigger. Pure; exported for tests.
|
|
127
|
+
function backupDeskRows(dump, running) {
|
|
128
|
+
const age = dump && Number.isFinite(Number(dump.age_hours)) ? Number(dump.age_hours) : null;
|
|
129
|
+
const fact = running ? 'backing up now' : age == null ? 'no backup yet' : age < 1 ? 'last one under an hour' : age < 48 ? `last ${Math.round(age)} hours ago` : `last ${Math.round(age / 24)} days ago`;
|
|
130
|
+
return [{ id: 'now', name: 'Back up now', fact, facts: dump && dump.stale ? ['the last one is stale'] : [], tool: true, ref: { type: 'backup', id: 'now' }, acts: running ? [] : [{ key: 'backup' }] }];
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
module.exports = function buildBackupRouter() {
|
|
134
|
+
seams.contribute('governor.docket', { desk: 'treasury.backup', gate: { permission: 'backup.manage' }, read: async () => backupDeskRows(await newestDumpAge(), triggerRunning) });
|
|
111
135
|
const router = express.Router();
|
|
112
136
|
|
|
113
137
|
// rank: metic+archon — any working builder may check or trigger a backup. Gate:
|
|
@@ -168,3 +192,5 @@ module.exports.parseDumpTimestamp = parseDumpTimestamp;
|
|
|
168
192
|
module.exports.dumpAgeHours = dumpAgeHours;
|
|
169
193
|
module.exports.isStaleDump = isStaleDump;
|
|
170
194
|
module.exports.STALE_AFTER_HOURS = STALE_AFTER_HOURS;
|
|
195
|
+
|
|
196
|
+
module.exports.backupDeskRows = backupDeskRows;
|
|
@@ -168,7 +168,45 @@ function fireDiscordBroadcast(text) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// The People desk's builder rows (task 1004559): one row per active builder the
|
|
172
|
+
// viewer may act on, with the acts the viewer's own atoms allow: set tier
|
|
173
|
+
// (builder.rank.set, PATCH /builders/:id/rank) and deactivate (builder.status.set,
|
|
174
|
+
// PATCH /builders/:id/status). Never the viewer's own row: both routes refuse it.
|
|
175
|
+
// Pure; exported for tests.
|
|
176
|
+
const STOP_REASONS = Object.freeze([
|
|
177
|
+
{ key: 'left', label: 'left the project' },
|
|
178
|
+
{ key: 'quiet', label: 'gone quiet' },
|
|
179
|
+
{ key: 'asked', label: 'asked to stop' },
|
|
180
|
+
]);
|
|
181
|
+
function builderDeskRows(builders, { viewerId, mayRank, mayStop, ranks }) {
|
|
182
|
+
if (!mayRank && !mayStop) return [];
|
|
183
|
+
return (builders || []).filter((b) => b && String(b.id) !== String(viewerId)).map((b) => {
|
|
184
|
+
const acts = [];
|
|
185
|
+
// one rung up at most (the route refuses a multi-rung promotion), any rung down
|
|
186
|
+
const at = (ranks || []).indexOf(b.rank);
|
|
187
|
+
// a rank off the live ladder offers no tier chip: there is no rung to move by one
|
|
188
|
+
if (mayRank && at >= 0) acts.push({ key: 'tier', options: (ranks || []).filter((r, i) => r !== b.rank && i <= at + 1).map((r) => ({ key: r, label: r })) });
|
|
189
|
+
if (mayStop) acts.push({ key: 'deactivate', options: STOP_REASONS });
|
|
190
|
+
return { id: b.id, name: b.display_name || b.github_login || `builder ${b.id}`, fact: b.rank || 'builder', facts: b.github_login ? [`@${b.github_login}`] : [], ref: { type: 'builder', id: b.id }, acts };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Contributed from the router factory: any builder passes the gate, and the read
|
|
194
|
+
// scopes itself to the viewer's atoms (an empty desk for a viewer holding neither).
|
|
195
|
+
function contributeBuilderDesk() {
|
|
196
|
+
seams.contribute('governor.docket', {
|
|
197
|
+
desk: 'people.builders',
|
|
198
|
+
gate: { anyBuilder: true },
|
|
199
|
+
read: async (ctx) => {
|
|
200
|
+
const mayRank = ctx.has('builder.rank.set');
|
|
201
|
+
const mayStop = ctx.has('builder.status.set');
|
|
202
|
+
if (!mayRank && !mayStop) return [];
|
|
203
|
+
return builderDeskRows(await db.listActiveBuilders(), { viewerId: ctx.builderId, mayRank, mayStop, ranks: db.LIVE_RANK_LADDER });
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
171
208
|
module.exports = function buildBuildersRouter() {
|
|
209
|
+
contributeBuilderDesk();
|
|
172
210
|
const router = express.Router();
|
|
173
211
|
|
|
174
212
|
// rank: archon — sole chokepoint that mutates builders.rank; ADR 0016 §enforcement.
|
|
@@ -707,3 +745,5 @@ module.exports = function buildBuildersRouter() {
|
|
|
707
745
|
|
|
708
746
|
return router;
|
|
709
747
|
};
|
|
748
|
+
|
|
749
|
+
module.exports.builderDeskRows = builderDeskRows;
|
|
@@ -180,8 +180,24 @@ function contributeModuleRechecksToDocket() {
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// The Rules desk's module rows (task 1004559), under the toggle's own atom
|
|
184
|
+
// (POST /modules/:key/enable, module.enable): each module on disk, turned on in
|
|
185
|
+
// one tap, or off with a one-tap 'yes' so a stray tap cannot pull one. Pure;
|
|
186
|
+
// exported for tests.
|
|
187
|
+
function moduleDeskRows(mods) {
|
|
188
|
+
return (mods || []).filter((m) => m && m.key).map((m) => ({
|
|
189
|
+
id: m.key, name: m.title || m.key, fact: m.enabled ? 'on' : 'off', facts: m.description ? [m.description] : [], tool: true,
|
|
190
|
+
ref: { type: 'module', id: m.key },
|
|
191
|
+
acts: [m.enabled ? { key: 'module_off', options: [{ key: 'yes', label: 'yes, turn off' }] } : { key: 'module_on' }],
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
function contributeModuleDesk() {
|
|
195
|
+
seams.contribute('governor.docket', { desk: 'rules.modules', gate: { permission: 'module.enable' }, read: async () => moduleDeskRows(listModules()) });
|
|
196
|
+
}
|
|
197
|
+
|
|
183
198
|
module.exports = function buildModulesRouter() {
|
|
184
199
|
contributeModuleRechecksToDocket();
|
|
200
|
+
contributeModuleDesk();
|
|
185
201
|
const router = express.Router();
|
|
186
202
|
// Checked how-tos by "key@version:tarball_sha256". Per router, so a test's fresh
|
|
187
203
|
// router starts empty.
|
|
@@ -702,3 +718,5 @@ module.exports = function buildModulesRouter() {
|
|
|
702
718
|
// own disk scan, no DB), so it's safe to require directly outside an Express
|
|
703
719
|
// request cycle, the same way tests/atlas.mjs requires gen-atlas.js's builders.
|
|
704
720
|
module.exports.listModules = listModules;
|
|
721
|
+
|
|
722
|
+
module.exports.moduleDeskRows = moduleDeskRows;
|
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.35'; // 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');
|
|
@@ -285,7 +285,7 @@ test('the response shape: counts, line, street-ordered buildings, sorted matters
|
|
|
285
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
286
|
const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'security.report.adjudicate'], contributions: c, now: NOW });
|
|
287
287
|
|
|
288
|
-
assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'floors', 'generated_at', 'history', 'history_sources', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
|
|
288
|
+
assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'desks', 'floors', 'generated_at', 'history', 'history_sources', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
|
|
289
289
|
assert.deepEqual(d.viewer, { id: '7' });
|
|
290
290
|
// board 11 (needs) + board 12 (settled, closed this week) + blocker + report; board 13 is too old
|
|
291
291
|
assert.deepEqual(d.counts, { needs: 2, moving: 1, settled: 1 });
|
|
@@ -734,3 +734,30 @@ test('GET /government/docket: 403 without board.vote.cast, the documented shape
|
|
|
734
734
|
assert.equal(body.sources.find((s) => s.source === 'access_requests').status, 'not_yours');
|
|
735
735
|
assert.ok(!JSON.stringify(body).includes('Admit Kai'), 'a hidden source leaks nothing');
|
|
736
736
|
});
|
|
737
|
+
|
|
738
|
+
// ── the floor desks' acts and rows (task 1004559) ────────────────────────────
|
|
739
|
+
|
|
740
|
+
test('acts: a matter carries only well-formed acts, at most two, with at most five one-tap options', () => {
|
|
741
|
+
const m = docket.normalizeMatter({ id: 1, title: 't', acts: [{ key: 'confirm', options: [1, 2, 3, 4, 5, 6].map((n) => ({ key: n, label: `option ${n}` })) }, { key: 'Bad Key' }, { key: 'fix' }, { key: 'third' }] }, { source: 's', building: 'review' });
|
|
742
|
+
assert.deepEqual(m.acts.map((a) => a.key), ['confirm', 'fix']);
|
|
743
|
+
assert.equal(m.acts[0].options.length, 5);
|
|
744
|
+
assert.equal('acts' in docket.normalizeMatter({ id: 1, title: 't' }, { source: 's', building: 'review' }), false, 'no acts, no field');
|
|
745
|
+
assert.deepEqual(docket.normalizeActs([{ key: 'allow', pick: 'open_goals', except: [1, 2] }, { key: 'pause', pick: 'everything' }]), [{ key: 'allow', pick: 'open_goals', except: ['1', '2'] }, { key: 'pause' }]);
|
|
746
|
+
assert.equal(docket.normalizeDeskRow({ id: 1, name: 'n', acts: [] }, { desk: 'people.builders', building: 'people' }), null, 'a desk row with nothing to do is dropped');
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test('desks: gated by the atom their write route checks; a gated-out desk is not_yours with no rows; a broken one is unavailable', async () => {
|
|
750
|
+
const c = none();
|
|
751
|
+
c.desks = new Map([
|
|
752
|
+
['people.builders', { desk: 'people.builders', gate: { permission: 'builder.rank.set' }, read: async () => [{ id: 7, name: 'Ana', fact: 'thetes', acts: [{ key: 'tier', options: [{ key: 'metic', label: 'metic' }] }] }, { id: 8, name: 'no acts', acts: [] }] }],
|
|
753
|
+
['automation.goals', { desk: 'automation.goals', gate: { anyBuilder: true }, read: async () => { throw new Error('boom'); } }],
|
|
754
|
+
]);
|
|
755
|
+
const without = await docket.buildDocket({ viewer: VIEWER, permissions: [], contributions: c, now: NOW });
|
|
756
|
+
const by = (d, k) => d.desks.find((x) => x.key === k);
|
|
757
|
+
assert.deepEqual(by(without, 'people.builders'), { key: 'people.builders', building: 'people', status: 'not_yours', rows: [] });
|
|
758
|
+
assert.equal(by(without, 'automation.goals').status, 'unavailable');
|
|
759
|
+
assert.equal(by(without, 'automation.runners').status, 'off');
|
|
760
|
+
const withAtom = await docket.buildDocket({ viewer: VIEWER, permissions: ['builder.rank.set'], contributions: c, now: NOW });
|
|
761
|
+
assert.deepEqual(by(withAtom, 'people.builders').rows.map((r) => [r.id, r.name, r.acts[0].key]), [['people.builders:7', 'Ana', 'tier']]);
|
|
762
|
+
assert.equal(withAtom.matters.length, 0, 'desk rows never become matters (no pins, no counts)');
|
|
763
|
+
});
|
|
@@ -177,9 +177,18 @@ test('every act maps to the route that does it, with the body that route accepts
|
|
|
177
177
|
assert.deepEqual(C.request(gate, 'release'), { method: 'POST', url: '/api/bongos/gate-approvals/812/approve', body: {} });
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
// Task 1004559 moved the core update onto the Release desk, check first: the ONLY
|
|
181
|
+
// way a card reaches the pin is the core_pin row, whose 'take' the server hands
|
|
182
|
+
// only after a passed dry run (tests/hall_street_desk.mjs pins that), and which
|
|
183
|
+
// needs the version chip as its confirmation. Nothing else may name the route.
|
|
184
|
+
test('only the core_pin row reaches the core pin, and never without its version', () => {
|
|
181
185
|
const src = read('street-card.js').replace(/^\s*\/\/.*$/gm, '');
|
|
182
|
-
|
|
186
|
+
const block = src.split('ACT_ROUTES.core_pin = {')[1].split('};')[0];
|
|
187
|
+
assert.equal(src.replace(block, '').match(/core-upgrade|provisioning|\/core-update/g), null, 'no other act names a provisioning route');
|
|
188
|
+
const cv = C.actsCard({ key: 'release.core:41', kind: 'core_pin', id: '41' });
|
|
189
|
+
assert.equal(C.request(cv, 'take', {}), null);
|
|
190
|
+
assert.equal(C.request(cv, 'check', {}), null);
|
|
191
|
+
assert.deepEqual(JSON.parse(JSON.stringify(C.request(cv, 'check', { option: '1.21.20' }))).body, { to: '1.21.20', dry_run: true });
|
|
183
192
|
});
|
|
184
193
|
|
|
185
194
|
test('an objection names its reason and its part before anything is sent; the parts are the server\'s own keys', () => {
|
|
@@ -326,3 +326,232 @@ test('the stylesheet: the desk in tokens only, its motion dies under reduced mot
|
|
|
326
326
|
assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{[^}]*animation: none !important; transition: none !important;/);
|
|
327
327
|
assert.match(read('street.html'), /id="streetTodayPips"/);
|
|
328
328
|
});
|
|
329
|
+
|
|
330
|
+
// ── the old-page actions on their floors (task 1004559) ───────────────────────
|
|
331
|
+
// Rows built by the REAL server mappers (each module's own pure function), run
|
|
332
|
+
// through the docket's own normalisers, into the real desk: so a field the
|
|
333
|
+
// server stops sending, or a desk row the desk cannot draw, reds here.
|
|
334
|
+
const SRV = (p) => require(path.join(ROOT, p));
|
|
335
|
+
const DK = SRV('modules/government/docket.js');
|
|
336
|
+
const reportsRoute = SRV('modules/security/routes/reports.js');
|
|
337
|
+
const lifeDocket = SRV('modules/lifecycle/governor-docket.js');
|
|
338
|
+
const autoDocket = SRV('modules/autonomy/governor-docket.js');
|
|
339
|
+
const buildersRoute = SRV('src/bongos/routes/builders.js');
|
|
340
|
+
const govFloors = SRV('modules/government/docket-floors.js');
|
|
341
|
+
const LADDER = ['xenos', 'thetes', 'metic', 'archon'];
|
|
342
|
+
const healthy = () => ({ ok: true, state: 'alive' });
|
|
343
|
+
|
|
344
|
+
function actedDocket({ longWords = false, force = true } = {}) {
|
|
345
|
+
const t = (s) => (longWords ? `${LONG} ${s}` : s);
|
|
346
|
+
const norm = (source, building, list) => list.map((m) => DK.normalizeMatter(m, { source, building })).filter(Boolean);
|
|
347
|
+
const matters = [
|
|
348
|
+
...norm('redteam_reports', 'disputes', [reportsRoute.redteamDocketMatter({ id: 2, title: t('sign-in limits'), severity: 'high', status: 'open', reported_at: '2026-10-01T10:00:00.000Z' })]),
|
|
349
|
+
...norm('removal_proposals', 'disputes', [lifeDocket.removalMatter({ proposal_task_id: '50', title: t('old search page'), filed_at: '2026-09-30T10:00:00.000Z' })]),
|
|
350
|
+
...norm('criteria_uat', 'review', [
|
|
351
|
+
lifeDocket.uatMatter({ id: 8, criterion_md: t('Refunds work'), goal_id: 4, shipped_total: 3, backend_only: true }, { force }),
|
|
352
|
+
lifeDocket.uatMatter({ id: 9, criterion_md: t('The city draws'), goal_id: 4, shipped_total: 2, backend_only: false }, { force }),
|
|
353
|
+
]),
|
|
354
|
+
...norm('autobongos_fence', 'automation', autoDocket.fenceDocketMatters({ enabled: false, paused_reason: 'payments touched', goals: [] }, true)),
|
|
355
|
+
];
|
|
356
|
+
const desk = (key, building, rows) => ({ key, building, status: 'live', rows: rows.map((r) => DK.normalizeDeskRow(r, { desk: key, building })).filter(Boolean) });
|
|
357
|
+
const project = { enabled: true, priority_goal_id: 1, goals: [{ goal_id: 1, added_at: '2026-09-20T10:00:00.000Z' }, { goal_id: 2, added_at: '2026-09-21T10:00:00.000Z' }] };
|
|
358
|
+
const desks = [
|
|
359
|
+
desk('people.builders', 'people', buildersRoute.builderDeskRows([{ id: 7, display_name: t('Ana Ruiz'), github_login: 'ana', rank: 'thetes' }, { id: 95, display_name: 'Me', rank: 'archon' }], { viewerId: '95', mayRank: true, mayStop: true, ranks: LADDER })),
|
|
360
|
+
desk('people.ranks', 'people', govFloors.rankDeskRows([{ rank_key: 'tester', label: t('Tester'), is_system: false }, { rank_key: 'archon', label: 'Archon', is_system: true }], [{ builder_id: 7, display_name: 'Ana Ruiz', rank_key: 'tester' }, { builder_id: 8, github_login: 'kai', rank_key: 'metic' }])),
|
|
361
|
+
desk('disputes.confirmed', 'disputes', [reportsRoute.confirmedDeskRow({ id: 3, title: t('token leak'), severity: 'low', confirmed_at: '2026-09-28T10:00:00.000Z' })]),
|
|
362
|
+
desk('automation.switch', 'automation', autoDocket.switchDeskRows(project)),
|
|
363
|
+
desk('automation.goals', 'automation', autoDocket.goalDeskRows(project)),
|
|
364
|
+
desk('automation.runners', 'automation', autoDocket.runnerDeskRows([{ builder_id: 7, display_name: t('Ana Ruiz'), runners: [{ working_task_id: 12 }] }, { builder_id: 8, owner_paused: true, owner_paused_reason: 'spending too much', runners: [] }], NOW, healthy)),
|
|
365
|
+
];
|
|
366
|
+
return docket(matters, { desks });
|
|
367
|
+
}
|
|
368
|
+
const GOALS = [{ id: '1', title: 'Governor city', status: 'open' }, { id: '2', title: 'Payments', status: 'open' }, { id: '3', title: LONG, status: 'open' }];
|
|
369
|
+
const actedRows = (dk, key, floor) => D.rowsFor(key, floor, dk, { now: NOW, viewerId: '95', goals: GOALS });
|
|
370
|
+
|
|
371
|
+
test('the moved acts keep the line rule on every floor, long words or short', () => {
|
|
372
|
+
for (const longWords of [false, true]) {
|
|
373
|
+
const dk = actedDocket({ longWords });
|
|
374
|
+
const desks = [];
|
|
375
|
+
for (const key of S.CIVIC_KEYS) I.PLAN[key].floors.forEach((_f, floor) => desks.push(D.deskModel({ key, floor, docket: dk, goals: GOALS, now: NOW, viewerId: '95' })));
|
|
376
|
+
assert.deepEqual(desks.flatMap((d) => D.breaks(d)), []);
|
|
377
|
+
const rows = desks.flatMap((d) => d.rows);
|
|
378
|
+
assert.ok(rows.length >= 14, 'every moved act drew its row');
|
|
379
|
+
assert.ok(rows.every((r) => r.wait === '' || r.actions.length === 0));
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('breaks() catches an asking button with no chips of its own', () => {
|
|
384
|
+
const dk = actedDocket();
|
|
385
|
+
const dm = D.deskModel({ key: 'people', floor: 2, docket: dk, goals: GOALS, now: NOW, viewerId: '95' });
|
|
386
|
+
assert.deepEqual(D.breaks(dm), []);
|
|
387
|
+
const bad = j(dm);
|
|
388
|
+
bad.rows[0].choices = {};
|
|
389
|
+
assert.ok(D.breaks(bad).some((b) => /without its own chips/.test(b)));
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('People: set tier and deactivate are chips on the builder row, never your own; ranks are given and taken back', () => {
|
|
393
|
+
const dk = actedDocket();
|
|
394
|
+
const rows = actedRows(dk, 'people', 2);
|
|
395
|
+
assert.deepEqual(rows.map((r) => r.name), ['Ana Ruiz'], 'your own row is never drawn');
|
|
396
|
+
const ana = rows[0];
|
|
397
|
+
assert.deepEqual(ana.actions.map((a) => [a.key, a.label]), [['tier', 'tier'], ['deactivate', 'deactivate']]);
|
|
398
|
+
assert.deepEqual(ana.choices.tier.options.map((o) => o.key), ['xenos', 'metic'], 'one rung up at most, any rung down');
|
|
399
|
+
assert.deepEqual(j(C.request(ana.card, 'tier', ana.choices.tier.options[1].input)), { method: 'PATCH', url: '/api/bongos/builders/7/rank', body: { rank: 'metic' } });
|
|
400
|
+
assert.deepEqual(j(C.request(ana.card, 'deactivate', ana.choices.deactivate.options[0].input)), { method: 'PATCH', url: '/api/bongos/builders/7/status', body: { status: 'inactive', reason: 'left the project' } });
|
|
401
|
+
const rank = actedRows(dk, 'people', 5)[0];
|
|
402
|
+
assert.deepEqual(rank.actions.map((a) => a.key), ['give', 'take']);
|
|
403
|
+
assert.deepEqual(rank.choices.give.options.map((o) => o.label), ['kai']);
|
|
404
|
+
assert.deepEqual(j(C.request(rank.card, 'give', rank.choices.give.options[0].input)), { method: 'POST', url: '/api/bongos/government/assignments', body: { builder_id: 8, rank_key: 'tester' } });
|
|
405
|
+
assert.deepEqual(j(C.request(rank.card, 'take', rank.choices.take.options[0].input)), { method: 'DELETE', url: '/api/bongos/government/assignments/7/tester', body: {} });
|
|
406
|
+
assert.deepEqual(buildersRoute.builderDeskRows([{ id: 7, rank: 'xenos' }], { viewerId: '95', mayRank: false, mayStop: false, ranks: LADDER }), [], 'no atom, no rows');
|
|
407
|
+
assert.deepEqual(buildersRoute.builderDeskRows([{ id: 7, rank: 'xenos' }], { viewerId: '95', mayRank: false, mayStop: true, ranks: LADDER })[0].acts.map((a) => a.key), ['deactivate']);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('Disputes: a report is confirmed or disputed, a confirmed one marked fixed, a removal confirmed or kept', () => {
|
|
411
|
+
const dk = actedDocket();
|
|
412
|
+
const [rep, fix] = actedRows(dk, 'disputes', 0);
|
|
413
|
+
assert.deepEqual(rep.actions.map((a) => a.label), ['confirm', 'dispute']);
|
|
414
|
+
assert.equal(C.request(rep.card, 'confirm').url, '/api/bongos/security/reports/2/confirm');
|
|
415
|
+
assert.equal(C.request(rep.card, 'dispute').url, '/api/bongos/security/reports/2/dispute');
|
|
416
|
+
assert.deepEqual(fix.actions.map((a) => a.label), ['fixed']);
|
|
417
|
+
assert.equal(C.request(fix.card, 'fix').url, '/api/bongos/security/reports/3/fix');
|
|
418
|
+
const rm = actedRows(dk, 'disputes', 1)[0];
|
|
419
|
+
assert.deepEqual(rm.actions.map((a) => a.label), ['remove', 'keep']);
|
|
420
|
+
assert.deepEqual(j(C.request(rm.card, 'remove')), { method: 'POST', url: '/api/bongos/removal-proposals/50/decide', body: { verdict: 'confirmed' } });
|
|
421
|
+
assert.deepEqual(j(C.request(rm.card, 'keep')).body, { verdict: 'dismissed' });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('Automation: switch on and off, allow a goal from the open goals, first in line, disallow, pause and resume a runner', () => {
|
|
425
|
+
const dk = actedDocket();
|
|
426
|
+
const sw = actedRows(dk, 'automation', 0);
|
|
427
|
+
const off = sw.find((r) => r.source === 'autobongos_fence');
|
|
428
|
+
assert.deepEqual(off.actions.map((a) => a.label), ['switch on']);
|
|
429
|
+
assert.deepEqual(j(C.request(off.card, 'switch_on')), { method: 'POST', url: '/api/bongos/autonomy/fence', body: { enabled: true } });
|
|
430
|
+
const on = sw.find((r) => r.desk);
|
|
431
|
+
assert.deepEqual(j(C.request(on.card, 'switch_off', on.choices.switch_off.options[0].input)).body, { enabled: false, reason: 'pause for now' });
|
|
432
|
+
const goals = actedRows(dk, 'automation', 1);
|
|
433
|
+
const first = goals.find((r) => r.name === 'Governor city');
|
|
434
|
+
assert.ok(first, 'an allowed goal is lettered with its title');
|
|
435
|
+
assert.deepEqual(first.actions.map((a) => a.key), ['disallow'], 'the goal first in line is not offered first again');
|
|
436
|
+
assert.equal(j(C.request(first.card, 'disallow')).method, 'DELETE');
|
|
437
|
+
assert.equal(C.request(first.card, 'disallow').url, '/api/bongos/autonomy/fence/goals/1');
|
|
438
|
+
const pay = goals.find((r) => r.name === 'Payments');
|
|
439
|
+
assert.deepEqual(j(C.request(pay.card, 'first')), { method: 'POST', url: '/api/bongos/autonomy/fence/priority', body: { goal_id: 2 } });
|
|
440
|
+
const add = goals.find((r) => r.actions.some((a) => a.key === 'allow'));
|
|
441
|
+
assert.deepEqual(add.choices.allow.options.map((o) => o.key), ['3'], 'only open goals not yet allowed');
|
|
442
|
+
const goalDesk = D.deskModel({ key: 'automation', floor: 1, docket: dk, goals: GOALS, now: NOW });
|
|
443
|
+
assert.match(goalDesk.status, /^2 allowed/, 'the allow row is a tool, not a goal: the floor counts two');
|
|
444
|
+
assert.deepEqual(j(C.request(add.card, 'allow', add.choices.allow.options[0].input)).body, { goal_id: 3 });
|
|
445
|
+
const runners = actedRows(dk, 'automation', 2);
|
|
446
|
+
const busy = runners.find((r) => r.actions[0].key === 'pause');
|
|
447
|
+
assert.deepEqual(j(C.request(busy.card, 'pause', busy.choices.pause.options[2].input)), { method: 'POST', url: '/api/bongos/autonomy/builders/7/pause', body: { paused: true, reason: 'it is stuck' } });
|
|
448
|
+
const paused = runners.find((r) => r.actions[0].key === 'resume');
|
|
449
|
+
assert.deepEqual(j(C.request(paused.card, 'resume')).body, { paused: false });
|
|
450
|
+
// with every open goal allowed, the allow row has no chip to offer and is not drawn
|
|
451
|
+
const none = D.rowsFor('automation', 1, dk, { now: NOW, goals: GOALS.slice(0, 2) });
|
|
452
|
+
assert.ok(!none.some((r) => r.actions.some((a) => a.key === 'allow')));
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('Review: a backend-only promise signs off in one tap; a recorded one goes to its page; force sign-off only with its atom', () => {
|
|
456
|
+
const rows = actedRows(actedDocket(), 'review', 1);
|
|
457
|
+
const backend = rows.find((r) => r.actions[0].key === 'signoff');
|
|
458
|
+
assert.deepEqual(j(C.request(backend.card, 'signoff', backend.choices.signoff.options[0].input)), { method: 'POST', url: '/api/bongos/done-when/8/uat', body: { kind: 'backend_signoff', live_attested: true } });
|
|
459
|
+
assert.deepEqual(j(C.request(backend.card, 'force', backend.choices.force.options[0].input)), { method: 'POST', url: '/api/bongos/done-when/8/satisfy', body: { override_reason: 'Signed off on the floor desk: tested by hand.' } });
|
|
460
|
+
const ui = rows.find((r) => r.actions[0].key === 'record');
|
|
461
|
+
assert.equal(ui.actions[0].go, '/goals#/goal/4');
|
|
462
|
+
const noForce = actedRows(actedDocket({ force: false }), 'review', 1);
|
|
463
|
+
assert.ok(noForce.every((r) => !r.actions.some((a) => a.key === 'force')));
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('a viewer the server hands no acts sees the quiet line, not rows', () => {
|
|
467
|
+
const dk = actedDocket();
|
|
468
|
+
dk.desks = dk.desks.map((d) => ({ ...d, status: 'not_yours', rows: [] }));
|
|
469
|
+
const dm = D.deskModel({ key: 'people', floor: 2, docket: dk, goals: GOALS, now: NOW, viewerId: '95' });
|
|
470
|
+
assert.equal(dm.quiet, true);
|
|
471
|
+
assert.deepEqual(D.breaks(dm), []);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ── Release, Rules, Treasury and the invite (task 1004559, second pass) ──────
|
|
475
|
+
const coreRoute = SRV('modules/provisioning/routes/core-upgrade.js');
|
|
476
|
+
const settingsRoute = SRV('modules/lifecycle/routes/project-settings.js');
|
|
477
|
+
const modulesRoute = SRV('src/bongos/routes/modules.js');
|
|
478
|
+
const backupRoute = SRV('src/bongos/routes/backup.js');
|
|
479
|
+
const accessRoute = SRV('modules/onboarding/routes/access-requests.js');
|
|
480
|
+
const PREVIEW = { instance_id: 41, served_version: '1.21.19', recommended_version: '1.21.20', behind: true, observed_at: '2026-10-03T10:00:00.000Z' };
|
|
481
|
+
|
|
482
|
+
function moreDesks({ preflight = null, longWords = false } = {}) {
|
|
483
|
+
const t = (s) => (longWords ? `${LONG} ${s}` : s);
|
|
484
|
+
const desk = (key, building, rows) => ({ key, building, status: 'live', rows: rows.map((r) => DK.normalizeDeskRow(r, { desk: key, building })).filter(Boolean) });
|
|
485
|
+
const dk = actedDocket({ longWords });
|
|
486
|
+
dk.matters.push(DK.normalizeMatter({ id: '1.21.20', rule: 'decide', state: 'needs', title: 'Take core 1.21.20 (running 1.21.19)', short: 'core 1.21.20', href: '/settings#software-update', ref: { type: 'core_version', id: '1.21.20' } }, { source: 'core_update', building: 'release' }));
|
|
487
|
+
dk.desks.push(
|
|
488
|
+
desk('release.core', 'release', coreRoute.coreDeskRows({ ...PREVIEW, preflight })),
|
|
489
|
+
desk('rules.settings', 'rules', settingsRoute.settingsDeskRows({ days: 14, categories: [{ active: true }, { active: false }] })),
|
|
490
|
+
desk('rules.modules', 'rules', modulesRoute.moduleDeskRows([{ key: 'discord', title: t('Discord'), enabled: true }, { key: 'economy', title: 'Economy', enabled: false }])),
|
|
491
|
+
desk('treasury.backup', 'treasury', backupRoute.backupDeskRows({ age_hours: 30, stale: false }, false)),
|
|
492
|
+
desk('people.invite', 'people', [accessRoute.INVITE_DESK_ROW]),
|
|
493
|
+
);
|
|
494
|
+
return dk;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
test('the second-pass desks keep the line rule on every floor', () => {
|
|
498
|
+
for (const longWords of [false, true]) {
|
|
499
|
+
for (const preflight of [null, { target: '1.21.20', ok: true }, { target: '1.21.20', ok: false, error: LONG }]) {
|
|
500
|
+
const dk = moreDesks({ preflight, longWords });
|
|
501
|
+
const desks = [];
|
|
502
|
+
for (const key of S.CIVIC_KEYS) I.PLAN[key].floors.forEach((_f, floor) => desks.push(D.deskModel({ key, floor, docket: dk, goals: GOALS, now: NOW, viewerId: '95' })));
|
|
503
|
+
assert.deepEqual(desks.flatMap((d) => D.breaks(d)), []);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('Release: the core update is checked first; take is offered only after a pass, for that version, and stands in for the link', () => {
|
|
509
|
+
const rows = (pf) => actedRows(moreDesks({ preflight: pf }), 'release', 3);
|
|
510
|
+
const fresh = rows(null);
|
|
511
|
+
assert.deepEqual(fresh.map((r) => r.source), ['release.core'], 'the core row replaces the plain core-update link');
|
|
512
|
+
assert.deepEqual(fresh[0].actions.map((a) => a.key), ['check'], 'no take before a check');
|
|
513
|
+
assert.deepEqual(j(C.request(fresh[0].card, 'check', fresh[0].choices.check.options[0].input)), { method: 'POST', url: '/api/bongos/provisioning/instances/41/core-upgrade', body: { to: '1.21.20', dry_run: true } });
|
|
514
|
+
assert.deepEqual(rows({ target: '1.21.20', ok: false, error: 'x' })[0].actions.map((a) => a.key), ['check'], 'a refused check offers no take');
|
|
515
|
+
assert.deepEqual(rows({ target: '1.21.19', ok: true })[0].actions.map((a) => a.key), ['check'], 'a pass for another version is no pass');
|
|
516
|
+
const passed = rows({ target: '1.21.20', ok: true })[0];
|
|
517
|
+
assert.deepEqual(passed.actions.map((a) => [a.key, a.label]), [['take', 'take it'], ['check', 'check']]);
|
|
518
|
+
assert.deepEqual(j(C.request(passed.card, 'take', passed.choices.take.options[0].input)), { method: 'POST', url: '/api/bongos/provisioning/instances/41/core-upgrade', body: { to: '1.21.20' } });
|
|
519
|
+
assert.equal(C.request(passed.card, 'take', {}), null, 'no version chip, no move');
|
|
520
|
+
assert.deepEqual(coreRoute.coreDeskRows({ ...PREVIEW, open_intent: { state: 'running' } }), [], 'a run in progress offers nothing');
|
|
521
|
+
assert.deepEqual(coreRoute.coreDeskRows({ ...PREVIEW, behind: false }), []);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('Rules: the rot timer as day chips, work categories as a link, modules on in one tap and off with a yes', () => {
|
|
525
|
+
const rows = actedRows(moreDesks(), 'rules', 0);
|
|
526
|
+
const rot = rows.find((r) => r.name === 'Rot timer');
|
|
527
|
+
assert.deepEqual(rot.choices.rot.options.map((o) => o.key), ['7', '30', '60', '90'], 'the current value is not offered');
|
|
528
|
+
assert.deepEqual(j(C.request(rot.card, 'rot', rot.choices.rot.options[1].input)), { method: 'PATCH', url: '/api/bongos/project-settings/rot-days', body: { days: 30 } });
|
|
529
|
+
const cats = rows.find((r) => r.name === 'Work categories');
|
|
530
|
+
assert.equal(cats.actions[0].go, '/project-settings');
|
|
531
|
+
const on = rows.find((r) => r.name === 'Economy');
|
|
532
|
+
assert.deepEqual(j(C.request(on.card, 'module_on')), { method: 'POST', url: '/api/bongos/modules/economy/enable', body: { enabled: true } });
|
|
533
|
+
const off = rows.find((r) => r.name === 'Discord');
|
|
534
|
+
assert.equal(off.choices.module_off.options[0].label, 'yes, turn off');
|
|
535
|
+
assert.deepEqual(j(C.request(off.card, 'module_off', off.choices.module_off.options[0].input)).body, { enabled: false });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('Treasury: back up now in one tap; People: invite takes one typed GitHub name', () => {
|
|
539
|
+
const dk = moreDesks();
|
|
540
|
+
const backup = actedRows(dk, 'treasury', 2)[0];
|
|
541
|
+
assert.equal(backup.fact, 'last 30 hours ago');
|
|
542
|
+
assert.deepEqual(j(C.request(backup.card, 'backup')), { method: 'POST', url: '/api/bongos/backup/trigger', body: {} });
|
|
543
|
+
assert.deepEqual(backupRoute.backupDeskRows(null, true)[0].acts, [], 'a backup already running offers no second one');
|
|
544
|
+
const invite = actedRows(dk, 'people', 4).find((r) => r.name === 'Invite someone');
|
|
545
|
+
assert.equal(invite.choices.invite.field, 'github_login');
|
|
546
|
+
assert.deepEqual(j(C.request(invite.card, 'invite', { option: ' ana-ruiz ' })), { method: 'POST', url: '/api/bongos/access-requests/invite', body: { github_login: 'ana-ruiz' } });
|
|
547
|
+
assert.equal(C.request(invite.card, 'invite', { option: ' ' }), null, 'nothing typed, nothing sent');
|
|
548
|
+
const tiles = D.deskModel({ key: 'treasury', floor: 2, docket: dk, goals: GOALS, now: NOW });
|
|
549
|
+
assert.deepEqual(D.breaks(tiles), []);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('the Treasury desk reads the newest backup without blocking (no sync disk read on the docket path)', () => {
|
|
553
|
+
const src = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/backup.js'), 'utf8');
|
|
554
|
+
assert.match(src, /desk: 'treasury\.backup'[^\n]*await newestDumpAge\(\)/);
|
|
555
|
+
const body = src.split('async function newestDumpAge')[1].split('\nfunction latestDump')[0];
|
|
556
|
+
assert.doesNotMatch(body, /Sync\(/);
|
|
557
|
+
});
|