@bongos/core 1.19.1081 → 1.20.2

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 (253) hide show
  1. package/.bongos-core.json +528 -193
  2. package/.claude/skills/planning-session/SKILL.md +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +32 -0
  5. package/clients/bongos-client/index.cjs +32 -0
  6. package/clients/bongos-client/index.d.ts +51 -6
  7. package/clients/bongos-client/index.mjs +32 -0
  8. package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
  9. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  10. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
  11. package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
  12. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  13. package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
  14. package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
  15. package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +851 -19
  18. package/docs/api-reference.md +33 -9
  19. package/docs/architecture.md +6 -2
  20. package/docs/copy-inventory.md +620 -553
  21. package/docs/copy-registry.json +1467 -820
  22. package/docs/file-map.md +2 -0
  23. package/docs/module-api-changelog.md +8 -0
  24. package/docs/modules-contract.md +1 -0
  25. package/docs/page-inventory.json +38 -4
  26. package/docs/page-readings.json +1666 -1558
  27. package/migrations/core_260_goals_working_area.sql +63 -0
  28. package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
  29. package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
  30. package/modules/agents/lib/validate.js +43 -0
  31. package/modules/autonomy/gauge.js +38 -2
  32. package/modules/grading/grader-subagent.js +37 -2
  33. package/modules/grading/grader-workers/reader.js +65 -5
  34. package/modules/hall-ui/public/approval-queue.css +33 -7
  35. package/modules/hall-ui/public/approval-queue.js +9 -2
  36. package/modules/hall-ui/public/atlas.html +1 -1
  37. package/modules/hall-ui/public/blockers.html +1 -1
  38. package/modules/hall-ui/public/board-room.html +1 -1
  39. package/modules/hall-ui/public/brand-holes.js +121 -0
  40. package/modules/hall-ui/public/collab.html +1 -1
  41. package/modules/hall-ui/public/collab.js +1 -1
  42. package/modules/hall-ui/public/copy-desk.html +1 -1
  43. package/modules/hall-ui/public/deploy.html +6 -1
  44. package/modules/hall-ui/public/deploy.js +22 -8
  45. package/modules/hall-ui/public/diagrams.html +1 -1
  46. package/modules/hall-ui/public/dom-utils.js +20 -0
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.html +1 -1
  50. package/modules/hall-ui/public/goal-inbox.js +1 -1
  51. package/modules/hall-ui/public/goals.html +1 -1
  52. package/modules/hall-ui/public/government.html +1 -1
  53. package/modules/hall-ui/public/idea.html +1 -1
  54. package/modules/hall-ui/public/idea.js +28 -0
  55. package/modules/hall-ui/public/ideas.html +1 -1
  56. package/modules/hall-ui/public/ideas.js +19 -3
  57. package/modules/hall-ui/public/index.html +5 -1
  58. package/modules/hall-ui/public/modules.html +1 -1
  59. package/modules/hall-ui/public/primer.html +1 -1
  60. package/modules/hall-ui/public/profile-nudge.js +21 -4
  61. package/modules/hall-ui/public/profile.html +1 -1
  62. package/modules/hall-ui/public/profile.js +1 -1
  63. package/modules/hall-ui/public/project-settings.html +1 -1
  64. package/modules/hall-ui/public/ranks.html +1 -1
  65. package/modules/hall-ui/public/roadmap.html +1 -1
  66. package/modules/hall-ui/public/roster.html +1 -1
  67. package/modules/hall-ui/public/sessions.html +1 -1
  68. package/modules/hall-ui/public/settings.html +19 -30
  69. package/modules/hall-ui/public/settings.js +24 -177
  70. package/modules/hall-ui/public/settings.states.json +1 -1
  71. package/modules/hall-ui/public/shell.js +4 -2
  72. package/modules/hall-ui/public/studio.css +14 -8
  73. package/modules/hall-ui/public/studio.html +5 -4
  74. package/modules/hall-ui/public/task.html +1 -1
  75. package/modules/hall-ui/public/thinking.css +6 -3
  76. package/modules/hall-ui/public/thinking.html +1 -1
  77. package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
  78. package/modules/hall-ui/public/tweak-editor.css +142 -11
  79. package/modules/hall-ui/public/tweak-editor.html +25 -9
  80. package/modules/hall-ui/public/tweak-editor.js +243 -10
  81. package/modules/hall-ui/public/watch.html +1 -1
  82. package/modules/hall-ui/public/work.html +1 -1
  83. package/modules/hall-ui/records/approval-queue.md +6 -0
  84. package/modules/hall-ui/records/tweak-editor.md +6 -0
  85. package/modules/ideas/ratify.js +36 -8
  86. package/modules/ideas/routes/ratify-goal.js +6 -0
  87. package/modules/lifecycle/db-analytics.js +79 -10
  88. package/modules/lifecycle/db-claims.js +48 -1
  89. package/modules/lifecycle/db-deps-criteria.js +94 -40
  90. package/modules/lifecycle/db-goals.js +4 -1
  91. package/modules/lifecycle/db-grade.js +9 -1
  92. package/modules/lifecycle/db-relevance-flags.js +183 -0
  93. package/modules/lifecycle/db-versions.js +4 -3
  94. package/modules/lifecycle/db.js +19 -0
  95. package/modules/lifecycle/done-when.js +5 -3
  96. package/modules/lifecycle/est-advisory.js +31 -4
  97. package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
  98. package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
  99. package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
  100. package/modules/lifecycle/module.json +2 -1
  101. package/modules/lifecycle/routes/claims.js +8 -1
  102. package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
  103. package/modules/lifecycle/routes/goals.js +1 -1
  104. package/modules/lifecycle/routes/tasks.js +27 -24
  105. package/modules/lifecycle/routes/visuals.js +9 -0
  106. package/modules/lifecycle/ship-preflight.js +7 -11
  107. package/modules/lifecycle/task-visuals.js +109 -5
  108. package/modules/npm-release/module.json +2 -1
  109. package/modules/npm-release/preview/commands.js +81 -0
  110. package/modules/npm-release/preview/divert.js +95 -0
  111. package/modules/npm-release/preview/env.js +78 -0
  112. package/modules/npm-release/preview/proxy.js +107 -0
  113. package/modules/npm-release/preview/runtime.js +83 -0
  114. package/modules/npm-release/preview/supervisor.js +175 -0
  115. package/modules/npm-release/public/work.js +65 -1
  116. package/modules/npm-release/routes/preview.js +133 -0
  117. package/modules/provisioning/app-status.js +83 -0
  118. package/modules/provisioning/module.json +2 -1
  119. package/modules/provisioning/pollers/app-liveness.js +110 -0
  120. package/modules/provisioning/render-lookup.js +68 -0
  121. package/modules/provisioning/render-standup.js +44 -1
  122. package/modules/provisioning/routes/render-standup.js +81 -6
  123. package/modules/provisioning/starter-bundles.js +4 -1
  124. package/modules/public-landing/public/account-private.states.json +14 -0
  125. package/modules/public-landing/public/account.html +513 -0
  126. package/modules/public-landing/public/account.probes.json +37 -0
  127. package/modules/public-landing/public/account.states.json +16 -0
  128. package/modules/public-landing/public/index.html +5 -2
  129. package/modules/public-landing/public/projects.html +661 -18
  130. package/modules/public-landing/public/projects.states.json +8 -1
  131. package/modules/render-deploy/deploys.js +203 -0
  132. package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
  133. package/modules/render-deploy/module.json +25 -0
  134. package/modules/render-deploy/public/deploy.css +10 -0
  135. package/modules/render-deploy/public/deploy.js +249 -0
  136. package/modules/render-deploy/render.js +56 -0
  137. package/modules/render-deploy/routes/act.js +88 -0
  138. package/modules/render-deploy/routes/app.js +65 -0
  139. package/modules/render-deploy/routes/history.js +38 -0
  140. package/modules/specialities/db.js +27 -8
  141. package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
  142. package/modules/specialities/routes/specialities.js +51 -3
  143. package/modules/specialities/skills.js +94 -0
  144. package/modules/specialities/specialities.js +56 -9
  145. package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
  146. package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
  147. package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
  148. package/modules/ui-design/kit/lib.js +3 -1
  149. package/modules/ui-design/kit/serve.js +96 -0
  150. package/package-lock.json +2 -2
  151. package/package.json +1 -1
  152. package/release-notes.json +169 -0
  153. package/scripts/gds/agents-sync.js +13 -3
  154. package/scripts/gds/autobongos-run.js +80 -2
  155. package/scripts/gds/autobongos-service.cmd +12 -0
  156. package/scripts/gds/copy-apply.js +14 -0
  157. package/scripts/gds/dev-box-guard.js +3 -2
  158. package/scripts/gds/fitness.js +9 -0
  159. package/scripts/gds/grade-correlation-audit.js +42 -12
  160. package/scripts/gds/grade-replay.js +1 -1
  161. package/scripts/gds/provision-core-upgrade.js +38 -7
  162. package/scripts/gds/provision-pin-key.js +176 -0
  163. package/scripts/gds/provision-render.js +62 -31
  164. package/scripts/gds/provision-teardown.js +7 -2
  165. package/scripts/gds/provision-units.js +31 -0
  166. package/scripts/gds/provision.js +10 -10
  167. package/scripts/gds/publish-manifest.js +1 -0
  168. package/scripts/gds/render-api.js +19 -0
  169. package/scripts/gds/render-payload.js +39 -65
  170. package/scripts/gds/run-unit-tests.js +10 -0
  171. package/scripts/gds/ship-finish.js +19 -20
  172. package/scripts/gds/smoke-dependencies.sh +37 -8
  173. package/scripts/gds/status.js +12 -4
  174. package/scripts/gds/upgrade.js +2 -2
  175. package/scripts/public-mirror-export.js +7 -1
  176. package/src/bongos/module-scope-map.js +13 -0
  177. package/src/bongos/serve-internal.js +3 -1
  178. package/src/module-api.js +10 -1
  179. package/src/platform-server.js +39 -0
  180. package/tests/account_privacy_flags.mjs +10 -6
  181. package/tests/agents_authoring.mjs +15 -1
  182. package/tests/agents_sync.mjs +106 -5
  183. package/tests/agents_validate.mjs +45 -0
  184. package/tests/api_path_404.mjs +6 -0
  185. package/tests/autobongos_cadence.mjs +7 -0
  186. package/tests/autobongos_loop.mjs +136 -1
  187. package/tests/autonomy_gauge.mjs +62 -0
  188. package/tests/blocker_hall_live_proof.mjs +275 -0
  189. package/tests/claim_gate_ci_unblock.mjs +116 -0
  190. package/tests/claim_gate_rebase.mjs +15 -3
  191. package/tests/collab_page.mjs +10 -0
  192. package/tests/conductor_main_e2e.mjs +97 -0
  193. package/tests/core_262_drop_box_blocked_db.mjs +143 -0
  194. package/tests/core_upgrade_runner.mjs +1 -0
  195. package/tests/dependency_writes_atomic.mjs +191 -0
  196. package/tests/deploy_page_projects.mjs +2 -1
  197. package/tests/effective_visibility_predicate.mjs +15 -0
  198. package/tests/est_advisory.mjs +27 -0
  199. package/tests/goal_map_page.mjs +26 -0
  200. package/tests/goal_task_relevance.mjs +518 -0
  201. package/tests/goal_working_area.mjs +146 -0
  202. package/tests/grade_attempts.mjs +61 -0
  203. package/tests/grade_attribution.mjs +23 -0
  204. package/tests/grade_correlation_audit.mjs +54 -1
  205. package/tests/grader_reader_lens.mjs +55 -2
  206. package/tests/grader_root_outage.mjs +6 -1
  207. package/tests/grader_subagent_tools_arg.mjs +102 -0
  208. package/tests/hall_approval_queue.mjs +54 -2
  209. package/tests/hall_settings_world.mjs +3 -1
  210. package/tests/hall_tweak_editor.mjs +316 -11
  211. package/tests/hub_account_page.mjs +394 -0
  212. package/tests/idea_detail_page.mjs +64 -0
  213. package/tests/idea_goal_ratification.mjs +64 -0
  214. package/tests/idea_spark_hall.mjs +60 -9
  215. package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
  216. package/tests/landing_page.mjs +6 -3
  217. package/tests/module-scope-map.mjs +31 -8
  218. package/tests/module_api.mjs +1 -0
  219. package/tests/module_loader.mjs +1 -1
  220. package/tests/nav_permission_atoms.mjs +1 -1
  221. package/tests/no_phantom_mirror_workflow.mjs +24 -0
  222. package/tests/npm_release_preview_commands.mjs +89 -0
  223. package/tests/npm_release_preview_divert.mjs +164 -0
  224. package/tests/npm_release_preview_env.mjs +100 -0
  225. package/tests/npm_release_preview_proxy.mjs +162 -0
  226. package/tests/npm_release_preview_routes.mjs +208 -0
  227. package/tests/npm_release_preview_supervisor.mjs +203 -0
  228. package/tests/pin_write_key.mjs +383 -0
  229. package/tests/planning_session_skill.mjs +27 -0
  230. package/tests/platform_boot.mjs +71 -0
  231. package/tests/profile_nudge_links.mjs +86 -0
  232. package/tests/profile_ui_cross_project.mjs +1 -1
  233. package/tests/projects_hub.mjs +2 -2
  234. package/tests/projects_hub_app_status.mjs +249 -0
  235. package/tests/projects_hub_app_step.mjs +282 -52
  236. package/tests/projects_hub_render_connect.mjs +295 -0
  237. package/tests/provision_render.mjs +121 -28
  238. package/tests/provision_settings_apply.mjs +62 -0
  239. package/tests/provisioning_app_status.mjs +204 -0
  240. package/tests/provisioning_render_route.mjs +126 -6
  241. package/tests/render_check.mjs +2 -1
  242. package/tests/render_deploy.mjs +495 -0
  243. package/tests/rot_inbox.mjs +8 -0
  244. package/tests/ship_preflight.mjs +61 -8
  245. package/tests/smoke_dependencies_witness.mjs +46 -0
  246. package/tests/speciality_skills.mjs +214 -0
  247. package/tests/studio_room_height.mjs +70 -0
  248. package/tests/task_detail_includes.mjs +10 -0
  249. package/tests/task_visuals_instance_root.mjs +150 -0
  250. package/tests/tweak_batch_apply.mjs +21 -0
  251. package/tests/ui_design_kit.mjs +36 -4
  252. package/tests/upgrade.mjs +1 -1
  253. package/tests/upgrade_persist_pin.mjs +24 -0
@@ -1,13 +1,19 @@
1
1
  // tests/projects_hub_app_step.mjs — the create wizard's "where your app runs" step,
2
- // EXECUTED (task 1004351, ADR 0345).
2
+ // EXECUTED (task 1004351, ADR 0345), and its Render setup (task 1004352, ADR 0348).
3
3
  //
4
- // The step is a native radio group, so almost all of its behaviour is one rule: the
5
- // review may only ever name a place the owner could actually pick. Render is listed
6
- // but disabled until the connect flow lands (task 1004352), and "decide later" is the
7
- // owner's ruling for the default. A static regex cannot tell a guard that works from
8
- // one that is never consulted, so — as tests/projects_hub_module_picker.mjs does for
9
- // the picker — this slices the block out of projects.html by its own sentinels and
10
- // runs it in `vm` under a small DOM stub.
4
+ // A static regex cannot tell a guard that works from one that is never consulted, so —
5
+ // 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.
7
+ //
8
+ // The load-bearing assertions for Render, because the key reaches the owner's WHOLE
9
+ // Render account:
10
+ // • the key never lands on `state` — the whole of state is the sessionStorage draft —
11
+ // and never in a URL; it is dropped once the setup request is sent
12
+ // • a key that reaches several workspaces lists them all and picks NONE: the owner
13
+ // must choose, and Next refuses until they do
14
+ // • a refused key says so at once, next to the field
15
+ // • a service building another repository cannot be linked
16
+ // • the review only ever names what the owner actually chose
11
17
  //
12
18
  // Run: node --test --test-reporter=tap tests/projects_hub_app_step.mjs
13
19
 
@@ -28,44 +34,110 @@ const to = HUB.indexOf(END, from);
28
34
  assert.ok(from > -1, 'the app-step block opens with its sentinel comment');
29
35
  assert.ok(to > from, 'the app-step block ends where renderReview begins');
30
36
  const BLOCK = HUB.slice(from, to);
37
+ // The Render flow itself is shared with an existing project's page (task 1004184), so it
38
+ // lives outside the wizard; its fence and the page's own node helper come along.
39
+ const CSTART = ' /* ── connecting Render (task 1004352';
40
+ const CEND = ' /* end of connecting Render */';
41
+ const cfrom = HUB.indexOf(CSTART), cto = HUB.indexOf(CEND, cfrom);
42
+ assert.ok(cfrom > -1 && cto > cfrom, 'the shared Render flow is fenced by its sentinels');
43
+ const CONNECT = HUB.slice(cfrom, cto);
44
+ const APPEL = (HUB.match(/function appEl\(tag, cls, text\) \{[\s\S]*?\n \}/) || [''])[0];
45
+ assert.ok(APPEL, 'the page\'s own appEl() is lifted');
31
46
 
32
47
  const PANEL = (HUB.match(/<section class="panel" id="panel-app"[\s\S]*?<\/section>/) || [''])[0];
48
+ const ESC = (HUB.match(/function esc\(s\) \{[\s\S]*?\n \}/) || [''])[0];
49
+ assert.ok(ESC, 'the page\'s own esc() is lifted, not re-implemented');
50
+
51
+ const KEY = 'rnd_abcdefghijklmnopqrstuvwx';
52
+ const TEA_A = 'tea-aaaaaaaa1111', TEA_B = 'tea-bbbbbbbb2222';
53
+ const flush = async () => { for (let i = 0; i < 6; i++) await new Promise((r) => setTimeout(r, 0)); };
33
54
 
34
- // Two radios, built from the real markup so the harness cannot drift from the page.
35
- function radiosFromMarkup(renderEnabled) {
55
+ // The radios, built from the real markup so the harness cannot drift from the page.
56
+ function radiosFromMarkup() {
36
57
  return [...PANEL.matchAll(/<input type="radio" name="appPlatform" value="([a-z-]+)"([^>]*)>/g)].map((m) => ({
37
- name: 'appPlatform',
38
- value: m[1],
39
- disabled: m[1] === 'render' ? !renderEnabled && /\sdisabled/.test(m[2]) : /\sdisabled/.test(m[2]),
40
- checked: /\schecked/.test(m[2]),
58
+ name: 'appPlatform', value: m[1], disabled: /\sdisabled/.test(m[2]), checked: /\schecked/.test(m[2]),
41
59
  }));
42
60
  }
43
61
 
44
- function load({ draft = {}, renderEnabled = false } = {}) {
45
- const inputs = radiosFromMarkup(renderEnabled);
62
+ // A fake DOM node: the flow builds its lists node by node, never from an HTML string,
63
+ // so a node records its children and serialises them for the assertions.
64
+ const escText = (x) => String(x).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
65
+ class Text { constructor(t) { this.text = String(t); } html() { return escText(this.text); } get textContent() { return this.text; } }
66
+ class El {
67
+ constructor(tag, id) { this.tagName = tag; this.id = id || ''; this.children = []; this.attrs = {}; this.listeners = {};
68
+ this.hidden = false; this.value = ''; this.className = ''; this.checked = false; this.disabled = false; this.selected = false; this.type = ''; this.name = ''; }
69
+ appendChild(c) { this.children.push(c); return c; }
70
+ setAttribute(k, v) { this.attrs[k] = String(v); }
71
+ getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }
72
+ set textContent(v) { this.children = v === '' || v == null ? [] : [new Text(v)]; }
73
+ get textContent() { return this.children.map((c) => c.textContent).join(''); }
74
+ addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); }
75
+ fire(ev, event) { (this.listeners[ev] || []).forEach((fn) => fn.call(this, event || { target: this })); }
76
+ html() {
77
+ const a = [this.className ? ` class="${this.className}"` : '', this.type ? ` type="${this.type}"` : '', this.name ? ` name="${this.name}"` : '',
78
+ ...Object.entries(this.attrs).map(([k, v]) => ` ${k}="${v}"`), this.checked ? ' checked' : '', this.disabled ? ' disabled' : '', this.selected ? ' selected' : ''].join('');
79
+ return `<${this.tagName}${a}>${this.children.map((c) => c.html()).join('')}</${this.tagName}>`;
80
+ }
81
+ }
82
+ const document = { createElement: (t) => new El(t), createTextNode: (t) => new Text(t) };
83
+ function el(id) { return new El('div', id); }
84
+
85
+ // fetch answers by URL; every call is recorded, body parsed.
86
+ function load({ draft = {}, answers = {} } = {}) {
87
+ const inputs = radiosFromMarkup();
88
+ const els = {};
89
+ const $ = (id) => (els[id] = els[id] || el(id));
46
90
  let changeHandler = null;
47
91
  const saves = [];
92
+ const calls = [];
48
93
  const root = {
49
94
  querySelector(sel) {
50
95
  if (sel === '#appOpts') return { addEventListener: (ev, fn) => { if (ev === 'change') changeHandler = fn; } };
51
96
  throw new Error('unexpected selector ' + sel);
52
97
  },
53
- querySelectorAll(sel) {
54
- assert.equal(sel, '#appOpts input[name=appPlatform]');
55
- return inputs;
56
- },
98
+ querySelectorAll(sel) { assert.equal(sel, '#appOpts input[name=appPlatform]'); return inputs; },
57
99
  };
58
- const state = Object.assign({ appPlatform: 'later' }, draft);
59
- const ctx = { root, state, saveDraft: () => saves.push(state.appPlatform) };
100
+ const state = Object.assign({ appPlatform: 'later', owner: 'example-owner', repo: 'spike' }, draft);
101
+ const fetch = async (url, init) => {
102
+ const body = init && init.body ? JSON.parse(init.body) : null;
103
+ calls.push({ url, method: init && init.method, body });
104
+ const key = url + (body && body.owner_id ? '#' + body.owner_id : '');
105
+ const a = answers[key] !== undefined ? answers[key] : answers[url];
106
+ if (a === undefined) return { ok: false, status: 404, json: async () => ({ error: { message: 'not found' } }) };
107
+ if (a.status) return { ok: false, status: a.status, json: async () => ({ error: { message: a.message } }) };
108
+ return { ok: true, status: 200, json: async () => a };
109
+ };
110
+ const watches = [];
111
+ const ctx = { root, state, $, document, Object, API: '/api/bongos', fetch, JSON, String, Number, encodeURIComponent, Array, saveDraft: () => saves.push(JSON.stringify(state)),
112
+ // the shared status watch (task 1004353) lives outside this block; record that it is started
113
+ watchApp: (id, box) => { watches.push({ id, box }); return () => {}; } };
60
114
  vm.createContext(ctx);
61
- vm.runInContext(BLOCK + '\nthis.api = { appPlatformChoice, syncAppStep, appRow };', ctx);
62
- return { ...ctx.api, inputs, state, saves, change: (value) => changeHandler({ target: inputs.find((i) => i.value === value) }) };
115
+ vm.runInContext(ESC + '\n' + APPEL + '\n' + CONNECT + '\n' + BLOCK + '\nthis.api = { appPlatformChoice, syncAppStep, appRow, syncRenderSetup, validateRender, renderRequestBody, sendRenderSetup, planLabel };', ctx);
116
+ const api = ctx.api;
117
+ return {
118
+ ...api, inputs, state, saves, calls, $, watches,
119
+ change: (value) => changeHandler({ target: inputs.find((i) => i.value === value) }),
120
+ typeKey: (v) => { $('f-renderKey').value = v; $('f-renderKey').fire('input', { target: $('f-renderKey') }); },
121
+ check: async () => { $('renderCheck').fire('click', { target: $('renderCheck') }); await flush(); },
122
+ pickWorkspace: async (i) => { $('renderWsList').fire('change', { target: { getAttribute: () => String(i) } }); await flush(); },
123
+ pickService: (t) => $('renderSvcList').fire('change', { target: { name: 'renderSvc', disabled: false, ...t } }),
124
+ };
63
125
  }
64
126
 
65
- test('the panel is real markup: Render listed but not pickable yet, Decide later the default', () => {
127
+ const LOOKUP = '/api/bongos/provisioning/render/lookup';
128
+ const TWO = { workspaces: [{ id: TEA_A, name: 'Mine', type: 'team' }, { id: TEA_B, name: 'A friend’s team', type: 'team' }] };
129
+ const SERVICES = { services: [
130
+ { id: 'srv-aaaaaaaa1111', name: 'shop', repo: 'https://github.com/example-owner/spike', url: 'https://shop.onrender.com' },
131
+ { id: 'srv-bbbbbbbb2222', name: 'other', repo: 'https://github.com/example-owner/other-app', url: null },
132
+ ] };
133
+ const ANSWERS = { [LOOKUP]: TWO, [LOOKUP + '#' + TEA_A]: SERVICES };
134
+
135
+ // ── the step itself (task 1004351) ──────────────────────────────────────────
136
+
137
+ test('the panel is real markup: Render and Decide later, Decide later the default', () => {
66
138
  assert.ok(PANEL, '#panel-app exists');
67
- assert.match(PANEL, /<input type="radio" name="appPlatform" value="render" disabled/, 'Render cannot be picked until task 1004352');
68
- assert.match(PANEL, /<span class="soonTag">Coming soon<\/span>/, 'and says so in words, not by colour alone');
139
+ assert.match(PANEL, /<input type="radio" name="appPlatform" value="render" aria-describedby="appRenderWhat">/, 'Render can be picked (task 1004352)');
140
+ assert.doesNotMatch(PANEL, /Coming soon/, 'and is no longer marked soon');
69
141
  assert.match(PANEL, /<input type="radio" name="appPlatform" value="later" checked>/, 'Decide later is the default');
70
142
  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');
71
143
  assert.match(PANEL, /<form data-next="8">/, 'Next leads to the review');
@@ -77,39 +149,28 @@ test('the step sits between Address and Review in the panels map and on the rail
77
149
  assert.match(HUB, /<li data-step="7"><span class="snum">7<\/span><span class="slbl">App<\/span><\/li>/);
78
150
  assert.match(HUB, /<li data-step="8"><span class="snum">8<\/span><span class="slbl">Create<\/span><\/li>/);
79
151
  assert.match(HUB, /data-back="7">← Back<\/button>\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the app step");
80
- assert.match(HUB, /if \(step === 7\) syncAppStep\(\);\n\s*if \(step === 8\) renderReview\(\);/, 'entering the step repaints it from the draft');
152
+ assert.match(HUB, /if \(step === 7\) \{ syncAppStep\(\); syncRenderSetup\(\); \}\n\s*if \(step === 8\) renderReview\(\);/, 'entering the step repaints it, Render setup included');
153
+ assert.match(HUB, /if \(step === 7\) return validateRender\(\);/, 'Next on the step runs the Render check');
81
154
  });
82
155
 
83
- test('a fresh draft reads "decide later"', () => {
156
+ test('a fresh draft reads "decide later", and the Render setup stays hidden', () => {
84
157
  const w = load();
85
158
  assert.equal(w.appPlatformChoice(), 'later');
86
159
  assert.equal(w.appRow(), 'Not chosen yet — you can decide later');
160
+ w.syncRenderSetup();
161
+ assert.equal(w.$('renderSetup').hidden, true);
162
+ assert.equal(w.validateRender(), true, 'decide later never blocks Next');
87
163
  });
88
164
 
89
- test('a draft that names Render while Render is not pickable still reads "decide later"', () => {
90
- // The rule: the review never names a place the owner could not actually pick.
91
- const w = load({ draft: { appPlatform: 'render' } });
92
- assert.equal(w.appPlatformChoice(), 'later');
93
- assert.equal(w.appRow(), 'Not chosen yet — you can decide later');
94
- w.syncAppStep();
95
- assert.deepEqual(w.inputs.filter((i) => i.checked).map((i) => i.value), ['later'], 'and the step shows Decide later checked');
96
- });
97
-
98
- test('once Render is pickable, choosing it is what the review names', () => {
99
- const w = load({ renderEnabled: true });
165
+ test('choosing Render saves the choice and opens its setup', () => {
166
+ const w = load();
100
167
  w.change('render');
101
168
  assert.equal(w.state.appPlatform, 'render');
102
- assert.deepEqual(w.saves, ['render'], 'the choice is saved to the draft');
103
- assert.equal(w.appRow(), 'Render, on your own account');
104
- w.syncAppStep();
105
- assert.deepEqual(w.inputs.filter((i) => i.checked).map((i) => i.value), ['render']);
169
+ assert.equal(JSON.parse(w.saves[0]).appPlatform, 'render');
170
+ assert.equal(w.$('renderSetup').hidden, false);
106
171
  });
107
172
 
108
- test('a change on a disabled radio is ignored, and an unknown or missing value reads "decide later"', () => {
109
- const w = load();
110
- w.change('render');
111
- assert.equal(w.state.appPlatform, 'later', 'a disabled option never reaches the draft');
112
- assert.deepEqual(w.saves, []);
173
+ test('an unknown or missing value reads "decide later"', () => {
113
174
  for (const v of [undefined, '', 'vercel', 'x"]']) {
114
175
  assert.equal(load({ draft: { appPlatform: v } }).appPlatformChoice(), 'later', `appPlatform=${JSON.stringify(v)}`);
115
176
  }
@@ -124,9 +185,6 @@ test('the review carries the hall row and the app row in place of the old Shape
124
185
  });
125
186
 
126
187
  test('the rail grid has one column per step, so adding a step never wraps the rail', () => {
127
- // Adding the app step made the rail eight beats in a seven-column grid: at 1440 the
128
- // Create beat wrapped onto a second row. The count lives in the shared sheet, so the
129
- // two must be checked against each other, not each on its own.
130
188
  const css = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'cosmos.css'), 'utf8');
131
189
  const cols = Number((css.match(/#view-new \.steps\{display:grid;grid-template-columns:repeat\((\d+),1fr\)/) || [])[1]);
132
190
  const beats = [...HUB.matchAll(/<li[^>]*data-step="\d+"/g)].length;
@@ -136,3 +194,175 @@ test('the rail grid has one column per step, so adding a step never wraps the ra
136
194
  test('the hall is spelled one way on this page', () => {
137
195
  assert.doesNotMatch(HUB, /Builders Hall/, 'no "Builders Hall" beside the page\'s own "Builders\' hall"');
138
196
  });
197
+
198
+ // ── Render (task 1004352) ───────────────────────────────────────────────────
199
+
200
+ test('the key field says what the key is used for, and claims no protection it lacks', () => {
201
+ const setup = (PANEL.match(/<div class="renderSetup"[\s\S]*?<div class="hint" id="h-render"/) || [''])[0];
202
+ assert.match(setup, /<input type="password" id="f-renderKey" autocomplete="off"/, 'masked, and never autofilled');
203
+ assert.match(setup, /used once, to create your app on your own Render account, and then thrown away/);
204
+ assert.match(setup, /Render bills you directly/);
205
+ assert.match(setup, /href="https:\/\/dashboard\.render\.com\/u\/settings#api-keys"/, 'links to where Render issues keys');
206
+ assert.doesNotMatch(setup, /encrypt|secure|safe|protected/i, 'no protection claim the design does not give (ADR 0327 §2)');
207
+ });
208
+
209
+ test('the key never lands on the draft, and never in a URL', async () => {
210
+ const w = load({ answers: ANSWERS, draft: { appPlatform: 'render' } });
211
+ w.typeKey(KEY);
212
+ await w.check();
213
+ await w.pickWorkspace(0);
214
+ w.pickService({ value: 'new' });
215
+ w.state.renderStart = 'npm start';
216
+ assert.ok(w.saves.length > 0);
217
+ for (const snap of w.saves) assert.ok(!snap.includes(KEY), 'no draft snapshot carries the key');
218
+ assert.ok(!JSON.stringify(w.state).includes(KEY), 'and neither does state');
219
+ for (const c of w.calls) {
220
+ assert.ok(!c.url.includes(KEY), 'no URL carries it');
221
+ assert.equal(c.method, 'POST', 'it only ever travels in a POST body');
222
+ }
223
+ });
224
+
225
+ test('a key that reaches several workspaces lists them all and picks none', async () => {
226
+ const w = load({ answers: ANSWERS, draft: { appPlatform: 'render' } });
227
+ w.typeKey(KEY);
228
+ await w.check();
229
+ assert.deepEqual(w.calls.map((c) => [c.url, c.body]), [[LOOKUP, { key: KEY }]]);
230
+ assert.equal(w.$('renderWs').hidden, false);
231
+ const list = w.$('renderWsList').html();
232
+ assert.equal((list.match(/name="renderWs"/g) || []).length, 2, 'both workspaces are offered');
233
+ assert.doesNotMatch(list, /checked/, 'and neither is chosen for the owner');
234
+ assert.match(w.$('h-renderKey').textContent, /reaches 2 workspaces — choose/);
235
+ assert.equal(w.validateRender(), false, 'Next refuses until one is chosen');
236
+ assert.match(w.$('h-render').textContent, /Choose the Render workspace/);
237
+ });
238
+
239
+ test('a refused key says so beside the field, and nothing is listed', async () => {
240
+ const w = load({ answers: { [LOOKUP]: { status: 422, message: 'Render refused that key. Check you copied all of it.' } }, draft: { appPlatform: 'render' } });
241
+ w.typeKey(KEY);
242
+ await w.check();
243
+ assert.match(w.$('h-renderKey').textContent, /Render refused that key/);
244
+ assert.match(w.$('h-renderKey').className, /err/);
245
+ assert.equal(w.$('renderWs').hidden, true);
246
+ assert.equal(w.validateRender(), false);
247
+ });
248
+
249
+ test('a malformed key is caught before Render is asked', async () => {
250
+ const w = load({ answers: ANSWERS, draft: { appPlatform: 'render' } });
251
+ w.typeKey('ghp_notrender');
252
+ await w.check();
253
+ assert.equal(w.calls.length, 0);
254
+ assert.match(w.$('h-renderKey').textContent, /starts with “rnd_”/);
255
+ });
256
+
257
+ test('the chosen workspace lists its apps; one building another repository cannot be linked', async () => {
258
+ const w = load({ answers: ANSWERS, draft: { appPlatform: 'render' } });
259
+ w.typeKey(KEY);
260
+ await w.check();
261
+ await w.pickWorkspace(0);
262
+ assert.deepEqual(w.calls[1].body, { key: KEY, owner_id: TEA_A });
263
+ assert.equal(w.state.renderOwnerId, TEA_A);
264
+ const list = w.$('renderSvcList').html();
265
+ assert.match(list, /value="new"/, 'a new app is always an option');
266
+ assert.match(list, /data-i="0">[\s\S]*Link shop/, 'this project\'s own app can be linked');
267
+ assert.match(list, /data-i="1" disabled>[\s\S]*Builds example-owner\/other-app, not this project/, 'another repository\'s cannot');
268
+ });
269
+
270
+ test('Next needs a complete answer: a mode, then a start command for a native runtime', async () => {
271
+ const w = load({ answers: ANSWERS, draft: { appPlatform: 'render' } });
272
+ w.typeKey(KEY);
273
+ await w.check();
274
+ await w.pickWorkspace(0);
275
+ assert.equal(w.validateRender(), false);
276
+ assert.match(w.$('h-render').textContent, /new app or link/);
277
+ w.pickService({ value: 'new' });
278
+ assert.equal(w.validateRender(), false);
279
+ assert.match(w.$('h-render').textContent, /how your app starts/);
280
+ w.state.renderStart = 'npm start';
281
+ assert.equal(w.validateRender(), true);
282
+ w.state.renderRuntime = 'docker'; w.state.renderStart = '';
283
+ assert.equal(w.validateRender(), true, 'docker needs no commands');
284
+ });
285
+
286
+ test('what Create sends: greenfield carries the build and the plans, a link only the service', async () => {
287
+ const w = load({ answers: ANSWERS, draft: { appPlatform: 'render' } });
288
+ w.typeKey(KEY);
289
+ await w.check();
290
+ await w.pickWorkspace(0);
291
+ w.pickService({ value: 'new' });
292
+ Object.assign(w.state, { renderRuntime: 'python', renderBuild: ' pip install -r requirements.txt ', renderStart: 'gunicorn app:app', renderPlan: 'standard', renderDb: true, renderDbPlan: 'basic_1gb' });
293
+ assert.deepEqual({ ...w.renderRequestBody() }, {
294
+ key: KEY, owner_id: TEA_A, mode: 'greenfield', runtime: 'python',
295
+ start_command: 'gunicorn app:app', build_command: 'pip install -r requirements.txt',
296
+ web_plan: 'standard', database: true, db_plan: 'basic_1gb',
297
+ });
298
+ w.state.renderRuntime = 'docker';
299
+ const d = w.renderRequestBody();
300
+ assert.equal(d.start_command, undefined); assert.equal(d.build_command, undefined);
301
+ w.state.renderDb = false;
302
+ assert.equal(w.renderRequestBody().db_plan, undefined, 'no database plan without a database');
303
+ w.pickService({ value: 'x', getAttribute: () => '0' });
304
+ assert.deepEqual({ ...w.renderRequestBody() }, { key: KEY, owner_id: TEA_A, mode: 'adopt', service_id: 'srv-aaaaaaaa1111' });
305
+ });
306
+
307
+ test('after Create the key is sent once, to the new project, and then dropped', async () => {
308
+ const w = load({ answers: { ...ANSWERS, '/api/bongos/provisioning/instances/42/render': { ok: true } }, draft: { appPlatform: 'render' } });
309
+ w.typeKey(KEY);
310
+ await w.check();
311
+ await w.pickWorkspace(0);
312
+ w.pickService({ value: 'new' });
313
+ w.state.renderStart = 'npm start';
314
+ w.sendRenderSetup(42);
315
+ await flush();
316
+ const sent = w.calls.filter((c) => c.url === '/api/bongos/provisioning/instances/42/render');
317
+ assert.equal(sent.length, 1);
318
+ assert.equal(sent[0].body.key, KEY);
319
+ assert.equal(w.$('f-renderKey').value, '', 'the field is emptied');
320
+ assert.equal(w.validateRender(), false, 'the key is gone from the page too');
321
+ assert.match(w.$('doneRender').textContent, /used once, then thrown away/);
322
+ assert.deepEqual(w.watches.map((x) => x.id), [42], 'and the app’s own progress is watched from there (task 1004353)');
323
+ assert.equal(w.watches[0].box, w.$('doneApp'));
324
+ w.sendRenderSetup(42);
325
+ assert.equal(w.calls.filter((c) => c.url.endsWith('/render')).length, 1, 'a second send has nothing to send');
326
+ });
327
+
328
+ test('a refused setup request says the project is still being created', async () => {
329
+ const w = load({ answers: { ...ANSWERS, '/api/bongos/provisioning/instances/42/render': { status: 409, message: 'This project has no GitHub repository linked.' } }, draft: { appPlatform: 'render' } });
330
+ w.typeKey(KEY);
331
+ await w.check();
332
+ await w.pickWorkspace(0);
333
+ w.pickService({ value: 'new' });
334
+ w.state.renderStart = 'npm start';
335
+ w.sendRenderSetup(42);
336
+ await flush();
337
+ assert.match(w.$('doneRender').textContent, /Your project is being created, but your app was not handed to Render: This project has no GitHub/);
338
+ assert.match(w.$('doneRender').className, /err/);
339
+ });
340
+
341
+ test('changing the key forgets what the old key could see', async () => {
342
+ const w = load({ answers: ANSWERS, draft: { appPlatform: 'render' } });
343
+ w.typeKey(KEY);
344
+ await w.check();
345
+ await w.pickWorkspace(0);
346
+ w.typeKey(KEY + 'x');
347
+ assert.equal(w.$('renderWs').hidden, true);
348
+ assert.equal(w.$('renderSvc').hidden, true);
349
+ assert.equal(w.validateRender(), false, 'the new key has to be checked before Next');
350
+ });
351
+
352
+ test('a resumed draft asks for the key again, and says why', () => {
353
+ const w = load({ draft: { appPlatform: 'render', renderOwnerId: TEA_A, renderOwnerName: 'Mine', renderMode: 'greenfield' } });
354
+ w.syncRenderSetup();
355
+ assert.match(w.$('h-renderKey').textContent, /Enter your Render key again — it is not kept, not even in this browser/);
356
+ assert.equal(w.validateRender(), false);
357
+ });
358
+
359
+ test('the review names exactly what was chosen, prices included', () => {
360
+ const g = load({ draft: { appPlatform: 'render', renderOwnerName: 'Mine', renderMode: 'greenfield', renderRuntime: 'node', renderPlan: 'starter', renderDb: true, renderDbPlan: 'basic_256mb' } });
361
+ 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.');
362
+ const a = load({ draft: { appPlatform: 'render', renderOwnerName: 'Mine', renderMode: 'adopt', renderServiceName: '<shop>' } });
363
+ assert.equal(a.appRow(), 'Render, workspace Mine — link <code>&lt;shop&gt;</code>, changing nothing on it', 'a name from Render is escaped');
364
+ });
365
+
366
+ test('Create hands the app to Render straight after the project is filed', () => {
367
+ assert.match(HUB, /show\(9, true\);\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
368
+ });