@bongos/core 1.21.104 → 1.21.106

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 (40) hide show
  1. package/.bongos-core.json +57 -47
  2. package/docs/copy-inventory.md +368 -366
  3. package/docs/copy-registry.json +475 -457
  4. package/docs/design/project-startup-direction.md +14 -0
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +10 -3
  7. package/docs/page-readings.json +1023 -977
  8. package/modules/lifecycle/check-rollup.js +11 -1
  9. package/modules/lifecycle/github-push.js +3 -7
  10. package/modules/public-landing/public/projects-hall-live.states.json +3 -2
  11. package/modules/public-landing/public/projects-setup.states.json +91 -0
  12. package/modules/public-landing/public/projects.html +535 -804
  13. package/modules/public-landing/public/projects.probes.json +3 -3
  14. package/modules/public-landing/public/projects.states.json +1 -1
  15. package/modules/ui-design/kit/fixtures/provisioning-instances-hall-live.json +22 -2
  16. package/modules/ui-design/kit/fixtures/provisioning-instances-setup.json +195 -0
  17. package/modules/ui-design/kit/serve.js +22 -4
  18. package/package-lock.json +2 -2
  19. package/package.json +1 -1
  20. package/release-notes.json +12 -0
  21. package/scripts/gds/ship-land.js +2 -2
  22. package/src/module-api.js +1 -1
  23. package/tests/cosmos_token_contrast.mjs +10 -18
  24. package/tests/github_handback_any_device.mjs +4 -3
  25. package/tests/github_one_trip.mjs +45 -47
  26. package/tests/github_org_account.mjs +2 -2
  27. package/tests/github_pick_up.mjs +40 -29
  28. package/tests/github_push_merge_driver.mjs +46 -0
  29. package/tests/onboard.mjs +13 -7
  30. package/tests/project_invite_ui.mjs +6 -3
  31. package/tests/project_modules_ui.mjs +3 -3
  32. package/tests/projects_hub.mjs +8 -9
  33. package/tests/projects_hub_cloud_hop.mjs +77 -125
  34. package/tests/projects_hub_dns_ready.mjs +42 -51
  35. package/tests/projects_hub_pre_uat.mjs +68 -69
  36. package/tests/public_landing_projects.mjs +45 -102
  37. package/tests/setup_status_facts.mjs +400 -0
  38. package/tests/wizard_github_feedback.mjs +90 -87
  39. package/tests/wizard_one_page.mjs +11 -9
  40. package/tests/wizard_auto_open.mjs +0 -94
@@ -1,21 +1,21 @@
1
1
  // tests/projects_hub_cloud_hop.mjs
2
2
  //
3
- // The create wizard's done panel in two moments (task 1004578, from idea 1001205):
4
- // WAIT — one status line, a thin bar and the cloud hop, a little game with one
5
- // cloud per PUBLIC project on the platform — then ARRIVE — the steps only the
6
- // founder can play, all on show as one checklist (part 03, task 1004648). The owner set the game's rules on a played
7
- // prototype, and the nausea ones are the ones a refactor would quietly lose: the
8
- // clouds stand still, the sky stands still, the view does not follow a jump.
3
+ // The cloud hop (task 1004578, from idea 1001205): a little game with one cloud per
4
+ // PUBLIC project on the platform, played while a project is being set up. Since task
5
+ // 1004683 (the design of record docs/design/mocks/day-night-rooms/setup-status.html) it
6
+ // waits behind a "Play cloud hop while you wait" link and opens in place only when
7
+ // asked: the wait never depends on it, and nothing on a timer stands in for a fact. The
8
+ // owner set the game's rules on a played prototype, and the nausea ones are the ones a
9
+ // refactor would quietly lose: the clouds stand still, the sky stands still, the view
10
+ // does not follow a jump.
9
11
  //
10
- // EXECUTED by the vm-slice idiom: momentFor/setMoment ride the same region the DNS
11
- // test runs (renderProgress); the physics (hopStep), the score line (hopScore),
12
- // the status sync (hopSync) and the checklist's state line (stepsPaint) are sliced function by
13
- // function and run under narrow stubs. Every slice is asserted to exist, so a
14
- // rename fails loudly instead of testing nothing. vm-born values are compared as
15
- // primitives or through JSON.
12
+ // EXECUTED by the vm-slice idiom: the link (hopOpen), the arrival (hopArrive), the
13
+ // physics (hopStep) and the score line (hopScore) are sliced function by function and
14
+ // run under narrow stubs. Every slice is asserted to exist, so a rename fails loudly
15
+ // instead of testing nothing. vm-born values are compared as primitives or through JSON.
16
+ // Part 03's facts themselves are tests/setup_status_facts.mjs.
16
17
  //
17
18
  // Run: node --test tests/projects_hub_cloud_hop.mjs
18
-
19
19
  import { test } from 'node:test';
20
20
  import { strict as assert } from 'node:assert';
21
21
  import fs from 'node:fs';
@@ -47,69 +47,71 @@ const HOP_CONSTS = (SCRIPT.match(/\n {4}var HOP_GAP = [^\n]*\n/) || [''])[0];
47
47
  assert.ok(HOP_CONSTS.includes('HOP_JUMP'), 'the hop constants line must be findable');
48
48
  const HOP_REGION = between('/* ══ THE CLOUD HOP (task 1004578)', '/* ── the hub\'s three doors ── */');
49
49
 
50
- // ── the moment ──────────────────────────────────────────────────────────────────
51
- const PROGRESS = between('var PROG_FLOWS = {', 'function startProgress(');
52
- const OVERLAY = between('function verifiedAt(inst)', 'function chipMarkup(');
53
50
 
54
- function bootMoment() {
55
- const attrs = {};
56
- const panel = { setAttribute: (k, v) => { attrs[k] = v; } };
51
+ // ── behind its link (task 1004683) ──────────────────────────────────────────────
52
+ // The wait never depends on the game: it opens in place only when asked, and puts
53
+ // itself away (after the curtain, if it was running) once every required fact is done.
54
+ function bootLink({ reduced = false, running = false, since = 0 } = {}) {
55
+ const box = { hidden: true, classList: { remove() {}, add() {} } };
56
+ const link = { textContent: 'Play cloud hop while you wait', attrs: {}, setAttribute(k, v) { this.attrs[k] = v; } };
57
+ const calls = { start: 0, stop: 0, curtain: 0, raf: 0, done: null };
57
58
  const sandbox = {
58
- $: (id) => (id === 'panel-done' ? panel : null),
59
- esc: (s) => String(s), projectPageLink: (l) => l, relTime: () => 'now', canRestart: () => false,
60
- FRESH_MS: 1, LIVE_MOMENT_MS: 1, state: {}, paintInviteGate() {}, lastProgHtml: '',
59
+ $: (id) => ({ hop: box, hopLink: link })[id] || null, REDUCED: reduced,
60
+ performance: { now: () => 10000 },
61
+ hopStart: () => { calls.start += 1; }, hopStop: () => { calls.stop += 1; },
62
+ hopCurtain: (done) => { calls.curtain += 1; calls.done = done; }, hopFrame() {},
63
+ requestAnimationFrame: () => { calls.raf += 1; return 1; }, cancelAnimationFrame() {},
61
64
  };
62
65
  vm.createContext(sandbox);
63
- vm.runInContext(PROGRESS + '\n' + OVERLAY, sandbox);
64
- return { sandbox, attrs };
66
+ vm.runInContext(`var hop = { target: '', raf: ${running ? 1 : 0}, since: ${since}, tr: 0 };\n${fn('hopOpen')}\n${fn('hopArrive')}`, sandbox);
67
+ return { sandbox, box, link, calls };
65
68
  }
66
- const NOW = () => new Date().toISOString();
67
- const live = { status: 'active', domain: 'mercury.cloudbongos.com', dns_resolves: true, last_verified_up: true, last_verified_at: NOW(), open_intent: null };
68
-
69
- test('the moment follows the same branches renderProgress speaks', () => {
70
- const { sandbox } = bootMoment();
71
- const m = (inst) => sandbox.momentFor(inst);
72
- assert.equal(m({ status: 'requested' }), 'wait', 'filed: the hop');
73
- assert.equal(m({ status: 'provisioning' }), 'wait', 'standing up: the hop');
74
- assert.equal(m({ ...live, dns_resolves: false, last_verified_up: null }), 'wait', 'address still being set up: keep playing');
75
- assert.equal(m({ ...live, last_verified_up: null }), 'wait', 'standing, not checked yet: keep playing');
76
- assert.equal(m({ ...live, last_verified_up: false }), 'wait', 'not answering right now: keep playing');
77
- assert.equal(m(live), 'arrive', 'answering on a resolved address: arrive');
78
- assert.equal(m({ ...live, open_intent: { action: 'standup', state: 'running' } }), 'arrive',
79
- 'an intent renderProgress does not speak for (a running standup) falls through to the status');
80
- assert.equal(m({ ...live, open_intent: { action: 'provision' } }), 'wait', 'a queued standup is still a wait');
81
- assert.equal(m({ status: 'torn_down', open_intent: { action: 'provision' } }), 'wait', 'a queued revive is a wait');
82
- });
83
69
 
84
- test('a snag, an ending, a settings change or an addressless project keeps the plain panel', () => {
85
- const { sandbox } = bootMoment();
86
- const m = (inst) => sandbox.momentFor(inst);
87
- assert.equal(m({ status: 'error' }), 'plain');
88
- assert.equal(m({ status: 'tearing_down' }), 'plain');
89
- assert.equal(m({ status: 'torn_down' }), 'plain');
90
- assert.equal(m({ ...live, open_intent: { action: 'teardown' } }), 'plain');
91
- assert.equal(m({ ...live, open_intent: { action: 'settings-apply', state: 'running' } }), 'plain');
92
- assert.equal(m({ ...live, domain: null }), 'plain', 'no address will never answer — nothing to arrive at');
70
+ test('the game opens in place only when asked, and its link says so', () => {
71
+ const h = bootLink();
72
+ h.sandbox.hopOpen(true);
73
+ assert.equal(h.box.hidden, false);
74
+ assert.equal(h.link.attrs['aria-expanded'], 'true');
75
+ assert.equal(h.sandbox.hop.target, 'wait', 'only an opened game may run (hopRunning reads it)');
76
+ assert.equal(h.calls.start, 1);
77
+ h.sandbox.hopOpen(false);
78
+ assert.equal(h.box.hidden, true);
79
+ assert.equal(h.link.attrs['aria-expanded'], 'false');
80
+ assert.equal(h.link.textContent, 'Play cloud hop while you wait');
81
+ assert.equal(h.sandbox.hop.target, '');
82
+ assert.equal(h.calls.stop, 1);
83
+ assert.match(HUB, /<button type="button" class="hopLink" id="hopLink" aria-expanded="false" aria-controls="hop">Play cloud hop while you wait<\/button>\s*<div class="hop" id="hop" hidden>/,
84
+ 'baked shut, behind the mockup\'s link');
85
+ assert.match(SCRIPT, /\$\('hopLink'\)\.addEventListener\('click', function \(\) \{ hopOpen\(\$\('hop'\)\.hidden\); \}\);/, 'the link is the only way in');
86
+ assert.match(fn('hopStart'), /if \(!hop \|\| !hopEl \|\| hopEl\.hidden\) return;/, 'a closed game never starts, not on a resize nor a tab coming back');
93
87
  });
94
88
 
95
- test('renderProgress sets the moment before it writes the strip, and only for a real read', () => {
96
- const { sandbox, attrs } = bootMoment();
97
- const nodes = { liveProgress: { innerHTML: '', querySelector: () => null }, 'done-h': { textContent: '' }, doneLede: { innerHTML: '' } };
98
- sandbox.$ = ((orig) => (id) => nodes[id] || orig(id))(sandbox.$);
99
- sandbox.renderProgress({ status: 'provisioning' });
100
- assert.equal(attrs['data-moment'], 'wait');
101
- sandbox.renderProgress(live);
102
- assert.equal(attrs['data-moment'], 'arrive');
103
- const body = fn('renderProgress');
104
- assert.ok(body.indexOf('setMoment(momentFor(inst))') < body.indexOf('setProgress('), 'the moment is decided first');
105
- assert.match(body, /if \(inst\) setMoment\(momentFor\(inst\)\);/, 'a null read never moves the panel');
89
+ test('every required fact done: a running game gets the curtain, then puts itself away; a closed one stays away', () => {
90
+ const run = bootLink({ running: true, since: 1000 });
91
+ run.box.hidden = false;
92
+ run.sandbox.hopArrive();
93
+ assert.equal(run.calls.curtain, 1, 'the curtain plays from a running game');
94
+ assert.equal(run.box.hidden, false, 'and the game stays until the curtain lands');
95
+ run.calls.done();
96
+ assert.equal(run.box.hidden, true, 'then it is put away');
97
+ const shut = bootLink();
98
+ shut.sandbox.hopArrive();
99
+ assert.equal(shut.calls.curtain, 0, 'a game nobody opened plays nothing');
100
+ assert.equal(shut.box.hidden, true);
101
+ const still = bootLink({ reduced: true, running: true, since: 1000 });
102
+ still.box.hidden = false;
103
+ still.sandbox.hopArrive();
104
+ assert.equal(still.calls.curtain, 0, 'reduced motion: no curtain');
105
+ assert.equal(still.box.hidden, true);
106
+ assert.match(fn('paintAlive'), /if \(on\) hopArrive\(\);/, 'the facts decide the arrival, never the game');
106
107
  });
107
108
 
108
- test('the panel leaves the hop on a stopped watch and starts on it at renderDone', () => {
109
- const sp = between('function startProgress(', 'function resetForNewProject(');
110
- assert.equal((sp.match(/setMoment\('plain'\);/g) || []).length, 2, 'the ~33-minute hand-off and a gone instance both drop to plain');
111
- assert.match(fn('renderDone'), /setMoment\('wait'\);/, 'a fresh done panel opens on the hop');
109
+ test('nothing on a timer stands in for a fact: no tick counter, no moment, no bar', () => {
110
+ assert.doesNotMatch(SCRIPT, /ticks > 400|ticks \+= 1/, 'the ~33-minute tick counter is gone');
111
+ assert.doesNotMatch(SCRIPT, /setMoment|momentFor|data-moment|hopMoment|hopSync/, 'no moment switch');
112
+ assert.doesNotMatch(HUB, /hopBar|id="hopStatus"/, 'no bar and no second status line');
112
113
  assert.match(fn('resetForNewProject'), /hopReset\(\);/, 'a second project starts with a fresh hop');
114
+ assert.match(fn('renderDone'), /hopOpen\(false\);/, 'a fresh part 03 opens with the game put away');
113
115
  });
114
116
 
115
117
  // ── the physics — the owner's rules ─────────────────────────────────────────────
@@ -199,7 +201,7 @@ test('the loop never runs under reduced motion; the rest frame is a full still s
199
201
  'the start, the frame (playing and curtain) and the curtain\'s kick-off — no other loop');
200
202
  assert.match(fn('hopFrame'), /if \(!hopRunning\(\)\) \{ hopDraw\(now\); return; \}/,
201
203
  'every frame re-asks the gate, so turning reduced motion on stops it');
202
- assert.match(fn('hopMoment'), /!REDUCED && hop\.raf/, 'the curtain plays only from a running game');
204
+ assert.match(fn('hopArrive'), /!REDUCED && hop\.raf/, 'the curtain plays only from a running game');
203
205
  assert.match(STYLE, /\.hop\.isStill \.hopRow\{display:none;\}/, 'reduced motion has nothing to steer');
204
206
  });
205
207
 
@@ -213,67 +215,17 @@ test('the game reads its colours off the page tokens — no literal but white or
213
215
  assert.match(fn('hopColors'), /hopRead\('var\(--' \+ nm \+ '\)'\)/, 'every colour resolves from a token');
214
216
  });
215
217
 
216
- // ── the status line and the arrival pager ───────────────────────────────────────
217
- function el(cls = []) {
218
- const set = new Set(cls);
219
- const attrs = {};
220
- return {
221
- classList: { contains: (c) => set.has(c), toggle: (c, on) => (on ? set.add(c) : set.delete(c)), add: (c) => set.add(c), remove: (c) => set.delete(c) },
222
- setAttribute: (k, v) => { attrs[k] = v; }, getAttribute: (k) => attrs[k], attrs, hidden: false, textContent: '', style: {},
223
- };
224
- }
225
-
226
- test('the status line and the bar read what the strip says', () => {
227
- const st = { textContent: '' }, fill = { style: {} };
228
- const sandbox = { $: (id) => ({ hopStatus: st, hopBarFill: fill })[id] || null, Math };
229
- vm.createContext(sandbox);
230
- vm.runInContext(fn('hopSync'), sandbox);
231
- const strip = {
232
- querySelector: (q) => (q === '.pnote' ? { textContent: 'Raising the server, database, and address — nothing needed from you.' } : q === '.stages li.now' ? {} : null),
233
- querySelectorAll: (q) => ({ length: q === '.stages li' ? 3 : 1 }),
234
- };
235
- sandbox.hopSync(strip);
236
- assert.equal(st.textContent, 'Raising the server, database, and address — nothing needed from you.');
237
- assert.equal(fill.style.width, '50%', 'one done and one under way of three');
238
- assert.match(fn('setProgress'), /hopSync\(el\)/, 'every strip write syncs the line');
239
- });
240
-
241
- // ── part 03's checklist (task 1004648): every step on show, the state line from it ──
242
- function bootSteps(items, { step = 2, inFlight = false } = {}) {
243
- const list = { children: items };
244
- const ghState = { textContent: '' };
245
- const sandbox = { $: (id) => ({ humanSteps: list, ghState })[id] || null, Array, state: { step }, manifestInFlight: () => inFlight };
246
- vm.createContext(sandbox);
247
- vm.runInContext(fn('stepsPaint'), sandbox);
248
- sandbox.stepsPaint();
249
- return ghState.textContent;
250
- }
251
- function li(key, cls = []) { const e = el(cls); e.key = key; return e; }
252
-
253
- test('part 03\'s state line reads the checklist: ready, how many done, waiting on GitHub, connected', () => {
254
- assert.equal(bootSteps([li('github-repo'), li('oauth-app')], { step: 1 }), 'opens once your project exists', 'before Create');
255
- assert.equal(bootSteps([li('github-repo'), li('oauth-app')]), 'ready');
256
- assert.equal(bootSteps([li('github-repo', ['sdone']), li('oauth-app'), li('enter-hall', ['slocked'])]), '1 of 3 done');
257
- assert.equal(bootSteps([li('github-repo', ['sdone']), li('oauth-app')], { inFlight: true }), 'waiting on GitHub…', 'a GitHub leg in flight says so');
258
- assert.equal(bootSteps([li('github-repo', ['sdone']), li('oauth-app', ['sdone'])]), 'connected ✓');
259
- assert.doesNotMatch(fn('stepsPaint'), /innerHTML|\.cur|data-n/, 'no paging, and no markup from strings');
260
- });
261
-
262
- // ── the markup and the sheet ────────────────────────────────────────────────────
263
- test('the wait stands the strip and the later lines down, the checklist stays on show, and nothing is paged', () => {
264
- for (const id of ['hop', 'hopStatus', 'hopBarFill', 'hopCanvas', 'hopLeft', 'hopJump', 'hopRight', 'hopScore', 'humanSteps', 'ghState']) {
218
+ // ── the markup ──────────────────────────────────────────────────────────────────
219
+ test('the hop is baked into part 03 below the facts and above "is alive"; the old wait is gone', () => {
220
+ for (const id of ['hop', 'hopLink', 'hopCanvas', 'hopLeft', 'hopJump', 'hopRight', 'hopScore']) {
265
221
  assert.ok(HUB.includes(`id="${id}"`), `#${id} is baked into part 03`);
266
222
  }
267
- for (const id of ['hopWhile', 'hopSignin', 'stepPager', 'stepCount', 'stepBack', 'stepNext']) {
268
- assert.ok(!HUB.includes(`id="${id}"`), `#${id} is gone: the checklist is on show the whole time (task 1004648)`);
223
+ for (const id of ['hopWhile', 'hopSignin', 'stepPager', 'stepCount', 'stepBack', 'stepNext', 'hopStatus', 'hopBarFill', 'humanSteps', 'liveProgress']) {
224
+ assert.ok(!HUB.includes(`id="${id}"`), `#${id} is gone (tasks 1004648, 1004683)`);
269
225
  }
270
226
  const panel = HUB.slice(HUB.indexOf('id="panel-done"'), HUB.indexOf('</section>', HUB.indexOf('id="panel-done"')));
271
- assert.ok(panel.indexOf('id="humanSteps"') < panel.indexOf('id="hop"') && panel.indexOf('id="hop"') < panel.indexOf('id="wizInvite"'),
272
- 'the checklist first, then the hop, then the invite');
273
- assert.match(panel, /id="hopStatus" role="status" aria-live="polite"/, 'the line is the live region while the strip is hidden');
274
- assert.match(STYLE, /#panel-done\[data-moment="wait"\] #liveProgress,/);
275
- assert.doesNotMatch(STYLE, /#panel-done\[data-moment="wait"\][^{]*#humanSteps\{display:none;\}/, 'the wait no longer hides the checklist');
276
- assert.doesNotMatch(STYLE, /humanSteps>li:not\(\.cur\)/, 'and the arrival does not page it');
277
- assert.match(STYLE, /#panel-done\[data-moment="arrive"\] #liveProgress \.stages\{display:none;\}/, 'the arrival keeps the note with the address, not the dots');
278
- assert.match(STYLE, /#panel-done \.humanSteps>li\.sdone::before\{content:"✓";/, 'a verified step wears the tick');
227
+ assert.ok(panel.indexOf('id="ghFacts"') < panel.indexOf('id="hopLink"') && panel.indexOf('id="hop"') < panel.indexOf('id="doneAlive"'),
228
+ 'the facts first, then the game behind its link, then "is alive"');
229
+ assert.doesNotMatch(panel, /id="hopScore"[^>]*aria-live/, 'the score is not a live region: part 03 speaks through one');
230
+ assert.match(STYLE, /\.hop\[hidden\]\{display:none;\}/, 'the game stays out of the layout until asked');
279
231
  });
@@ -7,7 +7,7 @@
7
7
  // carries `dns_resolves` (the server's sighting — modules/provisioning/dns-resolves.js)
8
8
  // or a verified up probe. The status alone, 'active' included, is never enough.
9
9
  //
10
- // EXECUTED by the vm-slice idiom: the done panel's renderProgress, the card's
10
+ // EXECUTED by the vm-slice idiom: part 03's Its home facts (homeFacts, task 1004683), the card's
11
11
  // addressRow and the row's orbFor are sliced verbatim out of projects.html and run
12
12
  // under a narrow stub, and the markup they write is read back. Every slice boundary is
13
13
  // asserted, so a rename fails loudly instead of testing nothing. vm-born values are
@@ -36,27 +36,23 @@ function slice(from, to) {
36
36
  return HUB.slice(a, b);
37
37
  }
38
38
 
39
- const PROGRESS = slice('var PROG_FLOWS = {', 'function startProgress(');
39
+ // part 03's Its home (task 1004683): the three facts read off the owner's instance read
40
+ function wizFn(name) {
41
+ const m = HUB.match(new RegExp(`\\n {4}function ${name}\\([^)]*\\) \\{[\\s\\S]*?\\n {4}\\}`));
42
+ assert.ok(m, `function ${name}() must be findable in projects.html`);
43
+ return m[0];
44
+ }
45
+ const FACTS = ['hostOk', 'whenSaid', 'homeFacts'].map(wizFn).join('\n');
40
46
  const OVERLAY = slice('function verifiedAt(inst)', 'function chipMarkup(');
41
47
  const ADDRESS = slice('var ADDRESS_SETTING_UP', 'function publishRow(');
42
48
  const ORB = slice('function orbFor(', '/* ── progress strips');
43
- for (const [name, region] of [['renderProgress', PROGRESS], ['addressReady', OVERLAY], ['addressRow', ADDRESS], ['orbFor', ORB]]) {
49
+ for (const [name, region] of [['homeFacts', FACTS], ['addressReady', OVERLAY], ['addressRow', ADDRESS], ['orbFor', ORB]]) {
44
50
  assert.ok(region.includes(`function ${name}(`), `the slice must carry ${name}`);
45
51
  }
46
52
 
47
53
  function boot() {
48
- const liveProgress = { innerHTML: '' };
49
- // the done panel's heading (task 1004504): "Setting up <name>" until the project answers
50
- const doneH = { textContent: 'Setting up Lantern' };
51
- // and the line under it (task 1004543): renderDone's "setting up" until the project answers
52
- const SETTING_UP = 'We’re setting up <strong>Lantern</strong>: its server, database and address.';
53
- const doneLede = { innerHTML: SETTING_UP };
54
- // the invite step's gate (task 1004503): renderProgress opens it only on a verified answer
55
- const gates = [];
56
54
  const sandbox = {
57
- $: (id) => ({ liveProgress, 'done-h': doneH, doneLede })[id] || null,
58
- state: { name: 'Lantern', slug: 'lantern' },
59
- paintInviteGate: (live) => gates.push(live),
55
+ live: { readAt: Date.now() },
60
56
  esc: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
61
57
  { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
62
58
  projectPageLink: (label) => '<a href="?view=manage&id=1">' + label + '</a>',
@@ -66,54 +62,47 @@ function boot() {
66
62
  planetAttrs: () => '',
67
63
  FRESH_MS: 12 * 60 * 1000,
68
64
  LIVE_MOMENT_MS: 10 * 60 * 1000,
69
- lastProgHtml: '',
70
65
  };
71
66
  vm.createContext(sandbox);
72
- vm.runInContext(PROGRESS + '\n' + OVERLAY + '\n' + ADDRESS + '\n' + ORB, sandbox);
73
- return { sandbox, progress: () => liveProgress.innerHTML, heading: () => doneH.textContent, lede: () => doneLede.innerHTML, SETTING_UP, gates };
67
+ vm.runInContext(OVERLAY + '\n' + ADDRESS + '\n' + ORB + '\n' + FACTS, sandbox);
68
+ // Its home as the founder sees it: each fact's state and every word it says
69
+ const home = (inst) => JSON.parse(JSON.stringify(sandbox.homeFacts(inst, Date.now())));
70
+ const words = (rows) => rows.map((r) => [r.title, [].concat(r.sub || []).map((p) => (typeof p === 'string' ? p : p.t)).join(''), r.right || ''].join(' ')).join('\n');
71
+ return { sandbox, home, words };
74
72
  }
75
73
 
76
74
  const NOW = () => new Date().toISOString();
77
- const base = { id: 1, slug: 'lantern', status: 'active', domain: 'lantern.cloudbongos.com', last_verified_at: null, last_verified_up: null, dns_note: null };
78
75
  const ADDRESS_LINK = /<a [^>]*href="https:\/\/lantern\.cloudbongos\.com/;
76
+ const base = { id: 1, slug: 'lantern', status: 'active', domain: 'lantern.cloudbongos.com', created_at: NOW(), provisioned_at: NOW(), last_verified_at: null, last_verified_up: null, dns_note: null, open_intent: null };
79
77
 
80
- test('active but not yet resolved: the done panel holds the link, says so, and keeps watching', () => {
78
+ test('active but not yet resolved: part 03 says the address is on its way, never names it, and keeps it a fact under way', () => {
81
79
  const h = boot();
82
- const terminal = h.sandbox.renderProgress({ ...base, dns_resolves: false });
83
- assert.equal(terminal, false, 'the panel keeps polling until the lookup answers');
84
- assert.doesNotMatch(h.progress(), ADDRESS_LINK);
85
- assert.doesNotMatch(h.progress(), /lantern\.cloudbongos\.com/, 'and does not even name it to be typed');
86
- assert.match(h.progress(), /your address is being set up/);
80
+ const rows = h.home({ ...base, dns_resolves: false });
81
+ assert.deepEqual(rows.map((r) => r.state), ['done', 'done', 'now'], 'kept, running, not answering yet');
82
+ assert.equal(rows[2].sub, 'New addresses can take a few minutes to reach everyone.');
83
+ assert.doesNotMatch(h.words(rows), /lantern\.cloudbongos\.com/, 'a name typed before it resolves is cached as not found: not even named');
87
84
  });
88
85
 
89
- test('a down probe on a never-resolved address is still "being set up", never a link', () => {
86
+ test('a down probe on a never-resolved address is still on its way, never named', () => {
90
87
  const h = boot();
91
- h.sandbox.renderProgress({ ...base, dns_resolves: false, last_verified_up: false, last_verified_at: NOW() });
92
- assert.doesNotMatch(h.progress(), ADDRESS_LINK);
93
- assert.match(h.progress(), /being set up/);
88
+ const rows = h.home({ ...base, dns_resolves: false, last_verified_up: false, last_verified_at: NOW() });
89
+ assert.equal(rows[2].state, 'now');
90
+ assert.doesNotMatch(h.words(rows), /lantern\.cloudbongos\.com/);
94
91
  });
95
92
 
96
- test('once the lookup answers, the done panel links the address', () => {
93
+ test('once the lookup answers, part 03 names the address; only a verified answer ticks "Address answers"', () => {
97
94
  const h = boot();
98
- const terminal = h.sandbox.renderProgress({ ...base, dns_resolves: true });
99
- assert.equal(terminal, false, 'resolving is not yet answering — the panel still waits for the probe');
100
- assert.match(h.progress(), ADDRESS_LINK);
101
- assert.equal(h.heading(), 'Setting up Lantern', 'and the heading claims nothing yet (task 1004504)');
102
- assert.deepEqual(h.gates, [], 'and the invite box stays shut: the hall has not answered (task 1004503)');
103
- assert.equal(h.lede(), h.SETTING_UP, 'and the line under it still says it is being set up (task 1004543)');
104
- const live = boot();
105
- assert.equal(live.sandbox.renderProgress({ ...base, dns_resolves: true, last_verified_up: true, last_verified_at: NOW() }), true);
106
- assert.match(live.progress(), ADDRESS_LINK);
107
- assert.equal(live.heading(), 'Lantern is alive.', 'the alive moment, only on a verified answer');
108
- assert.deepEqual(live.gates, [true], 'and the invite box opens with it (task 1004503)');
109
- assert.match(live.lede(), /^Its home is ready\. Watch the rest here, or any time on <a href="\?view=mine">your projects page<\/a>\.$/,
110
- 'and the line under it stops saying "setting up" (task 1004543)');
111
- assert.doesNotMatch(live.lede(), /setting up/i);
112
- const down = boot();
113
- down.sandbox.renderProgress({ ...base, dns_resolves: true, last_verified_up: false, last_verified_at: NOW() });
114
- assert.equal(down.heading(), 'Setting up Lantern', 'a down probe is never "alive"');
115
- assert.deepEqual(down.gates, [], 'nor opens the invite box');
116
- assert.equal(down.lede(), down.SETTING_UP, 'nor says its home is ready');
95
+ const resolved = h.home({ ...base, dns_resolves: true });
96
+ assert.equal(resolved[0].sub, 'lantern.cloudbongos.com', 'named once it resolves');
97
+ assert.equal(resolved[2].state, 'now', 'resolving is not yet answering');
98
+ assert.equal(resolved[2].sub, 'Checking that it answers.');
99
+ const up = h.home({ ...base, dns_resolves: true, last_verified_up: true, last_verified_at: NOW() });
100
+ assert.equal(up[2].state, 'done');
101
+ assert.match(up[2].right, /^checked \d+s ago$/, 'when the platform last saw it answer');
102
+ const down = h.home({ ...base, dns_resolves: true, last_verified_up: false, last_verified_at: NOW() });
103
+ assert.equal(down[2].state, 'now', 'a down probe is never "answers"');
104
+ // "is alive" and the Enter Genesis door ask the same two gates
105
+ assert.match(wizFn('paintAlive'), /var on = !!\(ready && inst && hostOk\(inst\.domain\) && addressReady\(inst\) && inst\.last_verified_up === true\);/);
117
106
  });
118
107
 
119
108
  test('the card and the manage page: no link and no name before the lookup answers, in every pre-live state', () => {
@@ -165,9 +154,11 @@ test('every door to the address on the page asks addressReady', () => {
165
154
  const SCRIPT = HUB;
166
155
  assert.match(SCRIPT, /inst\.status === 'active' && addressReady\(inst\)\) acts\.push\('<a class="btn small ghost" href="https:\/\/' \+ esc\(inst\.domain\) \+ '" target="_blank" rel="noopener">View<\/a>'\)/, 'the row\'s View');
167
156
  assert.match(SCRIPT, /inst\.status === 'active' && addressReady\(inst\)\) acts\.push\('<a class="btn small ghost" href="https:\/\/' \+ esc\(inst\.domain\) \+ '" target="_blank" rel="noopener">Open site →<\/a>'\)/, 'the manage page\'s Open site');
168
- // task 1004649: "Builders can sign in" opens its door only on a resolving address
169
- assert.match(SCRIPT, /var open = live\.instStatus === 'active' && live\.addrReady;\n\s*return '<li class="sdone" data-onboard-step="first-sign-in">/, 'the sign-in step\'s door waits for the address');
170
- assert.match(SCRIPT, /live\.addrReady = addressReady\(inst\); renderSteps\(\);/, 'and unlock on the tick the lookup answers');
157
+ // part 03 (task 1004683): "Builders can sign in" names the address only once it resolves,
158
+ // and the Enter Genesis door opens only on a resolved, answering address
159
+ assert.match(SCRIPT, /signIn\.sub = dom && live\.addrReady \? 'Tested on ' \+ dom : '';/, 'the sign-in fact waits for the address');
160
+ assert.match(SCRIPT, /if \(inst\) \{ live\.instStatus = inst\.status; live\.addrReady = addressReady\(inst\); \}\n\s*paintFacts\(\);/, 'and the read redraws the facts on the tick the lookup answers');
161
+ assert.match(SCRIPT, /if \(on\) \$\('enterGenesis'\)\.setAttribute\('href', 'https:\/\/' \+ inst\.domain\);/, 'the Genesis door, behind paintAlive\'s gate');
171
162
  assert.equal((SCRIPT.match(/inst\.status !== 'active' \|\| !addressReady\(inst\)\) \{ (vis|join)Runs\(''\); return; \}/g) || []).length, 2,
172
163
  'the browser\'s own manifest reads wait for the name too');
173
164
  // the only raw address hrefs left are behind one of those gates