@bongos/core 1.21.10 → 1.21.11

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.
@@ -0,0 +1,519 @@
1
+ // tests/hall_street_inside.mjs — INSIDE EVERY BUILDING AND DISTRICT (task
2
+ // 1004526, goal 1000125).
3
+ //
4
+ // WHAT IT GUARDS. The interiors are the reference design's buildingView() and
5
+ // districtView() (docs/design/mocks/governor-city/Street.dc.html), filled from
6
+ // the governor docket and the goal's tasks. What can go wrong:
7
+ // * the section drifts from the prototype (a slab, a wall, a lamp, the sign
8
+ // plate, a level number, the lift, the frame, the crown, the annex) —
9
+ // pinned by running the PROTOTYPE'S OWN Component class on its own sample
10
+ // floors beside street-inside.js and comparing what they emit, every
11
+ // building, both modes, before and after a pick;
12
+ // * the floor symbols or the district work drifting from the library;
13
+ // * a floor saying a number the docket does not hold (invented data), or a
14
+ // floor nothing reads yet looking like one that is read;
15
+ // * the lift not travelling (a node rebuilt on every draw cannot transition),
16
+ // the chosen floor's block not re-entering, the amendment button doing
17
+ // nothing silently;
18
+ // * a district drawing work from another goal, or a status as the wrong stage.
19
+ //
20
+ // Run: node --test tests/hall_street_inside.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import vm from 'node:vm';
27
+ import { createRequire } from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const PUB = path.join(ROOT, 'modules/hall-ui/public');
32
+ const require = createRequire(import.meta.url);
33
+ const City = require(path.join(PUB, 'city-draw.js'));
34
+ const S = require(path.join(PUB, 'street-draw.js'));
35
+ const I = require(path.join(PUB, 'street-inside.js'));
36
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
37
+ const NOW = Date.parse('2026-10-02T12:00:00.000Z');
38
+ const j = (v) => JSON.parse(JSON.stringify(v));
39
+
40
+ function prototype() {
41
+ const src = fs.readFileSync(path.join(ROOT, 'docs/design/mocks/governor-city/Street.dc.html'), 'utf8');
42
+ const script = src.split('data-dc-script')[1].split('>').slice(1).join('>').split('</script>')[0];
43
+ const DCLogic = class { constructor(p) { this.props = p || {}; this.state = {}; } setState() {} };
44
+ return new Function('DCLogic', `${script}\nreturn Component;`)(DCLogic);
45
+ }
46
+ const Proto = prototype();
47
+ function proto(mode, st = {}) {
48
+ const p = new Proto({ mode });
49
+ p.state = Object.assign(p.initial(), st);
50
+ return p;
51
+ }
52
+
53
+ // a docket in the route's shape (modules/government/docket.js buildDocket)
54
+ function matter(over = {}) {
55
+ 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 };
56
+ }
57
+ 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']];
58
+ const READINGS = [['flow', 'review'], ['promises', 'planning'], ['money', 'treasury'], ['people', 'people'], ['fairness', 'disputes'], ['rules', 'rules']];
59
+ function docket(matters = [], over = {}) {
60
+ const counts = { needs: 0, moving: 0, settled: 0 };
61
+ matters.forEach((m) => { counts[m.state] += 1; });
62
+ return {
63
+ viewer: { id: '95' }, counts, line: '', matters,
64
+ buildings: S.CIVIC_KEYS.map((key) => ({ key, name: key[0].toUpperCase() + key.slice(1), group: '', counts: {}, matter_ids: matters.filter((m) => m.building === key).map((m) => m.id) })),
65
+ sources: SOURCES.map(([source, building]) => ({ source, building, label: source, status: ['grade_appeals', 'standing_rules', 'treasury_spending'].includes(source) ? 'coming' : 'live', count: matters.filter((m) => m.source === source).length, reason: ['grade_appeals', 'standing_rules', 'treasury_spending'].includes(source) ? 'There is no way to appeal a grade yet.' : null })),
66
+ readings: READINGS.map(([key, building]) => ({ key, label: key, building, status: 'off', value: null, figure: null, line: null, hot: false, reason: 'The module that holds this is not enabled here.' })),
67
+ ...over,
68
+ };
69
+ }
70
+ const goal = (id, status = 'open', title = `Goal ${id}`) => ({ id: String(id), title, status });
71
+
72
+ // ── the drawing is the prototype's ───────────────────────────────────────────
73
+
74
+ const SECTION_KEYS = ['kicker', 'name', 'holds', 'whoText', 'howText', 'seats', 'fill', 'fillColor', 'growth', 'openKicker', 'sym', 'flipA', 'flipB', 'openTitle', 'openLines', 'drawerItems', 'onFile', 'shapes', 'texts', 'syms', 'lift', 'hasAnnex', 'annex'];
75
+
76
+ test('every building in section is the prototype\'s, to the path, in both modes, before and after a pick', () => {
77
+ let compared = 0;
78
+ for (const mode of ['dark', 'light']) {
79
+ for (const st of [{}, { reviewHeld: true, floor: 1, drawer: 1, picks: 3 }, { floor: 99, drawer: 7, picks: 2 }]) {
80
+ for (const key of S.CIVIC_KEYS) {
81
+ const p = proto(mode, { view: key, ...st });
82
+ const want = p.buildingView(p.theme(mode), key, p.matters());
83
+ const vacant = key === 'review' && !p.state.reviewHeld;
84
+ const got = I.buildingView(S.theme(mode), key, p.civicInfo(key), { floor: p.state.floor, drawer: p.state.drawer, picks: p.state.picks, vacant, reviewHeld: p.state.reviewHeld, evening: false });
85
+ for (const k of SECTION_KEYS) assert.deepEqual(j(got[k]), j(want[k]), `${mode} ${key} ${JSON.stringify(st)} ${k}`);
86
+ assert.deepEqual(got.floors.map((f) => [f.key, f.x, f.y, f.w, f.h, f.aria]), want.floors.map((f) => [f.key, f.x, f.y, f.w, f.h, f.aria]), `${mode} ${key} floor hits`);
87
+ assert.deepEqual(got.drawers.map((d) => [d.label, d.count, d.aria, d.bg, d.border, d.handle]), want.drawers.map((d) => [d.label, d.count, d.aria, d.bg, d.border, d.handle]), `${mode} ${key} drawers`);
88
+ compared += 1;
89
+ }
90
+ }
91
+ }
92
+ assert.equal(compared, 54);
93
+ });
94
+
95
+ test('the rule glyph is the shared library\'s, which is the prototype\'s for every rule the prototype draws', () => {
96
+ const p = proto('dark');
97
+ for (const rule of ['decide', 'majority', 'unanimous', 'consent', 'rule']) assert.deepEqual(City.glyph(rule, 9, 9, 16), p.glyph(rule, 9, 9, 16), rule);
98
+ const bv = I.buildingView(S.theme('dark'), 'review', p.civicInfo('review'), { vacant: true });
99
+ const g = City.glyph('signoff', 9, 9, 16);
100
+ assert.deepEqual([bv.gf, bv.gl], [g.fill, g.line], 'Review\'s sign-off glyph is drawn (the guide\'s, which the prototype\'s street left blank)');
101
+ });
102
+
103
+ test('every district is the prototype\'s road of work, both modes', () => {
104
+ for (const mode of ['dark', 'light']) {
105
+ for (const key of ['onboarding', 'payments', 'hall', 'modules']) {
106
+ for (const st of [{}, { reviewHeld: true, released: true }]) {
107
+ const p = proto(mode, { view: key, ...st });
108
+ const want = p.districtView(p.theme(mode), key);
109
+ const got = I.districtView(S.theme(mode), key, p.districtInfo(key));
110
+ for (const k of ['kicker', 'name', 'line', 'road', 'ground', 'tx', 'ty', 's', 'shapes', 'legend']) assert.deepEqual(j(got[k]), j(want[k]), `${mode} ${key} ${k}`);
111
+ assert.deepEqual(got.items.map((i) => [i.x, i.y, i.w, i.name, i.meta, i.stage, i.stageColor, i.stageEdge]), want.items.map((i) => [i.x, i.y, i.w, i.name, i.meta, i.stage, i.stageColor, i.stageEdge]), `${mode} ${key} items`);
112
+ }
113
+ }
114
+ }
115
+ });
116
+
117
+ test('workItem and crown are verbatim: every craft at every stage, every crown', () => {
118
+ for (const mode of ['dark', 'light']) {
119
+ const p = proto(mode);
120
+ const T = p.theme(mode);
121
+ for (const craft of ['engineer', 'artist', 'ui', 'ideator']) {
122
+ for (const stage of ['backlog', 'ready', 'being built', 'shipped', 'being graded', 'awaiting a tester']) {
123
+ const a = []; const b = [];
124
+ p.workItem(a, T, craft, stage, 86, 40, 92);
125
+ I.workItem(b, S.theme(mode), craft, stage, 86, 40, 92);
126
+ assert.deepEqual(b, a, `${mode} ${craft} ${stage}`);
127
+ }
128
+ }
129
+ for (const key of S.CIVIC_KEYS) {
130
+ const a = []; const b = [];
131
+ p.crown(a, T, key, 130, 460, 208);
132
+ I.crown(b, S.theme(mode), key, 130, 460, 208);
133
+ assert.deepEqual(b, a, `${mode} crown ${key}`);
134
+ }
135
+ }
136
+ });
137
+
138
+ test('the floor symbols are the prototype\'s forty, and every floor the plan names has one', () => {
139
+ const p = proto('dark');
140
+ assert.equal(I.SYMBOL_NAMES.length, 40);
141
+ for (const name of I.SYMBOL_NAMES) assert.deepEqual(I.symbolPath(name), p.symbolPath(name), name);
142
+ assert.deepEqual(I.symbolPath('no such'), p.symbolPath('crate'), 'the fallback is the crate');
143
+ // every floor name the prototype maps keeps its symbol
144
+ for (const key of S.CIVIC_KEYS) {
145
+ for (const st of [{}, { reviewHeld: true }]) {
146
+ for (const f of proto('dark', st).civicInfo(key).floors) assert.equal(I.floorSymbol(f.name), p.floorSymbol(f.name), f.name);
147
+ }
148
+ }
149
+ for (const [key, plan] of Object.entries(I.PLAN)) {
150
+ for (const [name] of plan.floors) {
151
+ const sym = I.floorSymbol(name);
152
+ // the prototype itself signs Ready to go with the crate
153
+ if (name !== 'Ready to go') assert.notEqual(sym, 'crate', `${key}: ${name} has a symbol of its own`);
154
+ assert.ok(I.SYMBOL_NAMES.includes(sym), `${name} → ${sym} is one of the library's`);
155
+ }
156
+ }
157
+ });
158
+
159
+ test('each building keeps the prototype\'s capacity, words and floor order', () => {
160
+ for (const key of S.CIVIC_KEYS) {
161
+ const want = proto('dark', { reviewHeld: true }).civicInfo(key);
162
+ const plan = I.PLAN[key];
163
+ assert.equal(plan.cap, want.cap, `${key} cap`);
164
+ assert.equal(plan.holds, want.holds, `${key} holds`);
165
+ assert.ok(plan.floors.length <= plan.cap, `${key} fits`);
166
+ const b = I.civicInfo(key, { docket: docket(), goals: [], now: NOW });
167
+ assert.equal(b.group, want.group, `${key} group`);
168
+ assert.equal(b.name, want.name, `${key} name`);
169
+ }
170
+ // where the prototype's floor is about the government, the plan keeps it, in its order
171
+ const names = (k) => I.PLAN[k].floors.map((f) => f[0]);
172
+ assert.deepEqual(names('charter'), proto('dark').civicInfo('charter').floors.map((f) => f.name));
173
+ assert.deepEqual(names('automation'), proto('dark').civicInfo('automation').floors.map((f) => f.name));
174
+ assert.deepEqual(names('people'), proto('dark').civicInfo('people').floors.map((f) => f.name));
175
+ });
176
+
177
+ // ── what a floor says comes from the docket, and only from it ───────────────
178
+
179
+ test('a floor no feed reads stands as not recorded yet: faint, no number, and says so', () => {
180
+ const b = I.civicInfo('charter', { docket: docket(), goals: [], now: NOW });
181
+ assert.ok(b.floors.every((f) => f.status === 'coming' && f.count === ''), 'nothing on the charter is read yet');
182
+ const bv = I.buildingModel({ key: 'charter', docket: docket(), mode: 'dark', now: NOW });
183
+ const quiet = bv.texts.filter((t) => t.v === 'not recorded yet');
184
+ assert.equal(quiet.length, 8, 'every used floor says it on the wall');
185
+ assert.ok(bv.texts.filter((t) => t.s === 14.5).every((t) => t.c === '#8e8e93'), 'its name lettered faint, like a proposed floor');
186
+ assert.equal(bv.openKicker, 'floor 8 · not recorded yet');
187
+ });
188
+
189
+ test('a floor bound to a source shows its open matters: the count, the line, needs you, the lines', () => {
190
+ const ms = [matter({ id: 'access_requests:1', source: 'access_requests', building: 'people', state: 'needs', title: 'Admit Kai', deadline: '2026-10-03T09:00:00.000Z' }), matter({ id: 'access_requests:2', source: 'access_requests', building: 'people', state: 'moving', title: 'Admit Lu' })];
191
+ const b = I.civicInfo('people', { docket: docket(ms), goals: [], now: NOW });
192
+ const f = b.floors.find((x) => x.name === 'Waiting to join');
193
+ assert.deepEqual([f.count, f.line, f.status], ['2', '1 needs you', 'pending']);
194
+ assert.deepEqual(f.lines, [['Admit Kai', 'needs you · sat'], ['Admit Lu', 'moving']]);
195
+ const bv = I.buildingModel({ key: 'people', docket: docket(ms), mode: 'dark', now: NOW });
196
+ assert.equal(bv.selFloor, 4, 'the floor being decided is the one chosen first, as the prototype');
197
+ assert.equal(bv.openKicker, 'floor 5 · being decided');
198
+ const goal2 = b.floors.find((x) => x.name === 'Goal teams');
199
+ assert.deepEqual([goal2.count, goal2.line, goal2.status], ['0', 'nothing open', 'ok'], 'a live source with nothing open says nothing open, and 0');
200
+ });
201
+
202
+ test('a source that is off, coming, not yours or unreadable never shows a number', () => {
203
+ const d = docket();
204
+ d.sources.find((s) => s.source === 'goal_join_requests').status = 'off';
205
+ d.sources.find((s) => s.source === 'access_requests').status = 'not_yours';
206
+ d.sources = d.sources.filter((s) => s.source !== 'redteam_reports');
207
+ const people = I.civicInfo('people', { docket: d, now: NOW }).floors;
208
+ const by = Object.fromEntries(people.map((f) => [f.name, f]));
209
+ assert.deepEqual([by['Goal teams'].status, by['Goal teams'].count], ['off', '']);
210
+ assert.deepEqual([by['Waiting to join'].status, by['Waiting to join'].count], ['not_yours', '']);
211
+ const disputes = Object.fromEntries(I.civicInfo('disputes', { docket: d, now: NOW }).floors.map((f) => [f.name, f]));
212
+ assert.deepEqual([disputes['Grade appeals'].status, disputes['Grade appeals'].lines[0]], ['coming', ['There is no way to appeal a grade yet.', 'coming']], 'coming says why, in the docket\'s words');
213
+ assert.deepEqual([disputes['Security reports'].status, disputes['Security reports'].count], ['unavailable', ''], 'a source missing from the docket is not read');
214
+ const none = I.civicInfo('release', { docket: null, now: NOW });
215
+ assert.ok(none.floors.every((f) => f.count === '' && f.status !== 'ok' && f.status !== 'pending'), 'no docket: no numbers anywhere');
216
+ assert.equal(none.onFile, 'the docket could not be read just now');
217
+ assert.deepEqual(none.table, []);
218
+ });
219
+
220
+ test('a floor bound to a reading shows its figure and line when live; Planning counts the open goals', () => {
221
+ const d = docket();
222
+ d.readings.find((r) => r.key === 'people').status = 'live';
223
+ Object.assign(d.readings.find((r) => r.key === 'people'), { figure: '12 builders', line: '7 shipped this month', hot: false });
224
+ Object.assign(d.readings.find((r) => r.key === 'money'), { status: 'live', figure: '$1,100 left', line: 'lasts about 3 weeks' });
225
+ const builders = I.civicInfo('people', { docket: d, now: NOW }).floors.find((f) => f.name === 'Builders');
226
+ assert.deepEqual([builders.count, builders.line, builders.lines[0]], ['12', '7 shipped this month', ['7 shipped this month', '12 builders']]);
227
+ assert.equal(I.civicInfo('treasury', { docket: d, now: NOW }).floors.find((f) => f.name === 'Reserve').count, '$1,100');
228
+ const goals = [goal(1), goal(2), goal(3), goal(4), goal(5), goal(6, 'achieved')];
229
+ const m = I.buildingModel({ key: 'planning', docket: d, goals, mode: 'dark', now: NOW });
230
+ const g = I.civicInfo('planning', { docket: d, goals: goals.filter((x) => x.status === 'open'), now: NOW }).floors[0];
231
+ assert.deepEqual([g.name, g.count, g.line], ['Goals', '5', '5 goals open']);
232
+ assert.equal(g.lines.length, 5, 'four goals and a line for the rest');
233
+ assert.ok(m.texts.some((t) => t.v === '5' && t.s === 20), 'only open goals are counted on the wall');
234
+ });
235
+
236
+ test('on the table: the building\'s matters, needs first; each row names the matter it opens', () => {
237
+ const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', title: 'B', deadline: '2026-10-04T09:00:00.000Z' }), matter({ id: 'board:2', source: 'board', building: 'charter', state: 'needs', title: 'A', deadline: '2026-10-02T20:00:00.000Z' }), matter({ id: 'blockers:3' })];
238
+ const m = I.buildingModel({ key: 'charter', docket: docket(ms), mode: 'light', now: NOW });
239
+ assert.deepEqual(m.table.map((r) => [r.matter, r.label, r.status, r.state]), [['board:2', 'A', 'needs you · today', 'needs'], ['board:1', 'B', 'moving · sun', 'moving']]);
240
+ assert.equal(m.table[0].color, S.theme('light').accentText);
241
+ assert.ok(m.hasTable);
242
+ });
243
+
244
+ test('the record files the building\'s matters by the month each was opened, newest drawer first', () => {
245
+ const ms = [matter({ id: 'a', opened_at: '2026-08-03T00:00:00.000Z', title: 'Aug' }), matter({ id: 'b', opened_at: '2026-10-01T00:00:00.000Z', title: 'Oct', state: 'settled' }), matter({ id: 'c', opened_at: '2025-12-30T00:00:00.000Z', title: 'Last year' }), matter({ id: 'd', opened_at: null, title: 'No date' })];
246
+ const r = I.recordOf(ms, NOW);
247
+ assert.deepEqual(r.map((x) => x.label), ['oct', 'aug', 'dec 25', 'undated']);
248
+ assert.deepEqual(r[0].items, [['Oct', 'settled']]);
249
+ const m = I.buildingModel({ key: 'planning', docket: docket(ms), mode: 'dark', now: NOW, drawer: 1 });
250
+ assert.deepEqual(m.drawers.map((d) => [d.label, d.count, d.selected]), [['oct', 1, false], ['aug', 1, true], ['dec 25', 1, false], ['undated', 1, false]]);
251
+ assert.deepEqual(m.drawerItems.map((x) => x.text), ['Aug']);
252
+ assert.equal(m.onFile, '4 matters on the docket');
253
+ const empty = I.buildingModel({ key: 'automation', docket: docket(), mode: 'dark', now: NOW });
254
+ assert.deepEqual([empty.drawers, empty.drawerItems, empty.onFile], [[], [], 'nothing on file yet']);
255
+ });
256
+
257
+ test('who holds it: the docket\'s holders when it names them, else an empty seat that says so', () => {
258
+ const none = I.buildingModel({ key: 'release', docket: docket(), mode: 'dark', now: NOW });
259
+ assert.deepEqual(none.seats.map((s) => s.key), ['—']);
260
+ assert.equal(none.whoText, 'who holds it is not set on this project yet');
261
+ assert.equal(none.howText, 'one person decides · by default', 'a default is named as one');
262
+ const fromMatter = I.holderOf('people', null, [matter({ rule: 'consent' })]);
263
+ assert.deepEqual([fromMatter.rule, fromMatter.how], ['consent', 'passes unless someone objects']);
264
+ const held = I.holderOf('treasury', { holders: [{ name: 'Mira' }], rule: 'majority', who: 'Mira, treasurer' }, []);
265
+ assert.deepEqual([held.seats, held.who, held.how], [['M'], 'Mira, treasurer', 'more than half must say yes']);
266
+ });
267
+
268
+ test('the annex lot stands only when the building is full; an unheld Review is the dashed ghost', () => {
269
+ assert.equal(I.buildingModel({ key: 'charter', docket: docket(), mode: 'dark', now: NOW }).hasAnnex, false, '8 of 9');
270
+ const save = I.PLAN.rules.cap;
271
+ try {
272
+ I.PLAN.rules.cap = 2;
273
+ const m = I.buildingModel({ key: 'rules', docket: docket(), mode: 'dark', now: NOW });
274
+ assert.deepEqual([m.hasAnnex, m.annex.text, m.annex.x, m.annex.w], [true, 'room for an annex, such as money rules', 628, 160]);
275
+ } finally { I.PLAN.rules.cap = save; }
276
+ const off = docket();
277
+ off.sources.find((s) => s.source === 'criteria_uat').status = 'off';
278
+ const ghost = I.buildingModel({ key: 'review', docket: off, mode: 'dark', now: NOW });
279
+ assert.equal(ghost.vacant, true);
280
+ assert.ok(ghost.shapes.some((s) => s.dash === '4 4'), 'drawn dashed');
281
+ assert.match(ghost.growth, /^Not built yet\. Nothing is built here until the charter says who holds the seat\.$/);
282
+ assert.equal(I.buildingModel({ key: 'review', docket: docket(), mode: 'dark', now: NOW }).vacant, false, 'a live source holds the seat, as on the street');
283
+ });
284
+
285
+ test('the lift runs to the chosen floor, and the chosen block flips on every pick', () => {
286
+ const a = I.buildingModel({ key: 'charter', docket: docket(), mode: 'dark', now: NOW, floor: 2, picks: 1 });
287
+ assert.equal(a.selFloor, 2);
288
+ assert.deepEqual(a.lift, { x: 92, y: 630, ty: -104, h: 36 });
289
+ assert.deepEqual([a.flipA, a.flipB], [false, true]);
290
+ const b = I.buildingModel({ key: 'charter', docket: docket(), mode: 'dark', now: NOW, floor: 50, picks: 2 });
291
+ assert.equal(b.selFloor, 7, 'a pick past the top is the top floor');
292
+ assert.deepEqual([b.flipA, b.flipB], [true, false]);
293
+ assert.ok(b.floors[7].selected && !b.floors[2].selected);
294
+ assert.ok(a.shapes.some((s) => s.s === '#fb9569' && s.w === 1.5), 'the orange frame');
295
+ assert.equal(a.syms[2].c, '#fb9569', 'and the orange sign on the chosen floor only');
296
+ assert.ok(a.syms.filter((s, i) => i !== 2).every((s) => s.c !== '#fb9569'));
297
+ });
298
+
299
+ // ── districts ──────────────────────────────────────────────────────────────
300
+
301
+ test('a district is its own goal\'s tasks: shape from the craft, stage from the status, label the kind', () => {
302
+ const t = (id, status, discipline, kind = 'feature', goal_id = '7', extra = {}) => ({ id: String(id), title: `task ${id}`, status, discipline, kind, goal_id, est_minutes: 120, ...extra });
303
+ const tasks = [t(1, 'shipped', 'engineer'), t(2, 'ready', 'artist'), t(3, 'active', 'ui'), t(4, 'confirmed', 'ideator', 'spike'), t(5, 'backlog', null, 'bug'), t(6, 'blocked', 'engineer'), t(7, 'completed', 'artist'), t(8, 'cut', 'engineer'), t(9, 'shipped', 'ui', 'feature', '8')];
304
+ const d = I.districtInfo('7', goal(7), tasks);
305
+ assert.deepEqual(d.items.map((i) => [i[0], i[1], i[2], i[3], i[5]]), [
306
+ ['task 1', 'feature', 'engineer', 'shipped', 'shipped'],
307
+ ['task 4', 'spike', 'ideator', 'graded · landing', 'graded · landing'],
308
+ ['task 7', 'feature', 'artist', 'being graded', 'being graded'],
309
+ ['task 3', 'feature', 'ui', 'being built', 'being built'],
310
+ ['task 2', 'feature', 'artist', 'ready', 'ready'],
311
+ ['task 6', 'feature', 'engineer', 'backlog', 'blocked'],
312
+ ['task 5', 'bug', 'engineer', 'backlog', 'backlog'],
313
+ ], 'in the order work moves; a cut task and another goal\'s are not drawn; no craft is a tower');
314
+ assert.equal(d.items[0][4], 56, 'height from the estimate, never a random');
315
+ assert.equal(d.items[4][4], 0, 'a slab or a stake has no height');
316
+ assert.equal(d.items[0][6], '/builders/task/1', 'each one opens its task');
317
+ assert.equal(d.line, 'the goal’s work · 1 shipped · 1 being built · 2 waiting on a check · 3 not started');
318
+ const m = I.districtModel({ goalId: '7', goals: [goal(7)], tasks, mode: 'dark' });
319
+ assert.deepEqual(m.items.map((i) => i.stage), ['shipped', 'graded · landing', 'being graded', 'being built', 'ready', 'blocked', 'backlog']);
320
+ assert.ok(m.shapes.some((s) => s.f === S.theme('dark').line && /h11l-3 4l3 4h-11Z$/.test(s.d)), 'a flag on the work that waits on a check');
321
+ });
322
+
323
+ test('past ten pieces of work the road keeps the ones in flight and counts the rest', () => {
324
+ const tasks = Array.from({ length: 30 }, (_, i) => ({ id: String(i + 1), title: `t${i + 1}`, status: i < 3 ? 'active' : i < 25 ? 'shipped' : 'backlog', discipline: 'engineer', kind: 'feature', goal_id: '9', shipped_at: `2026-09-${String(1 + (i % 28)).padStart(2, '0')}T00:00:00Z` }));
325
+ const d = I.districtInfo('9', goal(9), tasks);
326
+ assert.equal(d.items.length, I.MAX_ITEMS);
327
+ assert.equal(d.more, 20);
328
+ assert.equal(d.items.filter((i) => i[3] === 'being built').length, 3, 'all the work in flight stands');
329
+ const m = I.districtModel({ goalId: '9', goals: [goal(9)], tasks, mode: 'light' });
330
+ assert.equal(m.goalHref, '/builders/goals#/goal/9');
331
+ assert.ok(m.items[0].x >= 32 && m.items[m.items.length - 1].x + 124 <= 1408, 'ten fit the road');
332
+ });
333
+
334
+ test('a district says when its work is still being read, or could not be', () => {
335
+ assert.equal(I.districtInfo('3', goal(3), null).line, 'reading the goal’s work');
336
+ assert.equal(I.districtInfo('3', goal(3), false).line, 'the goal’s work could not be read just now');
337
+ assert.equal(I.districtInfo('3', goal(3), []).line, 'no work in this goal yet');
338
+ assert.equal(I.districtModel({ goalId: '404', goals: [], tasks: [] }).name, 'Goal 404');
339
+ });
340
+
341
+ test('the models are deterministic and pure', () => {
342
+ const input = { key: 'people', docket: docket([matter({ building: 'people', source: 'access_requests' })]), goals: [goal(1)], mode: 'light', now: NOW, floor: 1, picks: 1 };
343
+ assert.deepEqual(j(I.buildingModel(input)), j(I.buildingModel(input)));
344
+ const src = read('street-inside.js').replace(/^\s*\/\/.*$/gm, '');
345
+ assert.doesNotMatch(src, /document\.|fetch\(|Date\.now|Math\.random|innerHTML/);
346
+ });
347
+
348
+ // ── the page ────────────────────────────────────────────────────────────────
349
+
350
+ test('the stylesheet: the lift travels by CSS, dies under reduced motion, every [hidden] holds, tokens only', () => {
351
+ const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
352
+ assert.match(css, /\.inside-lift \{[^}]*transition: transform 650ms/);
353
+ assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{[^}]*animation: none !important; transition: none !important;/);
354
+ assert.match(css, /@keyframes street-zoom /);
355
+ assert.doesNotMatch(css, /#fb9569|#2a1305/i, 'the accent and its ink are tokens, never literals');
356
+ for (const sel of ['.inside[hidden]', '.inside-annex[hidden]', '.inside-open[hidden]', '.inside-note[hidden]', '.inside-lift[hidden]']) assert.ok(css.includes(sel), `${sel} is display: none`);
357
+ assert.match(css, /@media \(max-width: 899px\) \{[\s\S]*\.inside-board \{ position: static;/, 'narrow: the board is a column');
358
+ });
359
+
360
+ function fakeDom() {
361
+ const byId = new Map();
362
+ const focused = { el: null };
363
+ const mk = (tag) => {
364
+ const el = {
365
+ tagName: tag, attrs: {}, kids: [], className: '', listeners: {}, _text: '', hidden: false,
366
+ style: { setProperty(k, v) { this[k] = v; } },
367
+ clientWidth: 1440, clientHeight: 708, offsetLeft: 0, scrollLeft: 0, parentNode: null,
368
+ // like a real HTMLCollection, .children does NOT spread under concat
369
+ get children() { const a = this.kids.slice(); a[Symbol.isConcatSpreadable] = false; return a; },
370
+ get firstChild() { return this.kids[0] || null; },
371
+ setAttribute(k, v) { this.attrs[k] = String(v); if (k === 'id') byId.set(String(v), this); },
372
+ getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; },
373
+ appendChild(c) { this.kids.push(c); c.parentNode = this; return c; },
374
+ removeChild(c) { this.kids = this.kids.filter((x) => x !== c); return c; },
375
+ addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); },
376
+ fire(t, e = {}) { (this.listeners[t] || []).forEach((fn) => fn({ type: t, preventDefault() { e.prevented = true; }, ...e })); },
377
+ focus() { focused.el = this; },
378
+ set textContent(v) { this._text = String(v); this.kids = []; },
379
+ get textContent() { return this._text; },
380
+ get innerHTML() { throw new Error('street.js must not read innerHTML'); },
381
+ set innerHTML(v) { throw new Error('street.js must not write innerHTML'); },
382
+ };
383
+ return el;
384
+ };
385
+ const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
386
+ const ids = [...html.matchAll(/id="([A-Za-z]+)"/g)].map((m) => m[1]);
387
+ for (const id of ids) {
388
+ const el = mk('x');
389
+ el.setAttribute('id', id);
390
+ const tag = new RegExp(`<[^>]*\\bid="${id}"[^>]*>`).exec(html);
391
+ if (tag && /\shidden(?=[\s>])/.test(tag[0])) el.hidden = true; // as the markup ships it
392
+ }
393
+ const document = { documentElement: mk('html'), getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_ns, t) => mk(t) };
394
+ return { document, byId, focused, ids };
395
+ }
396
+
397
+ async function bootPage({ hash = '', feeds = {}, innerWidth = 1440 } = {}) {
398
+ const { document, byId, focused, ids } = fakeDom();
399
+ const calls = [];
400
+ const listeners = {};
401
+ const history = [];
402
+ const location = { hash };
403
+ const window = {
404
+ innerWidth, innerHeight: 900, location,
405
+ localStorage: { getItem: () => null, setItem() {} },
406
+ matchMedia: () => ({ matches: true, addEventListener() {} }),
407
+ addEventListener(t, fn) { (listeners[t] = listeners[t] || []).push(fn); },
408
+ history: { pushState(_s, _t, url) { history.push(url); location.hash = ''; } },
409
+ };
410
+ const fetch = (url) => {
411
+ calls.push(url);
412
+ const body = feeds[url];
413
+ return Promise.resolve(body === undefined ? { ok: false, json: async () => ({}) } : { ok: true, json: async () => body });
414
+ };
415
+ const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map });
416
+ ctx.globalThis = ctx;
417
+ for (const f of ['city-draw.js', 'street-draw.js', 'street-inside.js', 'street.js']) vm.runInContext(read(f), ctx);
418
+ await window.GovStreetPage.ready;
419
+ await new Promise((r) => setTimeout(r, 0));
420
+ const fireWin = (t, e) => (listeners[t] || []).forEach((fn) => fn(e));
421
+ return { window, byId, focused, calls, history, fireWin, ids, location };
422
+ }
423
+
424
+ const FEEDS = (ms, extra = {}) => ({
425
+ '/api/bongos/government/docket': docket(ms),
426
+ '/api/bongos/goals': { goals: [goal(7), goal(8)] },
427
+ '/api/bongos/goals/rollups': { rollups: [] },
428
+ ...extra,
429
+ });
430
+
431
+ test('the markup carries the interior: one persistent lift, two flip blocks, the panel and the back link', () => {
432
+ const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
433
+ for (const id of ['streetInside', 'insideBack', 'insideSvg', 'insideFloors', 'insideLift', 'insideAnnex', 'insidePanel', 'insideName', 'insideAmend', 'insideAmendNote', 'insideFill', 'insideTable', 'insideOpenA', 'insideOpenB', 'insideDrawers', 'insideDrawerItems', 'insideDName', 'insideLegend', 'insideItems']) assert.match(html, new RegExp(`id="${id}"`), id);
434
+ assert.equal((html.match(/<h1\b/g) || []).length, 1, 'the street keeps its one h1; the interior names are h2');
435
+ assert.match(html, /<section class="inside" id="streetInside"[^>]*hidden>/, 'hidden until an address names a building or a district');
436
+ assert.match(html, /<button type="button" class="inside-textbtn" id="insideAmend"/);
437
+ });
438
+
439
+ test('a building address draws the interior in place of the street, from the docket', async () => {
440
+ const ms = [matter({ id: 'access_requests:1', source: 'access_requests', building: 'people', state: 'needs', title: 'Admit Kai' })];
441
+ const p = await bootPage({ hash: '#building=people', feeds: FEEDS(ms) });
442
+ assert.equal(p.byId.get('streetInside').hidden, false);
443
+ assert.equal(p.byId.get('streetSizer').hidden, true, 'the street gives way');
444
+ assert.equal(p.byId.get('insideName').textContent, 'People');
445
+ assert.equal(p.byId.get('insideFloors').children.length, 7, 'a button per floor in use');
446
+ assert.equal(p.byId.get('insideFloors').children[4].attrs['aria-pressed'], 'true', 'the floor being decided is chosen');
447
+ assert.equal(p.byId.get('insideLift').style.transform, 'translateY(-208px)');
448
+ assert.equal(p.byId.get('insideTable').children.length, 1);
449
+ assert.equal(p.byId.get('insideTable').children[0].className, 'inside-row inside-row--needs');
450
+ assert.equal(p.byId.get('insideSeats').children[0].className, 'inside-seat inside-seat--open');
451
+ assert.equal(p.focused.el, null, 'a page opened at the address keeps the browser\'s own start');
452
+ p.location.hash = '#building=rules';
453
+ p.fireWin('hashchange', {});
454
+ assert.equal(p.byId.get('insideName').textContent, 'Rules');
455
+ assert.equal(p.focused.el, p.byId.get('insideName'), 'stepping in through a door puts focus on the building\'s name');
456
+ assert.equal(p.byId.get('streetSigns').children.find((a) => a.attrs['aria-current'] === 'location').attrs['data-reading'], 'rules', 'the reading for this building is marked');
457
+ });
458
+
459
+ test('picking a floor moves the SAME lift node and re-enters the chosen block; drawers and the amendment answer', async () => {
460
+ const ms = [matter({ id: 'access_requests:1', source: 'access_requests', building: 'people', state: 'needs', opened_at: '2026-09-30T00:00:00Z' }), matter({ id: 'access_requests:2', source: 'access_requests', building: 'people', state: 'moving', opened_at: '2026-08-30T00:00:00Z' })];
461
+ const p = await bootPage({ hash: '#building=people', feeds: FEEDS(ms) });
462
+ const lift = p.byId.get('insideLift');
463
+ assert.equal(p.byId.get('insideOpenA').hidden, false);
464
+ p.byId.get('insideFloors').children[0].fire('click');
465
+ assert.equal(p.byId.get('insideLift'), lift, 'one node, so the transform can transition');
466
+ assert.equal(lift.style.transform, 'translateY(0px)');
467
+ assert.deepEqual([p.byId.get('insideOpenA').hidden, p.byId.get('insideOpenB').hidden], [true, false], 'the block flips, so it re-enters');
468
+ assert.equal(p.focused.el.attrs['data-floor'], '0', 'focus stays on the floor picked');
469
+ p.byId.get('insideFloors').children[1].fire('click');
470
+ assert.deepEqual([p.byId.get('insideOpenA').hidden, p.byId.get('insideOpenB').hidden], [false, true]);
471
+ const words = p.byId.get('insideOpenA').children[0].children[1];
472
+ assert.equal(words.children[1].textContent, 'Testers');
473
+ assert.equal(p.byId.get('insideDrawers').children.length, 2);
474
+ p.byId.get('insideDrawers').children[1].fire('click');
475
+ assert.equal(p.byId.get('insideDrawers').children[1].attrs['aria-pressed'], 'true');
476
+ assert.equal(p.byId.get('insideAmendNote').hidden, true);
477
+ p.byId.get('insideAmend').fire('click');
478
+ assert.equal(p.byId.get('insideAmendNote').hidden, false, 'change how this works says it is not built yet, rather than nothing');
479
+ assert.match(p.byId.get('insideAmendNote').textContent, /not built yet/);
480
+ });
481
+
482
+ test('a table row opens the short card; Escape gives focus back to the row; back to the street returns to the door', async () => {
483
+ const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'needs', title: '<b>x</b>' })];
484
+ const p = await bootPage({ hash: '#building=charter', feeds: FEEDS(ms) });
485
+ p.byId.get('insideTable').children[0].fire('click');
486
+ assert.equal(p.byId.get('streetCard').hidden, false);
487
+ assert.equal(p.byId.get('streetCardTitle').textContent, '<b>x</b>', 'docket text lands as text');
488
+ p.fireWin('keydown', { key: 'Escape', preventDefault() {} });
489
+ assert.equal(p.byId.get('streetCard').hidden, true);
490
+ assert.equal(p.focused.el.attrs['data-matter'], 'board:1', 'focus goes back to the row');
491
+ p.byId.get('insideBack').fire('click');
492
+ assert.deepEqual(p.history, ['/builders/street']);
493
+ assert.equal(p.byId.get('streetInside').hidden, true);
494
+ assert.equal(p.byId.get('streetSizer').hidden, false);
495
+ assert.equal(p.focused.el.attrs['data-door'], 'charter', 'focus returns to the door used');
496
+ });
497
+
498
+ test('a district address reads its goal\'s tasks once and draws them as buildings', async () => {
499
+ const tasks = [{ id: '1', title: 'checkout', status: 'shipped', discipline: 'engineer', kind: 'feature', goal_id: '7' }, { id: '2', title: 'tax forms', status: 'active', discipline: 'ui', kind: 'feature', goal_id: '7' }, { id: '3', title: 'other goal', status: 'ready', goal_id: '8' }];
500
+ const p = await bootPage({ hash: '#district=7', feeds: FEEDS([], { '/api/bongos/tasks?goal_id=7': { tasks } }) });
501
+ await new Promise((r) => setTimeout(r, 0));
502
+ assert.equal(p.calls.filter((c) => c === '/api/bongos/tasks?goal_id=7').length, 1);
503
+ assert.equal(p.byId.get('streetInside').attrs['data-kind'], 'district');
504
+ assert.equal(p.byId.get('insidePanel').hidden, true);
505
+ assert.equal(p.byId.get('insideDName').textContent, 'Goal 7');
506
+ const items = p.byId.get('insideItems').children;
507
+ assert.deepEqual(items.map((a) => [a.tagName, a.attrs.href, a.children[2].textContent]), [['a', '/builders/task/1', 'shipped'], ['a', '/builders/task/2', 'being built']]);
508
+ assert.equal(p.byId.get('insideLift').hidden, true, 'a district has no lift');
509
+ assert.equal(p.byId.get('insideLegend').children.length, 2);
510
+ });
511
+
512
+ test('narrow: the board is a column and the drawing scales in its own strip, never under 0.6', async () => {
513
+ const p = await bootPage({ hash: '#building=charter', feeds: FEEDS([]), innerWidth: 390 });
514
+ p.byId.get('streetWindow').clientWidth = 390;
515
+ p.window.GovStreetPage.fit();
516
+ assert.equal(p.byId.get('insideBoard').style['--inside-scale'], '1');
517
+ assert.equal(p.byId.get('insideArtBox').style['--inside-art-scale'], '0.6');
518
+ assert.equal(p.byId.get('insideArtSpan').style.width, '480px');
519
+ });