@bongos/core 1.21.27 → 1.21.29

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 (66) hide show
  1. package/.bongos-core.json +89 -69
  2. package/docs/api/openapi.json +1 -1
  3. package/docs/api-reference.md +1 -1
  4. package/docs/copy-inventory.md +399 -391
  5. package/docs/copy-registry.json +532 -460
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/page-inventory.json +68 -0
  8. package/docs/page-readings.json +3737 -3587
  9. package/modules/hall-ui/public/atlas.html +3 -1
  10. package/modules/hall-ui/public/blockers.html +3 -1
  11. package/modules/hall-ui/public/board-room.html +3 -1
  12. package/modules/hall-ui/public/city.html +3 -1
  13. package/modules/hall-ui/public/collab.html +3 -1
  14. package/modules/hall-ui/public/commands.html +3 -1
  15. package/modules/hall-ui/public/copy-desk.html +3 -1
  16. package/modules/hall-ui/public/craft-mode.js +297 -0
  17. package/modules/hall-ui/public/deploy.html +3 -1
  18. package/modules/hall-ui/public/diagrams.html +3 -1
  19. package/modules/hall-ui/public/drachmae.html +3 -1
  20. package/modules/hall-ui/public/fleet.html +3 -1
  21. package/modules/hall-ui/public/gate.html +3 -1
  22. package/modules/hall-ui/public/goal-map.html +3 -1
  23. package/modules/hall-ui/public/goals.html +3 -1
  24. package/modules/hall-ui/public/government.html +3 -1
  25. package/modules/hall-ui/public/idea.html +3 -1
  26. package/modules/hall-ui/public/ideas.html +3 -1
  27. package/modules/hall-ui/public/index.html +3 -1
  28. package/modules/hall-ui/public/modules.html +3 -1
  29. package/modules/hall-ui/public/primer.html +3 -1
  30. package/modules/hall-ui/public/profile.html +3 -1
  31. package/modules/hall-ui/public/project-settings.html +3 -1
  32. package/modules/hall-ui/public/ranks.html +3 -1
  33. package/modules/hall-ui/public/roadmap.html +3 -1
  34. package/modules/hall-ui/public/roster.html +3 -1
  35. package/modules/hall-ui/public/sessions.html +3 -1
  36. package/modules/hall-ui/public/settings.html +3 -1
  37. package/modules/hall-ui/public/shell-trail.js +186 -0
  38. package/modules/hall-ui/public/shell.js +17 -172
  39. package/modules/hall-ui/public/street.html +3 -1
  40. package/modules/hall-ui/public/studio.html +3 -1
  41. package/modules/hall-ui/public/style.css +228 -0
  42. package/modules/hall-ui/public/task.html +3 -1
  43. package/modules/hall-ui/public/thinking.html +3 -1
  44. package/modules/hall-ui/public/tweak-editor.html +3 -1
  45. package/modules/hall-ui/public/watch.html +3 -1
  46. package/modules/hall-ui/public/work.html +3 -1
  47. package/modules/hall-ui/records/craft-mode.md +22 -0
  48. package/modules/hall-ui/records/hall-navigation.md +1 -1
  49. package/modules/provisioning/routes/callback-page.js +28 -65
  50. package/modules/provisioning/routes/provisioning.js +6 -6
  51. package/modules/public-landing/public/projects.html +68 -3
  52. package/package-lock.json +2 -2
  53. package/package.json +1 -1
  54. package/release-notes.json +12 -0
  55. package/src/module-api.js +1 -1
  56. package/tests/craft_mode.mjs +267 -112
  57. package/tests/goal_map_page.mjs +2 -2
  58. package/tests/hall_city.mjs +1 -1
  59. package/tests/hall_fullscreen.mjs +3 -0
  60. package/tests/hall_street.mjs +1 -1
  61. package/tests/hall_tweak_editor.mjs +1 -1
  62. package/tests/nav_permission_atoms.mjs +1 -1
  63. package/tests/provisioning_callback_page.mjs +5 -5
  64. package/tests/provisioning_github_app_routes.mjs +24 -25
  65. package/tests/wizard_auto_open.mjs +94 -0
  66. package/tests/wizard_intent_resume.mjs +17 -2
@@ -431,9 +431,9 @@ await test('the page is the sky\'s own surface — the same sheet and the same s
431
431
  assert.match(html, /<main id="hall-main" class="sky-stage">/, 'the sky is drawn in the stage, the skip link\'s target');
432
432
  const scripts = (h) => [...h.matchAll(/<script src="\/builders\/([^"?]+)\?v=([^"]+)"/g)].map((m) => [m[1], m[2]]);
433
433
  // criterion-uat-ui.js (task 1004402): the rings' UAT state words, shared with the goal page.
434
- assert.deepEqual(scripts(html).filter((s) => s[0] !== 'shell.js').map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
434
+ assert.deepEqual(scripts(html).filter((s) => !['shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0])).map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
435
435
  // Both doors load the shell (tasks 1004327, 1004430); the SKY's files are the same.
436
- const skyScripts = (h) => scripts(h).filter((s) => s[0] !== 'shell.js');
436
+ const skyScripts = (h) => scripts(h).filter((s) => !['shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0]));
437
437
  assert.deepEqual(skyScripts(html), skyScripts(thinking), 'the two doors load the same sky files at the same stamp');
438
438
  assert.equal(html.match(/thinking\.css\?v=([^"]+)/)[1], thinking.match(/thinking\.css\?v=([^"]+)/)[1]);
439
439
  assert.match(read('thinking.css'), /body\[data-page\] \.sky-stage \{/, 'the sheet dresses the one stage both doors draw in inside the hall');
@@ -253,7 +253,7 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
253
253
  for (const id of ['app-sidebar', 'app-scrim', 'app-topbar']) assert.match(html, new RegExp(`id="${id}"`), id);
254
254
  assert.match(html, /<main id="hall-main" class="city-main">\s*<div class="city-top">/, 'the stage is the one <main>, the strip inside it');
255
255
  const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
256
- assert.deepEqual(scripts, ['shell', 'palette', 'city-draw', 'city'], 'the shell first, as on every hall page');
256
+ assert.deepEqual(scripts, ['shell-trail', 'craft-mode', 'shell', 'palette', 'city-draw', 'city'], 'the shell first, as on every hall page');
257
257
  const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
258
258
  assert.match(css, /\.city-main \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
259
259
  assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
@@ -22,6 +22,8 @@ import { fileURLToPath } from 'node:url';
22
22
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
23
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
24
24
  const SHELL = fs.readFileSync(path.join(HALL, 'shell.js'), 'utf8');
25
+ // task 1004492: the way back lives in shell-trail.js, which every page loads first.
26
+ const TRAIL = fs.readFileSync(path.join(HALL, 'shell-trail.js'), 'utf8');
25
27
  const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
26
28
 
27
29
  // Boot the shell once — one page load. `store` is the browser's localStorage and
@@ -121,6 +123,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
121
123
  };
122
124
  sandbox.globalThis = sandbox;
123
125
  vm.createContext(sandbox);
126
+ vm.runInContext(TRAIL, sandbox, { filename: 'shell-trail.js' });
124
127
  vm.runInContext(SHELL, sandbox, { filename: 'shell.js' });
125
128
  const root = documentObj.documentElement;
126
129
  return {
@@ -376,7 +376,7 @@ test('the page holds the prototype\'s header, the street, the readings and the c
376
376
  assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
377
377
  for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
378
378
  const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
379
- assert.deepEqual(scripts, ['shell', 'palette', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-amend', 'street'], 'the hall shell (task 1004544), the shared kit, then the card (task 1004528), the interiors (task 1004526) and the drafted amendment (task 1004530)');
379
+ assert.deepEqual(scripts, ['shell-trail', 'craft-mode', 'shell', 'palette', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-amend', 'street'], 'the hall shell (task 1004544), the shared kit, then the card (task 1004528), the interiors (task 1004526) and the drafted amendment (task 1004530)');
380
380
  assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
381
381
  });
382
382
 
@@ -635,7 +635,7 @@ try {
635
635
  assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
636
636
  assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
637
637
  assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
638
- assert.match(HTML, /shell\.js\?v=2026-10-02-trail/);
638
+ assert.match(HTML, /shell\.js\?v=2026-10-02-craft/);
639
639
  assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
640
640
  for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
641
641
  assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = '901fc3252b096ce6';
90
+ const SHELL_SHA = '8932abeda77f250a';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -38,7 +38,7 @@ test('esc renders an absent value as the empty string, never "null"/"undefined"'
38
38
  assert.equal(esc(false), 'false');
39
39
  });
40
40
 
41
- test('the progress variant escapes the title + domain it interpolates', () => {
41
+ test('the hand-back variant escapes the title + domain it interpolates', () => {
42
42
  const html = callbackPage({
43
43
  ok: true,
44
44
  title: 'Setting up <your> instance',
@@ -49,9 +49,9 @@ test('the progress variant escapes the title + domain it interpolates', () => {
49
49
  assert.match(html, /evil&quot;\.example\.com/, 'the domain rides through esc');
50
50
  assert.doesNotMatch(html, /Setting up <your> instance/, 'no raw title anywhere on the page');
51
51
  assert.doesNotMatch(html, /https:\/\/evil"\.example\.com/, 'no unescaped domain in an href');
52
- // it IS the progress variant: the stepper + the config block the client script reads
53
- assert.match(html, /id="steps"/);
54
- assert.match(html, /__CB_SETUP__=\{"state":"GOODSTATE","domain":"evil\\"\.example\.com"\}/);
52
+ // it IS the hand-back variant (task 1004548): the way back + the config block the client script reads
53
+ assert.match(html, /id="back" href="\/projects\?view=new"/);
54
+ assert.match(html, /__CB_SETUP__=\{"state":"GOODSTATE","domain":"evil\\"\.example\.com","back":"\/projects\?view=new"\}/);
55
55
  });
56
56
 
57
57
  test('the message variant escapes the title but passes server-authored body HTML through', () => {
@@ -63,7 +63,7 @@ test('the message variant escapes the title but passes server-authored body HTML
63
63
  assert.match(html, /<h1>This setup link has &lt;expired&gt;<\/h1>/);
64
64
  assert.match(html, /<div class="sub"><p>Ask the owner for a fresh link\.<\/p><\/div>/,
65
65
  'body is trusted server-authored HTML — the route composes it, a request never does');
66
- assert.doesNotMatch(html, /id="steps"/, 'no stepper without ok + state + domain');
66
+ assert.doesNotMatch(html, /id="back"/, 'no hand-back without ok + state + domain');
67
67
  assert.match(html, /badge-warn">⚠️</, 'a failure wears the warning badge');
68
68
  });
69
69
 
@@ -93,44 +93,43 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
93
93
  {
94
94
  const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
95
95
  const html = await res.text();
96
- t('good state → 200 text/html progress page wired to auto-open', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /Setting up your instance/.test(html) && /Placing credentials/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
96
+ t('good state → 200 text/html hand-back page', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /GitHub app created/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
97
97
  t('the page names the instance host', /mercury\.example\.com/.test(html));
98
- // task 1003236: a same-tab return (pop-up blocked → form target=_self) must never be
99
- // navigated away from the checklist. The page carries a hidden way back, and the
100
- // countdown sits behind the pop-up guard.
101
- t('carries a hidden "finish your remaining setup steps" link back to the wizard', /<p class="muted hidden" id="back"><a href="\/projects\?view=new">Finish your remaining setup steps/.test(html));
102
- t('the auto-open countdown is reachable only past the pop-up guard', /var popup=!!window\.opener\|\|window\.name==='ghAppSetup';/.test(html) && /if\(!popup\)\{[^}]*return;\}\s*var n=4;\(function tick\(\)\{if\(n<=0\)\{location\.href=target;/.test(html) && (html.match(/location\.href=target/g) || []).length === 1);
98
+ // task 1004548: the founder no longer waits here — the page hands them back to the
99
+ // checklist, which shows the wait. A manual way back is always on the page.
100
+ t('carries a plain "back to my checklist" link (works without script)', /<a class="btn" id="back" href="\/projects\?view=new">Back to my checklist/.test(html));
101
+ t('no progress stepper or countdown left on the page', !/id="steps"/.test(html) && !/Placing credentials/.test(html) && !/location\.href=target/.test(html));
103
102
 
104
103
  // Run the served script itself (not a copy) against a minimal DOM, once as the
105
- // wizard's pop-up and once as a same-tab return, and watch what it does to location.
104
+ // wizard's pop-up and once as a same-tab return, and watch what it does.
106
105
  const script = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
107
- t('the progress page ships the client script', typeof script === 'string' && script.length > 200);
108
- async function runCallback({ opener, name }) {
106
+ t('the hand-back page ships the client script', typeof script === 'string' && script.length > 100);
107
+ async function runCallback({ opener }) {
109
108
  const vm = await import('node:vm');
110
- const el = () => { const cls = new Set(); return { textContent: '', classList: { add: (...c) => c.forEach((x) => cls.add(x)), remove: (...c) => c.forEach((x) => cls.delete(x)), contains: (c) => cls.has(c) }, querySelector: () => el() }; };
111
- const els = { steps: el(), status: el(), cta: el(), go: el(), foot: el(), back: el() };
112
- els.steps.querySelector = () => el();
113
- els.back.classList.add('hidden'); els.cta.classList.add('hidden');
109
+ const back = { href: '/projects?view=new' };
114
110
  const timers = [];
111
+ const calls = { close: 0, focusOpener: 0, replaced: null };
115
112
  const sandbox = {
116
- __CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com' },
117
- opener, name,
118
- document: { getElementById: (id) => els[id] || null },
119
- location: { pathname: '/api/bongos/provisioning/github-app/callback', href: 'about:callback' },
120
- fetch: async () => ({ ok: true, json: async () => ({ ok: true, ready: true, status: 'done' }) }),
113
+ __CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com', back: '/projects?view=new' },
114
+ opener: opener ? { closed: false, focus: () => { calls.focusOpener++; } } : null,
115
+ close: () => { calls.close++; },
116
+ document: { getElementById: (id) => (id === 'back' ? back : null) },
117
+ location: { replace: (u) => { calls.replaced = u; } },
121
118
  setTimeout: (fn) => { timers.push(fn); return timers.length; },
122
119
  encodeURIComponent,
123
120
  };
124
121
  sandbox.window = sandbox;
125
122
  vm.runInNewContext(script, sandbox);
126
- for (let i = 0; i < 5; i++) await new Promise((r) => setImmediate(r)); // let the poll's promise chain settle
127
- for (let guard = 0; guard < 10 && timers.length; guard++) { const fn = timers.shift(); fn(); }
128
- return { href: sandbox.location.href, go: els.go.textContent, backHidden: els.back.classList.contains('hidden'), foot: els.foot.textContent };
123
+ const replacedBeforeTimer = calls.replaced;
124
+ while (timers.length) timers.shift()();
125
+ return { ...calls, replacedBeforeTimer, backHref: back.href };
129
126
  }
130
- const popup = await runCallback({ opener: {}, name: 'ghAppSetup' });
131
- t('as the wizard’s pop-up it still counts down and opens the instance', popup.href === 'https://mercury.example.com', JSON.stringify(popup));
132
- const sameTab = await runCallback({ opener: null, name: '' });
133
- t('as a same-tab return it stays put: door shown, way back revealed, no navigation', sameTab.href === 'about:callback' && sameTab.go === 'Open mercury.example.com →' && sameTab.backHidden === false && /checklist is waiting/.test(sameTab.foot), JSON.stringify(sameTab));
127
+ const want = '/projects?view=new#ghstate=GOODSTATE';
128
+ const popup = await runCallback({ opener: true });
129
+ t('as the wizard’s pop-up it focuses the checklist tab and closes itself', popup.focusOpener === 1 && popup.close === 1 && popup.replacedBeforeTimer === null, JSON.stringify(popup));
130
+ t('…and if the browser refuses the close, the pop-up becomes the checklist', popup.replaced === want, JSON.stringify(popup));
131
+ const sameTab = await runCallback({ opener: false });
132
+ t('as a same-tab return it goes straight back to the checklist, carrying the attempt in the fragment', sameTab.close === 0 && sameTab.replaced === want && sameTab.backHref === want, JSON.stringify(sameTab));
134
133
  }
135
134
  {
136
135
  const res = await fetch(`${base}/provisioning/github-app/callback?code=X&state=NOPE`);
@@ -0,0 +1,94 @@
1
+ // tests/wizard_auto_open.mjs — the checklist's "Open my project automatically when it's
2
+ // ready" tick (task 1004548).
3
+ //
4
+ // WHY. The GitHub-App callback page used to hold the founder on its own stepper and open
5
+ // the instance itself. Since task 1004548 it hands them straight back to the checklist,
6
+ // so the auto-open moved there. The rule it must keep: open ONCE, only when sign-in is
7
+ // wired AND the project answers, only after a wait this page actually watched (so a
8
+ // revisit to a long-finished checklist never jumps away), never when the founder
9
+ // unticked it — and prefer a new tab, falling back to this one when the browser blocks
10
+ // an unprompted tab.
11
+ //
12
+ // Executed, not read: the real AUTO_OPEN_KEY…maybeAutoOpen region is sliced out of the
13
+ // page's inline script and run in a node:vm under stubs (the vm-slice idiom of
14
+ // tests/wizard_intent_resume.mjs). The slice boundaries are asserted, so a rename fails
15
+ // loudly instead of testing nothing.
16
+ //
17
+ // Run: node --test tests/wizard_auto_open.mjs
18
+
19
+ import assert from 'node:assert/strict';
20
+ import { test } from 'node:test';
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import vm from 'node:vm';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
28
+ const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
29
+ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
30
+
31
+ const START = SCRIPT.indexOf(" var AUTO_OPEN_KEY = 'cb-open-when-ready';");
32
+ const END = SCRIPT.indexOf(' /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow', START);
33
+ const SLICE = SCRIPT.slice(START, END);
34
+
35
+ test('the auto-open region is still findable (a rename must fail loudly)', () => {
36
+ assert.ok(START > 0 && END > START, 'both slice boundaries found');
37
+ assert.match(SLICE, /function maybeAutoOpen\(\)/);
38
+ assert.match(SLICE, /function autoOpenTick\(\)/);
39
+ });
40
+
41
+ const READY = { manifest: 'done', instStatus: 'active', addrReady: true };
42
+
43
+ function run({ live = READY, armed = true, pref = null, popupBlocked = false, ticks = 1 } = {}) {
44
+ const store = new Map();
45
+ if (pref !== null) store.set('cb-open-when-ready', pref);
46
+ const calls = { opened: [], assigned: [] };
47
+ const sandbox = {
48
+ live: { ...live },
49
+ resolvedDomain: () => 'mercury.example.com',
50
+ localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, v) },
51
+ window: {
52
+ open: (u, t) => { calls.opened.push([u, t]); return popupBlocked ? null : { opener: 'checklist' }; },
53
+ location: { assign: (u) => { calls.assigned.push(u); } },
54
+ },
55
+ };
56
+ sandbox.globalThis = sandbox;
57
+ vm.createContext(sandbox);
58
+ vm.runInContext(`${SLICE}\nautoOpenArmed = ${armed ? 'true' : 'false'};\nfor (var i = 0; i < ${ticks}; i++) maybeAutoOpen();\nvar tickHtml = autoOpenTick();`, sandbox);
59
+ return { calls, sandbox };
60
+ }
61
+
62
+ test('ready + armed + ticked (the default) → opens the project ONCE, in a new tab', () => {
63
+ const { calls } = run({ ticks: 3 });
64
+ assert.deepEqual(calls.opened, [['https://mercury.example.com', '_blank']], 'one new tab, however many renders follow');
65
+ assert.deepEqual(calls.assigned, [], 'this checklist stays put');
66
+ });
67
+
68
+ test('a browser that blocks the unprompted tab sends THIS tab instead — the founder asked', () => {
69
+ const { calls } = run({ popupBlocked: true });
70
+ assert.deepEqual(calls.assigned, ['https://mercury.example.com']);
71
+ });
72
+
73
+ test('nothing opens until sign-in is wired AND the project answers', () => {
74
+ for (const live of [
75
+ { ...READY, manifest: 'code_received' },
76
+ { ...READY, instStatus: 'provisioning' },
77
+ { ...READY, addrReady: false },
78
+ ]) {
79
+ const { calls } = run({ live });
80
+ assert.deepEqual([calls.opened, calls.assigned], [[], []], JSON.stringify(live));
81
+ }
82
+ });
83
+
84
+ test('unticked → never opens, and the tick renders unticked', () => {
85
+ const { calls, sandbox } = run({ pref: '0' });
86
+ assert.deepEqual([calls.opened, calls.assigned], [[], []]);
87
+ assert.doesNotMatch(sandbox.tickHtml, / checked/);
88
+ assert.match(run().sandbox.tickHtml, / checked>/, 'the default is ticked');
89
+ });
90
+
91
+ test('a checklist that never watched the wait (a revisit) does not jump away', () => {
92
+ const { calls } = run({ armed: false });
93
+ assert.deepEqual([calls.opened, calls.assigned], [[], []]);
94
+ });
@@ -70,10 +70,10 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
70
70
  // A faithful miniature: the stubs record what enterCreated ASKS FOR, which is the whole
71
71
  // contract — which panel is shown, whether the answers were reset, and what the offer
72
72
  // above step 1 was told to name.
73
- function runEnter({ focus, resumeIntent = false, draft = null, record }) {
73
+ function runEnter({ focus, resumeIntent = false, draft = null, record, handbackState = null }) {
74
74
  const store = new Map();
75
75
  if (draft) store.set('draft', JSON.stringify(draft));
76
- const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
76
+ const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0, remembered: [] };
77
77
  // state.step is the DONE step for the whole life of a finished standup — the value
78
78
  // a draft-blind reading would have trusted
79
79
  const state = { step: draft ? draft.step : 6, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
@@ -83,6 +83,8 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
83
83
  live: { manifest: null, appSlug: null },
84
84
  manifestState: null,
85
85
  manifestPending: false,
86
+ handbackState, // the #ghstate=… a GitHub hand-back carries (task 1004548); null on a plain arrival
87
+ rememberManifest(st, status) { calls.remembered.push([st, status]); },
86
88
  sessionStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null) },
87
89
  resetForNewProject() { calls.resets += 1; state.step = 1; state.name = ''; state.slug = ''; },
88
90
  paintResumeOffer(c) { calls.offer.push(c ? (c.name || c.slug) : null); },
@@ -111,6 +113,19 @@ test('a reload / Back / identity settle (focus false) still resumes the done pan
111
113
  assert.deepEqual(calls.offer, [null], 'the offer is down — the done panel is on stage, not step 1');
112
114
  });
113
115
 
116
+ test('a hand-back from the GitHub callback page resumes watching THAT attempt (task 1004548)', () => {
117
+ const { calls, sandbox } = runEnter({ focus: false, record: { ...REC }, handbackState: 'ab'.repeat(32) });
118
+ assert.deepEqual(calls.show, [[6, false]], 'the founder lands on their checklist');
119
+ assert.equal(sandbox.manifestState, 'ab'.repeat(32), 'the sign-in step polls the attempt GitHub just approved');
120
+ assert.equal(sandbox.manifestPending, true, 'and the poll is queued for resumePolls()');
121
+ assert.equal(sandbox.live.manifest, 'code_received', 'it shows the wait, not "Set up GitHub sign-in" again');
122
+ assert.deepEqual(calls.remembered, [['ab'.repeat(32), 'code_received']], 'the attempt is kept across a reload');
123
+ assert.equal(sandbox.handbackState, null, 'read once — a later arrival is not a hand-back');
124
+ const done = runEnter({ focus: false, record: { ...REC, manifest: 'done' }, handbackState: 'cd'.repeat(32) });
125
+ assert.equal(done.sandbox.live.manifest, 'done', 'a finished setup is never walked back to waiting');
126
+ assert.equal(done.sandbox.manifestPending, false, 'and nothing is polled for it');
127
+ });
128
+
114
129
  test('a deliberate "New project" click after a finished standup opens a FRESH step 1', () => {
115
130
  const { calls, state } = runEnter({ focus: true, record: REC });
116
131
  assert.equal(calls.done, 0, 'the previous project\'s done panel is not painted');