@bongos/core 1.24.36 → 1.24.38
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 +92 -87
- package/docs/copy-inventory.md +65 -64
- package/docs/copy-registry.json +170 -173
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/primer.md +2 -0
- package/docs/page-inventory.json +34 -34
- package/docs/page-readings.json +687 -684
- 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/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 +6 -4
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/fleet-costs.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/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +2 -2
- 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 +9 -2
- package/modules/hall-ui/public/primer-live.js +18 -105
- package/modules/hall-ui/public/primer.html +7 -8
- package/modules/hall-ui/public/primer.js +16 -18
- 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/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 +113 -94
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- 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/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/auth.js +0 -8
- package/src/bongos/serve-internal.js +15 -6
- package/src/module-api.js +1 -1
- package/tests/auth_resolve_failure.mjs +5 -4
- package/tests/craft_mode_nav.mjs +12 -10
- package/tests/fleet_cost_view.mjs +2 -18
- package/tests/goal_map_page.mjs +5 -2
- package/tests/hall_loading.mjs +1 -1
- package/tests/hall_money_page.mjs +272 -0
- package/tests/hall_nav.mjs +23 -15
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_page_gate_map.mjs +3 -2
- package/tests/hall_reading_room.mjs +21 -62
- package/tests/nav_permission_atoms.mjs +1 -1
- 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
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
chrome exists before the page script runs. -->
|
|
168
168
|
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
169
169
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
170
|
-
<script src="/builders/craft-mode.js?v=2026-11-
|
|
171
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
170
|
+
<script src="/builders/craft-mode.js?v=2026-11-05-sections"></script>
|
|
171
|
+
<script src="/builders/shell.js?v=2026-11-05-sections"></script>
|
|
172
172
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
173
173
|
<script src="/builders/palette.js" defer></script>
|
|
174
174
|
<!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
|
|
@@ -173,8 +173,8 @@
|
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
174
|
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
175
175
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
176
|
-
<script src="/builders/craft-mode.js?v=2026-11-
|
|
177
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
176
|
+
<script src="/builders/craft-mode.js?v=2026-11-05-sections"></script>
|
|
177
|
+
<script src="/builders/shell.js?v=2026-11-05-sections"></script>
|
|
178
178
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
179
179
|
<script src="/builders/palette.js" defer></script>
|
|
180
180
|
<!-- task 1004727: the ONE karma vote control every task/idea list renders -->
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.38",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.38",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.38",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11179,5 +11179,17 @@
|
|
|
11179
11179
|
"id": "1005046",
|
|
11180
11180
|
"text": "The Docs section now has one reading room instead of four pages: ranks, credits and karma, and the five diagrams all live in the Primer, each topic explained once with its picture beside it. Old links to Ranks, Economy and Dia"
|
|
11181
11181
|
}
|
|
11182
|
+
],
|
|
11183
|
+
"1.24.37": [
|
|
11184
|
+
{
|
|
11185
|
+
"id": "1005047",
|
|
11186
|
+
"text": "Owners now have one Money page in the Government group: what the project spent and paid out, its treasury, and what each hosted project costs, all in one place. Project settings and the Primer link to it instead of repeating t"
|
|
11187
|
+
}
|
|
11188
|
+
],
|
|
11189
|
+
"1.24.38": [
|
|
11190
|
+
{
|
|
11191
|
+
"id": "1005071",
|
|
11192
|
+
"text": "The hall sidebar is now seven labelled sections (Work, Make, Community, You, Govern, Run the project, Help), and every craft can reach Help."
|
|
11193
|
+
}
|
|
11182
11194
|
]
|
|
11183
11195
|
}
|
package/src/bongos/auth.js
CHANGED
|
@@ -406,10 +406,6 @@ const PAGE_GATES = Object.freeze({
|
|
|
406
406
|
// live project; which project they may read or move is still the routes' question,
|
|
407
407
|
// asked per request against that project's row.
|
|
408
408
|
requireCorePinPage: Object.freeze({ permission: 'core.pin.move', deniedTo: '/builders', alsoAdmits: 'isProjectOwner' }),
|
|
409
|
-
// The /fleet cost page (task 1001949) — on `provisioning.fleet.manage`, the SAME atom
|
|
410
|
-
// its one read (GET /provisioning/fleet/cost-ledger) checks: every owner's projects
|
|
411
|
-
// and what they cost, so the page reaches exactly as far as the data does.
|
|
412
|
-
requireFleetPage: Object.freeze({ permission: 'provisioning.fleet.manage', deniedTo: '/builders' }),
|
|
413
409
|
});
|
|
414
410
|
|
|
415
411
|
// The `alsoAdmits` door — "is this visitor the page's OTHER audience?", asked of
|
|
@@ -568,9 +564,6 @@ async function requireBoardVotePage(req, res, next) {
|
|
|
568
564
|
async function requireCorePinPage(req, res, next) {
|
|
569
565
|
return runPageGate('requireCorePinPage', req, res, next);
|
|
570
566
|
}
|
|
571
|
-
async function requireFleetPage(req, res, next) {
|
|
572
|
-
return runPageGate('requireFleetPage', req, res, next);
|
|
573
|
-
}
|
|
574
567
|
|
|
575
568
|
// ---------------------------------------------------------------------------
|
|
576
569
|
// Route-gate inventory (V3.R06 #231) — the canonical "who can call what" list.
|
|
@@ -862,7 +855,6 @@ module.exports = {
|
|
|
862
855
|
requireGovernmentPage,
|
|
863
856
|
requireBoardVotePage,
|
|
864
857
|
requireCorePinPage,
|
|
865
|
-
requireFleetPage,
|
|
866
858
|
requireRank,
|
|
867
859
|
rankMeetsThreshold,
|
|
868
860
|
requirePermission,
|
|
@@ -1010,8 +1010,9 @@ function mountInternalSurfaces(app) {
|
|
|
1010
1010
|
// statically, and a comment in that gap makes the page test as UNGATED while
|
|
1011
1011
|
// looking gated here. Prose goes above the const, as here.
|
|
1012
1012
|
const DEPLOY_PAGE_RE = /^\/builders\/deploy(?:\.html)?\/?$/;
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1013
|
+
// /money — the owner's one money page (task 1005047): the government floor; each
|
|
1014
|
+
// section's read keeps its own wall. NB as at PROJECT_SETTINGS_PAGE_RE.
|
|
1015
|
+
const MONEY_PAGE_RE = /^\/builders\/money(?:\.html)?\/?$/;
|
|
1015
1016
|
// /people and its legacy twin /scouting were the hall's cross-project builder
|
|
1016
1017
|
// directory. Task 1003440 deleted the /scouting door; task 1003444 deleted
|
|
1017
1018
|
// /people itself (owner call, 2026-09-25 — ADR 0337 D5): cross-project
|
|
@@ -1048,6 +1049,16 @@ function mountInternalSurfaces(app) {
|
|
|
1048
1049
|
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/market-watch${qs}#settings`);
|
|
1049
1050
|
});
|
|
1050
1051
|
|
|
1052
|
+
// task 1005047: Fleet costs is /money's #hosting section now; 302 and no gate, as above.
|
|
1053
|
+
const RETIRED_FLEET_RE = /^\/builders\/fleet(?:\.html)?\/?$/;
|
|
1054
|
+
app.use((req, res, next) => {
|
|
1055
|
+
if (isModuleSurfaceHost(req)) return next();
|
|
1056
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
1057
|
+
if (!RETIRED_FLEET_RE.test(path.posix.normalize(req.path))) return next();
|
|
1058
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
1059
|
+
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/money${qs}#hosting`);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1051
1062
|
// task 1005046 (owner decision 2026-11-03, idea 1001311): ONE reading room. Ranks,
|
|
1052
1063
|
// Economy (/drachmae) and Diagrams explained what the Primer already explained, so
|
|
1053
1064
|
// they are Primer chapters now, each with its diagram, and each old URL lands on the
|
|
@@ -1094,7 +1105,8 @@ function mountInternalSurfaces(app) {
|
|
|
1094
1105
|
if (!req.path.startsWith('/builders')) return next();
|
|
1095
1106
|
const norm = path.posix.normalize(req.path);
|
|
1096
1107
|
if (WATCH_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)
|
|
1097
|
-
|| SESSIONS_PAGE_RE.test(norm) || PROJECT_SETTINGS_PAGE_RE.test(norm)
|
|
1108
|
+
|| SESSIONS_PAGE_RE.test(norm) || PROJECT_SETTINGS_PAGE_RE.test(norm)
|
|
1109
|
+
|| MONEY_PAGE_RE.test(norm)) {
|
|
1098
1110
|
return gdsAuth.requireGovernmentPage(req, res, next);
|
|
1099
1111
|
}
|
|
1100
1112
|
// R16 (goal 1000069) widened the government shell from the manage atom
|
|
@@ -1125,9 +1137,6 @@ function mountInternalSurfaces(app) {
|
|
|
1125
1137
|
if (DEPLOY_PAGE_RE.test(norm)) {
|
|
1126
1138
|
return gdsAuth.requireCorePinPage(req, res, next);
|
|
1127
1139
|
}
|
|
1128
|
-
if (FLEET_PAGE_RE.test(norm)) {
|
|
1129
|
-
return gdsAuth.requireFleetPage(req, res, next);
|
|
1130
|
-
}
|
|
1131
1140
|
next();
|
|
1132
1141
|
});
|
|
1133
1142
|
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.38'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -97,18 +97,19 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
|
|
|
97
97
|
// requireBoardVotePage / BV1.R107 (and the ADR 0266 /board-room vote gate)
|
|
98
98
|
// requireCorePinPage / ADR 0293's /deploy door — the narrowest of them,
|
|
99
99
|
// on the archon-floor `core.pin.move` atom.
|
|
100
|
-
//
|
|
101
|
-
//
|
|
100
|
+
// Task 1005047 removed requireFleetPage with the /fleet page it fronted (its
|
|
101
|
+
// ledger is a section of /money now, behind the government page floor and the
|
|
102
|
+
// route's own atom). Task 1003450 removed two more — requireNonXenosPage and the R113
|
|
102
103
|
// requireGovernmentManagePage — which had both ended up dispatched from no
|
|
103
104
|
// route at all; tests/hall_page_gate_map.mjs now fails if that recurs. Task
|
|
104
105
|
// 1003444 removed requireProjectCuratePage with the /people page it fronted.
|
|
105
106
|
// Anything else async and router-mounted must be added with its own try/catch.
|
|
106
107
|
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
|
|
107
108
|
const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
|
|
108
|
-
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireCorePinPage', '
|
|
109
|
+
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireCorePinPage', 'requireGovernmentPage'],
|
|
109
110
|
'a new async router middleware appeared — give it a try/catch and add it here');
|
|
110
111
|
// The page gates must actually delegate (that is WHY they are safe).
|
|
111
|
-
for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireBoardVotePage', 'requireCorePinPage'
|
|
112
|
+
for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireBoardVotePage', 'requireCorePinPage']) {
|
|
112
113
|
const fn = src.slice(src.indexOf(`async function ${name}`));
|
|
113
114
|
assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
|
|
114
115
|
}
|
package/tests/craft_mode_nav.mjs
CHANGED
|
@@ -25,7 +25,7 @@ const CRAFT = read('craft-mode.js');
|
|
|
25
25
|
|
|
26
26
|
// Every NAV id, and the Government group's, read from shell.js itself.
|
|
27
27
|
const NAV_IDS = [...SHELL.matchAll(/\{ id: '([a-z-]+)', label:/g)].map((m) => m[1]);
|
|
28
|
-
const GOV_BLOCK = SHELL.slice(SHELL.indexOf("{ group: '
|
|
28
|
+
const GOV_BLOCK = SHELL.slice(SHELL.indexOf("{ group: 'Run the project'"));
|
|
29
29
|
const GOV_IDS = [...GOV_BLOCK.slice(0, GOV_BLOCK.indexOf('] }')).matchAll(/\{ id: '([a-z-]+)'/g)].map((m) => m[1]);
|
|
30
30
|
|
|
31
31
|
// A small element. Elements parsed from the sidebar's markup are built ONCE, so
|
|
@@ -132,8 +132,9 @@ test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, then Collab and
|
|
|
132
132
|
const { craft } = boot();
|
|
133
133
|
// task 1004685 (the living-city prototype): My agents joins as a PROPOSAL for the
|
|
134
134
|
// owner — every craft has agents, and round-3 board 6 pairs it with the city.
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// task 1005071 (owner, 2026-11-04): Help — the old Docs group — joins, so every craft has the Primer.
|
|
136
|
+
assert.deepEqual([...craft.ALWAYS_TABS], ['roster', 'collab', 'profile', 'my-agents', 'primer', 'commands', 'atlas', 'modules', 'apidocs'],
|
|
137
|
+
'Builders (NAV id roster) heads the list (owner, 2026-10-01); Collab and Profile join it (owner, 2026-10-03); My agents (prototype, task 1004685); Help (task 1005071)');
|
|
137
138
|
for (const m of CRAFTS) {
|
|
138
139
|
const tabs = craft.tabsFor(m);
|
|
139
140
|
assert.ok(Array.isArray(tabs), `${m} has a named tab list`);
|
|
@@ -180,7 +181,7 @@ test('each mode shows its own tabs, and switching updates the nav live', () => {
|
|
|
180
181
|
assert.equal(p.root.dataset.craft, m);
|
|
181
182
|
assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor(m)), `${m} shows exactly its list`);
|
|
182
183
|
for (const id of ['roster', 'collab', 'profile']) assert.ok(p.showing().includes(id), `${id} stays in ${m}`);
|
|
183
|
-
assert.equal(p.headShowing('
|
|
184
|
+
assert.equal(p.headShowing('run-the-project'), m === 'governing', `the Government head shows only where its tabs do (${m})`);
|
|
184
185
|
}
|
|
185
186
|
p.craft.choose('building');
|
|
186
187
|
assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor('building')), 'back to Building, its tabs return without a reload');
|
|
@@ -190,11 +191,12 @@ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mod
|
|
|
190
191
|
const p = boot({ session: { 'hall-craft': 'governing' } });
|
|
191
192
|
p.signIn('thetes');
|
|
192
193
|
assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
|
|
193
|
-
assert.ok(!p.headShowing('
|
|
194
|
-
|
|
194
|
+
assert.ok(!p.headShowing('run-the-project'), 'nor its group head');
|
|
195
|
+
// task 1005071: sidebar order — Work, Community, You, Govern, then Help (every craft).
|
|
196
|
+
assert.deepEqual(p.showing(), ['blockers', 'collab', 'roster', 'profile', 'my-agents', 'city', 'board-room', 'primer', 'commands', 'atlas', 'modules', 'apidocs'], 'what a thetes board member in Governing sees');
|
|
195
197
|
p.signIn('archon');
|
|
196
198
|
assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
|
|
197
|
-
assert.ok(p.headShowing('
|
|
199
|
+
assert.ok(p.headShowing('run-the-project'), 'and its group head comes back with it, which only a mode pass after the gate can know');
|
|
198
200
|
assert.ok(!p.showing().includes('work'), 'and the gate pass did not bring back a tab the mode left out');
|
|
199
201
|
});
|
|
200
202
|
|
|
@@ -257,9 +259,9 @@ function bootTree({ mode = null, activeId = null } = {}) {
|
|
|
257
259
|
let into = group;
|
|
258
260
|
if (label) {
|
|
259
261
|
const head = el('BUTTON', 'nav-group__head');
|
|
260
|
-
head.dataset.group = label.toLowerCase();
|
|
262
|
+
head.dataset.group = label.toLowerCase().replace(/\s+/g, '-');
|
|
261
263
|
group.appendChild(head);
|
|
262
|
-
into = group.appendChild(el('DIV', 'nav-items', 'nav-items-' + label.toLowerCase()));
|
|
264
|
+
into = group.appendChild(el('DIV', 'nav-items', 'nav-items-' + label.toLowerCase().replace(/\s+/g, '-')));
|
|
263
265
|
}
|
|
264
266
|
for (const m of block.matchAll(/\{ id: '([a-z-]+)', label:/g)) {
|
|
265
267
|
into.appendChild(el('A', 'nav-item' + (m[1] === activeId ? ' nav-item--active' : ''), 'nav-' + m[1]));
|
|
@@ -306,7 +308,7 @@ test('GOVERNING LEADS WITH THE CITY: its other tabs fold into one dropdown right
|
|
|
306
308
|
assert.equal(head.getAttribute('aria-controls'), 'nav-items-craft-fold', 'the head names the region it opens');
|
|
307
309
|
assert.match(head.innerHTML, /More governing/);
|
|
308
310
|
assert.ok(head.hasAttribute('data-craft-off') === false, 'its head shows while a tab inside does');
|
|
309
|
-
assert.ok(p.sidebar.querySelectorAll('.nav-group__head').find((h) => h.dataset.group === '
|
|
311
|
+
assert.ok(p.sidebar.querySelectorAll('.nav-group__head').find((h) => h.dataset.group === 'run-the-project').hasAttribute('data-craft-off'),
|
|
310
312
|
'the emptied Government head hides, so no label sits over nothing');
|
|
311
313
|
p.craft.choose('building');
|
|
312
314
|
assert.equal(p.fold(), null, 'leaving Governing removes the dropdown');
|
|
@@ -103,24 +103,8 @@ test('no projects renders a sentence, not an empty table', () => {
|
|
|
103
103
|
assert.match(costs.ledgerHtml(null), /No projects are hosted here yet/);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
const html = read(path.join(HALL, 'fleet.html'));
|
|
109
|
-
assert.ok(html.indexOf('fleet-costs.js') !== -1 && html.indexOf('fleet-costs.js') < html.indexOf('/fleet.js'), 'fleet-costs.js loads first');
|
|
110
|
-
const js = read(path.join(HALL, 'fleet.js'));
|
|
111
|
-
assert.match(js, /\/provisioning\/fleet\/cost-ledger/);
|
|
112
|
-
assert.match(js, /status === 403[\s\S]{0,200}fl-sealed/, 'a refusal shows the sealed state');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('the page gate is the atom the ledger route checks, and the nav mirrors its floor', () => {
|
|
116
|
-
const route = read(path.join(ROOT, 'modules', 'provisioning', 'routes', 'provisioning.js'));
|
|
117
|
-
assert.match(route, /router\.get\('\/provisioning\/fleet\/cost-ledger', auth\.requireBuilder, auth\.requirePermission\('provisioning\.fleet\.manage'\)/);
|
|
118
|
-
assert.match(read(path.join(ROOT, 'src', 'bongos', 'auth.js')), /requireFleetPage: Object\.freeze\(\{ permission: 'provisioning\.fleet\.manage'/);
|
|
119
|
-
// The nav is cosmetic (the page gate is the wall); 'government' is the metic floor.
|
|
120
|
-
const { byKey } = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
121
|
-
assert.equal(byKey('provisioning.fleet.manage').floor, 'metic');
|
|
122
|
-
assert.match(read(path.join(HALL, 'shell.js')), /href: '\/fleet', gate: 'government', module: 'provisioning'/);
|
|
123
|
-
});
|
|
106
|
+
// The page's wiring and its gate moved to tests/hall_money_page.mjs with the page
|
|
107
|
+
// (task 1005047): the ledger is the money page's #hosting section, and /fleet redirects.
|
|
124
108
|
|
|
125
109
|
// ── the owner's line on their own project page ───────────────────────────────
|
|
126
110
|
const HUB = read(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'));
|
package/tests/goal_map_page.mjs
CHANGED
|
@@ -415,8 +415,11 @@ await test('Sky sits directly under Goals in the nav, member-gated, at the URL G
|
|
|
415
415
|
const shell = read('shell.js');
|
|
416
416
|
const item = shell.indexOf("{ id: 'goal-map', label: 'Sky', icon: 'sky', href: '/goal-map', gate: 'member' }");
|
|
417
417
|
assert.ok(item > -1, 'the nav item, with the slot the map was denied (ADR 0227 §11)');
|
|
418
|
-
|
|
419
|
-
|
|
418
|
+
// task 1005071 moved Tasks up to second in Work (owner, 2026-11-04); Sky stays the
|
|
419
|
+
// item straight after Goals.
|
|
420
|
+
const goals = shell.indexOf("{ id: 'goals', label: 'Goals'");
|
|
421
|
+
const between = shell.slice(goals, item);
|
|
422
|
+
assert.ok(goals > -1 && goals < item && (between.match(/\{ id: '/g) || []).length === 1, 'directly under Goals');
|
|
420
423
|
assert.match(read('goals.html'), /href="\/builders\/goal-map"/, 'Goals still links to it');
|
|
421
424
|
});
|
|
422
425
|
|
package/tests/hall_loading.mjs
CHANGED
|
@@ -83,7 +83,7 @@ test('the page loaders step aside by themselves, and never show without :has()',
|
|
|
83
83
|
assert.match(STYLE, /\.hall-loading--wait, \.hall-loading--zones \{ display: none; \}/);
|
|
84
84
|
assert.match(STYLE, /@supports selector\(:has\(a\)\) \{\s*\.hall-loading--wait:not\(:has\(~ :not\(\[hidden\], script, template, style\)\)\) \{ display: flex; \}\s*\.hall-loading--zones:not\(:has\(~ :not\(\[hidden\]\) > :not\(\[hidden\], script, template, style\)\)\) \{ display: flex; \}\s*\}/);
|
|
85
85
|
// A page whose sections all start [hidden] carries the --wait loader FIRST in its main.
|
|
86
|
-
for (const page of ['board-room', 'deploy', '
|
|
86
|
+
for (const page of ['board-room', 'deploy', 'gate', 'government', 'money', 'project-settings', 'sessions']) {
|
|
87
87
|
const html = read(`${page}.html`);
|
|
88
88
|
const main = html.slice(html.indexOf('<main id="hall-main">'));
|
|
89
89
|
const first = main.replace(/<main id="hall-main">\s*(<!--[\s\S]*?-->\s*)*/, '');
|
|
@@ -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
|
+
});
|