@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
@@ -0,0 +1,249 @@
1
+ // tests/projects_hub_app_status.mjs — the app's setup status on the page, EXECUTED
2
+ // (task 1004353). One block serves the wizard's done panel and the project's page, so it
3
+ // is sliced out of projects.html by its own sentinels and run in `vm` over a small fake
4
+ // DOM — the block builds nodes, never HTML strings (modules/public-landing/CLAUDE.md,
5
+ // "No innerHTML"), so the fake records nodes and serialises them for the assertions.
6
+ //
7
+ // The load-bearing assertions:
8
+ // • a live app shows its address and the Render link; a building one says so
9
+ // • a failure shows its one sentence and a Retry — and nothing retries by itself: the
10
+ // watch stops polling on a failure, and only the owner's click sends anything
11
+ // • the Retry asks for the key again, sends it once with the saved answers, and empties
12
+ // the field; a malformed key never leaves the page
13
+ // • a repaint never lands on top of a Retry the owner has opened — even from a fresh
14
+ // watch on the same box — and a watch never paints a page that has moved on
15
+ // • env var NAMES are listed with the link to Render's environment page, and a name
16
+ // the platform already set is not
17
+ // • nothing from the server becomes markup, and a link off Render's own hosts is text
18
+ //
19
+ // Run: node --test --test-reporter=tap tests/projects_hub_app_status.mjs
20
+
21
+ import test from 'node:test';
22
+ import assert from 'node:assert/strict';
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import vm from 'node:vm';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
29
+ const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
30
+ const START = " /* ── the app's setup on Render (task 1004353)";
31
+ const END = " /* end of the app's setup on Render */";
32
+ const from = HUB.indexOf(START), to = HUB.indexOf(END);
33
+ assert.ok(from > -1 && to > from, 'the block is fenced by its sentinels');
34
+ const BLOCK = HUB.slice(from, to);
35
+
36
+ const KEY = 'rnd_abcdefghijklmnopqrstuvwx';
37
+ const LINKS = { service: 'https://dashboard.render.com/web/srv-aaaaaaaa1111', env: 'https://dashboard.render.com/web/srv-aaaaaaaa1111/env' };
38
+ const LIVE = { ok: true, app: { platform: 'render', state: 'live', stage: 'live', mode: 'greenfield', url: 'https://demo.onrender.com', links: LINKS, error: null, retry: null },
39
+ env_vars: [{ name: 'DATABASE_URL', required: true, set_for_you: true }, { name: 'SESSION_SECRET', required: true, set_for_you: false }, { name: 'STRIPE_KEY', required: false, set_for_you: false }] };
40
+ const BUILDING = { ok: true, app: { ...LIVE.app, state: 'deploying', stage: 'building' }, env_vars: [] };
41
+ const FAILED = { ok: true, app: { platform: 'render', state: 'failed', stage: null, mode: 'greenfield', url: null, links: null,
42
+ error: 'This Render key cannot reach workspace tea-aaaaaaaa1111.',
43
+ retry: { owner_id: 'tea-aaaaaaaa1111', mode: 'greenfield', runtime: 'node', start_command: 'npm start', web_plan: 'starter', database: false } }, env_vars: [] };
44
+
45
+ // ── a fake DOM: just what the block uses ──
46
+ const escText = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
47
+ class Text { constructor(t) { this.text = String(t); } html() { return escText(this.text); } get textContent() { return this.text; } }
48
+ class El {
49
+ constructor(tag) { this.tagName = tag; this.children = []; this.attrs = {}; this.className = ''; this.hidden = false; this.listeners = {}; this.value = ''; this.focused = false; }
50
+ appendChild(c) { this.children.push(c); return c; }
51
+ setAttribute(k, v) { this.attrs[k] = String(v); }
52
+ set textContent(v) { this.children = v === '' || v == null ? [] : [new Text(v)]; }
53
+ get textContent() { return this.children.map((c) => c.textContent).join(''); }
54
+ addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); }
55
+ click() { (this.listeners.click || []).forEach((fn) => fn({ target: this })); }
56
+ focus() { this.focused = true; }
57
+ all() { return [this, ...this.children.filter((c) => c instanceof El).flatMap((c) => c.all())]; }
58
+ querySelector(sel) {
59
+ if (sel === '.appRetry:not([hidden])') return this.all().find((e) => e !== this && e.className === 'appRetry' && !e.hidden) || null;
60
+ throw new Error('unexpected selector ' + sel);
61
+ }
62
+ find(pred) { return this.all().find(pred) || null; }
63
+ html() {
64
+ const a = [this.className ? ` class="${this.className}"` : '', this.href ? ` href="${this.href}"` : '', this.target ? ` target="${this.target}"` : '',
65
+ this.rel ? ` rel="${this.rel}"` : '', this.type ? ` type="${this.type}"` : '', this.autocomplete ? ` autocomplete="${this.autocomplete}"` : '',
66
+ ...Object.entries(this.attrs).map(([k, v]) => ` ${k}="${v}"`), this.hidden ? ' hidden' : ''].join('');
67
+ return `<${this.tagName}${a}>${this.children.map((c) => c.html()).join('')}</${this.tagName}>`;
68
+ }
69
+ }
70
+ const document = { createElement: (t) => new El(t), createTextNode: (t) => new Text(t) };
71
+
72
+ function load({ answers = [] } = {}) {
73
+ const calls = [], timers = [];
74
+ let i = 0;
75
+ const fetch = async (url, init) => {
76
+ const body = init && init.body ? JSON.parse(init.body) : null;
77
+ calls.push({ url, method: (init && init.method) || 'GET', body });
78
+ if (init && init.method === 'POST') return { ok: true, status: 200, json: async () => ({ ok: true }) };
79
+ const a = answers[Math.min(i++, answers.length - 1)];
80
+ return { ok: true, status: 200, json: async () => a };
81
+ };
82
+ const ctx = { API: '/api/bongos', fetch, document, JSON, String, Object, encodeURIComponent,
83
+ setTimeout: (fn, ms) => { timers.push({ fn, ms }); return timers.length; }, clearTimeout: () => {} };
84
+ vm.createContext(ctx);
85
+ vm.runInContext(BLOCK + '\nthis.api = { appStatusView, watchApp, appTerminal };', ctx);
86
+ const view = (d) => ctx.api.appStatusView(d);
87
+ return { ...ctx.api, view, html: (d) => { const v = view(d); return v ? v.root.html() : ''; }, calls, timers };
88
+ }
89
+ const flush = async () => { for (let i = 0; i < 6; i++) await new Promise((r) => setTimeout(r, 0)); };
90
+ const byClass = (root, cls) => root.find((e) => e.className === cls);
91
+ const byText = (root, text) => root.find((e) => e.tagName === 'button' && e.textContent === text);
92
+
93
+ test('the block builds nodes, never an HTML string', () => {
94
+ assert.doesNotMatch(BLOCK, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
95
+ });
96
+
97
+ test('a live app: its address, the Render link, and the env var names still to set', () => {
98
+ const w = load();
99
+ const html = w.html(LIVE);
100
+ assert.match(html, /<p>Your app is live at <a href="https:\/\/demo\.onrender\.com" target="_blank" rel="noopener noreferrer">demo\.onrender\.com<\/a>\.<\/p>/);
101
+ assert.match(html, /<a href="https:\/\/dashboard\.render\.com\/web\/srv-aaaaaaaa1111"[^>]*>Open it on Render<\/a>/);
102
+ assert.match(html, /<h3>Still to set on Render<\/h3>/);
103
+ assert.match(html, /<li><code>SESSION_SECRET<\/code><\/li>/);
104
+ assert.match(html, /<li><code>STRIPE_KEY<\/code> — optional<\/li>/);
105
+ assert.doesNotMatch(html, /DATABASE_URL/, 'a name the platform set for them is not asked of them');
106
+ assert.match(html, /Bongos keeps the names only/);
107
+ assert.match(html, /href="https:\/\/dashboard\.render\.com\/web\/srv-aaaaaaaa1111\/env"[^>]*>Open the environment page on Render/);
108
+ assert.match(html, /<li class="done">Creating<\/li><li class="done">Building<\/li><li class="done" aria-current="step">Live<\/li>/);
109
+ });
110
+
111
+ test('building and creating say where it is, with the stage marked', () => {
112
+ const w = load();
113
+ const b = w.html(BUILDING);
114
+ assert.match(b, /<li class="done">Creating<\/li><li class="now" aria-current="step">Building<\/li><li>Live<\/li>/);
115
+ assert.match(b, /Render is building your app/);
116
+ assert.match(w.html({ app: { ...BUILDING.app, stage: 'creating', waiting: true } }), /Waiting for the project to finish setting up/);
117
+ assert.match(w.html({ app: { ...BUILDING.app, mode: 'adopt' } }), /Linked\. Waiting for your app to answer/);
118
+ });
119
+
120
+ test('a failure: the sentence, a Retry, and no promise of a retry by itself', () => {
121
+ const w = load();
122
+ const v = w.view(FAILED);
123
+ const html = v.root.html();
124
+ assert.match(html, /<p class="err">This Render key cannot reach workspace tea-aaaaaaaa1111\.<\/p>/);
125
+ assert.match(html, /Nothing is retried by itself/);
126
+ assert.equal(v.retryBtn.textContent, 'Retry');
127
+ assert.equal(v.retryForm.hidden, true, 'the key field starts closed');
128
+ assert.equal(v.keyEl.type, 'password');
129
+ assert.equal(v.keyEl.autocomplete, 'off');
130
+ assert.doesNotMatch(html, /appStages/, 'no progress rail on a failure');
131
+ });
132
+
133
+ test('nothing from the server becomes markup, and a link off Render\'s hosts is plain text', () => {
134
+ const w = load();
135
+ const html = w.html({ app: { ...FAILED.app, error: '<img src=x onerror=alert(1)>' }, env_vars: [{ name: '<b>X</b>', required: true }] });
136
+ assert.doesNotMatch(html, /<img|<b>X/);
137
+ assert.match(html, /&lt;img src=x onerror=alert\(1\)&gt;/);
138
+ const bad = w.html({ app: { ...LIVE.app, url: 'javascript:alert(1)', links: { service: 'https://evil.example/x', env: 'https://dashboard.render.com.evil.io/' } }, env_vars: [{ name: 'A', required: true }] });
139
+ assert.doesNotMatch(bad, /href=/, 'no link is made from an address that is not Render\'s own');
140
+ assert.match(bad, /Open it on Render/, 'the words stay, as text');
141
+ });
142
+
143
+ test('nothing to show: no platform chosen, or disconnected', () => {
144
+ const w = load();
145
+ assert.equal(w.view({ app: null }), null);
146
+ assert.match(w.html({ app: { state: 'disconnected' } }), /no longer connected to Render/);
147
+ });
148
+
149
+ test('the watch polls while it is working, and STOPS on a failure — the platform never retries', async () => {
150
+ const w = load({ answers: [BUILDING, FAILED] });
151
+ const box = new El('div');
152
+ w.watchApp(42, box, () => true, 5000);
153
+ await flush();
154
+ assert.equal(box.hidden, false);
155
+ assert.match(box.html(), /Building/);
156
+ assert.equal(w.timers.length, 1, 'a working setup is asked again');
157
+ w.timers[0].fn(); await flush();
158
+ assert.match(box.html(), /cannot reach workspace/);
159
+ assert.equal(w.timers.length, 1, 'a failed one is not');
160
+ assert.ok(w.calls.every((c) => c.method === 'GET'), 'and nothing was sent');
161
+ });
162
+
163
+ test('the watch stops on live, hides the card when there is nothing to show, and never paints a page that moved on', async () => {
164
+ const w = load({ answers: [LIVE] });
165
+ const box = new El('div');
166
+ w.watchApp(42, box, () => true, 5000);
167
+ await flush();
168
+ assert.equal(w.timers.length, 0);
169
+ const card = new El('section'); const empty = new El('div');
170
+ load({ answers: [{ ok: true, app: null, env_vars: [] }] }).watchApp(43, empty, () => true, 5000, card);
171
+ await flush();
172
+ assert.equal(card.hidden, true, 'the card hides, not just its contents');
173
+ const gone = new El('div');
174
+ load({ answers: [LIVE] }).watchApp(44, gone, () => false, 5000);
175
+ await flush();
176
+ assert.equal(gone.children.length, 0);
177
+ });
178
+
179
+ test('an offer keeps the card up when there is no status to show — and is asked on every answer (task 1004184)', async () => {
180
+ const seen = [];
181
+ const card = new El('section'); const box = new El('div');
182
+ load({ answers: [{ ok: true, app: null, env_vars: [] }] }).watchApp(45, box, () => true, 5000, card, (d) => { seen.push(d); return true; });
183
+ await flush();
184
+ assert.equal(card.hidden, false, 'the offer holds the card open');
185
+ assert.equal(box.children.length, 0, 'with no status of its own');
186
+ assert.deepEqual(seen, [{ ok: true, app: null, env_vars: [] }], 'the offer sees the answer');
187
+ const c2 = new El('section');
188
+ load({ answers: [LIVE] }).watchApp(46, new El('div'), () => true, 5000, c2, () => false);
189
+ await flush();
190
+ assert.equal(c2.hidden, false, 'a status shows whatever the offer says');
191
+ });
192
+
193
+ test('Retry asks for the key, sends it once with the saved answers, and empties the field', async () => {
194
+ const w = load({ answers: [FAILED, BUILDING] });
195
+ const box = new El('div');
196
+ w.watchApp(42, box, () => true, 5000);
197
+ await flush();
198
+ byText(box, 'Retry').click();
199
+ const form = byClass(box, 'appRetry'), keyEl = byClass(box, 'appRetryKey');
200
+ assert.equal(form.hidden, false, 'the key field opens');
201
+ assert.equal(keyEl.focused, true);
202
+ assert.equal(byText(box, 'Retry').hidden, true);
203
+ keyEl.value = KEY;
204
+ byText(box, 'Retry with this key').click();
205
+ assert.equal(keyEl.value, '', 'the field is emptied at once');
206
+ await flush();
207
+ const posts = w.calls.filter((c) => c.method === 'POST');
208
+ assert.equal(posts.length, 1);
209
+ assert.equal(posts[0].url, '/api/bongos/provisioning/instances/42/render');
210
+ assert.deepEqual(posts[0].body, { ...FAILED.app.retry, key: KEY });
211
+ assert.match(box.html(), /Building/, 'and the status is read again');
212
+ });
213
+
214
+ test('a malformed key never leaves the page', async () => {
215
+ const w = load({ answers: [FAILED] });
216
+ const box = new El('div');
217
+ w.watchApp(42, box, () => true, 5000);
218
+ await flush();
219
+ byText(box, 'Retry').click();
220
+ byClass(box, 'appRetryKey').value = 'ghp_nope';
221
+ byText(box, 'Retry with this key').click();
222
+ await flush();
223
+ assert.equal(w.calls.filter((c) => c.method === 'POST').length, 0);
224
+ assert.match(byClass(box, 'hint appRetryHint err').textContent, /starts with “rnd_”/);
225
+ });
226
+
227
+ test('a repaint never lands on top of an open Retry — from this watch or a fresh one on the same box', async () => {
228
+ const w = load({ answers: [FAILED, FAILED] });
229
+ const box = new El('div');
230
+ w.watchApp(42, box, () => true, 5000);
231
+ await flush();
232
+ byText(box, 'Retry').click();
233
+ byClass(box, 'appRetryKey').value = 'rnd_half-typed';
234
+ const form = byClass(box, 'appRetry');
235
+ // the project page reloads and starts a new watch on the same box
236
+ w.watchApp(42, box, () => true, 5000);
237
+ await flush();
238
+ assert.equal(byClass(box, 'appRetry'), form, 'the open form is the same node');
239
+ assert.equal(byClass(box, 'appRetryKey').value, 'rnd_half-typed', 'with what the owner was typing');
240
+ });
241
+
242
+ test('both surfaces use it: the done panel and the project page', () => {
243
+ assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 9; \}, 5000\);/);
244
+ assert.match(HUB, /<div id="doneApp" hidden><\/div>/);
245
+ assert.match(HUB, /<div class="card mSec" id="mAppCard" hidden>\n\s*<h2>Where its app runs<\/h2>\n\s*<div id="mApp"><\/div>/);
246
+ assert.match(HUB, /loadAppStatus\(inst\.id \|\| asked, inst\);/);
247
+ // the project page also hands in its Connect Render offer (task 1004184, tests/projects_hub_render_connect.mjs)
248
+ assert.match(HUB, /stopAppWatch = watchApp\(id, box, function \(\) \{ return String\(manageId\) === mine; \}, 10000, card, offerAppConnect\);/);
249
+ });