@bongos/core 1.24.41 → 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.
Files changed (61) hide show
  1. package/.bongos-core.json +80 -65
  2. package/docs/copy-inventory.md +91 -112
  3. package/docs/copy-registry.json +177 -385
  4. package/docs/module-api-changelog.md +2 -0
  5. package/docs/page-readings.json +214 -213
  6. package/modules/hall-ui/public/ai-studio.html +1 -1
  7. package/modules/hall-ui/public/atlas.html +1 -1
  8. package/modules/hall-ui/public/blockers.html +1 -1
  9. package/modules/hall-ui/public/board-room.html +1 -1
  10. package/modules/hall-ui/public/builders.js +17 -262
  11. package/modules/hall-ui/public/city.html +1 -1
  12. package/modules/hall-ui/public/collab.html +1 -1
  13. package/modules/hall-ui/public/commands.html +1 -1
  14. package/modules/hall-ui/public/copy-desk.html +1 -1
  15. package/modules/hall-ui/public/deploy.html +1 -1
  16. package/modules/hall-ui/public/goal-map.html +1 -1
  17. package/modules/hall-ui/public/goals.html +1 -1
  18. package/modules/hall-ui/public/government.html +1 -1
  19. package/modules/hall-ui/public/hall-render.js +3 -248
  20. package/modules/hall-ui/public/hall-widgets.js +3 -14
  21. package/modules/hall-ui/public/idea.html +1 -1
  22. package/modules/hall-ui/public/ideas.html +1 -1
  23. package/modules/hall-ui/public/index.html +3 -4
  24. package/modules/hall-ui/public/market-watch-item.html +1 -1
  25. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  26. package/modules/hall-ui/public/market-watch.html +1 -1
  27. package/modules/hall-ui/public/modules.html +1 -1
  28. package/modules/hall-ui/public/money.html +1 -1
  29. package/modules/hall-ui/public/my-agents.html +1 -1
  30. package/modules/hall-ui/public/primer.html +1 -1
  31. package/modules/hall-ui/public/profile.html +1 -1
  32. package/modules/hall-ui/public/project-settings.html +1 -1
  33. package/modules/hall-ui/public/roadmap.html +1 -1
  34. package/modules/hall-ui/public/roster.html +1 -1
  35. package/modules/hall-ui/public/settings.html +1 -1
  36. package/modules/hall-ui/public/setup-gate.js +156 -0
  37. package/modules/hall-ui/public/shell.js +16 -1
  38. package/modules/hall-ui/public/street.html +1 -1
  39. package/modules/hall-ui/public/studio.html +1 -1
  40. package/modules/hall-ui/public/style.css +85 -117
  41. package/modules/hall-ui/public/task.html +1 -1
  42. package/modules/hall-ui/public/thinking.html +1 -1
  43. package/modules/hall-ui/public/tweak-editor.html +1 -1
  44. package/modules/hall-ui/public/watch.html +1 -1
  45. package/modules/hall-ui/public/work.html +1 -1
  46. package/modules/hall-ui/records/setup-gate.md +43 -0
  47. package/modules/lifecycle/routes/tasks.js +2 -2
  48. package/modules/onboarding/onboarding-state.js +41 -20
  49. package/package-lock.json +2 -2
  50. package/package.json +1 -1
  51. package/release-notes.json +6 -0
  52. package/src/bongos/routes/me.js +2 -2
  53. package/src/module-api.js +1 -1
  54. package/tests/hall_board_world.mjs +1 -1
  55. package/tests/hall_landing_boot.mjs +2 -3
  56. package/tests/hall_onboarding_bongos_setup.mjs +14 -14
  57. package/tests/module_contributions.mjs +4 -2
  58. package/tests/nav_permission_atoms.mjs +3 -2
  59. package/tests/onboarding_state.mjs +33 -0
  60. package/tests/setup_flow.mjs +242 -0
  61. package/tests/welcome_boot.mjs +3 -2
@@ -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 welcome panel's markup, from the setupSteps literal to the panel's innerHTML.
18
- function welcomeMarkup() {
19
- const src = read('modules/hall-ui/public/hall-render.js');
20
- const start = src.indexOf('const setupSteps = `');
21
- const end = src.indexOf('body.innerHTML = `', start);
22
- assert.ok(start > 0 && end > start, 'welcome panel markup located');
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('welcome panel: the steps and the clone block say npx bongos setup', () => {
27
- const w = welcomeMarkup();
28
- assert.match(w, /Run <code>npx bongos setup<\/code>/);
29
- assert.match(w, /\\nnpm install\\nnpx bongos setup`/, 'clone block ends npm install + npx bongos setup');
30
- assert.doesNotMatch(w, OLD, 'no legacy setup command on the welcome panel');
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 = /\.setup__getcode \.welcome__paste\s*\{([^}]*)\}/.exec(css);
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('welcome intro and checklist lead ask only for what a builder needs', () => {
70
- const src = read('modules/hall-ui/public/hall-render.js');
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');
@@ -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
- 'modules/hall-ui/public/builders.js',
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
- for (const p of ['modules/hall-ui/public/settings.js', 'modules/hall-ui/public/hall-render.js']) {
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`);
@@ -96,8 +96,9 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
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
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
- const SHELL_SHA = 'bb0abbadab2508ca';
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';
101
102
 
102
103
  test('shell.js has not changed without its cache-bust being bumped', () => {
103
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 () => {
@@ -0,0 +1,242 @@
1
+ // tests/setup_flow.mjs — the setup flow a new builder finishes once (task 1005076).
2
+ //
3
+ // Owner, 2026-11-05: a new builder is held on one setup screen until every step is done,
4
+ // and once set up they are in for good — no setup banner on Home. Three halves, each run
5
+ // for real: the claimable route ticks first_start only for a Claude (CLI) session; the
6
+ // shell loads setup-gate.js and draws nothing else over it while setup is incomplete; and
7
+ // the screen draws the steps, saves a craft, and lets the builder in once /me says done.
8
+ // The rule itself (which steps, who goes straight in) is in tests/onboarding_state.mjs.
9
+ //
10
+ // Run: node tests/setup_flow.mjs
11
+
12
+ import { strict as assert } from 'node:assert';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import vm from 'node:vm';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { makeEl, realOTB, settle } from './helpers.mjs';
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
21
+ const ROUTE = read('modules/lifecycle/routes/tasks.js');
22
+ const SHELL = read('modules/hall-ui/public/shell.js');
23
+ const GATE = read('modules/hall-ui/public/setup-gate.js');
24
+
25
+ let passed = 0;
26
+ let failed = 0;
27
+ const t = async (name, fn) => {
28
+ try { await fn(); passed += 1; console.log(` PASS ${name}`); }
29
+ catch (e) { failed += 1; console.log(` FAIL ${name}\n ${e.message}`); }
30
+ };
31
+
32
+ const STEPS = (cleared) => ['signed_in', 'cli_setup', 'pick_disciplines', 'first_start']
33
+ .map((id) => ({ id, label: `Label ${id}`, hint: `Hint ${id}`, cleared: cleared.includes(id) }));
34
+
35
+ console.log('the setup flow (task 1005076)');
36
+
37
+ // ---- the claimable route: first_start only from Claude --------------------
38
+
39
+ // The real guarded statement, run against each kind of session.
40
+ const from = ROUTE.indexOf("if (req.gdsSession && req.gdsSession.source === 'cli')");
41
+ const guard = ROUTE.slice(from, ROUTE.indexOf('\n', from));
42
+ const runGuard = (gdsSession) => {
43
+ const marks = [];
44
+ const seams = { resolveOptional: () => ({ safeMarkStage: (_pool, id, stage) => marks.push([id, stage]) }) };
45
+ vm.runInNewContext(guard, { req: { gdsSession, builder: { id: 7 } }, seams, pool: {} });
46
+ return marks;
47
+ };
48
+
49
+ await t('GET /tasks/claimable marks first_start for a Claude (CLI) session, never for the browser', () => {
50
+ assert.ok(from > 0, 'the guard is in the claimable handler');
51
+ assert.match(guard, /safeMarkStage\(pool, req\.builder\.id, 'first_start', req\.builder\)/);
52
+ assert.deepEqual(runGuard({ source: 'cli' }), [[7, 'first_start']]);
53
+ assert.deepEqual(runGuard({ source: 'web' }), [], 'the hall pages read this feed from the browser');
54
+ assert.deepEqual(runGuard(null), []);
55
+ assert.equal((ROUTE.match(/'first_start'/g) || []).length, 1, 'no unguarded mark elsewhere in the routes');
56
+ });
57
+
58
+ // ---- the screen ------------------------------------------------------------
59
+
60
+ function bootGate({ patchOk = true } = {}) {
61
+ const body = makeEl('body');
62
+ const htmlClasses = new Set();
63
+ const requests = [];
64
+ let reloaded = 0;
65
+ let me = null;
66
+ const api = { request: (method, p, opts) => {
67
+ requests.push({ method, p, body: opts && opts.body });
68
+ if (method === 'PATCH') return Promise.resolve({ ok: patchOk, status: patchOk ? 200 : 422, data: {} });
69
+ return Promise.resolve({ ok: true, status: 200, data: me });
70
+ } };
71
+ const intervals = [];
72
+ const document = {
73
+ body,
74
+ documentElement: { classList: { add: (c) => htmlClasses.add(c), remove: (c) => htmlClasses.delete(c) } },
75
+ createElement: (tag) => makeEl(tag),
76
+ getElementById: (id) => body.querySelector('#' + id),
77
+ activeElement: null,
78
+ };
79
+ // the crafts this project offers (dom-utils.js reads them off __MODULES__)
80
+ const window = { OTB: { ...realOTB(), disciplines: () => ['engineer', 'artist', 'ui'] }, location: { reload: () => { reloaded += 1; } } };
81
+ const sandbox = { window, document, setInterval: (fn) => { intervals.push(fn); return intervals.length; }, clearInterval() {} };
82
+ vm.createContext(sandbox);
83
+ vm.runInContext(GATE, sandbox, { filename: 'setup-gate.js' });
84
+ return {
85
+ show: (setup, builder) => window.OTBSetupGate.show(setup, { api, builder }),
86
+ gate: () => body.querySelector('#setup-gate'),
87
+ htmlClasses, requests, intervals,
88
+ setMe: (v) => { me = v; },
89
+ reloaded: () => reloaded,
90
+ click: (target) => body.querySelector('#setup-gate').fire('click', { target }),
91
+ };
92
+ }
93
+
94
+ await t('the screen lists every step in order, ticks the cleared ones, and opens the first gap', () => {
95
+ const g = bootGate();
96
+ g.show({ complete: false, steps: STEPS(['signed_in', 'cli_setup']) }, { display_name: 'Sam' });
97
+ const gate = g.gate();
98
+ assert.ok(gate, 'drawn');
99
+ assert.equal(gate.getAttribute('aria-modal'), 'true');
100
+ assert.ok(g.htmlClasses.has('setup-gate-open'), 'the hall behind it does not scroll');
101
+ assert.match(gate.querySelector('#setup-gate-h').textContent, /Welcome, Sam/);
102
+ assert.equal(gate.querySelector('.setup-gate__count').textContent, '2 of 4 done');
103
+ const steps = gate.querySelectorAll('.setup-gate__step');
104
+ assert.deepEqual(steps.map((s) => s.className), ['setup-gate__step is-done', 'setup-gate__step is-done', 'setup-gate__step is-current', 'setup-gate__step']);
105
+ assert.match(steps[2].textContent, /Hint pick_disciplines/, 'only the current step shows its hint');
106
+ assert.doesNotMatch(steps[3].textContent, /Hint first_start/);
107
+ assert.deepEqual(gate.querySelectorAll('[data-craft]').map((b) => b.textContent), ['Engineer', 'Artist', 'UI'],
108
+ 'the crafts this project offers');
109
+ });
110
+
111
+ await t('choosing a craft saves it through PATCH /me/disciplines, the route every picker uses', async () => {
112
+ const g = bootGate();
113
+ g.show({ complete: false, steps: STEPS(['signed_in', 'cli_setup']) });
114
+ const save = () => g.gate().querySelector('[data-save-crafts]');
115
+ assert.equal(save().disabled, true, 'nothing picked, nothing to save');
116
+ g.click(g.gate().querySelector('[data-craft="artist"]'));
117
+ assert.equal(g.gate().querySelector('[data-craft="artist"]').getAttribute('aria-pressed'), 'true');
118
+ assert.equal(save().disabled, false);
119
+ g.setMe({ onboarding: { setup: { complete: false, steps: STEPS(['signed_in', 'cli_setup', 'pick_disciplines']) } } });
120
+ g.click(save());
121
+ await settle(); await settle();
122
+ const patch = g.requests.find((r) => r.method === 'PATCH');
123
+ assert.equal(patch.p, '/api/bongos/me/disciplines');
124
+ assert.deepEqual(JSON.parse(JSON.stringify(patch.body)), { preferred_disciplines: ['artist'] });
125
+ assert.equal(g.gate().querySelector('.setup-gate__count').textContent, '3 of 4 done', 're-read /me and ticked');
126
+ });
127
+
128
+ await t('a craft that does not save says so and can be tried again', async () => {
129
+ const g = bootGate({ patchOk: false });
130
+ g.show({ complete: false, steps: STEPS(['signed_in', 'cli_setup']) });
131
+ g.click(g.gate().querySelector('[data-craft="engineer"]'));
132
+ g.click(g.gate().querySelector('[data-save-crafts]'));
133
+ await settle();
134
+ const err = g.gate().querySelector('[data-craft-err]');
135
+ assert.equal(err.hidden, false);
136
+ assert.match(err.textContent, /did not save/);
137
+ assert.equal(g.gate().querySelector('[data-save-crafts]').disabled, false);
138
+ });
139
+
140
+ await t('it checks /me while it is up, and lets the builder in the moment setup is complete', async () => {
141
+ const g = bootGate();
142
+ g.show({ complete: false, steps: STEPS(['signed_in', 'cli_setup', 'pick_disciplines']) });
143
+ assert.equal(g.intervals.length, 1, 'it polls');
144
+ g.setMe({ onboarding: { setup: { complete: true, steps: STEPS(['signed_in', 'cli_setup', 'pick_disciplines', 'first_start']) } } });
145
+ g.intervals[0]();
146
+ await settle();
147
+ assert.equal(g.gate(), null, 'the screen is gone');
148
+ assert.ok(!g.htmlClasses.has('setup-gate-open'));
149
+ assert.equal(g.reloaded(), 1, 'the page reloads into the hall');
150
+ });
151
+
152
+ await t('a complete setup draws nothing', () => {
153
+ const g = bootGate();
154
+ g.show({ complete: true, steps: STEPS(['signed_in', 'cli_setup', 'pick_disciplines', 'first_start']) });
155
+ assert.equal(g.gate(), null);
156
+ });
157
+
158
+ // ---- the shell: loads the screen, and nothing else is drawn over it ---------
159
+
160
+ function bootShell({ deferLoad = false } = {}) {
161
+ const loaded = [];
162
+ const pending = [];
163
+ const asked = [];
164
+ let sandbox = null;
165
+ const stubEl = () => ({
166
+ dataset: {}, style: {}, textContent: '', hidden: false, innerHTML: '',
167
+ classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
168
+ setAttribute() {}, getAttribute() { return null; }, removeAttribute() {}, addEventListener() {},
169
+ querySelector() { return null; }, querySelectorAll() { return []; }, appendChild() {}, focus() {},
170
+ });
171
+ const els = new Map();
172
+ const document = {
173
+ body: { dataset: { page: 'work' }, appendChild() {} },
174
+ documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
175
+ getElementById(id) { if (!els.has(id)) els.set(id, stubEl()); return els.get(id); },
176
+ querySelectorAll() { return []; }, createElement: () => stubEl(),
177
+ addEventListener() {}, dispatchEvent() {}, get cookie() { return ''; }, set cookie(_v) {},
178
+ head: { appendChild(tag) {
179
+ loaded.push(tag.src);
180
+ const land = () => {
181
+ if (/setup-gate/.test(tag.src)) sandbox.window.OTBSetupGate = { show: (setup) => asked.push(['gate', setup.complete]) };
182
+ if (tag.onload) tag.onload();
183
+ };
184
+ if (deferLoad && /setup-gate/.test(tag.src)) pending.push(land); else land();
185
+ } },
186
+ };
187
+ const location = { hostname: 'h', origin: 'https://h', pathname: '/work', hash: '', assign() {}, replace() {} };
188
+ const window = {
189
+ OTB: realOTB(), __BRANDING__: {}, __MODULES__: { enabled: {} },
190
+ BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
191
+ // the boot pass runs before anyone is signed in, when neither draws anything: count signed-in asks only
192
+ OTBWelcome: { onAccess: (access) => { if (access.authed === true) asked.push(['welcome']); return false; } },
193
+ OTBCraftMode: { onAccess: (access) => { if (access.authed === true) asked.push(['craft']); }, applyNav() {}, mount() {}, setPositions() {}, switchHtml: () => '' },
194
+ matchMedia: () => ({ matches: false, addEventListener() {} }),
195
+ localStorage: { getItem: () => null, setItem() {} },
196
+ requestAnimationFrame() {}, scrollTo() {}, location, addEventListener() {},
197
+ };
198
+ sandbox = { window, document, location, console, setTimeout, clearTimeout, queueMicrotask,
199
+ CustomEvent: class { constructor(tp, i) { this.type = tp; Object.assign(this, i); } } };
200
+ sandbox.globalThis = sandbox;
201
+ vm.createContext(sandbox);
202
+ vm.runInContext(SHELL, sandbox, { filename: 'shell.js' });
203
+ return { apply: (state) => window.OTBShell.applyAccess(state), land: () => pending.splice(0).forEach((f) => f()), loaded, asked };
204
+ }
205
+
206
+ await t('the shell holds an unfinished builder on the setup screen, with no welcome or craft picker over it', () => {
207
+ const s = bootShell();
208
+ s.apply({ authed: true, rank: 'xenos', builder: { id: 9 }, setup: { complete: false, steps: STEPS(['signed_in']) } });
209
+ assert.equal(s.loaded.filter((u) => /^\/builders\/setup-gate\.js/.test(u)).length, 1, 'its file is loaded');
210
+ assert.deepEqual(s.asked, [['gate', false]], 'the screen, and nothing over it');
211
+ });
212
+
213
+ await t('the setup screen loads once, however often the shell re-checks access before it lands', () => {
214
+ const s = bootShell({ deferLoad: true });
215
+ const held = { authed: true, rank: 'xenos', builder: { id: 9 }, setup: { complete: false, steps: STEPS(['signed_in']) } };
216
+ s.apply(held);
217
+ s.apply(held);
218
+ s.apply(held);
219
+ assert.equal(s.loaded.filter((u) => /setup-gate\.js/.test(u)).length, 1, 'one script tag, so one screen');
220
+ s.land();
221
+ assert.deepEqual(s.asked, [['gate', false]]);
222
+ });
223
+
224
+ await t('a set-up builder loads no setup file and gets the hall as before', () => {
225
+ const s = bootShell();
226
+ s.apply({ authed: true, rank: 'xenos', builder: { id: 9 }, setup: { complete: true, steps: [] } });
227
+ assert.equal(s.loaded.filter((u) => /setup-gate/.test(u)).length, 0);
228
+ assert.ok(!s.asked.some(([k]) => k === 'gate'), 'no setup screen');
229
+ assert.deepEqual(s.asked.slice(0, 2), [['welcome'], ['craft']], 'the daily welcome and craft picker as before');
230
+ const unknown = bootShell();
231
+ unknown.apply({ authed: true, rank: 'xenos', builder: { id: 9 }, setup: null });
232
+ assert.equal(unknown.loaded.filter((u) => /setup-gate/.test(u)).length, 0, 'an older server that sends no setup holds nobody');
233
+ });
234
+
235
+ await t('Home carries no setup banner: the Welcome and Getting started panels are gone', () => {
236
+ const widgets = read('modules/hall-ui/public/hall-widgets.js');
237
+ assert.doesNotMatch(widgets, /w\('welcome-scroll'|w\('onboarding-scroll'/);
238
+ assert.doesNotMatch(read('modules/hall-ui/public/index.html'), /welcome-reopen|Show the setup guide again/);
239
+ });
240
+
241
+ console.log(`\n${passed} passed, ${failed} failed`);
242
+ if (failed) process.exit(1);
@@ -122,6 +122,7 @@ test('Home loads the welcome before the shell, and the shell asks it before the
122
122
  'welcome-boot.js, then the shell trio');
123
123
  assert.ok(at('welcome-boot.css') > 0, 'and its sheet');
124
124
  const shell = fs.readFileSync(path.join(HALL, 'shell.js'), 'utf8');
125
- assert.match(shell, /if \(!\(window\.OTBWelcome && window\.OTBWelcome\.onAccess\(access, state && state\.builder\)\) && CRAFT\) CRAFT\.onAccess\(/,
126
- 'the picker is asked only when the welcome did not show');
125
+ // task 1005076: and neither is asked while the setup screen holds the builder (tests/setup_flow.mjs).
126
+ assert.match(shell, /if \(!holdForSetup\(state && state\.builder\) && !\(window\.OTBWelcome && window\.OTBWelcome\.onAccess\(access, state && state\.builder\)\) && CRAFT\) CRAFT\.onAccess\(/,
127
+ 'the picker is asked only when setup is done and the welcome did not show');
127
128
  });