@bongos/core 1.21.92 → 1.21.94
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 +86 -56
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +1 -0
- package/docs/api/openapi.json +38 -7
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +354 -354
- package/docs/copy-registry.json +420 -420
- package/docs/design/project-startup-direction.md +7 -0
- package/docs/module-api-changelog.md +9 -0
- package/docs/page-readings.json +963 -992
- package/modules/autonomy/routes/autonomy.js +14 -5
- package/modules/government/decision-board.js +13 -3
- package/modules/government/decision-rules.js +61 -13
- package/modules/lifecycle/routes/done-when.js +2 -2
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/modules/lifecycle/routes/goals.js +4 -4
- package/modules/lifecycle/routes/project-settings.js +2 -2
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/modules/lifecycle/routes/versions.js +1 -1
- package/modules/npm-release/routes/release.js +1 -0
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/provisioning/github-setup.js +129 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/provisioning.js +3 -0
- package/modules/provisioning/routes/callback-page.js +66 -1
- package/modules/provisioning/routes/core-upgrade.js +11 -0
- package/modules/provisioning/routes/github-setup.js +113 -0
- package/modules/provisioning/routes/provisioning.js +11 -45
- package/modules/public-landing/public/projects.html +105 -57
- package/modules/render-deploy/routes/act.js +1 -1
- package/modules/security/routes/reports.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/fitness.js +7 -0
- package/scripts/gds/oauth-secret.js +4 -2
- package/scripts/gds/provision-github-setup.js +148 -0
- package/scripts/gds/provision.js +19 -5
- package/src/bongos/act-alone.js +84 -0
- package/src/bongos/routes/builders.js +6 -1
- package/src/module-api.js +7 -1
- package/tests/autonomy_fence_priority.mjs +3 -1
- package/tests/github_one_trip.mjs +297 -0
- package/tests/government_act_alone_buildings.mjs +430 -0
- package/tests/module_api.mjs +1 -0
- package/tests/npm_release_release.mjs +2 -1
- package/tests/projects_hub_dns_ready.mjs +2 -1
- package/tests/projects_hub_pre_uat.mjs +17 -9
- package/tests/provision.mjs +51 -9
- package/tests/provisioning_github_app_routes.mjs +94 -8
- package/tests/ship_mode.mjs +2 -1
- package/tests/wizard_github_feedback.mjs +8 -5
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// tests/github_one_trip.mjs — "Connect GitHub in one trip: approving the app goes straight
|
|
2
|
+
// to installing it on the repo, and the checklist ticks itself" (task 1004649).
|
|
3
|
+
//
|
|
4
|
+
// Four layers, DB-free and network-free:
|
|
5
|
+
// 1. modules/provisioning/github-setup.js — each checklist step's state from server facts,
|
|
6
|
+
// the honest "not yet" states included; the install link; the verify window (PURE).
|
|
7
|
+
// 2. scripts/gds/provision-github-setup.js — the runner's check, asked of GitHub with the
|
|
8
|
+
// app's OWN key: the creds read back out of a REAL webEnvBody, a JWT that verifies,
|
|
9
|
+
// installed / not installed / cannot tell, device flow on / off / cannot tell, and the
|
|
10
|
+
// tick recording only what GitHub confirmed — never a secret in a log line.
|
|
11
|
+
// 3. the REAL projection (githubSetupRead) run into the REAL page renderers (projects.html,
|
|
12
|
+
// the vm-slice idiom): each server state draws the step the founder should see.
|
|
13
|
+
// 4. the device-flow finding is written down where the next builder will look.
|
|
14
|
+
//
|
|
15
|
+
// Run: node --test tests/github_one_trip.mjs
|
|
16
|
+
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import vm from 'node:vm';
|
|
22
|
+
import crypto from 'node:crypto';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const GS = require(path.join(ROOT, 'modules', 'provisioning', 'github-setup.js'));
|
|
29
|
+
const RUN = require(path.join(ROOT, 'scripts', 'gds', 'provision-github-setup.js'));
|
|
30
|
+
const UNITS = require(path.join(ROOT, 'scripts', 'gds', 'provision-units.js'));
|
|
31
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
32
|
+
const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
|
|
33
|
+
|
|
34
|
+
// ── 1. the checklist from server facts ──────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
test('each step reads only what the server has seen — the honest "not yet" states included', () => {
|
|
37
|
+
const none = GS.githubChecklist(null, null);
|
|
38
|
+
assert.deepEqual(none, { approve: 'todo', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false }, 'never started');
|
|
39
|
+
assert.equal(GS.githubChecklist('pending', null).approve, 'now', 'waiting for the approval on GitHub');
|
|
40
|
+
for (const s of ['code_received', 'converting']) {
|
|
41
|
+
const c = GS.githubChecklist(s, null);
|
|
42
|
+
assert.equal(c.approve, 'now', `${s}: approved, but no keys placed yet — not ticked`);
|
|
43
|
+
assert.equal(c.install, 'todo', `${s}: nothing to install before the app is named`);
|
|
44
|
+
}
|
|
45
|
+
const placed = GS.githubChecklist('converting', 'mercury-app');
|
|
46
|
+
assert.deepEqual([placed.approve, placed.install, placed.sign_in, placed.device_flow], ['done', 'now', 'now', 'todo'], 'keys placed: approve ticks, install and sign-in are under way');
|
|
47
|
+
const signedIn = GS.githubChecklist('done', 'mercury-app');
|
|
48
|
+
assert.deepEqual([signedIn.install, signedIn.sign_in, signedIn.device_flow, signedIn.connected], ['now', 'done', 'later', false],
|
|
49
|
+
'sign-in verified is NOT connected: the install has not been confirmed by GitHub');
|
|
50
|
+
const installed = GS.githubChecklist('done', 'mercury-app', { installedAt: '2026-10-08T10:00:00Z' });
|
|
51
|
+
assert.equal(installed.connected, true, 'approve + install + sign-in = connected');
|
|
52
|
+
assert.equal(installed.device_flow, 'later', 'device flow stays optional and never holds "connected" back');
|
|
53
|
+
assert.equal(GS.githubChecklist('done', 'mercury-app', { installedAt: 'x', deviceFlowAt: 'y' }).device_flow, 'done');
|
|
54
|
+
// a REPORTED install (the setup_url redirect, spoofable) ticks nothing
|
|
55
|
+
assert.equal(GS.githubChecklist('done', 'mercury-app', { installReportedAt: '2026-10-08T10:00:00Z' }).install, 'now');
|
|
56
|
+
// failures: where it stopped
|
|
57
|
+
assert.equal(GS.githubChecklist('error', null).approve, 'stopped');
|
|
58
|
+
assert.equal(GS.githubChecklist('expired', null).approve, 'stopped');
|
|
59
|
+
const signInFailed = GS.githubChecklist('error', 'mercury-app');
|
|
60
|
+
assert.deepEqual([signInFailed.approve, signInFailed.sign_in], ['done', 'stopped'], 'an app exists; sign-in is where it stopped');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('the install link names the app and carries the attempt; the repo is strict', () => {
|
|
64
|
+
assert.equal(GS.installUrlFor({ appSlug: null, state: 'abc' }), null);
|
|
65
|
+
assert.equal(GS.installUrlFor({ appSlug: 'mercury-app', state: 'ab cd' }), 'https://github.com/apps/mercury-app/installations/new?state=ab%20cd');
|
|
66
|
+
assert.equal(GS.installUrlFor({ appSlug: 'mercury-app' }), 'https://github.com/apps/mercury-app/installations/new');
|
|
67
|
+
assert.equal(GS.targetRepo({ target_ref: 'acme/mercury' }), 'acme/mercury');
|
|
68
|
+
assert.equal(GS.targetRepo({ target_ref: 'acme/mercury.git' }), 'acme/mercury');
|
|
69
|
+
for (const bad of ['', 'acme', '../x/y', 'a/b/c', 'acme/mer cury', '-acme/x']) assert.equal(GS.targetRepo({ target_ref: bad }), null, bad);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the runner checks for a bounded window after the attempt settles, re-opened by an install report', () => {
|
|
73
|
+
const now = Date.parse('2026-10-08T12:00:00Z');
|
|
74
|
+
assert.equal(GS.stillVerifying({ resolved_at: '2026-10-08T11:00:00Z' }, {}, now), true);
|
|
75
|
+
assert.equal(GS.stillVerifying({ resolved_at: '2026-10-08T09:00:00Z' }, {}, now), false);
|
|
76
|
+
assert.equal(GS.stillVerifying({ resolved_at: '2026-10-08T09:00:00Z' }, { installReportedAt: '2026-10-08T11:30:00Z' }, now), true);
|
|
77
|
+
assert.equal(GS.stillVerifying(null, {}, now), false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('githubSetupRead: one events read since the attempt began; the owner read leaves the state off', async () => {
|
|
81
|
+
const seen = [];
|
|
82
|
+
const db = { query: async (sql, params) => { seen.push({ sql, params }); return { rows: [{ event: GS.SETUP_EVENTS.installed, at: '2026-10-08T10:00:00Z' }] }; } };
|
|
83
|
+
const row = { instance_id: 42, state: 'S1', created_at: '2026-10-08T09:00:00Z', app_slug: 'mercury-app', status: 'done' };
|
|
84
|
+
const pub = await GS.githubSetupRead(db, { target_ref: 'acme/mercury' }, row, 'done', { now: Date.parse('2026-10-08T10:30:00Z') });
|
|
85
|
+
assert.equal(seen.length, 1);
|
|
86
|
+
assert.match(seen[0].sql, /FROM provisioning_events/);
|
|
87
|
+
assert.deepEqual(seen[0].params, [42, Object.values(GS.SETUP_EVENTS), '2026-10-08T09:00:00Z']);
|
|
88
|
+
assert.equal(pub.checklist.install, 'done');
|
|
89
|
+
assert.equal(pub.install_url, null, 'no install link once it is verified');
|
|
90
|
+
const notYet = await GS.githubSetupRead({ query: async () => ({ rows: [] }) }, {}, row, 'done', { withState: false });
|
|
91
|
+
assert.equal(notYet.install_url, 'https://github.com/apps/mercury-app/installations/new', 'the owner read never carries the state token');
|
|
92
|
+
const broken = await GS.githubSetupRead({ query: async () => { throw new Error('db down'); } }, {}, row, 'done');
|
|
93
|
+
assert.equal(broken.checklist.install, 'now', 'a failed read is "nothing verified yet", never a 500');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── 2. the runner's check ───────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
99
|
+
const PEM = privateKey.export({ type: 'pkcs1', format: 'pem' });
|
|
100
|
+
const INST = { id: 42, slug: 'mercury', port: 4100, domain: 'mercury.example.com', target_ref: 'acme/mercury', hosting_shape: 'co-tenant' };
|
|
101
|
+
const ENV = UNITS.webEnvBody(INST, { clientId: 'Iv23liABCDEF123456', clientSecret: 's'.repeat(40), appId: 5189633, appPem: PEM });
|
|
102
|
+
|
|
103
|
+
test('the app creds come back out of the web.env the runner itself writes', () => {
|
|
104
|
+
const c = RUN.appCredsFromEnv(ENV);
|
|
105
|
+
assert.equal(c.appId, '5189633');
|
|
106
|
+
assert.equal(c.clientId, 'Iv23liABCDEF123456');
|
|
107
|
+
assert.equal(c.pem.trim(), PEM.trim(), 'the one-line pem is restored to real newlines');
|
|
108
|
+
assert.deepEqual(RUN.appCredsFromEnv(''), { appId: null, pem: null, clientId: null });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('the app JWT is RS256, signed by the app key, issued by the app id, under ten minutes', () => {
|
|
112
|
+
const jwt = RUN.appJwt({ appId: 5189633, pem: PEM, nowSec: 1_800_000_000 });
|
|
113
|
+
const [h, b, s] = jwt.split('.');
|
|
114
|
+
const ok = crypto.createVerify('RSA-SHA256').update(`${h}.${b}`).verify(publicKey, Buffer.from(s, 'base64url'));
|
|
115
|
+
assert.equal(ok, true);
|
|
116
|
+
const body = JSON.parse(Buffer.from(b, 'base64url').toString());
|
|
117
|
+
assert.equal(body.iss, '5189633');
|
|
118
|
+
assert.ok(body.exp - body.iat <= 600);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('installed / not installed / cannot tell — only a 200 with an id is "installed"', async () => {
|
|
122
|
+
const calls = [];
|
|
123
|
+
const answer = (status, body) => async (url, opts) => { calls.push({ url, opts }); return { status, ok: status >= 200 && status < 300, json: async () => body }; };
|
|
124
|
+
assert.equal(await RUN.installedOnRepo({ appId: 1, pem: PEM, repo: 'acme/mercury', fetchImpl: answer(200, { id: 99 }) }), true);
|
|
125
|
+
assert.equal(calls[0].url, 'https://api.github.com/repos/acme/mercury/installation');
|
|
126
|
+
assert.match(calls[0].opts.headers.Authorization, /^Bearer [\w-]+\.[\w-]+\.[\w-]+$/, 'asked as the app');
|
|
127
|
+
assert.equal(await RUN.installedOnRepo({ appId: 1, pem: PEM, repo: 'acme/mercury', fetchImpl: answer(404, {}) }), false);
|
|
128
|
+
assert.equal(await RUN.installedOnRepo({ appId: 1, pem: PEM, repo: 'acme/mercury', fetchImpl: answer(500, {}) }), null);
|
|
129
|
+
assert.equal(await RUN.installedOnRepo({ appId: 1, pem: PEM, repo: 'acme/mercury', fetchImpl: async () => { throw new Error('offline'); } }), null);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('device flow on / off / cannot tell — read from GitHub\'s own answer to a device sign-in start', async () => {
|
|
133
|
+
const with_ = (body) => async () => ({ status: 200, ok: true, json: async () => body });
|
|
134
|
+
assert.equal(await RUN.deviceFlowOn({ clientId: 'Iv1', fetchImpl: with_({ device_code: 'dc', user_code: 'AB-CD' }) }), true);
|
|
135
|
+
assert.equal(await RUN.deviceFlowOn({ clientId: 'Iv1', fetchImpl: with_({ error: 'device_flow_disabled' }) }), false);
|
|
136
|
+
assert.equal(await RUN.deviceFlowOn({ clientId: 'Iv1', fetchImpl: with_({ error: 'slow_down' }) }), null, 'any other answer is never read as off');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// A fake db that answers the runner's two reads: the attempts in the window, and the facts.
|
|
140
|
+
function fakeDb({ attempts, facts = [] }) {
|
|
141
|
+
return { query: async (sql) => (/WITH latest AS/.test(sql) ? { rows: attempts } : /FROM provisioning_events/.test(sql) ? { rows: facts } : { rows: [] }) };
|
|
142
|
+
}
|
|
143
|
+
const ATTEMPT = { id: 18, instance_id: 42, created_at: '2026-10-08T09:00:00Z', requested_by: 7, slug: 'mercury', target_ref: 'acme/mercury', hosting_shape: 'co-tenant', instance_status: 'active' };
|
|
144
|
+
|
|
145
|
+
test('the runner records what GitHub confirmed — and only that — reading the creds on the box', async () => {
|
|
146
|
+
const events = [], logs = [], cmds = [];
|
|
147
|
+
const fetchImpl = async (url) => (/\/installation$/.test(url)
|
|
148
|
+
? { status: 200, ok: true, json: async () => ({ id: 31337 }) }
|
|
149
|
+
: { status: 200, ok: true, json: async () => ({ error: 'device_flow_disabled' }) });
|
|
150
|
+
const out = await RUN.verifyGithubAppSetups({
|
|
151
|
+
apply: true, privileged: true, log: (m) => logs.push(m), fetchImpl,
|
|
152
|
+
db: fakeDb({ attempts: [ATTEMPT] }),
|
|
153
|
+
exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: ENV }; },
|
|
154
|
+
provisioning: { recordEvent: async (_db, e) => { events.push(e); } },
|
|
155
|
+
});
|
|
156
|
+
assert.deepEqual(out, { checked: 1, installed: 1, deviceFlow: 0 });
|
|
157
|
+
assert.deepEqual(cmds, ['sudo cat /etc/mercury/web.env'], 'the creds are read where the runner placed them');
|
|
158
|
+
assert.deepEqual(events.map((e) => e.event), ['github-app-installed'], 'device flow off records nothing');
|
|
159
|
+
assert.match(events[0].detail, /acme\/mercury/);
|
|
160
|
+
const said = logs.join('\n') + JSON.stringify(events);
|
|
161
|
+
assert.doesNotMatch(said, /BEGIN RSA|s{40}|Iv23liABCDEF123456/, 'no key, secret or client id ever reaches a log or an event');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('an attempt already fully verified is skipped; dry-run asks GitHub nothing', async () => {
|
|
165
|
+
let asked = 0;
|
|
166
|
+
const deps = {
|
|
167
|
+
apply: true, log: () => {}, fetchImpl: async () => { asked++; return { status: 200, ok: true, json: async () => ({}) }; },
|
|
168
|
+
db: fakeDb({ attempts: [ATTEMPT], facts: [{ event: 'github-app-installed', at: '2026-10-08T10:00:00Z' }, { event: 'github-app-device-flow', at: '2026-10-08T10:00:00Z' }] }),
|
|
169
|
+
exec: () => { throw new Error('must not read the box'); }, provisioning: { recordEvent: async () => { throw new Error('nothing to record'); } },
|
|
170
|
+
};
|
|
171
|
+
assert.deepEqual(await RUN.verifyGithubAppSetups(deps), { checked: 0, installed: 0, deviceFlow: 0 });
|
|
172
|
+
const lines = [];
|
|
173
|
+
await RUN.verifyGithubAppSetups({ ...deps, apply: false, log: (m) => lines.push(m), db: fakeDb({ attempts: [ATTEMPT] }) });
|
|
174
|
+
assert.equal(asked, 0);
|
|
175
|
+
assert.match(lines.join('\n'), /would check the GitHub install \+ device flow for mercury/);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('the window query takes each project\'s LATEST attempt, past placement, still settling or newly reported', async () => {
|
|
179
|
+
let sql = '', params = null;
|
|
180
|
+
await RUN.attemptsToVerify({ query: async (q, p) => { sql = q; params = p; return { rows: [] }; } });
|
|
181
|
+
assert.match(sql, /DISTINCT ON \(instance_id\)[\s\S]*ORDER BY instance_id, created_at DESC/);
|
|
182
|
+
assert.match(sql, /l\.app_id IS NOT NULL AND l\.status IN \('done', 'error'\) AND i\.status = 'active'/);
|
|
183
|
+
assert.match(sql, /LIMIT 20/);
|
|
184
|
+
assert.deepEqual(params, [GS.VERIFY_WINDOW_MS / 1000, 'github-app-install-reported']);
|
|
185
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'provision.js'), 'utf8'), /require\('\.\/provision-github-setup\.js'\)\.verifyGithubAppSetups\(deps\)/, 'it runs on every runner tick, after the conversions');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ── 3. the real projection into the real renderers ──────────────────────────
|
|
189
|
+
|
|
190
|
+
const RS = SCRIPT.indexOf(' /* The four GitHub steps of part 03');
|
|
191
|
+
const RE = SCRIPT.indexOf(' var STEP_RENDERERS = {', RS);
|
|
192
|
+
const SS = SCRIPT.indexOf(' /* the checklist the server derived (task 1004649)');
|
|
193
|
+
const SE = SCRIPT.indexOf(' function pollManifest(stateToken) {', SS);
|
|
194
|
+
const PS = SCRIPT.indexOf(' function stepsPaint() {');
|
|
195
|
+
const PE = SCRIPT.indexOf(' (function hopInput()', PS);
|
|
196
|
+
|
|
197
|
+
test('the regions are findable (a rename must fail loudly)', () => {
|
|
198
|
+
assert.ok(RS > 0 && RE > RS && SS > 0 && SE > SS && PS > 0 && PE > PS);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
function page({ manifest, appSlug = null }) {
|
|
202
|
+
const sandbox = {
|
|
203
|
+
live: { manifest, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug, setup: null, installUrl: null, instStatus: 'active', addrReady: true },
|
|
204
|
+
state: { name: 'Mer<cury', slug: 'mercury', owner: 'acme', repo: 'mercury', step: 2 },
|
|
205
|
+
manifestStale: false, autoOpenArmed: false,
|
|
206
|
+
resolvedDomain: () => 'mercury.example.com', apexBase: () => 'example.com',
|
|
207
|
+
esc: (x) => String(x).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
208
|
+
GH_ICON: '', manualOauthDetails: () => '', projectPageLink: (t) => t, autoOpenTick: () => '',
|
|
209
|
+
progressMarkup: (a, b, note) => `<prog>${note}</prog>`, encodeURIComponent, JSON, Array,
|
|
210
|
+
};
|
|
211
|
+
vm.createContext(sandbox);
|
|
212
|
+
vm.runInContext(`${SCRIPT.slice(RS, RE)}\n${SCRIPT.slice(SS, SE)}\n${SCRIPT.slice(PS, PE)}`, sandbox);
|
|
213
|
+
return sandbox;
|
|
214
|
+
}
|
|
215
|
+
const ROWS = ['renderStepOauthApp', 'renderStepAppInstall', 'renderStepFirstSignIn', 'renderStepDeviceFlow'];
|
|
216
|
+
function draw(sb) {
|
|
217
|
+
return ROWS.map((f) => vm.runInContext(`${f}()`, sb));
|
|
218
|
+
}
|
|
219
|
+
function cls(li) { return ((li.match(/^<li(?: class="([^"]*)")?/) || [])[1] || '').split(/\s+/).filter(Boolean); }
|
|
220
|
+
|
|
221
|
+
async function serverSays(status, appSlug, eventRows) {
|
|
222
|
+
const db = { query: async () => ({ rows: eventRows }) };
|
|
223
|
+
const row = { instance_id: 42, state: 'S1', created_at: '2026-10-08T09:00:00Z', resolved_at: new Date().toISOString(), app_slug: appSlug };
|
|
224
|
+
return GS.githubSetupRead(db, { target_ref: 'acme/mercury' }, row, status);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test('the mockup\'s four steps, in its words and order, the typed name escaped', () => {
|
|
228
|
+
const sb = page({ manifest: null });
|
|
229
|
+
const rows = draw(sb);
|
|
230
|
+
const titles = rows.map((li) => (li.match(/<b>([^<]*)<\/b>/) || [])[1]);
|
|
231
|
+
assert.deepEqual(titles, ['Approve the Mer<cury app on GitHub', 'Install it on your repo', 'Builders can sign in', 'Command-line sign-in is on']);
|
|
232
|
+
assert.match(rows[0], /data-action="manifest-start"[^>]*>Connect GitHub ↗/, 'one click starts the one trip');
|
|
233
|
+
assert.equal(SCRIPT.match(/var STEP_ORDER = \[([^\]]*)\]/)[1], "'github-repo', 'oauth-app', 'app-install', 'first-sign-in', 'device-flow', 'enter-hall'", 'drawn in the mockup\'s order');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('each server state draws the step the founder should see — from the REAL projection', async () => {
|
|
237
|
+
// keys placed, nothing verified on GitHub yet
|
|
238
|
+
let sb = page({ manifest: 'converting', appSlug: 'mercury-app' });
|
|
239
|
+
let d = await serverSays('converting', 'mercury-app', []);
|
|
240
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
|
|
241
|
+
let rows = draw(sb);
|
|
242
|
+
assert.deepEqual(rows.map(cls), [['sdone'], [], ['slocked'], ['slocked', 'soptional']]);
|
|
243
|
+
assert.match(rows[1], /href="https:\/\/github\.com\/apps\/mercury-app\/installations\/new\?state=S1"/, 'the way back to the install, with the attempt');
|
|
244
|
+
assert.equal(vm.runInContext('stepsPaint.toString().length > 0', sb), true);
|
|
245
|
+
|
|
246
|
+
// sign-in verified, install still unconfirmed: NOT connected
|
|
247
|
+
sb = page({ manifest: 'done', appSlug: 'mercury-app' });
|
|
248
|
+
d = await serverSays('done', 'mercury-app', []);
|
|
249
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
|
|
250
|
+
rows = draw(sb);
|
|
251
|
+
assert.deepEqual(rows.map(cls), [['sdone'], [], ['sdone'], ['soptional']]);
|
|
252
|
+
assert.match(rows[3], /Optional — turn it on later\./);
|
|
253
|
+
assert.equal(vm.runInContext('setupOpen()', sb), true, 'the page keeps watching for the install');
|
|
254
|
+
|
|
255
|
+
// GitHub confirmed the install to the runner: connected, device flow still optional
|
|
256
|
+
sb = page({ manifest: 'done', appSlug: 'mercury-app' });
|
|
257
|
+
d = await serverSays('done', 'mercury-app', [{ event: 'github-app-installed', at: '2026-10-08T10:00:00Z' }]);
|
|
258
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
|
|
259
|
+
rows = draw(sb);
|
|
260
|
+
assert.deepEqual(rows.map(cls), [['sdone'], ['sdone'], ['sdone'], ['soptional']]);
|
|
261
|
+
assert.match(rows[1], /The app is on acme\/mercury/);
|
|
262
|
+
assert.equal(vm.runInContext('setupOpen()', sb), false);
|
|
263
|
+
// the part's state line, from these very rows: the optional step does not hold it back
|
|
264
|
+
const items = rows.map((li) => ({ classList: { contains: (c) => cls(li).includes(c) } }));
|
|
265
|
+
const ghState = { textContent: '' };
|
|
266
|
+
sb.$ = (id) => (id === 'humanSteps' ? { children: items } : id === 'ghState' ? ghState : null);
|
|
267
|
+
sb.manifestInFlight = () => false;
|
|
268
|
+
vm.runInContext('stepsPaint()', sb);
|
|
269
|
+
assert.equal(ghState.textContent, 'connected ✓');
|
|
270
|
+
|
|
271
|
+
// and once GitHub has device flow on, the fourth ticks too
|
|
272
|
+
sb = page({ manifest: 'done', appSlug: 'mercury-app' });
|
|
273
|
+
d = await serverSays('done', 'mercury-app', [{ event: 'github-app-installed', at: '2026-10-08T10:00:00Z' }, { event: 'github-app-device-flow', at: '2026-10-08T10:05:00Z' }]);
|
|
274
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
|
|
275
|
+
assert.deepEqual(cls(draw(sb)[3]), ['sdone', 'soptional']);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('seedSetup is change-only, and an install link must be GitHub\'s', () => {
|
|
279
|
+
const sb = page({ manifest: 'done', appSlug: 'a' });
|
|
280
|
+
const c = JSON.stringify(GS.githubChecklist('done', 'a'));
|
|
281
|
+
assert.equal(vm.runInContext(`seedSetup(${c}, 'https://github.com/apps/a/installations/new?state=S')`, sb), true);
|
|
282
|
+
assert.equal(vm.runInContext(`seedSetup(${c}, 'https://github.com/apps/a/installations/new?state=S')`, sb), false, 'nothing moved, nothing redrawn');
|
|
283
|
+
assert.equal(vm.runInContext(`seedSetup(${c}, 'https://github.com/apps/a/installations/new')`, sb), false, 'a stateless link never replaces the one carrying the attempt');
|
|
284
|
+
assert.equal(vm.runInContext('live.installUrl', sb), 'https://github.com/apps/a/installations/new?state=S');
|
|
285
|
+
const sb2 = page({ manifest: 'done', appSlug: 'a' });
|
|
286
|
+
vm.runInContext(`seedSetup(${c}, 'javascript:alert(1)')`, sb2);
|
|
287
|
+
assert.equal(vm.runInContext('live.installUrl', sb2), null);
|
|
288
|
+
assert.equal(vm.runInContext('seedSetup(null, null)', sb2), false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ── 4. the finding is written down ──────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
test('the device-flow finding lives in the direction doc, and the code points at it', () => {
|
|
294
|
+
const doc = fs.readFileSync(path.join(ROOT, 'docs', 'design', 'project-startup-direction.md'), 'utf8');
|
|
295
|
+
assert.match(doc, /Device flow cannot be switched on for the founder \(spike, task 1004649\)/);
|
|
296
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'github-setup.js'), 'utf8'), /docs\/design\/project-startup-direction\.md/);
|
|
297
|
+
});
|