@bongos/core 1.21.33 → 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.
Files changed (37) hide show
  1. package/.bongos-core.json +51 -41
  2. package/docs/copy-inventory.md +19 -17
  3. package/docs/copy-registry.json +39 -21
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/page-inventory.json +11 -1
  6. package/docs/page-readings.json +322 -194
  7. package/modules/autonomy/governor-docket.js +68 -1
  8. package/modules/government/docket-floors.js +20 -0
  9. package/modules/government/docket-vocab.js +14 -0
  10. package/modules/government/docket.js +103 -2
  11. package/modules/government/routes/government.js +13 -0
  12. package/modules/hall-ui/public/street-card.js +97 -1
  13. package/modules/hall-ui/public/street-desk.js +386 -0
  14. package/modules/hall-ui/public/street-inside.js +14 -10
  15. package/modules/hall-ui/public/street.css +60 -0
  16. package/modules/hall-ui/public/street.html +9 -3
  17. package/modules/hall-ui/public/street.js +238 -17
  18. package/modules/hall-ui/public/street.states.json +314 -6
  19. package/modules/lifecycle/governor-docket.js +24 -2
  20. package/modules/lifecycle/routes/project-settings.js +24 -0
  21. package/modules/onboarding/routes/access-requests.js +7 -0
  22. package/modules/provisioning/core-upgrade.js +1 -1
  23. package/modules/provisioning/routes/core-upgrade.js +36 -1
  24. package/modules/security/routes/reports.js +23 -0
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/release-notes.json +12 -0
  28. package/src/bongos/routes/backup.js +26 -0
  29. package/src/bongos/routes/builders.js +40 -0
  30. package/src/bongos/routes/modules.js +18 -0
  31. package/src/module-api.js +1 -1
  32. package/tests/governor_docket.mjs +28 -1
  33. package/tests/hall_matter_card.mjs +12 -3
  34. package/tests/hall_street.mjs +1 -1
  35. package/tests/hall_street_amend.mjs +1 -1
  36. package/tests/hall_street_desk.mjs +557 -0
  37. package/tests/hall_street_inside.mjs +1 -1
@@ -0,0 +1,557 @@
1
+ // tests/hall_street_desk.mjs — THE FLOOR DESK AND THE LINE RULE (task 1004558,
2
+ // goal 1000125; round 2 of the Governor City).
3
+ //
4
+ // WHAT IT GUARDS. The chosen floor of a building is the desk from the round 2
5
+ // board (docs/design/mocks/governor-city/Desks.dc.html), built by
6
+ // GovDesk.deskModel() (street-desk.js) and drawn by street.js. What can go wrong:
7
+ // * THE LINE RULE breaking on real data: a row name over three words, a
8
+ // second fact, a third button, a tile that reads zero or carries a
9
+ // sentence, a status that is orange when nothing needs you, an empty list
10
+ // where a quiet floor should be one lit line. Pinned by running EVERY
11
+ // building and EVERY floor of a deliberately long-winded docket through
12
+ // GovDesk.breaks() — the test that fails when the docket produces a line
13
+ // the rule does not allow;
14
+ // * fit() drifting from the board's own fit() (run from the board's
15
+ // Component class) or the limits drifting from the board's numbers;
16
+ // * a row's button sending anything but the matter card's own request to
17
+ // the existing route, or a reason arriving as free text instead of a chip;
18
+ // * the game layer going quiet: the act that does not clear its row, count a
19
+ // pip on the today meter, light the cleared floor with a tick, or say a toast.
20
+ //
21
+ // Run: node --test tests/hall_street_desk.mjs
22
+
23
+ import { test } from 'node:test';
24
+ import assert from 'node:assert/strict';
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import vm from 'node:vm';
28
+ import { createRequire } from 'node:module';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const PUB = path.join(ROOT, 'modules/hall-ui/public');
33
+ const require = createRequire(import.meta.url);
34
+ require(path.join(PUB, 'city-draw.js'));
35
+ const S = require(path.join(PUB, 'street-draw.js'));
36
+ const I = require(path.join(PUB, 'street-inside.js'));
37
+ const C = require(path.join(PUB, 'street-card.js'));
38
+ const D = require(path.join(PUB, 'street-desk.js'));
39
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
40
+ const NOW = Date.parse('2026-10-03T12:00:00.000Z');
41
+ const j = (v) => JSON.parse(JSON.stringify(v));
42
+
43
+ // the board's own Component class, run beside the desk
44
+ function board() {
45
+ const src = fs.readFileSync(path.join(ROOT, 'docs/design/mocks/governor-city/Desks.dc.html'), 'utf8');
46
+ const script = src.split('data-dc-script')[1].split('>').slice(1).join('>').split('</script>')[0];
47
+ const DCLogic = class { constructor(p) { this.props = p || {}; this.state = {}; } setState(s) { Object.assign(this.state, s); } };
48
+ const Comp = new Function('DCLogic', 'setTimeout', 'clearTimeout', `${script}\nreturn Component;`)(DCLogic, () => 0, () => {});
49
+ return new Comp({ mode: 'dark' });
50
+ }
51
+
52
+ // ── a long-winded docket: every source, long words everywhere ─────────────────
53
+ const LONG = 'The extraordinarily long-winded proposal to reorganise every single floor of the hall before winter';
54
+ const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['gate_approvals', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
55
+ function matter(over = {}) {
56
+ return { id: 'blockers:1', source: 'blockers', building: 'planning', rule: 'majority', state: 'needs', title: 'A matter', short: 'a matter', where: 'Planning', opened_at: '2026-09-29T10:00:00.000Z', deadline: null, proposer: null, tally: null, href: null, ref: null, ...over };
57
+ }
58
+ function docket(matters, over = {}) {
59
+ const counts = { needs: 0, moving: 0, settled: 0 };
60
+ matters.forEach((m) => { counts[m.state] += 1; });
61
+ return {
62
+ viewer: { id: '95' }, counts, line: '', matters,
63
+ buildings: S.CIVIC_KEYS.map((key) => ({ key, name: key[0].toUpperCase() + key.slice(1), group: '', counts: {}, matter_ids: [] })),
64
+ sources: SOURCES.map(([source, building]) => ({ source, building, label: source, status: 'live', count: 0, reason: null })),
65
+ readings: ['flow', 'promises', 'money', 'people', 'fairness', 'rules'].map((key) => ({ key, label: key, building: 'people', status: 'live', value: 0, figure: key === 'money' ? '$1,234,567.89 left this month' : '0 people', line: LONG, hot: false, reason: null })),
66
+ floors: Object.values(I.PLAN).flatMap((p) => p.floors).map((f) => f[2] && f[2].floor).filter(Boolean).map((key, i) => ({ key, building: key.split('.')[0], status: 'live', count: i % 3 === 0 ? '0' : i % 3 === 1 ? '1,204,331' : '', line: i % 2 ? LONG : '12 things that the floor counts', lines: [[LONG, 'today']], hot: i % 4 === 0, reason: null })),
67
+ history: S.CIVIC_KEYS.flatMap((b) => [0, 1, 2, 3, 4].map((n) => ({ id: `h:${b}:${n}`, source: 'government', building: b, title: LONG, at: '2026-09-2' + n + 'T10:00:00.000Z', outcome: 'passed after a very long sitting' }))),
68
+ ...over,
69
+ };
70
+ }
71
+ function longMatters() {
72
+ const out = [];
73
+ let n = 0;
74
+ for (const [source, building] of SOURCES) {
75
+ for (const state of ['needs', 'moving', 'settled']) {
76
+ n += 1;
77
+ out.push(matter({ id: `${source}:${n}`, source, building, state, title: `${LONG} ${n}`, short: 'a matter with a very long short description', where: 'Somewhere far away in the hall, vouched for by everyone', proposer: { name: 'Maximiliana Theodora Alexandrovna-Wellington III', login: 'maxi' }, deadline: '2026-10-05T10:00:00.000Z', opened_at: `2026-0${(n % 9) + 1}-1${n % 9}T10:00:00.000Z`, href: source === 'goal_join_requests' ? '/builders/goal/42' : source === 'core_update' ? '/settings#software-update' : null, tally: source === 'board' ? { open: 3, needs: 'needs 4 of 5' } : null }));
78
+ }
79
+ }
80
+ out.push(matter({ id: 'board:900', source: 'board', building: 'planning', short: 'idea to ratify', title: LONG, state: 'needs' }));
81
+ return out;
82
+ }
83
+ const ROOM = { open: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: LONG, author_id: '7' }, votes: [] }, { id: '900', subject_type: 'full_idea', subject: { title: LONG }, votes: [] }], closed: [] };
84
+
85
+ function everyDesk(dk, extra = {}) {
86
+ const out = [];
87
+ for (const key of S.CIVIC_KEYS) {
88
+ I.PLAN[key].floors.forEach((_f, floor) => out.push(D.deskModel({ key, floor, docket: dk, goals: [{ id: '1', title: LONG, status: 'open' }], now: NOW, room: ROOM, viewerId: '95', ...extra })));
89
+ }
90
+ return out;
91
+ }
92
+
93
+ // ── the line rule ─────────────────────────────────────────────────────────────
94
+
95
+ test('fit() is the board\'s fit(), with a word cap on top', () => {
96
+ const b = board();
97
+ for (const [s, n] of [['Governor city', 20], [LONG, 20], [LONG, 44], ['Ana Ruiz', 18], ['exactly twenty chars', 20], [' spaced out ', 30]]) {
98
+ assert.equal(D.fit(s, n), b.fit(String(s).replace(/\s+/g, ' ').trim(), n), `${s} @ ${n}`);
99
+ }
100
+ assert.equal(D.fit('Raise the hosting limit', 20, 3), 'Raise the hosting…', 'a cut by words says so');
101
+ assert.equal(D.fit('Ana Ruiz', 20, 3), 'Ana Ruiz', 'nothing cut, nothing marked');
102
+ for (const n of [5, 12, 20]) for (const w of [1, 2, 3]) assert.ok(D.fit(LONG, n, w).length <= n, 'never longer than its cap');
103
+ });
104
+
105
+ test('the limits are the board\'s numbers: name 20, fact 26, facts 24 (three), status 44, record 30 (four), three tiles, ten pips', () => {
106
+ const L = D.LIMITS;
107
+ assert.deepEqual([L.name, L.nameWords, L.fact, L.factLen, L.facts, L.actions, L.status, L.recordLen, L.record, L.tiles, L.tileWords, L.todayTarget], [20, 3, 26, 24, 3, 2, 44, 30, 4, 3, 2, 10]);
108
+ const src = fs.readFileSync(path.join(ROOT, 'docs/design/mocks/governor-city/Desks.dc.html'), 'utf8');
109
+ for (const pin of ['this.fit(r.t, 20)', 'this.fit(r.f, 26)', 'this.fit(t, 24)', '.slice(0, 3)', "this.fit('nothing needs you · ' + F.quiet, 44)", "this.fit(t + ' · now', 30)", '.slice(0, 4)', 'const target = 10']) assert.ok(src.includes(pin), `the board says ${pin}`);
110
+ });
111
+
112
+ test('THE LINE RULE: no row, tile or status any docket produces breaks it, on every floor of every building', () => {
113
+ const dk = docket(longMatters());
114
+ const desks = everyDesk(dk);
115
+ assert.equal(desks.length, S.CIVIC_KEYS.reduce((a, k) => a + I.PLAN[k].floors.length, 0));
116
+ const broken = desks.flatMap((d) => D.breaks(d));
117
+ assert.deepEqual(broken, []);
118
+ // the stress docket really reaches every part of the rule
119
+ const rows = desks.flatMap((d) => d.rows);
120
+ assert.ok(rows.length >= 30, 'rows were drawn');
121
+ assert.ok(rows.some((r) => r.actions.length === 2) && rows.some((r) => r.chips) && rows.some((r) => r.wait), 'two buttons, chips and a wait pill were all exercised');
122
+ assert.ok(desks.some((d) => d.quiet), 'a quiet floor was exercised');
123
+ assert.ok(desks.some((d) => d.tiles.length === 3), 'three tiles were exercised');
124
+ assert.ok(desks.some((d) => d.hot), 'an orange status was exercised');
125
+ const moving = everyDesk(docket(longMatters().filter((m) => m.state !== 'needs')));
126
+ assert.ok(moving.some((d) => !d.hot && d.rows.length), 'a grey status over rows was exercised');
127
+ assert.deepEqual(moving.flatMap((d) => D.breaks(d)), []);
128
+ assert.ok(rows.some((r) => r.name.endsWith('…')), 'a long title was cut, and says so');
129
+ // the same docket with acts done, floors cleared, and with nothing readable
130
+ assert.deepEqual(everyDesk(dk, { done: Object.fromEntries(dk.matters.map((m) => [m.id, true])), cleared: true, mine: [LONG, 'let in Ana'], today: 123456 }).flatMap((d) => D.breaks(d)), []);
131
+ assert.deepEqual(everyDesk(null).flatMap((d) => D.breaks(d)), []);
132
+ assert.deepEqual(everyDesk(docket([])).flatMap((d) => D.breaks(d)), []);
133
+ });
134
+
135
+ test('breaks() names every way a desk can break the rule (so the rule test can fail)', () => {
136
+ const dk = docket(longMatters());
137
+ const good = D.deskModel({ key: 'people', floor: 4, docket: dk, now: NOW });
138
+ assert.deepEqual(D.breaks(good), []);
139
+ const bad = j(good);
140
+ bad.rows[0].name = 'Four words in here';
141
+ bad.rows[0].fact = 'x'.repeat(27);
142
+ bad.rows[0].actions = [{ label: 'a' }, { label: 'b' }, { label: 'c' }];
143
+ bad.rows[0].facts = ['a', 'b', 'c', 'd'];
144
+ bad.tiles = [{ v: '0', l: 'need you' }, { v: '3', l: 'three whole words' }, { v: '4', l: 'ok' }, { v: '5', l: 'ok' }];
145
+ bad.status = 'x'.repeat(45);
146
+ bad.hot = false;
147
+ const why = D.breaks(bad).join('\n');
148
+ for (const w of ['row name', 'row fact', 'more than two buttons', 'row facts', 'tile value', 'tile label', 'more than three tiles', 'status too long', 'orange only when']) assert.match(why, new RegExp(w), w);
149
+ const empty = j(good); empty.rows = []; empty.quiet = false;
150
+ assert.match(D.breaks(empty).join(), /quiet and rows disagree/, 'never an empty list');
151
+ });
152
+
153
+ // ── which matters sit on which floor ──────────────────────────────────────────
154
+
155
+ test('a floor holds its own source; the ideas floor holds the open ideas; the rest sit at the front desk; settled is the record', () => {
156
+ const ms = [
157
+ matter({ id: 'access_requests:1', source: 'access_requests', building: 'people', state: 'needs' }),
158
+ matter({ id: 'access_requests:2', source: 'access_requests', building: 'people', state: 'settled' }),
159
+ matter({ id: 'board:5', source: 'board', building: 'people', state: 'needs' }),
160
+ matter({ id: 'board:6', source: 'board', building: 'planning', short: 'idea to ratify', state: 'moving' }),
161
+ ];
162
+ const dk = docket(ms);
163
+ const at = (k, f) => D.floorMatters(k, f, dk).map((m) => m.id);
164
+ assert.deepEqual(at('people', 4), ['access_requests:1'], 'Waiting to join: the open request only');
165
+ assert.deepEqual(at('people', 0), ['board:5'], 'a sitting sits at the front desk');
166
+ assert.deepEqual(at('planning', 2), ['board:6'], 'Ideas to ratify');
167
+ assert.deepEqual(at('planning', 0), [], 'an idea is not also at the front desk');
168
+ assert.equal(D.firstNeeds('people', dk), 0);
169
+ assert.equal(D.firstNeeds('rules', dk), null);
170
+ });
171
+
172
+ // ── the acts are the card's own requests ──────────────────────────────────────
173
+
174
+ test('a row\'s buttons are the card\'s acts under the board\'s words, and send the card\'s own request', () => {
175
+ const ms = [
176
+ matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 } }),
177
+ matter({ id: 'goal_join_requests:12', source: 'goal_join_requests', building: 'people', state: 'needs', proposer: { name: 'Kai Mo' }, href: '/builders/goal/42', ref: { id: 12 } }),
178
+ matter({ id: 'override_requests:13', source: 'override_requests', building: 'release', state: 'needs', ref: { id: 13 } }),
179
+ matter({ id: 'gate_approvals:14', source: 'gate_approvals', building: 'release', state: 'needs', ref: { id: 14 } }),
180
+ matter({ id: 'board:1', source: 'board', building: 'charter', state: 'needs', ref: { type: 'board_item', id: '1' } }),
181
+ ];
182
+ const dk = docket(ms);
183
+ const row = (k, f) => D.rowsFor(k, f, dk, { room: ROOM, now: NOW, viewerId: '95' })[0];
184
+ const ana = row('people', 4);
185
+ assert.equal(ana.name, 'Ana Ruiz', 'a person is named by their name');
186
+ assert.deepEqual(ana.actions.map((a) => [a.key, a.label, a.tone]), [['fine', 'let in', 'primary'], ['refuse', 'not now', 'quiet']]);
187
+ assert.deepEqual(j(C.request(ana.card, 'fine')), { method: 'PATCH', url: '/api/bongos/access-requests/11', body: { status: 'invited' } });
188
+ assert.deepEqual(j(C.request(row('people', 6).card, 'refuse')), { method: 'POST', url: '/api/bongos/goals/42/requests/12/respond', body: { action: 'deny' } });
189
+ const rel = D.rowsFor('release', 0, dk, { now: NOW });
190
+ assert.deepEqual(rel[0].actions.map((a) => a.label), ['release', 'hold']);
191
+ assert.equal(C.request(rel[0].card, 'release').url, '/api/bongos/override-requests/13/decide');
192
+ assert.equal(C.request(D.rowsFor('release', 5, dk, { now: NOW })[0].card, 'release').url, '/api/bongos/gate-approvals/14/approve');
193
+ // a vote: yes and no, and the no's reason is a chip, never free text
194
+ const vote = row('charter', 0);
195
+ assert.deepEqual(vote.actions.map((a) => [a.key, a.label]), [['for', 'yes'], ['object', 'no']]);
196
+ assert.equal(vote.chips.for, 'object');
197
+ assert.deepEqual(vote.chips.options.map((c) => c.key), C.SECTIONS.map((s) => s.key), 'every chip is a section the server accepts');
198
+ const chip = vote.chips.options[0];
199
+ assert.equal(C.check(vote.card, 'object', chip.input), null, 'one tap is a complete objection');
200
+ assert.deepEqual(j(C.request(vote.card, 'object', chip.input).body), { direction: 'no', reason_md: chip.input.reason, section_key: 'scope' });
201
+ // before the sitting is read the row waits, never guesses a ballot
202
+ assert.equal(D.rowsFor('charter', 0, dk, { now: NOW })[0].wait, 'reading…');
203
+ });
204
+
205
+ // ── the page: the desk acts in place, and the game layer answers ──────────────
206
+
207
+ function fakeDom() {
208
+ const byId = new Map();
209
+ const focused = { el: null };
210
+ const mk = (tag) => ({
211
+ tagName: tag, attrs: {}, kids: [], className: '', listeners: {}, _text: '', hidden: false,
212
+ style: { setProperty(k, v) { this[k] = v; } },
213
+ clientWidth: 1440, clientHeight: 708, offsetLeft: 0, scrollLeft: 0, parentNode: null,
214
+ get children() { const a = this.kids.slice(); a[Symbol.isConcatSpreadable] = false; return a; },
215
+ get firstChild() { return this.kids[0] || null; },
216
+ setAttribute(k, v) { this.attrs[k] = String(v); if (k === 'id') byId.set(String(v), this); },
217
+ getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; },
218
+ appendChild(c) { this.kids.push(c); c.parentNode = this; return c; },
219
+ removeChild(c) { this.kids = this.kids.filter((x) => x !== c); return c; },
220
+ addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); },
221
+ fire(t) { (this.listeners[t] || []).forEach((fn) => fn({ type: t, preventDefault() {} })); },
222
+ focus() { focused.el = this; },
223
+ set textContent(v) { this._text = String(v); this.kids = []; },
224
+ get textContent() { return this._text; },
225
+ get innerHTML() { throw new Error('no innerHTML'); },
226
+ set innerHTML(v) { throw new Error('no innerHTML'); },
227
+ });
228
+ const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
229
+ for (const id of [...html.matchAll(/id="([A-Za-z]+)"/g)].map((m) => m[1])) {
230
+ const el = mk('x');
231
+ el.setAttribute('id', id);
232
+ const tag = new RegExp(`<[^>]*\\bid="${id}"[^>]*>`).exec(html);
233
+ if (tag && /\shidden(?=[\s>])/.test(tag[0])) el.hidden = true;
234
+ }
235
+ return { document: { documentElement: mk('html'), getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_n, t) => mk(t) }, byId, focused };
236
+ }
237
+ async function bootPage(hash, ms, { refuse = false } = {}) {
238
+ const { document, byId, focused } = fakeDom();
239
+ const sent = [];
240
+ const store = new Map();
241
+ let matters = ms.slice();
242
+ const feeds = () => ({ '/api/bongos/government/docket': docket(matters), '/api/bongos/goals': { goals: [] }, '/api/bongos/goals/rollups': { rollups: [] } });
243
+ const location = { hash, href: '' };
244
+ const window = {
245
+ innerWidth: 1440, innerHeight: 900, location,
246
+ localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)) },
247
+ matchMedia: () => ({ matches: true, addEventListener() {} }),
248
+ addEventListener() {}, history: { pushState() {} },
249
+ };
250
+ const fetch = (url, init) => {
251
+ if (init && init.method && init.method !== 'GET') {
252
+ sent.push({ method: init.method, url, body: JSON.parse(init.body || '{}') });
253
+ if (refuse) return Promise.resolve({ ok: false, status: 409, json: async () => ({ error: { code: 'not_pending' } }) });
254
+ matters = matters.filter((m) => !url.includes('/' + String(m.id).split(':')[1]));
255
+ return Promise.resolve({ ok: true, status: 200, json: async () => ({ ok: true }) });
256
+ }
257
+ const body = feeds()[url];
258
+ return Promise.resolve(body === undefined ? { ok: false, json: async () => ({}) } : { ok: true, json: async () => body });
259
+ };
260
+ const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map, Set });
261
+ ctx.globalThis = ctx;
262
+ for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
263
+ await window.GovStreetPage.ready;
264
+ await new Promise((r) => setTimeout(r, 0));
265
+ const deskEl = () => (byId.get('insideOpenA').hidden ? byId.get('insideOpenB') : byId.get('insideOpenA'));
266
+ const part = (cls) => deskEl().children.find((c) => c.className.split(' ').includes(cls));
267
+ return { window, byId, focused, sent, store, deskEl, part };
268
+ }
269
+
270
+ test('the page: the desk acts in place — the row clears, the floor lights with a tick, the meter takes a pip, the toast speaks', async () => {
271
+ const ms = [matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 }, opened_at: '2026-10-01T12:00:00.000Z' })];
272
+ const p = await bootPage('#building=people', ms);
273
+ const html = read('street.html');
274
+ assert.ok(html.indexOf('/builders/street-desk.js?') > html.indexOf('/builders/street-inside.js?') && html.indexOf('/builders/street-desk.js?') < html.indexOf('/builders/street.js?'), 'the desk loads after the interior and before the page');
275
+ assert.equal(p.byId.get('insideFloors').children[4].attrs['aria-pressed'], 'true', 'the building opens on the floor that needs you');
276
+ const head = p.deskEl().children[0].children[1];
277
+ assert.deepEqual([head.children[1].textContent, head.children[2].textContent, head.children[2].className], ['Waiting to join', '1 waiting · oldest 2 days', 'desk-status desk-status--hot']);
278
+ assert.deepEqual(p.part('desk-tiles').children.map((t) => [t.children[0].textContent, t.children[1].textContent]), [['1', 'waiting'], ['5', 'on record']], 'the first tile is the count in the floor noun, as on the board; a zero tile (nothing moving) is hidden');
279
+ const row = p.part('desk-rows').children[0];
280
+ const name = row.children[0].children[1];
281
+ assert.equal(name.children[0].textContent, 'Ana Ruiz');
282
+ name.fire('click');
283
+ const opened = p.part('desk-rows').children[0];
284
+ assert.equal(opened.children[0].children[1].attrs['aria-expanded'], 'true');
285
+ assert.ok(opened.children[1].children.length >= 1 && opened.children[1].children.length <= 3, 'at most three facts open in place');
286
+ const acts = opened.children[0].children[2].children;
287
+ assert.deepEqual(acts.map((b) => b.textContent), ['not now', 'let in'], 'the primary sits rightmost');
288
+ acts[1].fire('click');
289
+ for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 0));
290
+ assert.deepEqual(p.sent, [{ method: 'PATCH', url: '/api/bongos/access-requests/11', body: { status: 'invited' } }], 'the card\'s own route, nothing else');
291
+ assert.match(p.byId.get('streetToast').textContent, /Ana Ruiz is invited in/);
292
+ assert.equal(p.byId.get('streetToast').hidden, false);
293
+ assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
294
+ assert.equal(p.byId.get('streetTodayPips').children.filter((x) => x.className.includes('--on')).length, 1, 'one pip per decision');
295
+ assert.equal(JSON.parse(p.store.get('gov-desk-today')).n, 1, 'the day keeps its count');
296
+ const quiet = p.part('desk-quiet');
297
+ assert.ok(quiet && quiet.className.includes('desk-quiet--cleared'), 'a cleared floor is one lit line, never an empty list');
298
+ assert.deepEqual(quiet.children.slice(1).map((x) => x.textContent), ['Everyone is in', 'lit up · 1 decided today']);
299
+ assert.equal(p.part('desk-rows'), undefined);
300
+ assert.equal(p.part('desk-record').children[1].textContent, 'let in Ana Ruiz · now', 'the record line starts with what you did');
301
+ assert.equal(p.byId.get('insideFloors').children[4].attrs['aria-pressed'], 'true', 'you stay on the floor you cleared');
302
+ // the drawing: the cleared floor's windows lit and its tick drawn
303
+ const bv = I.buildingModel({ key: 'people', docket: docket([]), goals: [], mode: 'dark', now: NOW, floor: 4, cleared: [4] });
304
+ const plain = I.buildingModel({ key: 'people', docket: docket([]), goals: [], mode: 'dark', now: NOW, floor: 0 });
305
+ assert.ok(bv.shapes.some((s) => /l6 6 12-13$/.test(s.d)), 'a tick on the cleared floor');
306
+ assert.ok(!plain.shapes.some((s) => /l6 6 12-13$/.test(s.d)), 'no tick on a floor nobody cleared');
307
+ });
308
+
309
+ test('the page: a refused act keeps the row and says why; no pip is counted', async () => {
310
+ const ms = [matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 } })];
311
+ const p = await bootPage('#building=people', ms, { refuse: true });
312
+ p.part('desk-rows').children[0].children[0].children[2].children[1].fire('click');
313
+ for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 0));
314
+ assert.equal(p.byId.get('streetToast').textContent, 'Someone already decided this.');
315
+ assert.equal(p.part('desk-rows').children.length, 1);
316
+ assert.equal(p.byId.get('streetTodayN').textContent, '0 decided');
317
+ });
318
+
319
+ test('the stylesheet: the desk in tokens only, its motion dies under reduced motion, the chips clear 24px', () => {
320
+ const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
321
+ const desk = css.split('\n').filter((l) => /^\.(desk|street-today)/.test(l)).join('\n');
322
+ assert.ok(desk.length > 0);
323
+ assert.doesNotMatch(desk, /#[0-9a-f]{3,8}\b|rgba?\(/i, 'no literal colours on the desk');
324
+ assert.match(css, /\.desk-chip \{[^}]*height: 34px/);
325
+ assert.match(css, /\.desk-row--gone \{[^}]*animation: desk-gone/);
326
+ assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{[^}]*animation: none !important; transition: none !important;/);
327
+ assert.match(read('street.html'), /id="streetTodayPips"/);
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
+ });
@@ -508,7 +508,7 @@ async function bootPage({ hash = '', feeds = {}, innerWidth = 1440 } = {}) {
508
508
  };
509
509
  const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map });
510
510
  ctx.globalThis = ctx;
511
- for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street.js']) vm.runInContext(read(f), ctx);
511
+ for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
512
512
  await window.GovStreetPage.ready;
513
513
  await new Promise((r) => setTimeout(r, 0));
514
514
  const fireWin = (t, e) => (listeners[t] || []).forEach((fn) => fn(e));