@bongos/core 1.24.40 → 1.24.42
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 +103 -103
- package/docs/canonical-permissions.md +2 -1
- package/docs/copy-inventory.md +198 -223
- package/docs/copy-registry.json +316 -580
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -69
- package/docs/page-readings.json +348 -728
- 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 +17 -262
- 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 +1 -1
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/gate.js +12 -5
- 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 +3 -248
- package/modules/hall-ui/public/hall-widgets.js +3 -14
- 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 -5
- 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 +2 -2
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/palette.js +10 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.js +21 -10
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/setup-gate.js +156 -0
- package/modules/hall-ui/public/shell.js +17 -3
- 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 +85 -117
- 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 +120 -7
- package/modules/hall-ui/public/watch.js +11 -0
- package/modules/hall-ui/public/watch.states.json +49 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/setup-gate.md +43 -0
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/onboarding/onboarding-state.js +41 -20
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/gen-hall-sitemap.js +2 -0
- package/src/bongos/routes/me.js +2 -2
- package/src/bongos/serve-internal.js +15 -22
- package/src/module-api.js +1 -1
- package/tests/autonomy_builder_scope.mjs +2 -1
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_board_world.mjs +1 -1
- package/tests/hall_landing_boot.mjs +2 -3
- package/tests/hall_loading.mjs +13 -2
- package/tests/hall_nav.mjs +5 -2
- package/tests/hall_onboarding_bongos_setup.mjs +14 -14
- package/tests/hall_oversight_tabs.mjs +162 -0
- package/tests/hall_oversight_world.mjs +23 -9
- package/tests/hall_page_gate_map.mjs +7 -2
- package/tests/hall_roster_world.mjs +14 -9
- package/tests/hall_sessions_ledger.mjs +2 -1
- package/tests/module_contributions.mjs +4 -2
- package/tests/nav_permission_atoms.mjs +4 -2
- package/tests/onboarding_state.mjs +33 -0
- package/tests/sessions_kit_pager.mjs +8 -4
- package/tests/setup_flow.mjs +242 -0
- package/tests/welcome_boot.mjs +3 -2
- package/modules/hall-ui/public/gate.html +0 -76
- package/modules/hall-ui/public/gate.states.json +0 -60
- package/modules/hall-ui/public/sessions.html +0 -128
- package/modules/hall-ui/public/sessions.states.json +0 -59
|
@@ -92,15 +92,14 @@ async function boot({ meStatus, settle = false }) {
|
|
|
92
92
|
},
|
|
93
93
|
OTBHallRender: () => ({
|
|
94
94
|
analyticsState: {}, lbState: {},
|
|
95
|
-
shipsPager: { page: 1 },
|
|
95
|
+
shipsPager: { page: 1 },
|
|
96
96
|
// task 1004854: Home asks for this many ships; the ledger's full read lives in hall-render.js.
|
|
97
97
|
HOME_SHIPS_LIMIT: 5, setRecentShips: spy('setRecentShips'), populateShipVersionFilter: spy('populateShipVersionFilter'),
|
|
98
98
|
mountHallWidgets: spy('mountHallWidgets'),
|
|
99
99
|
renderLanding: spy('renderLanding'),
|
|
100
100
|
renderCurveBody: spy('renderCurveBody'), renderGreeting: spy('renderGreeting'),
|
|
101
|
-
renderNav: spy('renderNav'),
|
|
101
|
+
renderNav: spy('renderNav'),
|
|
102
102
|
renderProfile: spy('renderProfile'), renderRecentShips: spy('renderRecentShips'),
|
|
103
|
-
renderWelcomeCore: spy('renderWelcomeCore'),
|
|
104
103
|
// task 1003459 — wireEvents() calls this unconditionally now (the Home
|
|
105
104
|
// Next-up Claim button), so an incomplete stub throws mid-wire.
|
|
106
105
|
wireNextUpClaim: spy('wireNextUpClaim'),
|
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', 'government', 'money', 'project-settings']) {
|
|
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*)*/, '');
|
|
@@ -95,6 +95,17 @@ test('the page loaders step aside by themselves, and never show without :has()',
|
|
|
95
95
|
}
|
|
96
96
|
assert.match(read('index.html'), /<section class="view" id="view-home"[^>]*>\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…<\/p>/, 'Home waits on its zones');
|
|
97
97
|
assert.match(read('watch.html'), /<main id="hall-main">\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…<\/p>/, 'Watch waits on its panes');
|
|
98
|
+
// task 1005049: the Sessions and Autobongos tabs (once their own pages) open on the
|
|
99
|
+
// --wait loader, so a tab whose sections are all still hidden says it is loading.
|
|
100
|
+
for (const pane of ['pane-sessions', 'pane-autobongos']) {
|
|
101
|
+
const html = read('watch.html');
|
|
102
|
+
const inner = html.slice(html.indexOf(`<div id="${pane}"`));
|
|
103
|
+
const body = inner.slice(inner.indexOf('>') + 1).replace(/^\s*(<!--[\s\S]*?-->\s*)*/, '');
|
|
104
|
+
assert.match(body, /^<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…<\/p>/, `${pane} opens on the page loader`);
|
|
105
|
+
const kids = [...body.slice(body.indexOf('</p>') + 4, body.indexOf('\n </div>')).matchAll(/^ {10}<(section|div|p|nav)\b([^>]*)>/gm)];
|
|
106
|
+
assert.ok(kids.length >= 1, `${pane} has sections after the loader`);
|
|
107
|
+
for (const [, tag, attrs] of kids) assert.match(attrs, /\bhidden\b/, `${pane}: a <${tag}> after the loader starts hidden`);
|
|
108
|
+
}
|
|
98
109
|
// Settings' layout is never [hidden] while it loads, so its loader is put away by hideLayout().
|
|
99
110
|
assert.match(read('settings.html'), /id="settings-groups"[^>]*>\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page" id="settings-loading" role="status">/);
|
|
100
111
|
assert.match(read('settings.js'), /function hideLayout\(hide\) \{[\s\S]*?getElementById\('settings-loading'\)[\s\S]*?loading\.hidden = true;/);
|
|
@@ -105,7 +116,7 @@ test('no hall page waits on a bare line of text any more', () => {
|
|
|
105
116
|
// the class would stay behind on the finished sentence. Each is named with why.
|
|
106
117
|
const IN_PLACE = new Set([
|
|
107
118
|
'atlas.html#caption', // the map's caption, rewritten by atlas.js
|
|
108
|
-
'
|
|
119
|
+
'watch.html#ledger-status', // the Sessions tab's status line, rewritten by sessions.js
|
|
109
120
|
'goals.html#goal-deps-card-lede', // the dependency card's lede, rewritten by goals-page.js
|
|
110
121
|
'goals.html#goal-deps-lede',
|
|
111
122
|
'profile.html#activity-placeholder', // reused for the signed-out notice by profile.js
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -335,7 +335,10 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
335
335
|
// 'market-watch-settings' (task 1004938) LEFT the group in task 1005045: the owner saw
|
|
336
336
|
// "Market Watch" twice and had to guess which was which (owner decision 2026-11-03,
|
|
337
337
|
// idea 1001310). The settings are an owner-only Settings tab on /market-watch now.
|
|
338
|
-
|
|
338
|
+
// 'gate' (Autobongos) and 'sessions' LEFT the group in task 1005049 (owner decision
|
|
339
|
+
// 2026-11-03, idea 1001315): they are tabs on Watch, and /gate + /sessions redirect there.
|
|
340
|
+
assert.deepEqual(nav[5].items.map((i) => i.id), ['watch', 'project-settings', 'money', 'deploy', 'government']);
|
|
341
|
+
assert.ok(!nav.flatMap((g) => g.items).some((i) => i.href === '/gate' || i.href === '/sessions'), 'no item points at a page that became a Watch tab');
|
|
339
342
|
// task 1005045: and no label is left twice anywhere in the sidebar, so the next pair
|
|
340
343
|
// fails here instead of in front of the owner.
|
|
341
344
|
const labels = [...boot().sidebarHtml.matchAll(/<a class="nav-item" [^>]*>[\s\S]*?<span class="nav-item__label">([^<]*)<\/span>/g)].map((m) => m[1]);
|
|
@@ -787,7 +790,7 @@ test('no glyph path data reads as an IP address to the release leak scan', () =>
|
|
|
787
790
|
// block (task 1004713). These drive the real shell and the real gov-positions.js.
|
|
788
791
|
|
|
789
792
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
790
|
-
const GOVERNING = ['city', 'board-room', 'watch', '
|
|
793
|
+
const GOVERNING = ['city', 'board-room', 'watch', 'project-settings', 'money', 'deploy', 'government'];
|
|
791
794
|
const lead = (building, title) => ({ kind: 'lead', seat: 'lead:' + building, building, title });
|
|
792
795
|
const block = (held) => ({ held, title: held.length ? held[0].title : null, has_position: held.length > 0 });
|
|
793
796
|
|
|
@@ -14,20 +14,20 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
14
14
|
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
|
15
15
|
const OLD = /\/builder-setup|setup-builder\.(sh|js)/;
|
|
16
16
|
|
|
17
|
-
// The
|
|
18
|
-
function
|
|
19
|
-
const src = read('modules/hall-ui/public/
|
|
20
|
-
const start = src.indexOf(
|
|
21
|
-
const end = src.indexOf(
|
|
22
|
-
assert.ok(start > 0 && end > start, '
|
|
17
|
+
// The setup screen's machine-linking step (task 1005076 replaced the Home welcome panel).
|
|
18
|
+
function setupStepMarkup() {
|
|
19
|
+
const src = read('modules/hall-ui/public/setup-gate.js');
|
|
20
|
+
const start = src.indexOf("if (step.id === 'cli_setup') {");
|
|
21
|
+
const end = src.indexOf("if (step.id === 'pick_disciplines')", start);
|
|
22
|
+
assert.ok(start > 0 && end > start, 'the setup screen\'s cli_setup step located');
|
|
23
23
|
return src.slice(start, end).split('\n').filter((l) => !/^\s*\/\//.test(l)).join('\n');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
test('
|
|
27
|
-
const w =
|
|
28
|
-
assert.match(w, /
|
|
29
|
-
assert.match(w, /\\nnpm install\\nnpx bongos setup
|
|
30
|
-
assert.doesNotMatch(w, OLD, 'no legacy setup command on the
|
|
26
|
+
test('setup screen: the steps and the clone block say npx bongos setup', () => {
|
|
27
|
+
const w = setupStepMarkup();
|
|
28
|
+
assert.match(w, /run <code>npx bongos setup<\/code>/);
|
|
29
|
+
assert.match(w, /\\nnpm install\\nnpx bongos setup<\/pre>/, 'clone block ends npm install + npx bongos setup');
|
|
30
|
+
assert.doesNotMatch(w, OLD, 'no legacy setup command on the setup screen');
|
|
31
31
|
assert.doesNotMatch(w, /On Windows, run/, 'one command on every OS — no Windows-only line');
|
|
32
32
|
assert.match(w, /nodejs\.org/, 'the tooling list names Node, which npm install needs');
|
|
33
33
|
});
|
|
@@ -51,7 +51,7 @@ test('primer: both routes sign in with npx bongos setup', () => {
|
|
|
51
51
|
|
|
52
52
|
test('the get-the-code block keeps one command per line (screen and copy)', () => {
|
|
53
53
|
const css = read('modules/hall-ui/public/style.css');
|
|
54
|
-
const m = /\.
|
|
54
|
+
const m = /\.setup-gate__code\s*\{([^}]*)\}/.exec(css);
|
|
55
55
|
assert.ok(m, 'scoped rule present');
|
|
56
56
|
assert.match(m[1], /white-space:\s*pre/, 'copy-on-click reads innerText, so pre is what keeps the line breaks');
|
|
57
57
|
assert.match(m[1], /overflow-y:\s*hidden/, 'a one-axis strip names both axes');
|
|
@@ -66,8 +66,8 @@ test('bin/bongos.js is committed executable, so npx leaves a fresh clone clean',
|
|
|
66
66
|
assert.match(entry, /^100755 /, `expected mode 100755, got: ${entry.trim()}`);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
test('
|
|
70
|
-
const src = read('modules/hall-ui/public/
|
|
69
|
+
test('the setup screen and checklist lead ask only for what a builder needs', () => {
|
|
70
|
+
const src = read('modules/hall-ui/public/setup-gate.js');
|
|
71
71
|
assert.doesNotMatch(src, /run the platform locally/, 'a builder does not run the platform');
|
|
72
72
|
assert.match(src, /Clone the project to your machine and run <code>npm install<\/code> in it\./);
|
|
73
73
|
const st = read('modules/onboarding/onboarding-state.js');
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// tests/hall_oversight_tabs.mjs — one oversight page (task 1005049, owner decision
|
|
2
|
+
// 2026-11-03, idea 1001315 item 1).
|
|
3
|
+
//
|
|
4
|
+
// Watch, Sessions and Autobongos (/gate) were three oversight pages in the same
|
|
5
|
+
// sidebar group. Sessions and Autobongos are tabs on Watch now:
|
|
6
|
+
//
|
|
7
|
+
// * OLD URLS LAND ON THE RIGHT TAB. /sessions and /gate 302 to /watch#sessions
|
|
8
|
+
// and /watch#autobongos (serve-internal.js RETIRED_TO_SECTION), with any query
|
|
9
|
+
// string kept; the real middleware runs here, on a bare app.
|
|
10
|
+
// * EVERYTHING THE TWO PAGES SHOWED IS ON WATCH. The tabs exist, their panes hold
|
|
11
|
+
// the ledger, the transcript reader and the runner panel, and the scripts that
|
|
12
|
+
// drew the pages still draw them.
|
|
13
|
+
// * A TAB LOADS WHEN IT OPENS. Each session-ledger load is an audited read on the
|
|
14
|
+
// server, so sessions.js and gate.js ask for nothing until Watch first opens
|
|
15
|
+
// their tab, and only once.
|
|
16
|
+
// * THE PALETTE STILL FINDS THEM: its Watch jumps are real tab ids.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/hall_oversight_tabs.mjs
|
|
19
|
+
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import http from 'node:http';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import vm from 'node:vm';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
32
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
33
|
+
const WATCH_HTML = read('watch.html');
|
|
34
|
+
const WATCH_JS = read('watch.js');
|
|
35
|
+
|
|
36
|
+
async function withHall(fn) {
|
|
37
|
+
const express = require('express');
|
|
38
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
39
|
+
const app = express();
|
|
40
|
+
mountInternalSurfaces(app);
|
|
41
|
+
const server = app.listen(0, '127.0.0.1');
|
|
42
|
+
await new Promise((r) => server.once('listening', r));
|
|
43
|
+
const { port } = server.address();
|
|
44
|
+
const get = (p, host) => new Promise((resolve, reject) => {
|
|
45
|
+
http.get({ host: '127.0.0.1', port, path: p, headers: { host } }, (res) => {
|
|
46
|
+
res.resume();
|
|
47
|
+
res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || '' }));
|
|
48
|
+
}).on('error', reject);
|
|
49
|
+
});
|
|
50
|
+
try { await fn(get); } finally { server.close(); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const TAB_OF = { sessions: 'sessions', gate: 'autobongos' };
|
|
54
|
+
|
|
55
|
+
test('RUN: /sessions and /gate each 302 to their tab on Watch', async () => {
|
|
56
|
+
await withHall(async (get) => {
|
|
57
|
+
for (const [page, tab] of Object.entries(TAB_OF)) {
|
|
58
|
+
for (const form of [`/${page}`, `/${page}/`, `/${page}.html`]) {
|
|
59
|
+
const r = await get(form, 'builders.127.0.0.1');
|
|
60
|
+
assert.equal(r.status, 302, `${form} is a temporary redirect`);
|
|
61
|
+
assert.equal(r.location, `/watch#${tab}`, `${form} lands on #${tab}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const q = await get('/sessions?from=mail', 'builders.127.0.0.1');
|
|
65
|
+
assert.equal(q.location, '/watch?from=mail#sessions', 'a query string rides along; the tab is the fragment');
|
|
66
|
+
// The map the two joined still carries the page it was built for (task 1005047).
|
|
67
|
+
assert.equal((await get('/fleet', 'builders.127.0.0.1')).location, '/money#hosting');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Watch has a Sessions and an Autobongos tab, each with its pane', () => {
|
|
72
|
+
assert.match(WATCH_JS, /\{ id: 'sessions', label: 'Sessions', panel: 'pane-sessions' \}/);
|
|
73
|
+
assert.match(WATCH_JS, /\{ id: 'autobongos', label: 'Autobongos', panel: 'pane-autobongos' \}/);
|
|
74
|
+
for (const pane of ['pane-sessions', 'pane-autobongos']) {
|
|
75
|
+
assert.match(WATCH_HTML, new RegExp(`<div id="${pane}" role="tabpanel" aria-labelledby="tab-${pane.slice(5)}" hidden>`));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('everything the two pages showed is in those panes', () => {
|
|
80
|
+
const sessions = WATCH_HTML.slice(WATCH_HTML.indexOf('<div id="pane-sessions"'), WATCH_HTML.indexOf('<div id="pane-autobongos"'));
|
|
81
|
+
for (const id of ['sess-sealed', 'stats-scroll', 'sess-ledger-scroll', 'session-type-filter', 'sessions-table', 'sessions-pager-mount', 'transcript-scroll']) {
|
|
82
|
+
assert.match(sessions, new RegExp(`id="${id}"`), `the Sessions tab holds #${id}`);
|
|
83
|
+
}
|
|
84
|
+
const runner = WATCH_HTML.slice(WATCH_HTML.indexOf('<div id="pane-autobongos"'));
|
|
85
|
+
for (const id of ['gate-sealed', 'runner-body', 'gate-archon-only', 'fence-panel', 'fence-body', 'fence-builders']) {
|
|
86
|
+
assert.match(runner, new RegExp(`id="${id}"`), `the Autobongos tab holds #${id}`);
|
|
87
|
+
}
|
|
88
|
+
// Their scripts load before watch.js, which boots them.
|
|
89
|
+
const i = (s) => WATCH_HTML.indexOf(s);
|
|
90
|
+
assert.ok(i('/builders/sessions.js') > 0 && i('/builders/sessions.js') < i('/builders/watch.js'));
|
|
91
|
+
assert.ok(i('/builders/gate.js') > 0 && i('/builders/gate.js') < i('/builders/watch.js'));
|
|
92
|
+
// No element id on the page is used twice: three pages' markup now share one document.
|
|
93
|
+
const ids = [...WATCH_HTML.replace(/<!--[\s\S]*?-->/g, '').matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
|
|
94
|
+
assert.deepEqual(ids.filter((x, k) => ids.indexOf(x) !== k), [], 'every id on Watch is said once');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('the pages are gone, and nothing in the sidebar points at them', () => {
|
|
98
|
+
assert.ok(!fs.existsSync(path.join(HALL, 'sessions.html')));
|
|
99
|
+
assert.ok(!fs.existsSync(path.join(HALL, 'gate.html')));
|
|
100
|
+
assert.ok(!/href: '\/(sessions|gate)'/.test(read('shell.js')));
|
|
101
|
+
assert.ok(!/'gate', 'sessions'/.test(read('craft-mode.js')), 'Governing mode no longer lists them');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function bootTab(file, { sharedMe = false } = {}) {
|
|
105
|
+
const requests = [];
|
|
106
|
+
const el = () => ({ hidden: true, textContent: '', innerHTML: '', value: '', dataset: {}, style: {}, addEventListener() {}, querySelector: () => null, querySelectorAll: () => [], appendChild() {} });
|
|
107
|
+
const els = {};
|
|
108
|
+
const getEl = (id) => (els[id] = els[id] || el());
|
|
109
|
+
const document = { getElementById: getEl, querySelector: (s) => getEl(String(s).replace(/^#/, '')), querySelectorAll: () => [], createElement: el, addEventListener() {} };
|
|
110
|
+
const window = {
|
|
111
|
+
BongosClient: { createClient: () => ({ request: async (m, p) => { requests.push(p); return { status: 200, ok: true, data: { builder: { id: 1, rank: 'xenos' } } }; } }) },
|
|
112
|
+
OTB: { escapeHtml: (s) => String(s), $: document.querySelector, num: Number, fmtInt: String, fmtCompact: String, fmtDate: String, rankLabel: (r) => r, toast() {} },
|
|
113
|
+
OTBKit: { emptyStateHtml: (m) => m, makePager: () => ({ page: 1, perPage: 25 }) },
|
|
114
|
+
location: { assign() {} },
|
|
115
|
+
localStorage: { getItem: () => null, setItem() {} },
|
|
116
|
+
addEventListener() {},
|
|
117
|
+
};
|
|
118
|
+
// The shell's own /me read (shell.js probeMe shares it on window.__hallReads).
|
|
119
|
+
// A viewer below the floor, so each tab stops at its own sealed notice after /me.
|
|
120
|
+
if (sharedMe) window.__hallReads = { '/me': Promise.resolve({ builder: { id: 1, rank: 'xenos' } }) };
|
|
121
|
+
const sandbox = { window, document, console: { log() {}, warn() {}, error() {} }, setTimeout, clearTimeout, Promise, URLSearchParams };
|
|
122
|
+
vm.createContext(sandbox);
|
|
123
|
+
vm.runInContext(read(file), sandbox, { filename: file });
|
|
124
|
+
return { window, requests, getEl };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [file, tab] of [['sessions.js', 'sessions'], ['gate.js', 'autobongos']]) {
|
|
128
|
+
test(`${file} asks for nothing until Watch opens its tab, then boots once`, async () => {
|
|
129
|
+
const t = bootTab(file);
|
|
130
|
+
await new Promise((r) => setImmediate(r));
|
|
131
|
+
assert.equal(t.requests.length, 0, `${file} read the server before its tab opened`);
|
|
132
|
+
assert.equal(typeof t.window.OTBWatchTabs[tab], 'function', `${file} hands Watch a boot for #${tab}`);
|
|
133
|
+
t.window.OTBWatchTabs[tab]();
|
|
134
|
+
await new Promise((r) => setImmediate(r));
|
|
135
|
+
const first = t.requests.length;
|
|
136
|
+
assert.ok(first >= 1, 'opening the tab loads it');
|
|
137
|
+
t.window.OTBWatchTabs[tab]();
|
|
138
|
+
await new Promise((r) => setImmediate(r));
|
|
139
|
+
assert.equal(t.requests.length, first, 'opening it again does not load it twice');
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const [file, tab] of [['sessions.js', 'sessions'], ['gate.js', 'autobongos']]) {
|
|
144
|
+
test(`${file} adopts the shell's /me read rather than asking again`, async () => {
|
|
145
|
+
const t = bootTab(file, { sharedMe: true });
|
|
146
|
+
t.window.OTBWatchTabs[tab]();
|
|
147
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setImmediate(r));
|
|
148
|
+
assert.ok(!t.requests.some((p) => /\/me$/.test(p)), `${file} asked /me although the shell had`);
|
|
149
|
+
assert.equal(t.getEl(file === 'gate.js' ? 'gate-sealed' : 'sess-sealed').hidden, false, 'and still acts on the answer');
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
test('watch.js boots a tab from its own change hook', () => {
|
|
154
|
+
assert.match(WATCH_JS, /onChange: \(id\) => \{\s*const boot = window\.OTBWatchTabs && window\.OTBWatchTabs\[id\];\s*if \(boot\) boot\(\);/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the palette\'s Watch jumps are real watch.js tab ids', () => {
|
|
158
|
+
const ids = [...WATCH_JS.matchAll(/\{ id: '([a-z-]+)', label: '[^']*', panel:/g)].map((m) => m[1]);
|
|
159
|
+
const declared = [...read('palette.js').matchAll(/href:\s*'\/watch#([a-z-]+)'/g)].map((m) => m[1]);
|
|
160
|
+
assert.deepEqual(declared, ids, 'one jump per Watch tab, in order');
|
|
161
|
+
assert.ok(declared.includes('sessions') && declared.includes('autobongos'));
|
|
162
|
+
});
|
|
@@ -17,7 +17,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
18
|
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
19
19
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
|
|
20
|
-
|
|
20
|
+
// task 1005049: the /gate page is Watch's Autobongos tab; its script and sheet stay.
|
|
21
|
+
const PAGES = ['government', 'watch'];
|
|
21
22
|
|
|
22
23
|
test('every oversight page links the family sheet, before its own', () => {
|
|
23
24
|
for (const p of PAGES) {
|
|
@@ -27,6 +28,11 @@ test('every oversight page links the family sheet, before its own', () => {
|
|
|
27
28
|
assert.ok(fam > 0, `${p}.html links oversight.css`);
|
|
28
29
|
assert.ok(own > fam, `${p}.html links its own sheet after the family's`);
|
|
29
30
|
}
|
|
31
|
+
// Watch's two folded-in tabs bring their sheets, after the family's too.
|
|
32
|
+
const watch = read('watch.html');
|
|
33
|
+
for (const sheet of ['sessions', 'gate']) {
|
|
34
|
+
assert.ok(watch.indexOf(`/builders/${sheet}.css`) > watch.indexOf('/builders/oversight.css'), `watch.html links ${sheet}.css after the family's`);
|
|
35
|
+
}
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
test('the family sheet spends only the fifteen: no literal colour', () => {
|
|
@@ -110,6 +116,10 @@ test('the sealed state is a panel with the floor written from the pack, on all f
|
|
|
110
116
|
assert.match(read(`${p}.html`), /class="scroll" id="[a-z-]*sealed(-scroll)?" /, `${p}.html seals with a .scroll panel`);
|
|
111
117
|
assert.match(read(`${p}.js`), /rankLabel\('metic'\)/, `${p}.js writes the floor from the pack's rank label`);
|
|
112
118
|
}
|
|
119
|
+
// The Autobongos and Sessions tabs keep their own sealed panels inside Watch.
|
|
120
|
+
assert.match(read('watch.html'), /class="scroll" id="gate-sealed" /);
|
|
121
|
+
assert.match(read('watch.html'), /class="scroll" id="sess-sealed" /);
|
|
122
|
+
assert.match(read('gate.js'), /rankLabel\('metic'\)/);
|
|
113
123
|
});
|
|
114
124
|
|
|
115
125
|
test('the vocabulary wall holds: the family is named oversight on the government surface', () => {
|
|
@@ -278,7 +288,9 @@ test('RUN: a metric its snapshots never carried is skipped, not drawn as a cliff
|
|
|
278
288
|
|
|
279
289
|
// ---- task 1004829: the government pages read like the rebuilt Home ----------
|
|
280
290
|
|
|
281
|
-
const GOV_PAGES = ['watch', '
|
|
291
|
+
const GOV_PAGES = ['watch', 'project-settings', 'money', 'deploy', 'government'];
|
|
292
|
+
// task 1005049: gate.js and sessions.js draw Watch's tabs now; their scripts still must not.
|
|
293
|
+
const GOV_SCRIPTS = [...GOV_PAGES, 'gate', 'sessions'];
|
|
282
294
|
|
|
283
295
|
test('every government page head holds only its h1, and no script writes into it', () => {
|
|
284
296
|
// No page shows its head (task 1004836). A sub-line there is a line nobody
|
|
@@ -291,7 +303,7 @@ test('every government page head holds only its h1, and no script writes into it
|
|
|
291
303
|
assert.doesNotMatch(head[1], /page-head__eyebrow|page-head__sub/, `${page}.html's hidden head holds only the h1`);
|
|
292
304
|
}
|
|
293
305
|
const subIds = ['watch-sub', 'gate-sub', 'ledger-sub', 'ps-sub', 'dp-sub', 'fl-sub', 'gov-sub'];
|
|
294
|
-
for (const page of
|
|
306
|
+
for (const page of GOV_SCRIPTS) {
|
|
295
307
|
const js = read(`${page}.js`);
|
|
296
308
|
for (const id of subIds) assert.ok(!js.includes(id), `${page}.js no longer writes into #${id}`);
|
|
297
309
|
}
|
|
@@ -300,7 +312,7 @@ test('every government page head holds only its h1, and no script writes into it
|
|
|
300
312
|
test('a failed load on Watch and Sessions is said in the page body', () => {
|
|
301
313
|
assert.match(read('watch.js'), /showSealed\('Could not check your access\. Reload to try again\.', 'Could not load'\)/);
|
|
302
314
|
assert.match(read('sessions.js'), /showSealed\('Could not check your access\. Reload to try again\.', 'Could not load'\)/);
|
|
303
|
-
assert.match(read('
|
|
315
|
+
assert.match(read('watch.html'), /id="ledger-status" role="status"/, 'the count and a failed page load sit on the ledger itself');
|
|
304
316
|
});
|
|
305
317
|
|
|
306
318
|
test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is told the controls are the Archon\'s', () => {
|
|
@@ -312,10 +324,12 @@ test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is
|
|
|
312
324
|
assert.ok(!/renderRow\(pr|OTBAsk/.test(js), 'no held-PR row or ask card is drawn');
|
|
313
325
|
assert.match(js, /if \(rank !== 'archon'\) \{ showArchonOnly\(\); return; \}/, 'a Metic gets the note, not an empty page');
|
|
314
326
|
assert.match(js, /The Autobongos controls are for the \$\{rankLabel\('archon'\)\}/, 'the rank is named from the pack');
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
assert.match(
|
|
327
|
+
// task 1005049: the panel is Watch's Autobongos tab, and /gate lands there.
|
|
328
|
+
const html = read('watch.html');
|
|
329
|
+
const pane = html.slice(html.indexOf('<div id="pane-autobongos"'));
|
|
330
|
+
assert.match(pane, /id="gate-archon-only"/);
|
|
319
331
|
assert.ok(!/gate-list|hall-ask\.js/.test(html), 'the list mount and the ask-card script are gone');
|
|
320
|
-
assert.match(read('
|
|
332
|
+
assert.match(read('watch.js'), /\{ id: 'autobongos', label: 'Autobongos', panel: 'pane-autobongos' \}/, 'the tab says what it is');
|
|
333
|
+
assert.ok(!/href: '\/gate'/.test(read('shell.js')), 'the sidebar no longer lists it');
|
|
334
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8'), /gate: 'watch#autobongos'/, 'and the old URL lands on the tab');
|
|
321
335
|
});
|
|
@@ -120,8 +120,10 @@ const ORACLE = {
|
|
|
120
120
|
|
|
121
121
|
// Metic+ operator surfaces (the Governance nav group).
|
|
122
122
|
'/builders/watch': 'requireGovernmentPage',
|
|
123
|
-
|
|
124
|
-
'
|
|
123
|
+
// Gone (task 1005049, owner decision 2026-11-03): Sessions and Autobongos are tabs
|
|
124
|
+
// on Watch, behind Watch's gate, and both URLs 302 to their tab before any gate runs.
|
|
125
|
+
'/builders/gate': null,
|
|
126
|
+
'/builders/sessions': null,
|
|
125
127
|
// The five fundamentals of the project (task 1003280, goal 1000072). Same tier as
|
|
126
128
|
// its neighbours: a steering surface, metic+ per ADR 0157. Its two WRITES gate
|
|
127
129
|
// independently on `project.settings.manage`, so this row is only about the shell.
|
|
@@ -360,6 +362,9 @@ const GONE_PAGES = {
|
|
|
360
362
|
'/builders/ranks': 'task 1005046 — the rank ladder is a Primer chapter; /ranks redirects there',
|
|
361
363
|
'/builders/drachmae': 'task 1005046 — credits and karma is a Primer chapter; /drachmae redirects there',
|
|
362
364
|
'/builders/diagrams': 'task 1005046 — each diagram sits in its Primer chapter; /diagrams redirects there',
|
|
365
|
+
// Task 1005049 — one oversight page: each is a tab on Watch, and the old URL 302s to it.
|
|
366
|
+
'/builders/sessions': 'task 1005049 — the session log is Watch\'s Sessions tab; /sessions redirects there',
|
|
367
|
+
'/builders/gate': 'task 1005049 — the runner panel is Watch\'s Autobongos tab; /gate redirects there',
|
|
363
368
|
};
|
|
364
369
|
|
|
365
370
|
test('a row that is null because the page left the hall serves no page', () => {
|
|
@@ -230,12 +230,15 @@ test('sessions: the nowrap + wrapper pair is gone, and with it the sticky-header
|
|
|
230
230
|
const css = decomment(read('sessions.css'));
|
|
231
231
|
assert.doesNotMatch(css, /white-space:\s*nowrap/, 'no blanket nowrap on every cell');
|
|
232
232
|
assert.doesNotMatch(css, /ledger-table-wrap/, 'no wrapper — the shell\'s card strip handles a phone');
|
|
233
|
-
assert.doesNotMatch(
|
|
233
|
+
assert.doesNotMatch(sessionsPane(), /ledger-table-wrap/);
|
|
234
234
|
assert.doesNotMatch(css, /var\(--info\)|ledger-sid/, 'the session id is no longer painted in the info colour');
|
|
235
235
|
noAlias(css, 'sessions.css');
|
|
236
236
|
tokensDefined(css, 'sessions.css');
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// task 1005049: the session ledger is Watch's Sessions tab; its markup is that pane.
|
|
240
|
+
const sessionsPane = () => { const w = read('watch.html'); return w.slice(w.indexOf('<div id="pane-sessions"'), w.indexOf('<div id="pane-autobongos"')); };
|
|
241
|
+
|
|
239
242
|
test('sessions: every id on a row is a door, the earned figure is shown, the totals ride the top strip', () => {
|
|
240
243
|
const src = read('sessions.js');
|
|
241
244
|
assert.match(src, /<a class="fact-chip" href="\/builders\/task\/\$\{encodeURIComponent\(String\(t\)\)\}">task \$\{escapeHtml\(String\(t\)\)\}<\/a>/,
|
|
@@ -244,8 +247,8 @@ test('sessions: every id on a row is a door, the earned figure is shown, the tot
|
|
|
244
247
|
assert.match(src, /<span class="ov-mono" title="\$\{escapeHtml\(s\.session_id\)\}">/, 'the session id is a mono fact with the full id on hover');
|
|
245
248
|
assert.match(src, /fmtInt\(s\.drachmae_earned\)/, 'what the session earned is on the row');
|
|
246
249
|
assert.doesNotMatch(src, /`#\$\{escapeHtml\(t\)\}`/, 'no bare hash-prefixed task text');
|
|
247
|
-
const html =
|
|
248
|
-
assert.match(html, /<section class="ov-summary" id="stats-scroll"/, 'the totals are the top-of-
|
|
250
|
+
const html = sessionsPane();
|
|
251
|
+
assert.match(html, /<section class="ov-summary" id="stats-scroll"/, 'the totals are the top-of-tab strip');
|
|
249
252
|
assert.match(html, /<div class="stat-row" id="stats-body"><\/div>/);
|
|
250
253
|
const ths = (html.match(/<th[ >]/g) || []).length;
|
|
251
254
|
// task 1002516: a tenth column, "Plain speech" — the transcript accessibility
|
|
@@ -259,8 +262,9 @@ test('sessions: every id on a row is a door, the earned figure is shown, the tot
|
|
|
259
262
|
// empty state short by a column, which no screenshot pass would catch.
|
|
260
263
|
assert.equal(ths, 11, 'eleven columns: session, builder, kind, tasks, earned, turns, tokens, tool errors, duration, plain speech, recorded');
|
|
261
264
|
assert.match(src, new RegExp(`colspan="${ths}"`), 'the empty row spans them all');
|
|
262
|
-
const
|
|
263
|
-
|
|
265
|
+
const page = read('watch.html');
|
|
266
|
+
const fam = page.indexOf('/builders/oversight.css');
|
|
267
|
+
assert.ok(fam > 0 && page.indexOf('/builders/sessions.css') > fam, 'watch.html links the family sheet before the sessions sheet');
|
|
264
268
|
});
|
|
265
269
|
|
|
266
270
|
test('sessions: the floor is written from the pack\'s rank label, never a hardcoded rank', () => {
|
|
@@ -268,9 +272,9 @@ test('sessions: the floor is written from the pack\'s rank label, never a hardco
|
|
|
268
272
|
const sealed = src.slice(src.indexOf('function showSealed'), src.indexOf('// ---- paging state'));
|
|
269
273
|
assert.match(sealed, /const floor = rankLabel\('metic'\)/);
|
|
270
274
|
// a failed load names its own heading; the rank floor is the default (task 1004829)
|
|
271
|
-
assert.match(sealed, /\$\('#
|
|
275
|
+
assert.match(sealed, /\$\('#sess-sealed-h'\)\.textContent = heading \|\| `\$\{floor\} and up`/);
|
|
272
276
|
assert.doesNotMatch(decomment(src), /Archons only|Metic rank up|Archon-only/);
|
|
273
|
-
assert.doesNotMatch(
|
|
277
|
+
assert.doesNotMatch(sessionsPane(), /Archon/);
|
|
274
278
|
});
|
|
275
279
|
|
|
276
280
|
// =========================================================================
|
|
@@ -299,8 +303,9 @@ test('the states files exercise the redesign\'s new states', () => {
|
|
|
299
303
|
const goals = JSON.parse(read('goals.states.json'));
|
|
300
304
|
assert.ok(goals.states.filters && goals.states.cards, 'goals: the Filters disclosure and the cards view are measured');
|
|
301
305
|
assert.deepEqual(goals.states.cards.expect.value, [['#f-goal-view', 'cards']]);
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
// task 1005049: the ledger is Watch's Sessions tab, measured as that page's state.
|
|
307
|
+
const watch = JSON.parse(read('watch.states.json'));
|
|
308
|
+
assert.deepEqual(watch.states.sessions.expect.visible, ['#pane-sessions', '#stats-scroll', '#sess-ledger-scroll']);
|
|
304
309
|
});
|
|
305
310
|
|
|
306
311
|
test('the roster draws the circuit divider in CSS, on the first row\'s cell and not the row', () => {
|
|
@@ -19,7 +19,8 @@ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
|
19
19
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
|
|
20
20
|
|
|
21
21
|
const JS = read('sessions.js');
|
|
22
|
-
|
|
22
|
+
// task 1005049: the ledger is Watch's Sessions tab; its markup lives in watch.html.
|
|
23
|
+
const HTML = (() => { const w = read('watch.html'); return w.slice(w.indexOf('<div id="pane-sessions"'), w.indexOf('<div id="pane-autobongos"')); })();
|
|
23
24
|
|
|
24
25
|
// Slice `const ACCESSIBILITY_TITLE = {...}` through the end of accessibilityCell
|
|
25
26
|
// and run it with the one helper it closes over.
|
|
@@ -228,7 +228,8 @@ test('the role axis has exactly one user-facing word, and no surface types it',
|
|
|
228
228
|
'modules/hall-ui/public/work.js',
|
|
229
229
|
'modules/hall-ui/public/goals-page.js',
|
|
230
230
|
'modules/hall-ui/public/profile.js',
|
|
231
|
-
'
|
|
231
|
+
// task 1005076: the setup screen's role step replaced builders.js's checklist picker.
|
|
232
|
+
'modules/hall-ui/public/setup-gate.js',
|
|
232
233
|
];
|
|
233
234
|
for (const p of SURFACES) {
|
|
234
235
|
const s = readSrc(p);
|
|
@@ -266,7 +267,8 @@ test('craft picker options: neither picker hardcodes the craft list any more', (
|
|
|
266
267
|
// silently dropped a module-contributed craft. Whichever picker is edited next
|
|
267
268
|
// must keep reading the server list.
|
|
268
269
|
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
|
269
|
-
|
|
270
|
+
// task 1005076: the second picker is the setup screen's role step (it was the Home checklist's).
|
|
271
|
+
for (const p of ['modules/hall-ui/public/settings.js', 'modules/hall-ui/public/setup-gate.js']) {
|
|
270
272
|
const src = read(p);
|
|
271
273
|
assert.ok(/OTB\.disciplines\(\)/.test(src), `${p} builds its options from OTB.disciplines()`);
|
|
272
274
|
assert.ok(!/\['engineer',\s*'artist',\s*'ideator'\]/.test(src), `${p} no longer hardcodes the craft triple`);
|
|
@@ -95,8 +95,10 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
95
95
|
// task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
|
|
96
96
|
// Market Watch settings nav item and its fuel glyph; task 1005041: the Update pill replaced the
|
|
97
97
|
// core-update banner; task 1005045: the Market Watch settings item left (one sidebar item);
|
|
98
|
-
// task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer)
|
|
99
|
-
|
|
98
|
+
// task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer);
|
|
99
|
+
// task 1005049: Sessions and Autobongos left the sidebar (tabs on Watch);
|
|
100
|
+
// task 1005076: the setup flow's holdForSetup.
|
|
101
|
+
const SHELL_SHA = '248d104831bae0de';
|
|
100
102
|
|
|
101
103
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
102
104
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
|
@@ -322,6 +322,39 @@ test('graduation is engine-only (display signals never gate it)', async () => {
|
|
|
322
322
|
assert.ok(shaped.stages.every((s) => s.cleared === true));
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
+
// ---- the setup flow (task 1005076) -----------------------------------------
|
|
326
|
+
|
|
327
|
+
const SETUP_IDS = ['signed_in', 'cli_setup', 'pick_disciplines', 'first_start'];
|
|
328
|
+
const setupOf = (state, opts) => onboarding.shapeForApi(state, opts).setup;
|
|
329
|
+
const clearedIds = (setup) => setup.steps.filter((s) => s.cleared).map((s) => s.id);
|
|
330
|
+
|
|
331
|
+
test('setup: a brand-new xenos is held, with only signed_in cleared', () => {
|
|
332
|
+
const setup = setupOf(null, { rank: 'xenos' });
|
|
333
|
+
assert.equal(setup.complete, false);
|
|
334
|
+
assert.deepEqual(setup.steps.map((s) => s.id), SETUP_IDS);
|
|
335
|
+
assert.deepEqual(clearedIds(setup), ['signed_in']);
|
|
336
|
+
assert.ok(setup.steps.every((s) => s.label && s.hint), 'every step says what to do');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('setup: each step ticks from its own signal, and all four complete it', () => {
|
|
340
|
+
const state = { current_stage: 'pick_disciplines', completed_stages: ['auth_complete'] };
|
|
341
|
+
assert.deepEqual(clearedIds(setupOf(state, { rank: 'xenos', cliAuthed: true })), ['signed_in', 'cli_setup']);
|
|
342
|
+
const picked = { current_stage: 'first_start', completed_stages: ['auth_complete', 'pick_disciplines'] };
|
|
343
|
+
const almost = setupOf(picked, { rank: 'xenos', cliAuthed: true });
|
|
344
|
+
assert.equal(almost.complete, false, 'first_start not yet run from Claude');
|
|
345
|
+
const started = { current_stage: 'first_claim', completed_stages: ['auth_complete', 'pick_disciplines', 'first_start'] };
|
|
346
|
+
assert.equal(setupOf(started, { rank: 'xenos', cliAuthed: true }).complete, true);
|
|
347
|
+
assert.equal(setupOf(started, { rank: 'xenos', cliAuthed: false }).complete, false, 'no linked machine, not set up');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('setup: anyone already past it goes straight in', () => {
|
|
351
|
+
const fresh = { current_stage: 'auth_complete', completed_stages: [] };
|
|
352
|
+
for (const rank of ['thetes', 'metic', 'archon']) assert.equal(setupOf(fresh, { rank }).complete, true, rank);
|
|
353
|
+
assert.equal(setupOf({ current_stage: 'graduated', completed_stages: [] }, { rank: 'xenos' }).complete, true, 'graduated');
|
|
354
|
+
assert.equal(setupOf({ current_stage: 'ship_three', completed_stages: ['first_claim'] }, { rank: 'xenos' }).complete, true, 'claimed a task');
|
|
355
|
+
assert.equal(setupOf(fresh, {}).complete, false, 'an unknown rank is not a pass');
|
|
356
|
+
});
|
|
357
|
+
|
|
325
358
|
// ---- wrap-up --------------------------------------------------------------
|
|
326
359
|
|
|
327
360
|
Promise.resolve().then(async () => {
|
|
@@ -33,7 +33,8 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
33
33
|
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
34
34
|
const KIT_JS = read('modules/hall-ui/public/hall-kit.js');
|
|
35
35
|
const SESSIONS_JS = read('modules/hall-ui/public/sessions.js');
|
|
36
|
-
|
|
36
|
+
// task 1005049: the ledger is Watch's Sessions tab, so its markup lives in watch.html.
|
|
37
|
+
const SESSIONS_HTML = read('modules/hall-ui/public/watch.html');
|
|
37
38
|
|
|
38
39
|
const { test, summary } = makeRunner();
|
|
39
40
|
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
@@ -147,6 +148,9 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
|
|
|
147
148
|
vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
|
|
148
149
|
if (dropKit) delete sandbox.window.OTBKit;
|
|
149
150
|
vm.runInContext(SESSIONS_JS, sandbox, { filename: 'sessions.js' });
|
|
151
|
+
// The tab boots when Watch first opens it (task 1005049); a second open is a no-op.
|
|
152
|
+
sandbox.window.OTBWatchTabs.sessions();
|
|
153
|
+
sandbox.window.OTBWatchTabs.sessions();
|
|
150
154
|
|
|
151
155
|
const searches = () => requests.filter((u) => u.includes('/sessions/search'));
|
|
152
156
|
const q = (n) => {
|
|
@@ -161,7 +165,7 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
|
|
|
161
165
|
// 1. the page markup no longer carries a pager of its own
|
|
162
166
|
// ---------------------------------------------------------------------------
|
|
163
167
|
|
|
164
|
-
await test('
|
|
168
|
+
await test('the Sessions tab carries a mount, not a hand-rolled pager', () => {
|
|
165
169
|
assert.ok(!/id="ledger-pager"/.test(SESSIONS_HTML), 'the bespoke <nav class="pager"> is gone');
|
|
166
170
|
assert.ok(!/id="pager-(prev|next|pos)"/.test(SESSIONS_HTML), 'its bespoke button ids are gone');
|
|
167
171
|
assert.match(SESSIONS_HTML, /id="sessions-pager-mount"/);
|
|
@@ -307,14 +311,14 @@ await test('a missing kit degrades to a named failure, not a blank page', async
|
|
|
307
311
|
await tick(); await tick();
|
|
308
312
|
assert.equal(h.searches().length, 0, 'no corpus request without a pager to place it');
|
|
309
313
|
// in the page body: the head that used to carry it is hidden (task 1004829)
|
|
310
|
-
assert.match(h.getEl('
|
|
314
|
+
assert.match(h.getEl('sess-sealed-body').textContent, /could not be loaded/);
|
|
311
315
|
});
|
|
312
316
|
|
|
313
317
|
await test('the rank gate still runs before any corpus request', async () => {
|
|
314
318
|
const h = harness({ total: 320, viewerRank: 'xenos' });
|
|
315
319
|
await tick(); await tick();
|
|
316
320
|
assert.equal(h.searches().length, 0, 'a xenos must not even ask');
|
|
317
|
-
assert.equal(h.getEl('
|
|
321
|
+
assert.equal(h.getEl('sess-sealed').hidden, false, 'they get the sealed notice');
|
|
318
322
|
});
|
|
319
323
|
|
|
320
324
|
summary();
|