@bongos/core 1.21.4 → 1.21.6

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 (126) hide show
  1. package/.bongos-core.json +233 -108
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -1
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
  8. package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +37 -2
  11. package/docs/api-reference.md +4 -3
  12. package/docs/copy-inventory.md +541 -658
  13. package/docs/copy-registry.json +1205 -2340
  14. package/docs/module-api-changelog.md +4 -0
  15. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
  16. package/docs/onboarding/diagrams/assertions.json +1 -1
  17. package/docs/page-inventory.json +47 -28
  18. package/docs/page-readings.json +1128 -1339
  19. package/modules/autonomy/config-idle.js +192 -0
  20. package/modules/autonomy/governor-docket.js +101 -0
  21. package/modules/autonomy/routes/autonomy.js +27 -0
  22. package/modules/autonomy/runner-health.js +22 -1
  23. package/modules/discord/board-broadcast.js +3 -1
  24. package/modules/discord/craft-broadcast.js +107 -0
  25. package/modules/discord/routes/discord.js +10 -0
  26. package/modules/discord/ship-broadcast.js +30 -2
  27. package/modules/economy/reward.js +3 -0
  28. package/modules/government/board.js +20 -5
  29. package/modules/government/docket-vocab.js +82 -0
  30. package/modules/government/docket.js +458 -0
  31. package/modules/government/routes/government.js +34 -0
  32. package/modules/hall-ui/public/city-draw.js +438 -0
  33. package/modules/hall-ui/public/city.css +110 -0
  34. package/modules/hall-ui/public/city.html +71 -0
  35. package/modules/hall-ui/public/city.js +177 -0
  36. package/modules/hall-ui/public/city.states.json +30 -0
  37. package/modules/hall-ui/public/gate.js +11 -4
  38. package/modules/hall-ui/public/genesis-home.js +9 -10
  39. package/modules/hall-ui/public/profile.css +4 -0
  40. package/modules/hall-ui/public/profile.js +25 -1
  41. package/modules/hall-ui/public/settings-autobongos.js +19 -1
  42. package/modules/hall-ui/public/settings.css +26 -0
  43. package/modules/hall-ui/public/settings.html +8 -0
  44. package/modules/hall-ui/public/settings.js +43 -3
  45. package/modules/ideas/governor-docket.js +93 -0
  46. package/modules/ideas/routes/blockers.js +5 -0
  47. package/modules/lifecycle/governor-docket.js +192 -0
  48. package/modules/lifecycle/routes/gate-approvals.js +78 -33
  49. package/modules/lifecycle/routes/lifecycle.js +3 -0
  50. package/modules/onboarding/routes/access-requests.js +68 -27
  51. package/modules/platform-identity/craft-rollup.js +72 -0
  52. package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
  53. package/modules/platform-identity/platform-identity.js +12 -6
  54. package/modules/platform-identity/routes/sso.js +4 -0
  55. package/modules/platform-identity/tests/platform-identity.mjs +1 -1
  56. package/modules/provisioning/core-upgrade.js +34 -7
  57. package/modules/provisioning/module.json +2 -1
  58. package/modules/provisioning/seams.js +2 -0
  59. package/modules/public-landing/public/assets/cosmos.css +6 -50
  60. package/modules/public-landing/public/projects.html +1170 -1659
  61. package/modules/public-landing/public/projects.probes.json +18 -18
  62. package/modules/public-landing/public/projects.states.json +24 -31
  63. package/modules/security/routes/reports.js +39 -0
  64. package/package-lock.json +2 -2
  65. package/package.json +1 -1
  66. package/release-notes.json +72 -0
  67. package/scripts/gds/autobongos-grade-cap.js +187 -0
  68. package/scripts/gds/autobongos-loop.js +4 -0
  69. package/scripts/gds/autobongos-run.js +154 -13
  70. package/scripts/gds/autobongos-verify.js +211 -14
  71. package/scripts/gds/provision-core-upgrade.js +14 -2
  72. package/scripts/gds/provision-repo.js +77 -13
  73. package/scripts/gds/ship-flow.js +5 -0
  74. package/scripts/gds/ship-preflight-steps.js +2 -1
  75. package/scripts/gds/ship.js +10 -0
  76. package/scripts/gds/update-sweep.js +24 -8
  77. package/scripts/gds/upgrade-outcome.js +1 -0
  78. package/src/bongos/auth-admission.js +95 -13
  79. package/src/bongos/db-kernel.js +1 -1
  80. package/src/bongos/module-rechecks.js +77 -0
  81. package/src/bongos/route-rank-check.js +3 -0
  82. package/src/bongos/routes/core-update.js +68 -2
  83. package/src/bongos/routes/modules.js +17 -0
  84. package/src/bongos/serve-internal.js +14 -0
  85. package/src/bongos/software-update.js +3 -2
  86. package/src/module-api.js +1 -1
  87. package/tests/activity_rollup_order_db.mjs +3 -1
  88. package/tests/activity_snapshots_db.mjs +2 -0
  89. package/tests/autobongos_grade_cap.mjs +125 -0
  90. package/tests/autobongos_loop.mjs +230 -1
  91. package/tests/autobongos_verify.mjs +239 -3
  92. package/tests/autonomy_config_idle.mjs +272 -0
  93. package/tests/core_update_banner.mjs +29 -0
  94. package/tests/core_upgrade_door.mjs +31 -1
  95. package/tests/core_upgrade_runner.mjs +20 -0
  96. package/tests/discord_craft_broadcast.mjs +162 -0
  97. package/tests/genesis_home.mjs +1 -1
  98. package/tests/government_routes.mjs +3 -1
  99. package/tests/governor_docket.mjs +552 -0
  100. package/tests/hall_audit.mjs +10 -1
  101. package/tests/hall_city.mjs +369 -0
  102. package/tests/hall_page_gate_map.mjs +5 -0
  103. package/tests/hub_craft_rollup.mjs +350 -0
  104. package/tests/onboard.mjs +10 -4
  105. package/tests/platform_boot.mjs +1 -1
  106. package/tests/profile_rollup_consent.mjs +2 -0
  107. package/tests/profile_route.mjs +16 -2
  108. package/tests/projects_hub.mjs +196 -89
  109. package/tests/projects_hub_app_status.mjs +3 -1
  110. package/tests/projects_hub_app_step.mjs +72 -48
  111. package/tests/projects_hub_dns_ready.mjs +10 -2
  112. package/tests/projects_hub_look.mjs +32 -22
  113. package/tests/projects_hub_module_picker.mjs +112 -62
  114. package/tests/projects_hub_pre_uat.mjs +48 -41
  115. package/tests/provision.mjs +78 -0
  116. package/tests/public_landing_projects.mjs +12 -6
  117. package/tests/settings_main_role.mjs +190 -0
  118. package/tests/software_update.mjs +56 -1
  119. package/tests/update_subscription_engine.mjs +55 -3
  120. package/tests/wizard_demo.mjs +37 -11
  121. package/tests/wizard_draft_resume.mjs +34 -22
  122. package/tests/wizard_front_door.mjs +173 -103
  123. package/tests/wizard_intent_resume.mjs +28 -25
  124. package/tests/wizard_physics.mjs +94 -76
  125. package/tests/wizard_physics_more.mjs +428 -277
  126. package/tests/wizard_six_screens.mjs +126 -0
@@ -1,9 +1,11 @@
1
- // tests/projects_hub_app_step.mjs — the create wizard's "where your app runs" step,
2
- // EXECUTED (task 1004351, ADR 0345), and its Render setup (task 1004352, ADR 0348).
1
+ // tests/projects_hub_app_step.mjs — the create wizard's "where your app runs" question,
2
+ // EXECUTED (task 1004351, ADR 0345), and its Render setup (task 1004352, ADR 0348). Since
3
+ // task 1004504 the old App step is the review's "App hosting" row: its controls moved into
4
+ // the row whole, and "Use this" runs the check its Next used to.
3
5
  //
4
6
  // A static regex cannot tell a guard that works from one that is never consulted, so —
5
7
  // as tests/projects_hub_module_picker.mjs does for the picker — this slices the block out
6
- // of projects.html by its own sentinels and runs it in `vm` under a small DOM stub.
8
+ // of projects.html by the code at its edges and runs it in `vm` under a small DOM stub.
7
9
  //
8
10
  // The load-bearing assertions for Render, because the key reaches the owner's WHOLE
9
11
  // Render account:
@@ -13,7 +15,7 @@
13
15
  // must choose, and Next refuses until they do
14
16
  // • a refused key says so at once, next to the field
15
17
  // • a service building another repository cannot be linked
16
- // • the review only ever names what the owner actually chose
18
+ // • the App hosting row only ever names what the owner actually chose
17
19
  //
18
20
  // Run: node --test --test-reporter=tap tests/projects_hub_app_step.mjs
19
21
 
@@ -29,13 +31,20 @@ const require = createRequire(import.meta.url);
29
31
  const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
30
32
  const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
31
33
 
32
- const START = '/* the app step (task 1004351)';
33
- const END = ' function renderReview() {';
34
- const from = HUB.indexOf(START);
35
- const to = HUB.indexOf(END, from);
36
- assert.ok(from > -1, 'the app-step block opens with its sentinel comment');
37
- assert.ok(to > from, 'the app-step block ends where renderReview begins');
38
- const BLOCK = HUB.slice(from, to);
34
+ // Two regions since task 1004504: the row's change listener and the Render wiring (from
35
+ // the #appOpts listener to the Look step), and the choice + the row's words (from
36
+ // appPlatformChoice to the review's ROWS list). The row's value comes from rowText(), and
37
+ // whether the row is fixed from appSkipped(); both come along, real.
38
+ function slice(start, end, what) {
39
+ const a = HUB.indexOf(start), b = HUB.indexOf(end, a);
40
+ assert.ok(a > -1, what + ' opens where it should');
41
+ assert.ok(b > a, what + ' ends where it should');
42
+ return HUB.slice(a, b);
43
+ }
44
+ const BLOCK = slice(" var appOptsEl = root.querySelector('#appOpts');", ' /* ── the Look step (task 1004421', 'the app wiring');
45
+ const CHOICE = slice(' function appPlatformChoice() {', ' var ROWS = [', 'the app choice and its row detail');
46
+ const ROWTEXT = slice(' function rowText(key) {', ' /* paint every row from the draft', "the review rows' words");
47
+ const SKIPPED = slice(' function appSkipped() {', ' /* What the create sends as `detail`.', 'appSkipped()');
39
48
  // The Render flow itself is shared with an existing project's page (task 1004184), so it
40
49
  // lives outside the wizard; its fence and the page's own node helper come along.
41
50
  const CSTART = ' /* ── connecting Render (task 1004352';
@@ -46,7 +55,7 @@ const CONNECT = HUB.slice(cfrom, cto);
46
55
  const APPEL = (HUB.match(/function appEl\(tag, cls, text\) \{[\s\S]*?\n \}/) || [''])[0];
47
56
  assert.ok(APPEL, 'the page\'s own appEl() is lifted');
48
57
 
49
- const PANEL = (HUB.match(/<section class="panel" id="panel-app"[\s\S]*?<\/section>/) || [''])[0];
58
+ const PANEL = (HUB.match(/<div class="setRow" data-row="app">[\s\S]*?<span class="acts">/) || [''])[0];
50
59
  const ESC = (HUB.match(/function esc\(s\) \{[\s\S]*?\n \}/) || [''])[0];
51
60
  assert.ok(ESC, 'the page\'s own esc() is lifted, not re-implemented');
52
61
 
@@ -90,6 +99,7 @@ function load({ draft = {}, answers = {} } = {}) {
90
99
  const els = {};
91
100
  const $ = (id) => (els[id] = els[id] || el(id));
92
101
  let changeHandler = null;
102
+ let painted = 0;
93
103
  const saves = [];
94
104
  const calls = [];
95
105
  const root = {
@@ -115,14 +125,18 @@ function load({ draft = {}, answers = {} } = {}) {
115
125
  const ctx = { root, state, $, document, Object, API: '/api/bongos', fetch, JSON, String, Number, encodeURIComponent, Array, saveDraft: () => saves.push(JSON.stringify(state)),
116
126
  // the shared status watch (task 1004353) lives outside this block; record that it is started
117
127
  watchApp: (id, box) => { watches.push({ id, box }); return () => {}; },
118
- // the planet-physics step skip (task 1004419) lives outside this block too; the
119
- // framing that skips this step is wizard_physics_more.mjs's to test
120
- physStepSkipped: () => false };
128
+ // the review repaints its rows on a change (task 1004504); count it
129
+ paintSetup: () => { painted += 1; },
130
+ // the planet-physics answers live outside these regions; rowText reads them through phys()
131
+ phys: () => { state.physics = state.physics || { answers: {} }; return state.physics; } };
121
132
  vm.createContext(ctx);
122
- vm.runInContext(ESC + '\n' + APPEL + '\n' + CONNECT + '\n' + BLOCK + '\nthis.api = { appPlatformChoice, syncAppStep, appRow, syncRenderSetup, validateRender, renderRequestBody, sendRenderSetup, planLabel };', ctx);
133
+ vm.runInContext(ESC + '\n' + APPEL + '\n' + CONNECT + '\n' + CHOICE + '\n' + SKIPPED + '\n' + BLOCK + '\n' + ROWTEXT +
134
+ '\nthis.api = { appPlatformChoice, syncAppStep, appRowDetail, rowText, syncRenderSetup, validateRender, renderRequestBody, sendRenderSetup, planLabel };', ctx);
123
135
  const api = ctx.api;
124
136
  return {
125
- ...api, inputs, state, saves, calls, $, watches,
137
+ ...api, inputs, state, saves, calls, $, watches, painted: () => painted,
138
+ // the row as the founder reads it: its value, then the line under it
139
+ appRow: () => { const t = api.rowText('app'); return t.v + ' | ' + t.s; },
126
140
  change: (value) => changeHandler({ target: inputs.find((i) => i.value === value) }),
127
141
  typeKey: (v) => { $('f-renderKey').value = v; $('f-renderKey').fire('input', { target: $('f-renderKey') }); },
128
142
  check: async () => { $('renderCheck').fire('click', { target: $('renderCheck') }); await flush(); },
@@ -139,32 +153,33 @@ const SERVICES = { services: [
139
153
  ] };
140
154
  const ANSWERS = { [LOOKUP]: TWO, [LOOKUP + '#' + TEA_A]: SERVICES };
141
155
 
142
- // ── the step itself (task 1004351) ──────────────────────────────────────────
156
+ // ── the question itself (task 1004351), as the review's App hosting row (task 1004504) ──
143
157
 
144
- test('the panel is real markup: Render and Decide later, Decide later the default', () => {
145
- assert.ok(PANEL, '#panel-app exists');
158
+ test('the row is real markup: Render and Decide later, Decide later the default', () => {
159
+ assert.ok(PANEL, 'the review carries a data-row="app" row');
160
+ assert.match(PANEL, /<span class="k">App hosting<\/span>/, 'the row is named for the app alone');
146
161
  assert.match(PANEL, /<input type="radio" name="appPlatform" value="render" aria-describedby="appRenderWhat">/, 'Render can be picked (task 1004352)');
147
162
  assert.doesNotMatch(PANEL, /Coming soon/, 'and is no longer marked soon');
148
163
  assert.match(PANEL, /<input type="radio" name="appPlatform" value="later" checked>/, 'Decide later is the default');
149
- assert.match(PANEL, /We host your Builders' hall/, 'the step says the hall is ours to host (ADR 0345 D1), in the page\'s one spelling of it');
150
- assert.match(PANEL, /<form data-next="11">/, 'Next leads to the Look step (task 1004421), then the review');
151
- assert.match(PANEL, /data-back="9"/, 'Back returns to the address step');
164
+ assert.ok(PANEL.indexOf('value="later"') < PANEL.indexOf('value="render"'), 'Decide later comes first');
165
+ assert.match(PANEL, /<button type="button" class="btn small rowUse" data-row-use>Use this<\/button>/, 'Use this confirms the row');
166
+ assert.doesNotMatch(HUB, /id="panel-app"/, 'the old App panel is gone');
152
167
  });
153
168
 
154
- test('the step sits between Address and the Look step in the panels map and on the rail', () => {
155
- assert.match(HUB, /9: 'panel-address', 10: 'panel-app', 11: 'panel-look', 12: 'panel-review', 13: 'panel-done'/);
156
- assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">App<\/span><\/li>/);
157
- assert.match(HUB, /<li data-step="11"><span class="snum">9<\/span><span class="slbl">Look<\/span><\/li>/);
158
- assert.match(HUB, /<li data-step="12"><span class="snum">10<\/span><span class="slbl">Create<\/span><\/li>/);
159
- assert.match(HUB, /data-back="11">← Back<\/button>\r?\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the Look step");
160
- assert.match(HUB, /if \(step === 10\) \{ syncAppStep\(\); syncRenderSetup\(\); \}\r?\n\s*if \(step === 11\) renderLook\(\);\r?\n\s*if \(step === 12\) renderReview\(\);/, 'entering the step repaints it, Render setup included');
161
- assert.match(HUB, /if \(step === 10\) return validateRender\(\);/, 'Next on the step runs the Render check');
169
+ test("the row sits in the review, between Address and Extras, and its checks are the old step's (task 1004504)", () => {
170
+ assert.match(HUB, /var panels = \{ 0: 'panel-gate', 1: 'panel-fork', 2: 'panel-what', 3: 'panel-checks', 4: 'panel-review', 5: 'panel-look', 6: 'panel-done' \};/);
171
+ assert.match(HUB, /var ROWS = \['code', 'address', 'app', 'extras', 'team', 'rewards', 'run'\];/);
172
+ assert.doesNotMatch(HUB, /<li[^>]*data-step="\d+"/, 'no rail beat names the step any more');
173
+ assert.match(HUB, /if \(step === 4\) \{[^}]*syncAppStep\(\); syncRenderSetup\(\);\r?\n\s*paintSetup\(\);\r?\n\s*\}/, 'entering the review repaints the row, Render setup included');
174
+ assert.match(HUB, /if \(key === 'app'\) \{ syncAppStep\(\); syncRenderSetup\(\); \}/, 'opening the row repaints it too');
175
+ assert.match(HUB, /if \(key === 'app'\) return rowFixed\('app'\) \|\| validateRender\(\);/, 'confirming the row runs the Render check');
162
176
  });
163
177
 
164
178
  test('a fresh draft reads "decide later", and the Render setup stays hidden', () => {
165
179
  const w = load();
166
180
  assert.equal(w.appPlatformChoice(), 'later');
167
- assert.equal(w.appRow(), 'Not chosen yet — you can decide later');
181
+ // the row says the hall is ours to host (ADR 0345 D1), in the page's one spelling of it
182
+ assert.equal(w.appRow(), 'Decide later | We host your builders’ hall. There’s no app to host until your builders write one.');
168
183
  w.syncRenderSetup();
169
184
  assert.equal(w.$('renderSetup').hidden, true);
170
185
  assert.equal(w.validateRender(), true, 'decide later never blocks Next');
@@ -176,6 +191,7 @@ test('choosing Render saves the choice and opens its setup', () => {
176
191
  assert.equal(w.state.appPlatform, 'render');
177
192
  assert.equal(JSON.parse(w.saves[0]).appPlatform, 'render');
178
193
  assert.equal(w.$('renderSetup').hidden, false);
194
+ assert.equal(w.painted(), 1, 'and the review repaints its rows');
179
195
  });
180
196
 
181
197
  test('an unknown or missing value reads "decide later"', () => {
@@ -184,19 +200,25 @@ test('an unknown or missing value reads "decide later"', () => {
184
200
  }
185
201
  });
186
202
 
187
- test('the review carries the hall row and the app row in place of the old Shape row', () => {
188
- const render = HUB.slice(HUB.indexOf('function renderReview()'));
189
- const rows = render.slice(0, render.indexOf("$('reviewList').innerHTML"));
190
- assert.ok(rows.includes("['Builders\\' hall', 'Hosted by us, on the platform’s current release'],"), 'the hall row');
191
- assert.match(rows, /\['App', appRow\(\)\]/);
192
- assert.doesNotMatch(rows, /\['Shape',/, 'the Shape row is gone');
203
+ test('the review names the app alone: no hall row, no Shape row (task 1004504)', () => {
204
+ assert.doesNotMatch(HUB, /function renderReview\(\)|id="reviewList"/, 'the old ledger is gone');
205
+ assert.doesNotMatch(HUB, /data-row="(hall|shape)"/, 'the review has no hall or Shape row');
206
+ assert.doesNotMatch(HUB, /Hosted by us, on the platform’s current release/, 'the hall line left with the ledger');
207
+ });
208
+
209
+ test('a local or open-source answer makes the row fixed: it says so and asks for nothing', () => {
210
+ const w = load({ draft: { physics: { answers: { framing: 'local' } } } });
211
+ assert.equal(w.appRow(), 'Runs on your own machines | As your answer says. Choose a platform later from the project’s page.');
212
+ w.state.physics.answers.framing = 'open-source';
213
+ assert.match(w.appRow(), /^Runs on your own machines/);
214
+ w.state.demo = { people: 2, hours_each: 3 };
215
+ assert.match(w.appRow(), /^Decide later/, 'a demo is all managed, whatever framing a walk left behind');
193
216
  });
194
217
 
195
- test('the rail grid has one column per step, so adding a step never wraps the rail', () => {
218
+ test('the rail grid left with the rail (task 1004504), so adding a step has no column count to keep in step', () => {
196
219
  const css = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'cosmos.css'), 'utf8');
197
- const cols = Number((css.match(/#view-new \.steps\{display:grid;grid-template-columns:repeat\((\d+),1fr\)/) || [])[1]);
198
- const beats = [...HUB.matchAll(/<li[^>]*data-step="\d+"/g)].length;
199
- assert.equal(cols, beats, 'the #view-new rail grid carries exactly one column per rail step');
220
+ assert.doesNotMatch(css, /#view-new \.steps/, 'no #view-new rail grid is left in cosmos.css');
221
+ assert.equal([...HUB.matchAll(/<li[^>]*data-step="\d+"/g)].length, 0, 'and no rail step is left on the page');
200
222
  });
201
223
 
202
224
  test('the hall is spelled one way on this page', () => {
@@ -377,7 +399,7 @@ test('Free is on the plan list with its catch spelled out, and Starter stays the
377
399
  const dbOpts = w.$('f-renderDbPlan').children.map((o) => o.value);
378
400
  assert.equal(dbOpts.includes('free'), false, 'the database stays paid-only');
379
401
  const r = load({ draft: { appPlatform: 'render', renderOwnerName: 'Mine', renderMode: 'greenfield', renderRuntime: 'node', renderPlan: 'free' } });
380
- assert.equal(r.appRow(), 'Render, workspace Mine — a new app (Node.js, Free, $0, but it sleeps when idle and takes about a minute to wake up), no database. Render bills you directly.');
402
+ assert.equal(r.appRow(), 'Render | Workspace Mine — a new app (Node.js, Free, $0, but it sleeps when idle and takes about a minute to wake up), no database. Render bills you directly.');
381
403
  });
382
404
 
383
405
  test('what Create sends: greenfield carries the build and the plans, a link only the service', async () => {
@@ -430,7 +452,7 @@ test('a brand-new repo choosing Render asks for no key, says when it happens, an
430
452
  assert.equal(w.$('renderLater').hidden, false, 'the note shows');
431
453
  assert.equal(w.$('renderSetup').hidden, true, 'no key, workspace or plan questions');
432
454
  assert.equal(w.validateRender(), true, 'Next is not blocked on a key nobody needs yet');
433
- assert.match(w.appRow(), /^Render — set up from your project’s page once your app has code\. Nothing is created on Render now\.$/);
455
+ assert.equal(w.appRow(), 'Render | Set up from your project’s page once your app has code. Nothing is created on Render now.');
434
456
  w.sendRenderSetup(42);
435
457
  await flush();
436
458
  assert.equal(w.calls.filter((c) => c.url.endsWith('/render')).length, 0, 'nothing is sent to Render');
@@ -484,13 +506,15 @@ test('a resumed draft asks for the key again, and says why', () => {
484
506
  assert.equal(w.validateRender(), false);
485
507
  });
486
508
 
487
- test('the review names exactly what was chosen, prices included', () => {
509
+ test('the row names exactly what was chosen, prices included', () => {
488
510
  const g = load({ draft: { appPlatform: 'render', renderOwnerName: 'Mine', renderMode: 'greenfield', renderRuntime: 'node', renderPlan: 'starter', renderDb: true, renderDbPlan: 'basic_256mb' } });
489
- assert.equal(g.appRow(), 'Render, workspace Mine — a new app (Node.js, Starter, about $7/month), with a database (Basic 256 MB, about $6/month). Render bills you directly.');
511
+ assert.equal(g.appRow(), 'Render | Workspace Mine — a new app (Node.js, Starter, about $7/month), with a database (Basic 256 MB, about $6/month). Render bills you directly.');
490
512
  const a = load({ draft: { appPlatform: 'render', renderOwnerName: 'Mine', renderMode: 'adopt', renderServiceName: '<shop>' } });
491
- assert.equal(a.appRow(), 'Render, workspace Mine — link <code>&lt;shop&gt;</code>, changing nothing on it', 'a name from Render is escaped');
513
+ assert.equal(a.appRow(), 'Render | Workspace Mine — link <shop>, changing nothing on it.', 'a name from Render stays plain text');
514
+ // ...because the row is painted as TEXT, never parsed as markup
515
+ assert.match(HUB, /row\.querySelector\('\[data-row-v\]'\)\.textContent = t\.v;\r?\n\s*row\.querySelector\('\[data-row-s\]'\)\.textContent = t\.s;/);
492
516
  });
493
517
 
494
518
  test('Create hands the app to Render straight after the project is filed', () => {
495
- assert.match(HUB, /show\(13, true\);\r?\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
519
+ assert.match(HUB, /show\(6, true\);\r?\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
496
520
  });
@@ -46,8 +46,11 @@ for (const [name, region] of [['renderProgress', PROGRESS], ['addressReady', OVE
46
46
 
47
47
  function boot() {
48
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' };
49
51
  const sandbox = {
50
- $: (id) => (id === 'liveProgress' ? liveProgress : null),
52
+ $: (id) => (id === 'liveProgress' ? liveProgress : id === 'done-h' ? doneH : null),
53
+ state: { name: 'Lantern', slug: 'lantern' },
51
54
  esc: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
52
55
  { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
53
56
  projectPageLink: (label) => '<a href="?view=manage&id=1">' + label + '</a>',
@@ -61,7 +64,7 @@ function boot() {
61
64
  };
62
65
  vm.createContext(sandbox);
63
66
  vm.runInContext(PROGRESS + '\n' + OVERLAY + '\n' + ADDRESS + '\n' + ORB, sandbox);
64
- return { sandbox, progress: () => liveProgress.innerHTML };
67
+ return { sandbox, progress: () => liveProgress.innerHTML, heading: () => doneH.textContent };
65
68
  }
66
69
 
67
70
  const NOW = () => new Date().toISOString();
@@ -89,9 +92,14 @@ test('once the lookup answers, the done panel links the address', () => {
89
92
  const terminal = h.sandbox.renderProgress({ ...base, dns_resolves: true });
90
93
  assert.equal(terminal, false, 'resolving is not yet answering — the panel still waits for the probe');
91
94
  assert.match(h.progress(), ADDRESS_LINK);
95
+ assert.equal(h.heading(), 'Setting up Lantern', 'and the heading claims nothing yet (task 1004504)');
92
96
  const live = boot();
93
97
  assert.equal(live.sandbox.renderProgress({ ...base, dns_resolves: true, last_verified_up: true, last_verified_at: NOW() }), true);
94
98
  assert.match(live.progress(), ADDRESS_LINK);
99
+ assert.equal(live.heading(), 'Lantern is alive.', 'the alive moment, only on a verified answer');
100
+ const down = boot();
101
+ down.sandbox.renderProgress({ ...base, dns_resolves: true, last_verified_up: false, last_verified_at: NOW() });
102
+ assert.equal(down.heading(), 'Setting up Lantern', 'a down probe is never "alive"');
95
103
  });
96
104
 
97
105
  test('the card and the manage page: no link and no name before the lookup answers, in every pre-live state', () => {
@@ -5,13 +5,13 @@
5
5
  // • the previews paint the REAL looks: every colour lookKit carries equals its
6
6
  // pack.json, and the night shares equal src/night-tint.js — a preview that drifted
7
7
  // from the hall would promise a look the hall does not wear;
8
- // • the step sits between App and Review (look 10, review 11, done 12, draft v7), on
9
- // the rail as a beat of its own, and every kit walk past App passes it;
8
+ // • the screen sits after the review and holds Create (review 4, look 5, done 6,
9
+ // draft v9 since task 1004504; no rail), and every kit walk files only from it;
10
10
  // • EXECUTED by the vm-slice idiom: the picker offers the four looks and "make your
11
11
  // own", shows the accent only for your own, shows the ADJUSTED colour in the
12
12
  // preview and says so, paints a black night when the tint is off, refuses an SVG
13
13
  // or oversized logo before reading it, and sends the look then the logo;
14
- // • Create sends the look right after the project is filed, and the review names it;
14
+ // • Create sends the look right after the project is filed, from the screen it was chosen on;
15
15
  // • the manage page reads the look once per project, never on the minute tick.
16
16
  //
17
17
  // Run: node --test tests/projects_hub_look.mjs
@@ -105,33 +105,39 @@ test('every colour the previews paint is its pack\'s, and the night shares are t
105
105
  assert.equal(kit.NIGHT.card, '#101012');
106
106
  });
107
107
 
108
- test('the step sits between App and Review: look 10, review 11, done 12, draft v7, a beat on the rail', () => {
109
- assert.match(HUB, /10: 'panel-app', 11: 'panel-look', 12: 'panel-review', 13: 'panel-done' \};/);
110
- assert.match(HUB, /var DRAFT_KEY = 'cb-create-draft-v8';/);
111
- assert.match(HUB, /<li data-step="11"><span class="snum">9<\/span><span class="slbl">Look<\/span><\/li>\r?\n\s*<li data-step="12"><span class="snum">10<\/span><span class="slbl">Create<\/span><\/li>/);
108
+ test('the screen sits after the review and holds Create: review 4, look 5, done 6, draft v9, no rail (task 1004504)', () => {
109
+ // task 1004504 blended the wizard into six screens; the look moved AFTER the review and
110
+ // became the screen Create stands on, so the look is the last thing chosen
111
+ assert.match(HUB, /4: 'panel-review', 5: 'panel-look', 6: 'panel-done' \};/);
112
+ assert.match(HUB, /var DRAFT_KEY = 'cb-create-draft-v9';/);
113
+ assert.doesNotMatch(HUB, /<li[^>]*data-step="\d+"/, 'no rail beat is left on the page');
114
+ assert.doesNotMatch(HUB, /id="panel-app"/, 'the App step is the review\'s App hosting row now');
112
115
  const panel = (HUB.match(/<section class="panel" id="panel-look"[\s\S]*?<\/section>/) || [''])[0];
113
- assert.match(panel, /<h1 id="look-h">Give it a look<\/h1>/);
114
- assert.match(panel, /<form data-next="12">/);
115
- assert.match(panel, /data-back="10"/);
116
+ assert.match(panel, /<h1 id="look-h">Give it a look\.<\/h1>/);
117
+ assert.doesNotMatch(panel, /<form|data-next=|data-back=/, 'no form and no per-panel Back: the one door is Create, the one Back is the wizard\'s');
116
118
  assert.match(panel, /<div class="lookPick" id="wizLook"><\/div>/);
117
- assert.ok(HUB.indexOf('id="panel-app"') < HUB.indexOf('id="panel-look"') && HUB.indexOf('id="panel-look"') < HUB.indexOf('id="panel-review"'), 'in document order too');
118
- assert.match(HUB, /if \(step === 11\) renderLook\(\);\r?\n\s*if \(step === 12\) renderReview\(\);/);
119
- assert.match(fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8'), /#view-new \.steps\{display:grid;grid-template-columns:repeat\(10,1fr\);/, 'a track per numbered step');
119
+ assert.match(panel, /<p class="wizRecap" id="wizRecap"><\/p>/, 'the one-line recap of what Create makes');
120
+ assert.match(panel, /<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'Create stands on this screen');
121
+ assert.ok(HUB.indexOf('id="panel-review"') < HUB.indexOf('id="panel-look"') && HUB.indexOf('id="panel-look"') < HUB.indexOf('id="panel-done"'), 'in document order too');
122
+ assert.match(HUB, /if \(step === 5\) \{ renderLook\(\); renderRecap\(\); \}/);
123
+ assert.doesNotMatch(fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8'), /#view-new \.steps/, 'and no rail grid in cosmos.css');
120
124
  });
121
125
 
122
- test('every kit walk past the App step passes the Look step, and the kit has a state on it', () => {
126
+ test('every kit walk that files a project passes the review and the Look screen first, and the kit has a state on it', () => {
123
127
  const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
124
128
  const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
125
129
  const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions]), ...probes.map((p) => [p.name, p.steps])];
126
130
  let past = 0;
127
131
  for (const [name, steps] of walks) {
128
- const at = (steps || []).findIndex((a) => a[1] === '#panel-app button[type=submit]');
129
- if (at < 0 || /^in-new-look/.test(name)) continue; // the Look states stop ON the step
132
+ const at = (steps || []).findIndex((a) => a[1] === '#reviewGo');
133
+ if (at < 0) continue;
130
134
  past++;
131
- const look = steps.findIndex((a) => a[1] === '#panel-look button[type=submit]');
132
- assert.ok(look > at, `${name}: submits the Look step after the App step`);
135
+ // the retired App and Look submits must not linger in a walk (they would stop it dead)
136
+ assert.ok(!steps.some((a) => a[1] === '#panel-app button[type=submit]' || a[1] === '#panel-look button[type=submit]'), `${name}: walks no retired step`);
137
+ const create = steps.findIndex((a) => a[1] === '#doCreate');
138
+ if (create >= 0) assert.ok(create > at, `${name}: files only after the review's Continue lands it on the Look screen`);
133
139
  }
134
- assert.ok(past >= 5, `sanity: the walks that reach the review were found (${past})`);
140
+ assert.ok(past >= 4, `sanity: the walks that leave the review were found (${past})`);
135
141
  assert.deepEqual(states['in-new-look'].expect.visible.slice(0, 2), ['#panel-look', '#wizLook']);
136
142
  assert.ok(states['in-new-look-custom'].actions.some((a) => a[1] === '#wizLook-custom'), 'a state pictures "make your own"');
137
143
  });
@@ -233,13 +239,17 @@ test('executed: sendLook sends the look, then the logo — and a removed logo is
233
239
  assert.deepEqual(calls.map((c) => [c.method, c.url.split('/').pop(), c.body]), [['PATCH', 'look', { look: 'grove', night_tint: 'on' }], ['DELETE', 'logo', null]]);
234
240
  });
235
241
 
236
- test('Create sends the look right after the project is filed, and the review names it', () => {
242
+ test('Create sends the look right after the project is filed, and the look is chosen on the screen Create stands on', () => {
237
243
  assert.match(HUB, /sendRenderSetup\(data\.instance && data\.instance\.id\);[^\n]*\n\s*sendLookSetup\(data\.instance && data\.instance\.id\);/);
238
244
  assert.match(HUB, /lookKit\.sendLook\(instanceId, state\.look \|\| defaultLook\(\)\)/);
239
245
  assert.match(HUB, /function defaultLook\(\) \{ return \{ look: 'chrome-world', nightTint: true, accent: '#b48cf2', logo: null \}; \}/, 'the default is the platform\'s look with the tint ON (owner D2)');
240
- assert.match(HUB, /\['Look', lookRow\(\)\]/);
246
+ // the ledger review and its Look row are gone (task 1004504): the picker and Create share
247
+ // one screen, so the look being filed is the one on screen beside the button
248
+ assert.doesNotMatch(HUB, /function lookRow\(|\['Look', lookRow\(\)\]/, 'no review row restates the look');
249
+ const panel = (HUB.match(/<section class="panel" id="panel-look"[\s\S]*?<\/section>/) || [''])[0];
250
+ assert.ok(panel.indexOf('id="wizLook"') > -1 && panel.indexOf('id="wizLook"') < panel.indexOf('id="doCreate"'), 'the picker sits above Create on the same screen');
241
251
  assert.match(HUB, /<p class="hint" id="doneLook" role="status" hidden><\/p>/, 'a refused look is said on the done panel');
242
- assert.match(HUB, /look: null, termsAck: false \};/, 'the draft carries the look (and its reset clears it)');
252
+ assert.equal((HUB.match(/look: null, termsAck: false, confirmed: \{\} \};/g) || []).length, 2, 'the draft carries the look (and its reset clears it)');
243
253
  });
244
254
 
245
255
  test('the manage page has "Its look", read once per project — never on the minute tick', () => {