@bongos/core 1.19.1080 → 1.20.1
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.
- package/.bongos-core.json +573 -188
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +34 -0
- package/clients/bongos-client/index.cjs +34 -0
- package/clients/bongos-client/index.d.ts +53 -4
- package/clients/bongos-client/index.mjs +34 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +2 -0
- package/docs/adr/0347-every-store-module-ships-a-how-to.md +135 -0
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +4 -0
- package/docs/api/openapi.json +995 -40
- package/docs/api-reference.md +33 -8
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1674 -1566
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/migrations/provisioning_028_render_standup.sql +46 -0
- package/modules/provisioning/module.json +4 -2
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/provisioning.js +5 -5
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +134 -0
- package/modules/provisioning/routes/render-standup.js +165 -0
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +173 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +189 -0
- package/scripts/gds/provision-teardown.js +9 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +12 -12
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +197 -0
- package/scripts/gds/render-payload.js +84 -0
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +362 -0
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +336 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/render_api.mjs +153 -0
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- 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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
//
|
|
35
|
-
function radiosFromMarkup(
|
|
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
|
-
|
|
45
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
68
|
-
assert.
|
|
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\(\)
|
|
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('
|
|
90
|
-
|
|
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.
|
|
103
|
-
assert.equal(w
|
|
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('
|
|
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><shop></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
|
+
});
|