@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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// tests/projects_hub_render_connect.mjs — Connect Render from an existing project's page,
|
|
2
|
+
// EXECUTED (task 1004184, ADR 0345).
|
|
3
|
+
//
|
|
4
|
+
// A project whose app has no platform yet (made before the wizard's app step, or whose
|
|
5
|
+
// owner chose "Decide later") gets Connect Render on its "Where its app runs" card. It is
|
|
6
|
+
// the wizard's own flow — its markup cloned, its logic renderConnect() — so this slices the
|
|
7
|
+
// page's region and the shared flow out of projects.html by their sentinels and runs them
|
|
8
|
+
// in `vm` over a small fake DOM built from the wizard's real Render markup.
|
|
9
|
+
//
|
|
10
|
+
// The load-bearing assertions:
|
|
11
|
+
// • it is offered only on the owner's own "no platform" answer, and only for a project
|
|
12
|
+
// the server would take it for; a project that never connects is left as it was
|
|
13
|
+
// • it is the wizard's flow, not a second one: the same ids, prefixed, and the same checks
|
|
14
|
+
// • the key is sent once, in a POST body to THIS project, and forgotten at once — on a
|
|
15
|
+
// refusal too — and the card then shows the app's own progress
|
|
16
|
+
// • moving to another project forgets every answer
|
|
17
|
+
//
|
|
18
|
+
// Run: node --test --test-reporter=tap tests/projects_hub_render_connect.mjs
|
|
19
|
+
|
|
20
|
+
import test from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import vm from 'node:vm';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
29
|
+
|
|
30
|
+
function fence(start, end) {
|
|
31
|
+
const from = HUB.indexOf(start), to = HUB.indexOf(end, from);
|
|
32
|
+
assert.ok(from > -1 && to > from, `fenced: ${start.trim()}`);
|
|
33
|
+
return HUB.slice(from, to);
|
|
34
|
+
}
|
|
35
|
+
const CONNECT = fence(' /* ── connecting Render (task 1004352', ' /* end of connecting Render */');
|
|
36
|
+
const PAGE = fence(' var stopAppWatch = null;', " /* end of Connect Render from the project's page */");
|
|
37
|
+
const APPEL = (HUB.match(/function appEl\(tag, cls, text\) \{[\s\S]*?\n \}/) || [''])[0];
|
|
38
|
+
assert.ok(APPEL, 'the page\'s own appEl() is lifted');
|
|
39
|
+
const SETUP = (HUB.match(/<div class="renderSetup" id="renderSetup" hidden>[\s\S]*?<div class="hint" id="h-render" role="status"> <\/div>/) || [''])[0];
|
|
40
|
+
assert.ok(SETUP, 'the wizard\'s Render markup is found');
|
|
41
|
+
const CARD = (HUB.match(/<div class="card mSec" id="mAppCard" hidden>[\s\S]*?\n <\/div>\n/) || [''])[0];
|
|
42
|
+
|
|
43
|
+
const KEY = 'rnd_abcdefghijklmnopqrstuvwx';
|
|
44
|
+
const TEA_A = 'tea-aaaaaaaa1111';
|
|
45
|
+
const LOOKUP = '/api/bongos/provisioning/render/lookup';
|
|
46
|
+
const RENDER = '/api/bongos/provisioning/instances/7/render';
|
|
47
|
+
const SERVICES = { services: [{ id: 'srv-aaaaaaaa1111', name: 'shop', repo: 'https://github.com/example-owner/shop', url: null }] };
|
|
48
|
+
const INST = { id: 7, slug: 'shop', status: 'active', target_ref: 'example-owner/shop' };
|
|
49
|
+
const flush = async () => { for (let i = 0; i < 8; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
50
|
+
|
|
51
|
+
// ── a fake DOM: what the region and the flow use ──
|
|
52
|
+
const escText = (x) => String(x).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
53
|
+
class Text { constructor(t) { this.text = String(t); } html() { return escText(this.text); } get textContent() { return this.text; } }
|
|
54
|
+
class El {
|
|
55
|
+
constructor(tag, id) { this.tagName = tag; this.id = id || ''; this.children = []; this.attrs = {}; this.listeners = {};
|
|
56
|
+
this.hidden = false; this.value = ''; this.className = ''; this.checked = false; this.disabled = false; this.type = ''; this.name = ''; this.focused = false; }
|
|
57
|
+
appendChild(c) { this.children.push(c); return c; }
|
|
58
|
+
insertBefore(c, ref) { const i = this.children.indexOf(ref); if (i < 0) this.children.push(c); else this.children.splice(i, 0, c); return c; }
|
|
59
|
+
get firstChild() { return this.children[0] || null; }
|
|
60
|
+
setAttribute(k, v) { this.attrs[k] = String(v); }
|
|
61
|
+
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }
|
|
62
|
+
set textContent(v) { this.children = v === '' || v == null ? [] : [new Text(v)]; }
|
|
63
|
+
get textContent() { return this.children.map((c) => c.textContent).join(''); }
|
|
64
|
+
addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); }
|
|
65
|
+
fire(ev, event) { (this.listeners[ev] || []).forEach((fn) => fn.call(this, event || { target: this })); }
|
|
66
|
+
focus() { this.focused = true; }
|
|
67
|
+
all() { return [this, ...this.children.filter((c) => c instanceof El).flatMap((c) => c.all())]; }
|
|
68
|
+
querySelectorAll(sel) {
|
|
69
|
+
const kids = this.all().slice(1);
|
|
70
|
+
if (sel === '[id]') return kids.filter((e) => e.id);
|
|
71
|
+
if (sel === '[aria-describedby]') return kids.filter((e) => e.getAttribute('aria-describedby') != null);
|
|
72
|
+
throw new Error('unexpected selector ' + sel);
|
|
73
|
+
}
|
|
74
|
+
cloneNode() {
|
|
75
|
+
const c = new El(this.tagName, this.id);
|
|
76
|
+
Object.assign(c.attrs, this.attrs); c.className = this.className; c.hidden = this.hidden; c.value = this.value; c.type = this.type;
|
|
77
|
+
this.children.forEach((k) => c.appendChild(k instanceof El ? k.cloneNode(true) : new Text(k.text)));
|
|
78
|
+
return c;
|
|
79
|
+
}
|
|
80
|
+
html() {
|
|
81
|
+
const a = [this.id ? ` id="${this.id}"` : '', this.className ? ` class="${this.className}"` : '', this.type ? ` type="${this.type}"` : '', this.name ? ` name="${this.name}"` : '',
|
|
82
|
+
...Object.entries(this.attrs).map(([k, v]) => ` ${k}="${v}"`), this.checked ? ' checked' : '', this.disabled ? ' disabled' : ''].join('');
|
|
83
|
+
return `<${this.tagName}${a}>${this.children.map((c) => c.html()).join('')}</${this.tagName}>`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The wizard's #renderSetup, one node per id in the REAL markup (flat: the flow only ever
|
|
88
|
+
// finds its elements by id), so a field added to the step is here the day it lands.
|
|
89
|
+
function wizardSetup() {
|
|
90
|
+
const root = new El('div', 'renderSetup');
|
|
91
|
+
root.className = 'renderSetup'; root.hidden = true;
|
|
92
|
+
for (const m of SETUP.matchAll(/<(\w+)[^>]*\sid="([^"]+)"([^>]*)>/g)) {
|
|
93
|
+
if (m[2] === 'renderSetup') continue;
|
|
94
|
+
const e = new El(m[1], m[2]);
|
|
95
|
+
const d = (m[0].match(/aria-describedby="([^"]+)"/) || [])[1];
|
|
96
|
+
if (d) e.setAttribute('aria-describedby', d);
|
|
97
|
+
if (m[1] === 'input') e.type = (m[0].match(/type="([^"]+)"/) || [])[1] || 'text';
|
|
98
|
+
root.appendChild(e);
|
|
99
|
+
}
|
|
100
|
+
return root;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function load({ answers = {} } = {}) {
|
|
104
|
+
const roots = [wizardSetup()];
|
|
105
|
+
const card = new El('div', 'mAppCard'); card.hidden = true;
|
|
106
|
+
['mApp', 'mAppConnect', 'mAppConnectOpen', 'mAppConnectForm', 'mAppConnectGo', 'mAppConnectCancel'].forEach((id) => {
|
|
107
|
+
const e = new El(id.endsWith('Open') || id.endsWith('Go') || id.endsWith('Cancel') ? 'button' : 'div', id);
|
|
108
|
+
if (id === 'mAppConnect' || id === 'mAppConnectForm') e.hidden = true;
|
|
109
|
+
card.appendChild(e);
|
|
110
|
+
});
|
|
111
|
+
roots.push(card);
|
|
112
|
+
const byId = (id) => { for (const r of roots) { const e = r.all().find((x) => x.id === id); if (e) return e; } return null; };
|
|
113
|
+
const document = { getElementById: byId, createElement: (t) => new El(t), createTextNode: (t) => new Text(t) };
|
|
114
|
+
const calls = [], watches = [];
|
|
115
|
+
const fetch = async (url, init) => {
|
|
116
|
+
const body = init && init.body ? JSON.parse(init.body) : null;
|
|
117
|
+
calls.push({ url, method: (init && init.method) || 'GET', body });
|
|
118
|
+
const k = url + (body && body.owner_id && url === LOOKUP ? '#' + body.owner_id : '');
|
|
119
|
+
const a = answers[k] !== undefined ? answers[k] : answers[url];
|
|
120
|
+
if (a === undefined) return { ok: false, status: 404, json: async () => ({ error: { message: 'not found' } }) };
|
|
121
|
+
if (a.status) return { ok: false, status: a.status, json: async () => ({ error: { message: a.message } }) };
|
|
122
|
+
return { ok: true, status: 200, json: async () => a };
|
|
123
|
+
};
|
|
124
|
+
// the status watch lives in the task-1004353 block; record it, and let a test drive its offer
|
|
125
|
+
const watchApp = (id, box, alive, everyMs, cardEl, offer) => { watches.push({ id, box, alive, everyMs, card: cardEl, offer }); return () => {}; };
|
|
126
|
+
const ctx = { document, fetch, watchApp, API: '/api/bongos', manageId: 7, JSON, String, Number, Object, encodeURIComponent };
|
|
127
|
+
ctx.$ = byId;
|
|
128
|
+
vm.createContext(ctx);
|
|
129
|
+
vm.runInContext(APPEL + '\n' + CONNECT + '\n' + PAGE + '\nthis.api = { loadAppStatus, offerAppConnect };', ctx);
|
|
130
|
+
const q = (id) => byId('m-' + id);
|
|
131
|
+
// What the watch does with an answer: show the card when the status view or the offer has something.
|
|
132
|
+
const paint = (data) => { const w = watches[watches.length - 1]; const offered = w.offer(data); card.hidden = !(data && data.app) && !offered; return offered; };
|
|
133
|
+
return { ...ctx.api, ctx, card, byId, q, calls, watches, paint, wizard: roots[0],
|
|
134
|
+
open: () => byId('mAppConnectOpen').fire('click'),
|
|
135
|
+
typeKey: (v) => { q('f-renderKey').value = v; q('f-renderKey').fire('input'); },
|
|
136
|
+
check: async () => { q('renderCheck').fire('click'); await flush(); },
|
|
137
|
+
pickWorkspace: async (i) => { q('renderWsList').fire('change', { target: { getAttribute: () => String(i) } }); await flush(); },
|
|
138
|
+
pickNew: () => q('renderSvcList').fire('change', { target: { name: 'm-renderSvc', value: 'new', disabled: false } }),
|
|
139
|
+
go: async () => { byId('mAppConnectGo').fire('click'); await flush(); },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const NONE = { ok: true, app: null, env_vars: [], values_held: false };
|
|
143
|
+
const ANSWERS = { [LOOKUP]: { workspaces: [{ id: TEA_A, name: 'Mine', type: 'team' }] }, [LOOKUP + '#' + TEA_A]: SERVICES };
|
|
144
|
+
|
|
145
|
+
async function readyToSend(w) {
|
|
146
|
+
w.loadAppStatus(7, INST); w.paint(NONE); w.open();
|
|
147
|
+
w.typeKey(KEY); await w.check(); await w.pickWorkspace(0); w.pickNew();
|
|
148
|
+
w.q('f-renderStart').value = 'npm start';
|
|
149
|
+
w.q('renderNew').fire('input', { target: w.q('f-renderStart') });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── the markup and the one-flow rule ────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
test('the card carries Connect Render, and the page builds nodes, never an HTML string', () => {
|
|
155
|
+
assert.ok(CARD, '#mAppCard exists');
|
|
156
|
+
assert.match(CARD, /<div id="mApp"><\/div>\n\s*<div class="appConnect" id="mAppConnect" hidden>/, 'under the status, hidden until offered');
|
|
157
|
+
assert.match(CARD, /<button type="button" class="btn small" id="mAppConnectOpen">Connect Render<\/button>/);
|
|
158
|
+
assert.match(CARD, /We keep hosting this project’s Builders' hall either way/, 'it says the hall stays with us (ADR 0345 D1)');
|
|
159
|
+
for (const [name, src] of [['the page region', PAGE], ['the shared flow', CONNECT]]) {
|
|
160
|
+
assert.doesNotMatch(src, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/, `${name} builds nodes`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('the wizard and the page run the same flow — renderConnect() is called from both, and defined once', () => {
|
|
165
|
+
assert.equal((HUB.match(/function renderConnect\(/g) || []).length, 1);
|
|
166
|
+
assert.match(HUB, /var rc = renderConnect\(\$, state, \{/, 'the wizard');
|
|
167
|
+
assert.match(PAGE, /appConnect = renderConnect\(appConnectQ, appConnectSt, \{/, 'the project page');
|
|
168
|
+
assert.match(PAGE, /\$\('renderSetup'\)\.cloneNode\(true\)/, 'the page borrows the wizard\'s markup, not a copy of it');
|
|
169
|
+
assert.equal((HUB.match(/function renderLookup\(|function paintWorkspaces\(\) \{\n var list = renderWorkspaces/g) || []).length, 0, 'the wizard\'s old private copy is gone');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('the added styles keep links on the link ink', () => {
|
|
173
|
+
const css = (HUB.match(/\/\* the same step on a project's page \(task 1004184\)[\s\S]*?\n(?!\.appConnect)/) || [''])[0];
|
|
174
|
+
assert.ok(css.includes('.appConnect'), 'the block is found');
|
|
175
|
+
assert.doesNotMatch(css, /--accent-deep/);
|
|
176
|
+
assert.match(HUB, /\.renderWhy a\{[^}]*color:var\(--link-ink\)/, 'the key-help link the page borrows is on --link-ink');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── when it is offered ──────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
test('offered on the owner\'s own "no platform" answer, for a project the server would take', () => {
|
|
182
|
+
const w = load();
|
|
183
|
+
w.loadAppStatus(7, INST);
|
|
184
|
+
assert.equal(w.watches.length, 1);
|
|
185
|
+
assert.equal(w.watches[0].card, w.byId('mAppCard'), 'the watch is handed the card');
|
|
186
|
+
assert.equal(w.paint(NONE), true);
|
|
187
|
+
assert.equal(w.byId('mAppConnect').hidden, false);
|
|
188
|
+
assert.equal(w.card.hidden, false);
|
|
189
|
+
assert.equal(w.byId('mAppConnectForm').hidden, true, 'the form waits for the owner to open it');
|
|
190
|
+
assert.equal(w.calls.length, 0, 'nothing is asked of Render until then');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('not offered — and the card stays hidden — when the project could not take it or the answer is not the owner\'s', () => {
|
|
194
|
+
for (const [why, inst, data] of [
|
|
195
|
+
['someone else\'s project: the owner-only read answered nothing', INST, null],
|
|
196
|
+
['a project already on Render', INST, { ok: true, app: { platform: 'render', state: 'live', stage: 'live' }, env_vars: [] }],
|
|
197
|
+
['a removed project', { ...INST, status: 'torn_down' }, NONE],
|
|
198
|
+
['a project being removed', { ...INST, status: 'tearing_down' }, NONE],
|
|
199
|
+
['no repository to build', { ...INST, target_ref: null }, NONE],
|
|
200
|
+
]) {
|
|
201
|
+
const w = load();
|
|
202
|
+
w.loadAppStatus(7, inst);
|
|
203
|
+
assert.equal(w.paint(data), false, why);
|
|
204
|
+
assert.equal(w.byId('mAppConnect').hidden, true, why);
|
|
205
|
+
if (!(data && data.app)) assert.equal(w.card.hidden, true, why + ': the card hides');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── the flow ────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
test('opening it borrows the wizard\'s step under prefixed ids, and leaves the wizard\'s own alone', () => {
|
|
212
|
+
const w = load();
|
|
213
|
+
w.loadAppStatus(7, INST); w.paint(NONE);
|
|
214
|
+
w.open();
|
|
215
|
+
const copy = w.byId('m-renderSetup');
|
|
216
|
+
assert.ok(copy, 'the step is cloned in');
|
|
217
|
+
assert.equal(w.byId('mAppConnectForm').firstChild, copy, 'above the Connect / Cancel buttons');
|
|
218
|
+
assert.equal(copy.hidden, false);
|
|
219
|
+
assert.equal(w.byId('mAppConnectForm').hidden, false);
|
|
220
|
+
assert.equal(w.byId('mAppConnectOpen').hidden, true);
|
|
221
|
+
assert.equal(w.q('f-renderKey').focused, true, 'focus lands on the key');
|
|
222
|
+
assert.equal(w.q('f-renderKey').getAttribute('aria-describedby'), 'm-h-renderKey', 'its description points at its own hint');
|
|
223
|
+
assert.ok(w.wizard.all().every((e) => !e.id.startsWith('m-')), 'the wizard keeps its ids');
|
|
224
|
+
w.byId('mAppConnectCancel').fire('click'); w.open();
|
|
225
|
+
assert.equal(w.byId('mAppConnectForm').children.filter((c) => c.id === 'm-renderSetup').length, 1, 'cloned once, however often it is opened');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('the same checks as the wizard, with the page\'s own words for a missing key', async () => {
|
|
229
|
+
const w = load({ answers: ANSWERS });
|
|
230
|
+
w.loadAppStatus(7, INST); w.paint(NONE); w.open();
|
|
231
|
+
await w.go();
|
|
232
|
+
assert.match(w.q('h-render').textContent, /^Check your Render key first\.$/, 'no "Decide later" here — there is no such choice on this card');
|
|
233
|
+
w.typeKey('ghp_notrender'); await w.check();
|
|
234
|
+
assert.match(w.q('h-renderKey').textContent, /starts with “rnd_”/);
|
|
235
|
+
assert.equal(w.calls.length, 0, 'a malformed key never leaves the page');
|
|
236
|
+
w.typeKey(KEY); await w.check(); await w.pickWorkspace(0);
|
|
237
|
+
const list = w.q('renderSvcList').html();
|
|
238
|
+
assert.match(list, /name="m-renderSvc" value="new"/, 'its own radio group, so it never joins the wizard\'s');
|
|
239
|
+
assert.match(list, /Render builds it from <code>example-owner\/shop<\/code>/, 'the project\'s own repository');
|
|
240
|
+
assert.match(list, /data-i="0">[\s\S]*Link shop/, 'its own app can be linked');
|
|
241
|
+
await w.go();
|
|
242
|
+
assert.match(w.q('h-render').textContent, /new app or link/);
|
|
243
|
+
assert.equal(w.calls.filter((c) => c.url === RENDER).length, 0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('Connect sends the key once, to this project, forgets it, and the card shows the app\'s progress', async () => {
|
|
247
|
+
const w = load({ answers: { ...ANSWERS, [RENDER]: { ok: true, queued: true } } });
|
|
248
|
+
await readyToSend(w);
|
|
249
|
+
await w.go();
|
|
250
|
+
const sent = w.calls.filter((c) => c.url === RENDER);
|
|
251
|
+
assert.equal(sent.length, 1);
|
|
252
|
+
assert.equal(sent[0].method, 'POST');
|
|
253
|
+
assert.deepEqual(sent[0].body, { key: KEY, owner_id: TEA_A, mode: 'greenfield', runtime: 'node', start_command: 'npm start', web_plan: 'starter', database: false });
|
|
254
|
+
for (const c of w.calls) assert.ok(!c.url.includes(KEY), 'no URL carries the key');
|
|
255
|
+
assert.equal(w.q('f-renderKey').value, '', 'the field is emptied');
|
|
256
|
+
assert.equal(w.byId('mAppConnectForm').hidden, true, 'the form closes');
|
|
257
|
+
assert.equal(w.watches.length, 2, 'the status is read again');
|
|
258
|
+
assert.equal(String(w.watches[1].id), '7');
|
|
259
|
+
assert.equal(w.paint({ ok: true, app: { platform: 'render', state: 'provisioning', stage: 'creating', waiting: true }, env_vars: [] }), false, 'and once the app has a platform, the offer steps aside');
|
|
260
|
+
assert.equal(w.byId('mAppConnect').hidden, true);
|
|
261
|
+
assert.equal(w.card.hidden, false, 'for the progress panel');
|
|
262
|
+
await w.go();
|
|
263
|
+
assert.equal(w.calls.filter((c) => c.url === RENDER).length, 1, 'a second press has no key to send');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('a refusal says so in the server\'s words, and the key is gone all the same', async () => {
|
|
267
|
+
const w = load({ answers: { ...ANSWERS, [RENDER]: { status: 409, message: 'A \'provision\' run is running for this project, so your key was not kept. Try again once it has finished.' } } });
|
|
268
|
+
await readyToSend(w);
|
|
269
|
+
await w.go();
|
|
270
|
+
assert.match(w.q('h-render').textContent, /A 'provision' run is running for this project/);
|
|
271
|
+
assert.match(w.q('h-render').className, /err/);
|
|
272
|
+
assert.equal(w.q('f-renderKey').value, '');
|
|
273
|
+
assert.equal(w.byId('mAppConnectGo').disabled, false, 'the owner can try again');
|
|
274
|
+
assert.equal(w.watches.length, 1, 'no progress to watch');
|
|
275
|
+
await w.go();
|
|
276
|
+
assert.match(w.q('h-render').textContent, /Check your Render key first/, 'a retry starts with the key');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('Cancel forgets the key; a reload of the same project keeps the open form; another project forgets every answer', async () => {
|
|
280
|
+
const w = load({ answers: ANSWERS });
|
|
281
|
+
w.loadAppStatus(7, INST); w.paint(NONE); w.open();
|
|
282
|
+
w.typeKey(KEY); await w.check(); await w.pickWorkspace(0);
|
|
283
|
+
w.loadAppStatus(7, INST); w.paint(NONE);
|
|
284
|
+
assert.equal(w.byId('mAppConnectForm').hidden, false, 'the page\'s reload of this project leaves the form open');
|
|
285
|
+
assert.equal(w.q('f-renderKey').value, KEY, 'with what the owner typed');
|
|
286
|
+
w.byId('mAppConnectCancel').fire('click');
|
|
287
|
+
assert.equal(w.q('f-renderKey').value, '');
|
|
288
|
+
assert.equal(w.byId('mAppConnectForm').hidden, true);
|
|
289
|
+
assert.equal(w.byId('mAppConnectOpen').hidden, false);
|
|
290
|
+
w.ctx.manageId = 8;
|
|
291
|
+
w.loadAppStatus(8, { ...INST, id: 8, target_ref: 'example-owner/other' });
|
|
292
|
+
w.paint(NONE); w.open();
|
|
293
|
+
assert.equal(w.q('renderWs').hidden, true, 'nothing Render said about project 7 is shown for project 8');
|
|
294
|
+
assert.match(w.q('h-renderKey').textContent, /^\s*$/, 'and it is not asked to re-enter a key it never gave');
|
|
295
|
+
});
|