@bongos/core 1.24.35 → 1.24.37
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 +128 -168
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md +1 -1
- package/docs/copy-inventory.md +164 -219
- package/docs/copy-registry.json +323 -892
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/README.md +1 -2
- package/docs/onboarding/primer.md +66 -22
- package/docs/page-inventory.json +39 -133
- package/docs/page-readings.json +882 -1346
- package/docs/recipes/ui-look-before-you-ship.md +2 -2
- package/modules/design-styles/skills/style/SKILL.md +2 -2
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/builders.js +8 -6
- package/modules/hall-ui/public/city.html +2 -2
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +2 -2
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +1 -1
- package/modules/hall-ui/public/diagram-viewer.js +5 -5
- package/modules/hall-ui/public/fleet-costs.js +3 -2
- package/modules/hall-ui/public/founding-mark.js +3 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +62 -1
- package/modules/hall-ui/public/hall-widgets.js +11 -1
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/money.html +92 -0
- package/modules/hall-ui/public/money.js +302 -0
- package/modules/hall-ui/public/money.states.json +15 -0
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/palette.js +22 -0
- package/modules/hall-ui/public/primer-live.js +101 -0
- package/modules/hall-ui/public/primer.html +10 -6
- package/modules/hall-ui/public/primer.js +93 -23
- package/modules/hall-ui/public/primer.states.json +54 -7
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.css +2 -1
- package/modules/hall-ui/public/project-settings.html +6 -16
- package/modules/hall-ui/public/project-settings.js +2 -142
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +23 -121
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +21 -26
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +8 -55
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/founding-mark.md +4 -4
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/hall-ui/records/reference-room.md +2 -0
- package/modules/onboarding/founding-mode.js +6 -2
- package/modules/ui-design/kit/render.js +1 -1
- package/modules/ui-design/styles/README.md +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/gen-hall-sitemap.js +3 -0
- package/scripts/hall-preview/server.js +6 -11
- package/src/bongos/auth.js +0 -8
- package/src/bongos/serve-internal.js +43 -43
- package/src/module-api.js +1 -1
- package/tests/auth_resolve_failure.mjs +5 -4
- package/tests/craft_mode_home.mjs +6 -2
- package/tests/fleet_cost_view.mjs +2 -18
- package/tests/founding_builders.mjs +6 -5
- package/tests/founding_mode.mjs +7 -4
- package/tests/hall_board_world.mjs +44 -1
- package/tests/hall_fullscreen.mjs +1 -1
- package/tests/hall_loading.mjs +1 -1
- package/tests/hall_money_page.mjs +272 -0
- package/tests/hall_nav.mjs +29 -11
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_page_gate_map.mjs +17 -7
- package/tests/hall_reading_room.mjs +385 -0
- package/tests/hall_reading_world.mjs +36 -30
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/rank_tier_single_source.mjs +0 -1
- package/tests/ui_design_kit.mjs +1 -1
- package/modules/hall-ui/public/diagrams.css +0 -45
- package/modules/hall-ui/public/diagrams.html +0 -106
- package/modules/hall-ui/public/diagrams.states.json +0 -28
- package/modules/hall-ui/public/drachmae.html +0 -110
- package/modules/hall-ui/public/drachmae.js +0 -431
- package/modules/hall-ui/public/drachmae.states.json +0 -58
- package/modules/hall-ui/public/fleet.html +0 -69
- package/modules/hall-ui/public/fleet.js +0 -41
- package/modules/hall-ui/public/fleet.states.json +0 -14
- package/modules/hall-ui/public/ranks-founding.states.json +0 -12
- package/modules/hall-ui/public/ranks.html +0 -83
- package/modules/hall-ui/public/ranks.js +0 -229
- package/modules/hall-ui/public/ranks.states.json +0 -15
- package/tests/hall_reference_world.mjs +0 -352
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// tests/hall_money_page.mjs — the owner's ONE money page (task 1005047, owner
|
|
2
|
+
// decision 2026-11-03, idea 1001312).
|
|
3
|
+
//
|
|
4
|
+
// What the project spent and paid out was drawn on three pages: Project settings
|
|
5
|
+
// ("Where the money went"), the Primer ("The treasury, so far", from the retired
|
|
6
|
+
// Economy page) and /fleet (what each hosted project costs). They are three sections
|
|
7
|
+
// of /money now. This file holds what makes that true and keeps it true:
|
|
8
|
+
//
|
|
9
|
+
// * THE PAGE DRAWS ALL THREE, each only when its own read answers. money.js is
|
|
10
|
+
// RUN in a vm against the hall-preview fixtures: a section whose read refuses
|
|
11
|
+
// (a 403, or a hall without the provisioning module) stays hidden, and a page
|
|
12
|
+
// where nothing answers says so in one line instead of loading forever.
|
|
13
|
+
// * NO MONEY FIGURE IS DRAWN ON TWO HALL PAGES. Project settings and the Primer
|
|
14
|
+
// keep their knobs and their prose, read none of the three feeds, and link here.
|
|
15
|
+
// * OLD URLS LAND ON THE RIGHT SECTION. /fleet 302s to /money#hosting through the
|
|
16
|
+
// real middleware, and /money sits behind the government
|
|
17
|
+
// page floor.
|
|
18
|
+
//
|
|
19
|
+
// The treasury cases moved here from tests/hall_reading_room.mjs, and the fleet
|
|
20
|
+
// page's wiring cases from tests/fleet_cost_view.mjs, with the code they test.
|
|
21
|
+
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import http from 'node:http';
|
|
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
|
+
import { realOTB } from './helpers.mjs';
|
|
31
|
+
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
35
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
36
|
+
const fixture = (name) => JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', `${name}.json`), 'utf8'));
|
|
37
|
+
|
|
38
|
+
const moneyHtml = read('money.html');
|
|
39
|
+
const moneyJs = read('money.js');
|
|
40
|
+
|
|
41
|
+
const OUTFLOW = '/api/bongos/project-account/outflow';
|
|
42
|
+
const CHECKS = '/api/bongos/project-account/ledger-checks?limit=1';
|
|
43
|
+
const TREASURY = '/api/bongos/public/cost-summary';
|
|
44
|
+
const LEDGER = '/api/bongos/provisioning/fleet/cost-ledger';
|
|
45
|
+
|
|
46
|
+
const costSummaryFixture = JSON.parse(
|
|
47
|
+
fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'cost-summary.json'), 'utf8'),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Just enough of an element for money.js: it writes a body's innerHTML and a
|
|
51
|
+
// section's `hidden`, and asks a section to scroll into view.
|
|
52
|
+
function fakeEl(id) {
|
|
53
|
+
return { id, hidden: true, innerHTML: '', scrolled: false, scrollIntoView() { this.scrolled = true; } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// money.js in a vm. `responses` maps a URL to a payload (200), a number (that
|
|
57
|
+
// status, no body) or an Error (the request threw). `page` boots it as the page.
|
|
58
|
+
async function loadMoney({ responses = {}, page = false, hash = '', kit = true, branding = { currency: { label: 'drachmae' } } } = {}) {
|
|
59
|
+
const els = {};
|
|
60
|
+
for (const id of ['payouts', 'payouts-body', 'the-treasury', 'the-treasury-body', 'hosting', 'hosting-body', 'money-empty']) els[id] = fakeEl(id);
|
|
61
|
+
const warnings = [];
|
|
62
|
+
const requests = [];
|
|
63
|
+
const window = {
|
|
64
|
+
OTB: realOTB(),
|
|
65
|
+
__BRANDING__: branding,
|
|
66
|
+
location: { hash },
|
|
67
|
+
BongosClient: {
|
|
68
|
+
createClient: () => ({
|
|
69
|
+
request: async (method, url) => {
|
|
70
|
+
requests.push(url);
|
|
71
|
+
const r = responses[url];
|
|
72
|
+
if (r instanceof Error) throw r;
|
|
73
|
+
if (r === undefined) return { ok: false, status: 404, data: null };
|
|
74
|
+
if (typeof r === 'number') return { ok: false, status: r, data: null };
|
|
75
|
+
return { ok: true, status: 200, data: r };
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
if (kit) {
|
|
81
|
+
const kitPath = path.join(HALL, 'hall-kit.js');
|
|
82
|
+
window.OTBKit = await import(`file://${kitPath.replace(/\\/g, '/')}`).then((m) => m.default);
|
|
83
|
+
}
|
|
84
|
+
const sandbox = { window, console: { warn: (...a) => warnings.push(a.join(' ')) } };
|
|
85
|
+
vm.createContext(sandbox);
|
|
86
|
+
// the hosting section draws through the real fleet-costs.js, as the page loads it
|
|
87
|
+
vm.runInContext(read('fleet-costs.js'), sandbox, { filename: 'fleet-costs.js' });
|
|
88
|
+
sandbox.document = page
|
|
89
|
+
? { readyState: 'complete', getElementById: (id) => els[id] || null, addEventListener() {} }
|
|
90
|
+
: { readyState: 'complete', getElementById: () => null, addEventListener() {} };
|
|
91
|
+
vm.runInContext(moneyJs, sandbox, { filename: 'money.js' });
|
|
92
|
+
return { money: window.OTBMoney, els, warnings, requests };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ALL = { [OUTFLOW]: fixture('project-account__outflow'), [TREASURY]: costSummaryFixture, [LEDGER]: fixture('provisioning__fleet__cost-ledger') };
|
|
96
|
+
|
|
97
|
+
// ---- 1. one page, three sections, each on its own read ------------------------
|
|
98
|
+
|
|
99
|
+
test('RUN: an owner finds spend and payouts, the treasury and the hosting bills on one page', async () => {
|
|
100
|
+
const t = await loadMoney({ responses: ALL, page: true });
|
|
101
|
+
await t.money.ready;
|
|
102
|
+
for (const id of ['payouts', 'the-treasury', 'hosting']) assert.equal(t.els[id].hidden, false, `#${id} is shown`);
|
|
103
|
+
assert.equal(t.els['money-empty'].hidden, true);
|
|
104
|
+
assert.match(t.els['payouts-body'].innerHTML, /Owed but not paid/);
|
|
105
|
+
assert.match(t.els['payouts-body'].innerHTML, /Money out, by month/);
|
|
106
|
+
assert.match(t.els['the-treasury-body'].innerHTML, /id="treasury-spend"/);
|
|
107
|
+
assert.match(t.els['hosting-body'].innerHTML, /example-project/);
|
|
108
|
+
assert.deepEqual([...t.requests].sort(), [CHECKS, LEDGER, OUTFLOW, TREASURY].sort(), 'the three feeds the three pages read, and nothing new');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('RUN: a section whose read refuses stays hidden — never an empty panel', async () => {
|
|
112
|
+
// payout.approve refused, and no provisioning module (its route is not mounted)
|
|
113
|
+
const t = await loadMoney({ responses: { [OUTFLOW]: 403, [TREASURY]: costSummaryFixture, [LEDGER]: 404 }, page: true });
|
|
114
|
+
await t.money.ready;
|
|
115
|
+
assert.equal(t.els.payouts.hidden, true);
|
|
116
|
+
assert.equal(t.els.hosting.hidden, true, 'the hosting section shows only where the provisioning module answers, as /fleet did');
|
|
117
|
+
assert.equal(t.els['the-treasury'].hidden, false);
|
|
118
|
+
assert.ok(!t.requests.includes(CHECKS), 'no ledger check is read for a reader the outflow refused');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('RUN: a page where nothing answers says so in one line instead of loading forever', async () => {
|
|
122
|
+
for (const responses of [{}, { [OUTFLOW]: new Error('down'), [TREASURY]: { daily: [], daily_credits: [] }, [LEDGER]: 403 }]) {
|
|
123
|
+
const t = await loadMoney({ responses, page: true });
|
|
124
|
+
await t.money.ready;
|
|
125
|
+
assert.equal(t.els['money-empty'].hidden, false);
|
|
126
|
+
for (const id of ['payouts', 'the-treasury', 'hosting']) assert.equal(t.els[id].hidden, true);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('RUN: an old /fleet link lands on the hosting section once it is drawn', async () => {
|
|
131
|
+
const t = await loadMoney({ responses: ALL, page: true, hash: '#hosting' });
|
|
132
|
+
await t.money.ready;
|
|
133
|
+
assert.equal(t.els.hosting.scrolled, true);
|
|
134
|
+
const none = await loadMoney({ responses: {}, page: true, hash: '#hosting' });
|
|
135
|
+
await none.money.ready;
|
|
136
|
+
assert.equal(none.els.hosting.scrolled, false, 'a hidden section is not scrolled to');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('the page waits for its sections: a Loading… line until one is shown', () => {
|
|
140
|
+
assert.match(moneyHtml, /<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…<\/p>/);
|
|
141
|
+
for (const id of ['payouts', 'the-treasury', 'hosting', 'money-empty']) {
|
|
142
|
+
assert.match(moneyHtml, new RegExp(`<section class="scroll" id="${id}" [^>]*hidden>`), `#${id} starts hidden`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('the page loads the kit and the hosting builders before its own script', () => {
|
|
147
|
+
const loaded = [...moneyHtml.matchAll(/<script[^>]+src="([^"?]+)/g)].map((m) => m[1].split('/').pop());
|
|
148
|
+
const at = (f) => loaded.indexOf(f);
|
|
149
|
+
assert.ok(at('hall-kit.js') > -1 && at('fleet-costs.js') > -1 && at('money.js') > -1);
|
|
150
|
+
assert.ok(at('hall-kit.js') < at('money.js') && at('fleet-costs.js') < at('money.js'));
|
|
151
|
+
assert.match(moneyHtml, /project-settings\.css/, 'the payout figures keep their sheet');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ---- 2. the payouts section (moved from project-settings.js) --------------------
|
|
155
|
+
|
|
156
|
+
test('RUN: the payouts lead with what is left after what is owed', async () => {
|
|
157
|
+
const { money } = await loadMoney();
|
|
158
|
+
const html = money.payoutsHtml(fixture('project-account__outflow'), null);
|
|
159
|
+
for (const label of ['Balance', 'Owed but not paid', 'Approved, not started', 'Left after all of that']) {
|
|
160
|
+
assert.ok(html.includes(`<span class="ps-money__label">${label}</span>`), label);
|
|
161
|
+
}
|
|
162
|
+
assert.equal(money.payoutsHtml(null, null), '');
|
|
163
|
+
assert.equal(money.payoutsHtml({ balance: null }, null), '');
|
|
164
|
+
assert.equal(money.lastTryWords({ last_outcome: 'held', last_reason: 'no_processor' }), 'Waiting — no payment processor connected yet');
|
|
165
|
+
assert.equal(money.lastTryWords({ last_outcome: 'failed', last_reason: 'some_code' }), 'Did not go through', 'an unknown code is left out, never shown as a slug');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---- 3. the treasury (moved from primer-live.js) -------------------------------
|
|
169
|
+
|
|
170
|
+
test('RUN: the treasury draws from the payload, and its line ends on the total the prose states', async () => {
|
|
171
|
+
const { money } = await loadMoney();
|
|
172
|
+
const html = money.treasuryHtml(costSummaryFixture);
|
|
173
|
+
const usd = `$${Number(costSummaryFixture.total_usd).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
|
|
174
|
+
assert.ok(html.includes(`<strong>${usd}</strong>`), 'the spend total is stated');
|
|
175
|
+
assert.match(html, /id="treasury-spend"/);
|
|
176
|
+
assert.match(html, /id="treasury-earned"/);
|
|
177
|
+
assert.match(html, /Drachmae awarded, cumulative/, 'the currency word is the branding pack\'s');
|
|
178
|
+
assert.match(html, /<h3 class="ps-money__h" id="treasury-by-category">Spent, by kind<\/h3>/, 'under the section\'s h2, named apart from "Where the money went"');
|
|
179
|
+
assert.doesNotMatch(html, /NaN|Infinity/, 'a NaN in a path renders as an invisible chart with no error anywhere');
|
|
180
|
+
assert.doesNotMatch(html, /tabindex="0"/, 'no focus stop: nothing here wires a readout');
|
|
181
|
+
assert.doesNotMatch(html, /&amp;|&#/, 'nothing is escaped twice');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('RUN: a treasury that holds nothing yet, or no chart kit, draws nothing', async () => {
|
|
185
|
+
const { money } = await loadMoney();
|
|
186
|
+
assert.equal(money.treasuryHtml(null), '');
|
|
187
|
+
assert.equal(money.treasuryHtml({ daily: [], daily_credits: [] }), '');
|
|
188
|
+
const noKit = await loadMoney({ responses: { [TREASURY]: costSummaryFixture }, page: true, kit: false });
|
|
189
|
+
await noKit.money.ready;
|
|
190
|
+
assert.equal(noKit.els['the-treasury'].hidden, true, 'without the chart kit there is nothing honest to draw');
|
|
191
|
+
assert.match(noKit.warnings.join('\n'), /OTBKit missing/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('the treasury reads the endpoint the status page draws, and draws only through the kit', () => {
|
|
195
|
+
assert.match(moneyJs, /TREASURY_URL = `\$\{API\}\/public\/cost-summary`/);
|
|
196
|
+
for (const fn of ['cumulateByDay', 'timeStepSeriesHtml', 'plotSvgHtml', 'plotValueLabelHtml', 'barSeriesHtml']) {
|
|
197
|
+
assert.match(moneyJs, new RegExp(`kit\\.${fn}\\(`), `the treasury draws through ${fn}`);
|
|
198
|
+
}
|
|
199
|
+
assert.doesNotMatch(moneyJs, /<(?:path|polyline|rect)\b[^>]*\b(?:d|points)="\$\{/, 'no hand-built SVG geometry');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ---- 4. no money figure on two hall pages ---------------------------------------
|
|
203
|
+
|
|
204
|
+
test('no other hall page reads the three money feeds', () => {
|
|
205
|
+
const FEEDS = [/project-account\/outflow/, /project-account\/ledger-checks/, /fleet\/cost-ledger/];
|
|
206
|
+
// fleet-costs.js is the hosting section's own drawing, loaded only by money.html.
|
|
207
|
+
const files = fs.readdirSync(HALL).filter((f) => /\.(js|html)$/.test(f) && !['money.js', 'fleet-costs.js'].includes(f));
|
|
208
|
+
for (const f of files) {
|
|
209
|
+
for (const re of FEEDS) assert.doesNotMatch(read(f), re, `${f} reads ${re} — that figure belongs on /money`);
|
|
210
|
+
}
|
|
211
|
+
// The treasury feed is also the Tasks board's credits tile (a count of credits,
|
|
212
|
+
// not money) — but no other page draws the treasury itself.
|
|
213
|
+
for (const f of ['project-settings.js', 'project-settings.html', 'primer.js', 'primer-live.js']) {
|
|
214
|
+
assert.doesNotMatch(read(f), /cost-summary|treasury-spend|Where the money went<\/h2>/, `${f} draws no money figure`);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('Project settings and the Primer link to the money page instead', () => {
|
|
219
|
+
assert.match(read('project-settings.html'), /<a href="\/builders\/money">Money<\/a>/);
|
|
220
|
+
assert.doesNotMatch(read('project-settings.html'), /id="ps-money"/);
|
|
221
|
+
const md = fs.readFileSync(path.join(ROOT, 'docs', 'onboarding', 'primer.md'), 'utf8');
|
|
222
|
+
assert.match(md, /\[Money\]\(\.\.\/\.\.\/modules\/hall-ui\/public\/money\.html\)/, 'a hall-page link primer.js opens in the hall');
|
|
223
|
+
assert.doesNotMatch(read('primer-live.js'), /fillTreasury|treasuryHtml/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ---- 5. the gate, the nav and the old URL ---------------------------------------
|
|
227
|
+
|
|
228
|
+
test('/money is behind the government page floor, and the nav mirrors it', () => {
|
|
229
|
+
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8');
|
|
230
|
+
assert.match(src, /const MONEY_PAGE_RE = \/\^\\\/builders\\\/money\(\?:\\\.html\)\?\\\/\?\$\/;/);
|
|
231
|
+
assert.match(src, /\|\| MONEY_PAGE_RE\.test\(norm\)\) \{\n\s+return gdsAuth\.requireGovernmentPage\(req, res, next\);/);
|
|
232
|
+
assert.match(read('shell.js'), /\{ id: 'money', label: 'Money', icon: 'money', href: '\/money', gate: 'government', position: true \}/);
|
|
233
|
+
assert.doesNotMatch(read('shell.js'), /id: 'fleet'/, 'Fleet costs left the sidebar');
|
|
234
|
+
// the hosting section's data keeps its own wall: the route's atom, unchanged
|
|
235
|
+
const route = fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'routes', 'provisioning.js'), 'utf8');
|
|
236
|
+
assert.match(route, /router\.get\('\/provisioning\/fleet\/cost-ledger', auth\.requireBuilder, auth\.requirePermission\('provisioning\.fleet\.manage'\)/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
async function withHall(fn) {
|
|
240
|
+
const express = require('express');
|
|
241
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
242
|
+
const app = express();
|
|
243
|
+
mountInternalSurfaces(app);
|
|
244
|
+
const server = app.listen(0, '127.0.0.1');
|
|
245
|
+
await new Promise((r) => server.once('listening', r));
|
|
246
|
+
const { port } = server.address();
|
|
247
|
+
const get = (p, host) => new Promise((resolve, reject) => {
|
|
248
|
+
http.get({ host: '127.0.0.1', port, path: p, headers: { host } }, (res) => {
|
|
249
|
+
res.resume();
|
|
250
|
+
res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || '' }));
|
|
251
|
+
}).on('error', reject);
|
|
252
|
+
});
|
|
253
|
+
try { await fn(get); } finally { server.close(); }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
test('RUN: /fleet 302s to the money page\'s hosting section, in every form it was linked', async () => {
|
|
257
|
+
await withHall(async (get) => {
|
|
258
|
+
for (const form of ['/fleet', '/fleet/', '/fleet.html']) {
|
|
259
|
+
const r = await get(form, 'builders.127.0.0.1');
|
|
260
|
+
assert.equal(r.status, 302, `${form} is a temporary redirect, so a later re-point is never cached past`);
|
|
261
|
+
assert.equal(r.location, '/money#hosting');
|
|
262
|
+
}
|
|
263
|
+
const q = await get('/fleet?from=mail', 'builders.127.0.0.1');
|
|
264
|
+
assert.equal(q.location, '/money?from=mail#hosting', 'a query string rides along');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('the states file shoots the redirected deep link too', () => {
|
|
269
|
+
const states = JSON.parse(read('money.states.json'));
|
|
270
|
+
assert.equal(states.page, '/builders/money');
|
|
271
|
+
assert.match(states.states.hosting.url, /#hosting$/);
|
|
272
|
+
});
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -308,8 +308,11 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
308
308
|
assert.ok(!nav.flatMap((g) => g.items).some((i) => /^\/people\b/.test(i.href)), 'no nav item points at the deleted /people');
|
|
309
309
|
// task 1004473: 'commands' — every skill and CLI verb, read live — is reference
|
|
310
310
|
// material that reads the same for every builder, so it joins Docs.
|
|
311
|
+
// task 1005046 (owner decision 2026-11-03, idea 1001311): ONE reading room. Ranks,
|
|
312
|
+
// Economy and Diagrams explained what the Primer explains; they are its chapters
|
|
313
|
+
// now, so Docs is the Primer and the four references that are not a document.
|
|
311
314
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
312
|
-
['primer', '
|
|
315
|
+
['primer', 'commands', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
313
316
|
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|
|
314
317
|
// 'project-settings' joins the Government group at the metic floor (task 1003280,
|
|
315
318
|
// goal 1000072) — it sits before 'government', which is the one archon-only item.
|
|
@@ -319,12 +322,13 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
319
322
|
// 'deploy' (task 1003159, ADR 0293) is the SECOND archon item, beside 'government':
|
|
320
323
|
// its gate is the `core.pin.move` atom, archon-floor and system:true. It sits with
|
|
321
324
|
// the steering surfaces because that is what it is — the owner's third release gate.
|
|
322
|
-
// '
|
|
323
|
-
//
|
|
325
|
+
// 'money' (task 1005047, owner decision 2026-11-03) follows 'project-settings' at the
|
|
326
|
+
// same government floor: the one money page — spend and payouts, the treasury and the
|
|
327
|
+
// hosting bills. It replaced 'fleet' (task 1001949), now its #hosting section.
|
|
324
328
|
// 'market-watch-settings' (task 1004938) LEFT the group in task 1005045: the owner saw
|
|
325
329
|
// "Market Watch" twice and had to guess which was which (owner decision 2026-11-03,
|
|
326
330
|
// idea 1001310). The settings are an owner-only Settings tab on /market-watch now.
|
|
327
|
-
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'gate', 'sessions', 'project-settings', '
|
|
331
|
+
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'gate', 'sessions', 'project-settings', 'money', 'deploy', 'government']);
|
|
328
332
|
// task 1005045: and no label is left twice anywhere in the sidebar, so the next pair
|
|
329
333
|
// fails here instead of in front of the owner.
|
|
330
334
|
const labels = [...boot().sidebarHtml.matchAll(/<a class="nav-item" [^>]*>[\s\S]*?<span class="nav-item__label">([^<]*)<\/span>/g)].map((m) => m[1]);
|
|
@@ -333,12 +337,26 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
333
337
|
assert.ok(!nav.flatMap((g) => g.items).some((i) => i.href === '/market-watch-settings'), 'no item points at the retired settings page');
|
|
334
338
|
});
|
|
335
339
|
|
|
336
|
-
|
|
340
|
+
// task 1005046: the three pages that left Docs left the hall. Their nav items are
|
|
341
|
+
// gone, their files are gone, and each old URL 302s to the Primer chapter that
|
|
342
|
+
// replaced it (tests/hall_page_gate_map.mjs pins the redirect; tests/
|
|
343
|
+
// hall_reading_world.mjs pins that the chapter exists and carries its diagram).
|
|
344
|
+
test('Ranks, Economy and Diagrams are Primer chapters, not pages', () => {
|
|
337
345
|
const nav = parseNav(boot().sidebarHtml);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
346
|
+
const items = nav.flatMap((g) => g.items);
|
|
347
|
+
for (const id of ['ranks', 'economy', 'diagrams']) {
|
|
348
|
+
assert.equal(items.some((i) => i.id === id), false, `the nav renders no '${id}' item`);
|
|
349
|
+
assert.doesNotMatch(SRC, new RegExp(`id: '${id}'`), `shell.js NAV declares no '${id}' item`);
|
|
350
|
+
assert.doesNotMatch(SRC, new RegExp(`\\n\\s*${id}: '<`), `and ICONS keeps no '${id}' glyph for it`);
|
|
351
|
+
}
|
|
352
|
+
for (const href of ['/ranks', '/drachmae', '/diagrams']) {
|
|
353
|
+
assert.equal(items.some((i) => i.href === href), false, `no item points at ${href}`);
|
|
354
|
+
}
|
|
355
|
+
for (const f of ['ranks.html', 'drachmae.html', 'diagrams.html']) {
|
|
356
|
+
assert.ok(!fs.existsSync(path.join(HALL, f)), `${f} left the hall`);
|
|
357
|
+
}
|
|
358
|
+
const primer = nav.find((g) => g.label === 'Docs').items.find((i) => i.id === 'primer');
|
|
359
|
+
assert.equal(primer.href, '/primer', 'the Primer is the reading room the three became');
|
|
342
360
|
});
|
|
343
361
|
|
|
344
362
|
test('every nav link lands on a page that exists', () => {
|
|
@@ -370,7 +388,7 @@ test('THE ARCHIVE: Ideas and Copy desk are off the nav, and both pages still exi
|
|
|
370
388
|
|
|
371
389
|
test('href check actually CATCHES a dead link', () => {
|
|
372
390
|
assert.equal(hrefResolves({ href: '/no-such-page', external: false }), false);
|
|
373
|
-
assert.equal(hrefResolves({ href: '/
|
|
391
|
+
assert.equal(hrefResolves({ href: '/primer', external: false }), true);
|
|
374
392
|
});
|
|
375
393
|
|
|
376
394
|
// ── Community carries no rank flag (task 1003442, goal 1000075) ─────────────
|
|
@@ -762,7 +780,7 @@ test('no glyph path data reads as an IP address to the release leak scan', () =>
|
|
|
762
780
|
// block (task 1004713). These drive the real shell and the real gov-positions.js.
|
|
763
781
|
|
|
764
782
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
765
|
-
const GOVERNING = ['board-room', 'city', 'watch', 'gate', 'sessions', 'project-settings', '
|
|
783
|
+
const GOVERNING = ['board-room', 'city', 'watch', 'gate', 'sessions', 'project-settings', 'money', 'deploy', 'government'];
|
|
766
784
|
const lead = (building, title) => ({ kind: 'lead', seat: 'lead:' + building, building, title });
|
|
767
785
|
const block = (held) => ({ held, title: held.length ? held[0].title : null, has_position: held.length > 0 });
|
|
768
786
|
|
|
@@ -278,7 +278,7 @@ test('RUN: a metric its snapshots never carried is skipped, not drawn as a cliff
|
|
|
278
278
|
|
|
279
279
|
// ---- task 1004829: the government pages read like the rebuilt Home ----------
|
|
280
280
|
|
|
281
|
-
const GOV_PAGES = ['watch', 'gate', 'sessions', 'project-settings', '
|
|
281
|
+
const GOV_PAGES = ['watch', 'gate', 'sessions', 'project-settings', 'money', 'deploy', 'government'];
|
|
282
282
|
|
|
283
283
|
test('every government page head holds only its h1, and no script writes into it', () => {
|
|
284
284
|
// No page shows its head (task 1004836). A sub-line there is a line nobody
|
|
@@ -112,7 +112,10 @@ const ORACLE = {
|
|
|
112
112
|
// gate those two rows left behind; see GONE_GATES below.)
|
|
113
113
|
'/builders/primer': 'requireBuilderPage',
|
|
114
114
|
'/builders/primer.md': 'requireBuilderPage',
|
|
115
|
-
|
|
115
|
+
// Gone (task 1005046, owner decision 2026-11-03): the Diagrams page is the Primer's
|
|
116
|
+
// chapters now, and /builders/diagrams 302s to the first of them before any gate
|
|
117
|
+
// runs. Its IMAGES are still served, on the Primer's tier, by the prefix gate.
|
|
118
|
+
'/builders/diagrams': null,
|
|
116
119
|
'/builders/diagrams/02-b.png': 'requireBuilderPage',
|
|
117
120
|
|
|
118
121
|
// Metic+ operator surfaces (the Governance nav group).
|
|
@@ -123,6 +126,9 @@ const ORACLE = {
|
|
|
123
126
|
// its neighbours: a steering surface, metic+ per ADR 0157. Its two WRITES gate
|
|
124
127
|
// independently on `project.settings.manage`, so this row is only about the shell.
|
|
125
128
|
'/builders/project-settings': 'requireGovernmentPage',
|
|
129
|
+
// The one money page (task 1005047): the same floor as project-settings, whose money
|
|
130
|
+
// section it took. Its hosting section's data keeps its own atom on the route.
|
|
131
|
+
'/builders/money': 'requireGovernmentPage',
|
|
126
132
|
// The deploy door (task 1003159, ADR 0293) — the NARROWEST gate in this table, on
|
|
127
133
|
// the `core.pin.move` atom: archon-floor and system:true, the same key its preview
|
|
128
134
|
// and its move both check. Correct for the one page whose button restarts the live
|
|
@@ -130,8 +136,6 @@ const ORACLE = {
|
|
|
130
136
|
// its write on purpose: the preview discloses the exact core version the box serves
|
|
131
137
|
// and how far behind it is, which is a vulnerability window.
|
|
132
138
|
'/builders/deploy': 'requireCorePinPage',
|
|
133
|
-
// The fleet cost page (task 1001949), on the atom its one read checks.
|
|
134
|
-
'/builders/fleet': 'requireFleetPage',
|
|
135
139
|
|
|
136
140
|
// Gone (task 1003444, owner call 2026-09-25 — ADR 0337 D5): the cross-project
|
|
137
141
|
// directory left the hall, and /people now redirects to /roster before this
|
|
@@ -167,8 +171,9 @@ const ORACLE = {
|
|
|
167
171
|
// same reason (its governed read is the same docket).
|
|
168
172
|
'/builders/street': 'requireBoardVotePage',
|
|
169
173
|
|
|
170
|
-
// Deliberately UNGATED — the landing card
|
|
171
|
-
//
|
|
174
|
+
// Deliberately UNGATED — the landing card and a builder's public portfolio
|
|
175
|
+
// render for a stranger. (/ranks and /drachmae sat here too; since task 1005046
|
|
176
|
+
// they are Primer chapters, the old URLs 302 there, and GONE_PAGES holds them.)
|
|
172
177
|
//
|
|
173
178
|
// /roster joins them (task 1003441): it is this project’s own ledger, and its
|
|
174
179
|
// one data source — GET /leaderboard — is deliberately unauthenticated,
|
|
@@ -186,8 +191,8 @@ const ORACLE = {
|
|
|
186
191
|
// status dashboard is meant to read it too — "anyone landing on /builders or
|
|
187
192
|
// the status dashboard can see what's stuck on the owner", the route's own
|
|
188
193
|
// handler comment), so the page must not be gated tighter than the API it
|
|
189
|
-
// fronts — the same rule that puts /roster
|
|
190
|
-
//
|
|
194
|
+
// fronts — the same rule that puts /roster here. The nav link is still
|
|
195
|
+
// cosmetically 'signed', same as that one.
|
|
191
196
|
'/builders/blockers': null,
|
|
192
197
|
|
|
193
198
|
// The bare detail slugs, gated by task 1003629 — the decision idea 1000907
|
|
@@ -350,6 +355,11 @@ const GONE_PAGES = {
|
|
|
350
355
|
'/builders/people': 'task 1003444 — the hall has no cross-project directory; /people redirects to /roster',
|
|
351
356
|
// Task 1005045 — one sidebar item for Market Watch: its settings are a tab on the page.
|
|
352
357
|
'/builders/market-watch-settings': 'task 1005045 — the settings are the Settings tab on /market-watch; the old URL redirects there',
|
|
358
|
+
// Task 1005046 — one reading room: each is a Primer chapter, and the old URL 302s to it
|
|
359
|
+
// (tests/hall_reading_room.mjs runs the redirect).
|
|
360
|
+
'/builders/ranks': 'task 1005046 — the rank ladder is a Primer chapter; /ranks redirects there',
|
|
361
|
+
'/builders/drachmae': 'task 1005046 — credits and karma is a Primer chapter; /drachmae redirects there',
|
|
362
|
+
'/builders/diagrams': 'task 1005046 — each diagram sits in its Primer chapter; /diagrams redirects there',
|
|
353
363
|
};
|
|
354
364
|
|
|
355
365
|
test('a row that is null because the page left the hall serves no page', () => {
|