@bongos/core 1.21.104 → 1.21.106
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 +57 -47
- package/docs/copy-inventory.md +368 -366
- package/docs/copy-registry.json +475 -457
- package/docs/design/project-startup-direction.md +14 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +10 -3
- package/docs/page-readings.json +1023 -977
- package/modules/lifecycle/check-rollup.js +11 -1
- package/modules/lifecycle/github-push.js +3 -7
- package/modules/public-landing/public/projects-hall-live.states.json +3 -2
- package/modules/public-landing/public/projects-setup.states.json +91 -0
- package/modules/public-landing/public/projects.html +535 -804
- package/modules/public-landing/public/projects.probes.json +3 -3
- package/modules/public-landing/public/projects.states.json +1 -1
- package/modules/ui-design/kit/fixtures/provisioning-instances-hall-live.json +22 -2
- package/modules/ui-design/kit/fixtures/provisioning-instances-setup.json +195 -0
- package/modules/ui-design/kit/serve.js +22 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/ship-land.js +2 -2
- package/src/module-api.js +1 -1
- package/tests/cosmos_token_contrast.mjs +10 -18
- package/tests/github_handback_any_device.mjs +4 -3
- package/tests/github_one_trip.mjs +45 -47
- package/tests/github_org_account.mjs +2 -2
- package/tests/github_pick_up.mjs +40 -29
- package/tests/github_push_merge_driver.mjs +46 -0
- package/tests/onboard.mjs +13 -7
- package/tests/project_invite_ui.mjs +6 -3
- package/tests/project_modules_ui.mjs +3 -3
- package/tests/projects_hub.mjs +8 -9
- package/tests/projects_hub_cloud_hop.mjs +77 -125
- package/tests/projects_hub_dns_ready.mjs +42 -51
- package/tests/projects_hub_pre_uat.mjs +68 -69
- package/tests/public_landing_projects.mjs +45 -102
- package/tests/setup_status_facts.mjs +400 -0
- package/tests/wizard_github_feedback.mjs +90 -87
- package/tests/wizard_one_page.mjs +11 -9
- package/tests/wizard_auto_open.mjs +0 -94
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// app's OWN key: the creds read back out of a REAL webEnvBody, a JWT that verifies,
|
|
9
9
|
// installed / not installed / cannot tell, device flow on / off / cannot tell, and the
|
|
10
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
|
|
12
|
-
// the vm-slice idiom): each server state draws the
|
|
11
|
+
// 3. the REAL projection (githubSetupRead) run into the REAL page facts (projects.html,
|
|
12
|
+
// ghFacts, the vm-slice idiom; task 1004683): each server state draws the fact the founder should see.
|
|
13
13
|
// 4. the device-flow finding is written down where the next builder will look.
|
|
14
14
|
//
|
|
15
15
|
// Run: node --test tests/github_one_trip.mjs
|
|
@@ -185,38 +185,35 @@ test('the window query takes each project\'s LATEST attempt, past placement, sti
|
|
|
185
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
186
|
});
|
|
187
187
|
|
|
188
|
-
// ── 3. the real projection into the real
|
|
188
|
+
// ── 3. the real projection into the real page facts ─────────────────────────
|
|
189
|
+
// Since task 1004683 part 03 draws FACTS, not checklist markup: ghFacts() is pure over
|
|
190
|
+
// what the reads said, and paintFacts draws it from nodes (tests/setup_status_facts.mjs
|
|
191
|
+
// runs the whole route-to-DOM path). Here: the GitHub half, fed by the real projection.
|
|
189
192
|
|
|
190
|
-
const
|
|
191
|
-
const
|
|
193
|
+
const FS = SCRIPT.indexOf(' function ghStep(key) {');
|
|
194
|
+
const FE = SCRIPT.indexOf(' /* ── drawing it: nodes and textContent only', FS);
|
|
192
195
|
const SS = SCRIPT.indexOf(' /* the checklist the server derived (task 1004649)');
|
|
193
196
|
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
|
|
|
197
198
|
test('the regions are findable (a rename must fail loudly)', () => {
|
|
198
|
-
assert.ok(
|
|
199
|
+
assert.ok(FS > 0 && FE > FS && SS > 0 && SE > SS);
|
|
200
|
+
assert.match(SCRIPT.slice(FS, FE), /function ghFacts\(now\)/);
|
|
199
201
|
});
|
|
200
202
|
|
|
201
203
|
function page({ manifest, appSlug = null }) {
|
|
202
204
|
const sandbox = {
|
|
203
|
-
live: { manifest, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug, setup: null, installUrl: null, instStatus: 'active', addrReady: true },
|
|
205
|
+
live: { manifest, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false, instStatus: 'active', addrReady: true, inst: null, readAt: 0, gone: '', repo: null },
|
|
204
206
|
state: { name: 'Mer<cury', slug: 'mercury', owner: 'acme', repo: 'mercury', step: 2 },
|
|
205
|
-
|
|
206
|
-
|
|
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,
|
|
207
|
+
resolvedDomain: () => 'mercury.example.com', apexBase: () => 'example.com', addressReady: () => true,
|
|
208
|
+
document: { hidden: false, hasFocus: () => true },
|
|
210
209
|
};
|
|
211
210
|
vm.createContext(sandbox);
|
|
212
|
-
vm.runInContext(`${SCRIPT.slice(
|
|
211
|
+
vm.runInContext(`${SCRIPT.slice(FS, FE)}\n${SCRIPT.slice(SS, SE)}`, sandbox);
|
|
213
212
|
return sandbox;
|
|
214
213
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
function cls(li) { return ((li.match(/^<li(?: class="([^"]*)")?/) || [])[1] || '').split(/\s+/).filter(Boolean); }
|
|
214
|
+
// vm-born values compared through JSON
|
|
215
|
+
const draw = (sb) => JSON.parse(JSON.stringify(vm.runInContext('ghFacts(Date.now())', sb)));
|
|
216
|
+
const states = (rows) => rows.map((r) => r.state);
|
|
220
217
|
|
|
221
218
|
async function serverSays(status, appSlug, eventRows) {
|
|
222
219
|
const db = { query: async () => ({ rows: eventRows }) };
|
|
@@ -224,55 +221,50 @@ async function serverSays(status, appSlug, eventRows) {
|
|
|
224
221
|
return GS.githubSetupRead(db, { target_ref: 'acme/mercury' }, row, status);
|
|
225
222
|
}
|
|
226
223
|
|
|
227
|
-
test('the mockup\'s four
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
assert.
|
|
232
|
-
assert.
|
|
233
|
-
assert.equal(
|
|
224
|
+
test('the mockup\'s four facts, in its words and order; the typed name is text, never markup', () => {
|
|
225
|
+
const rows = draw(page({ manifest: null }));
|
|
226
|
+
assert.deepEqual(rows.map((r) => r.title), ['Approve the Mer<cury app on GitHub', 'Install it on your repo', 'Builders can sign in', 'Command-line sign-in']);
|
|
227
|
+
assert.deepEqual(rows.map((r) => r.key), ['approve', 'install', 'sign_in', 'device_flow']);
|
|
228
|
+
assert.equal(rows[0].act, 'connect', 'one click starts the one trip, from inside the Approve fact');
|
|
229
|
+
assert.equal(rows[0].state, 'you', 'it is the founder\'s turn');
|
|
230
|
+
assert.equal(rows[3].opt, true, 'command-line sign-in is optional');
|
|
231
|
+
assert.doesNotMatch(SCRIPT.slice(FS, FE), /esc\(|innerHTML/, 'the facts carry text; the painter writes nodes');
|
|
234
232
|
});
|
|
235
233
|
|
|
236
|
-
test('each server state draws the
|
|
234
|
+
test('each server state draws the facts the founder should see — from the REAL projection', async () => {
|
|
237
235
|
// keys placed, nothing verified on GitHub yet
|
|
238
236
|
let sb = page({ manifest: 'converting', appSlug: 'mercury-app' });
|
|
239
237
|
let d = await serverSays('converting', 'mercury-app', []);
|
|
240
|
-
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
|
|
238
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
|
|
241
239
|
let rows = draw(sb);
|
|
242
|
-
assert.deepEqual(rows
|
|
243
|
-
assert.
|
|
244
|
-
assert.equal(
|
|
240
|
+
assert.deepEqual(states(rows), ['done', 'you', 'now', 'todo']);
|
|
241
|
+
assert.equal(rows[1].href, 'https://github.com/apps/mercury-app/installations/new?state=S1', 'the way back to the install, with the attempt');
|
|
242
|
+
assert.equal(rows[1].sub, 'acme/mercury', 'the repo the server says it installs on');
|
|
243
|
+
assert.equal(rows[1].right, 'not yet seen');
|
|
245
244
|
|
|
246
245
|
// sign-in verified, install still unconfirmed: NOT connected
|
|
247
246
|
sb = page({ manifest: 'done', appSlug: 'mercury-app' });
|
|
248
247
|
d = await serverSays('done', 'mercury-app', []);
|
|
249
|
-
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
|
|
248
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
|
|
250
249
|
rows = draw(sb);
|
|
251
|
-
assert.deepEqual(rows
|
|
252
|
-
assert.
|
|
250
|
+
assert.deepEqual(states(rows), ['done', 'you', 'done', 'todo']);
|
|
251
|
+
assert.equal(JSON.stringify(rows[3].sub), JSON.stringify(['Turn it on later in ', { t: 'the app settings', href: 'https://github.com/settings/apps/mercury-app' }, '.']));
|
|
253
252
|
assert.equal(vm.runInContext('setupOpen()', sb), true, 'the page keeps watching for the install');
|
|
254
253
|
|
|
255
254
|
// GitHub confirmed the install to the runner: connected, device flow still optional
|
|
256
255
|
sb = page({ manifest: 'done', appSlug: 'mercury-app' });
|
|
257
256
|
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);
|
|
257
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
|
|
259
258
|
rows = draw(sb);
|
|
260
|
-
assert.deepEqual(rows
|
|
261
|
-
assert.match(rows[1], /The app is on acme\/mercury/);
|
|
259
|
+
assert.deepEqual(states(rows), ['done', 'done', 'done', 'todo']);
|
|
262
260
|
assert.equal(vm.runInContext('setupOpen()', sb), false);
|
|
263
|
-
|
|
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 ✓');
|
|
261
|
+
assert.equal(vm.runInContext('setupCount([], ghFacts(Date.now()))', sb), 'ready ✓', 'the optional fact does not hold the count back');
|
|
270
262
|
|
|
271
263
|
// and once GitHub has device flow on, the fourth ticks too
|
|
272
264
|
sb = page({ manifest: 'done', appSlug: 'mercury-app' });
|
|
273
265
|
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.
|
|
266
|
+
vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
|
|
267
|
+
assert.equal(draw(sb)[3].state, 'done');
|
|
276
268
|
});
|
|
277
269
|
|
|
278
270
|
test('seedSetup is change-only, and an install link must be GitHub\'s', () => {
|
|
@@ -286,6 +278,12 @@ test('seedSetup is change-only, and an install link must be GitHub\'s', () => {
|
|
|
286
278
|
vm.runInContext(`seedSetup(${c}, 'javascript:alert(1)')`, sb2);
|
|
287
279
|
assert.equal(vm.runInContext('live.installUrl', sb2), null);
|
|
288
280
|
assert.equal(vm.runInContext('seedSetup(null, null)', sb2), false);
|
|
281
|
+
// the repo it installs on rides the read, strict
|
|
282
|
+
const sb3 = page({ manifest: 'done', appSlug: 'a' });
|
|
283
|
+
vm.runInContext(`seedSetup(${c}, null, { repo: 'acme/mer cury' })`, sb3);
|
|
284
|
+
assert.equal(vm.runInContext('live.repo', sb3), null, 'a malformed repo never reaches the page');
|
|
285
|
+
vm.runInContext(`seedSetup(${c}, null, { repo: 'acme/mercury' })`, sb3);
|
|
286
|
+
assert.equal(vm.runInContext('live.repo', sb3), 'acme/mercury');
|
|
289
287
|
});
|
|
290
288
|
|
|
291
289
|
// ── 4. the finding is written down ──────────────────────────────────────────
|
|
@@ -113,7 +113,7 @@ const PICKER = region(" /* ── whose GitHub account: mine or an organisati
|
|
|
113
113
|
const ENSURE_REPO = region(' function ensureFreshRepo() {', " $('doCreate').addEventListener('click'");
|
|
114
114
|
const NOTE_ORG = region(" /* the organisation the server says this attempt's app is on: change-only */", ' function startManifest(btn) {');
|
|
115
115
|
const START = region(' function startManifest(btn) {', ' /* the "start again" offer once a wait has clearly gone wrong');
|
|
116
|
-
const SETTINGS = region(' /* the organisation that owns the app, when the founder made it on one (task 1004651) */', ' /*
|
|
116
|
+
const SETTINGS = region(' /* the organisation that owns the app, when the founder made it on one (task 1004651) */', ' /* When a fact was seen, said plainly');
|
|
117
117
|
|
|
118
118
|
test('the page regions are still findable (a rename must fail loudly, not test nothing)', () => {
|
|
119
119
|
for (const [name, r] of Object.entries({ PICKER, ENSURE_REPO, NOTE_ORG, START, SETTINGS })) assert.ok(r, `${name} region not found`);
|
|
@@ -165,7 +165,7 @@ function page({ state = {}, live = {} } = {}) {
|
|
|
165
165
|
live: { manifest: null, manifestMsg: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, ...live },
|
|
166
166
|
repoAccess: true, instanceId: 42, me: { github_login: 'founder' },
|
|
167
167
|
hopForRepoAccess: () => false, syncHome() {}, saveDraft() {},
|
|
168
|
-
rememberManifest() {},
|
|
168
|
+
rememberManifest() {}, paintFacts() {}, repaintFacts() {}, pollManifest() {}, watchGhTab() {},
|
|
169
169
|
};
|
|
170
170
|
sandbox.manifestStartedAt = 0;
|
|
171
171
|
vm.createContext(sandbox);
|
package/tests/github_pick_up.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// 2. the runner — a stopped sign-in is re-asked with the keys already placed and becomes
|
|
14
14
|
// 'done'; a placement that throws after GitHub's conversion is a SPENT code (never
|
|
15
15
|
// re-queued); GitHub's 404 on a conversion drops the dead code.
|
|
16
|
-
// 3. the REAL status route's answer run into the REAL page (seedSetup +
|
|
16
|
+
// 3. the REAL status route's answer run into the REAL page (seedSetup + part 03's facts, task 1004683):
|
|
17
17
|
// every reachable state draws a sentence and an action, none a chore or "contact us".
|
|
18
18
|
// 4. the page's own actions: pick-up posts to the pick-up route and watches again, Reopen
|
|
19
19
|
// GitHub goes through the mint route (which hands back the SAME state), and "Send it to
|
|
@@ -184,29 +184,29 @@ const express = require('express');
|
|
|
184
184
|
const app = express();
|
|
185
185
|
app.use('/api/bongos', require(path.join(ROOT, 'modules', 'provisioning', 'routes', 'github-setup.js'))());
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
const
|
|
187
|
+
// part 03's facts (task 1004683): the pure half of the facts region, the seed, and the actions
|
|
188
|
+
const FS = SCRIPT.indexOf(' function ghStep(key) {');
|
|
189
|
+
const FE = SCRIPT.indexOf(' /* ── drawing it: nodes and textContent only', FS);
|
|
189
190
|
const SS = SCRIPT.indexOf(' /* the checklist the server derived (task 1004649)');
|
|
190
191
|
const SE = SCRIPT.indexOf(' function setupOpen() {', SS);
|
|
191
192
|
const AS = SCRIPT.indexOf(' /* ── Pick up where it stopped (task 1004652).');
|
|
192
193
|
const AE = SCRIPT.indexOf(' function startManifest(btn) {', AS);
|
|
193
194
|
|
|
194
195
|
test('the page regions are findable (a rename must fail loudly)', () => {
|
|
195
|
-
assert.ok(
|
|
196
|
+
assert.ok(FS > 0 && FE > FS && SS > 0 && SE > SS && AS > 0 && AE > AS);
|
|
196
197
|
});
|
|
197
198
|
|
|
198
|
-
const esc = (x) => String(x).replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
|
199
199
|
function page(extra = {}) {
|
|
200
|
+
const now = new Date().toISOString();
|
|
200
201
|
const sandbox = {
|
|
201
|
-
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, instStatus: 'active', addrReady: true, pickUp: null, verifying: false, askedPlatform: false
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
progressMarkup: (a, b, note) => `<prog>${note}</prog>`, encodeURIComponent, JSON,
|
|
202
|
+
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, instStatus: 'active', addrReady: true, pickUp: null, verifying: false, askedPlatform: false,
|
|
203
|
+
inst: { id: 42, status: 'active', domain: 'mercury.example.com', dns_resolves: true, last_verified_up: true, last_verified_at: now, created_at: now, provisioned_at: now, open_intent: null }, readAt: Date.now(), gone: '', repo: null },
|
|
204
|
+
state: { name: 'Mercury', slug: 'mercury', owner: 'acme', repo: 'mercury' },
|
|
205
|
+
resolvedDomain: () => 'mercury.example.com', apexBase: () => 'example.com', addressReady: () => true, API: '/api/bongos',
|
|
206
206
|
...extra,
|
|
207
207
|
};
|
|
208
208
|
vm.createContext(sandbox);
|
|
209
|
-
vm.runInContext(`${SCRIPT.slice(
|
|
209
|
+
vm.runInContext(`${SCRIPT.slice(FS, FE)}\n${SCRIPT.slice(SS, SE)}\n${SCRIPT.slice(AS, AE)}`, sandbox);
|
|
210
210
|
return sandbox;
|
|
211
211
|
}
|
|
212
212
|
// what the page's poll does with one answer (pollManifest's tick, minus the timer)
|
|
@@ -216,7 +216,12 @@ function seedFrom(sb, d) {
|
|
|
216
216
|
sb.live.manifest = d.ready ? 'done' : d.status;
|
|
217
217
|
sb.live.manifestFailure = d.failure || null;
|
|
218
218
|
}
|
|
219
|
-
|
|
219
|
+
// the GitHub facts and the one sentence, as the founder reads them (vm-born values through JSON)
|
|
220
|
+
function facts(sb) {
|
|
221
|
+
return JSON.parse(JSON.stringify(vm.runInContext('(function () { var n = Date.now(), h = homeFacts(live.inst, n), g = ghFacts(n); return { gh: g, said: setupSentence(h, g) }; })()', sb)));
|
|
222
|
+
}
|
|
223
|
+
const words = (r) => [r.title, [].concat(r.sub || []).map((p) => (typeof p === 'string' ? p : p.t)).join('')].join(' ');
|
|
224
|
+
const acts = (r) => [].concat(r.act || []);
|
|
220
225
|
const CHORE = /\/contact|Contact us|delete it|Developer settings → GitHub Apps\) so|Try again anyway|Start the setup again|by hand/;
|
|
221
226
|
|
|
222
227
|
// every shape the status derivation can reach, as rows the real route reads
|
|
@@ -256,31 +261,37 @@ test('every state the setup can reach: the REAL status answer draws a sentence a
|
|
|
256
261
|
assert.equal(d.ok, true, name);
|
|
257
262
|
const sb = page();
|
|
258
263
|
seedFrom(sb, d);
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
const f = facts(sb);
|
|
265
|
+
const all = f.gh.map(words).join('\n') + '\n' + f.said.map((p) => p.t).join('');
|
|
266
|
+
const allActs = f.gh.flatMap(acts);
|
|
267
|
+
seen[name] = { pick: d.pick_up, f, all, allActs };
|
|
268
|
+
assert.doesNotMatch(all, CHORE, `${name}: no manual chore and no "contact us"`);
|
|
269
|
+
// a stopped fact always carries the action that fixes it
|
|
270
|
+
for (const r of f.gh.filter((x) => x.state === 'bad')) {
|
|
271
|
+
assert.ok(acts(r).some((a) => /^(pick-up|pick-up-link|connect|ask-platform|manage)$/.test(a)), `${name}: the stopped "${r.title}" has its button`);
|
|
272
|
+
}
|
|
273
|
+
// a pick-up the server offers is on the page, once
|
|
274
|
+
if (d.pick_up === 'check' || d.pick_up === 'convert') {
|
|
275
|
+
assert.equal(allActs.filter((a) => /^pick-up/.test(a) || a === 'check-again').length, 1, `${name}: ${d.pick_up} is offered once`);
|
|
265
276
|
}
|
|
266
|
-
|
|
267
|
-
if (d.pick_up === '
|
|
268
|
-
if (d.pick_up === 'operator') assert.match(html, /data-action="ask-platform"/, `${name}: the platform team is one click away`);
|
|
269
|
-
if (d.pick_up === 'reopen') assert.match(html, /data-action="reopen-github"/, `${name}: the same link can be reopened`);
|
|
277
|
+
if (d.pick_up === 'operator') assert.ok(allActs.includes('ask-platform'), `${name}: the platform team is one click away`);
|
|
278
|
+
if (d.pick_up === 'reopen') assert.ok(allActs.includes('reopen'), `${name}: the same link can be reopened`);
|
|
270
279
|
// a new link is offered ONLY where nothing was made on GitHub
|
|
271
|
-
if (
|
|
280
|
+
if (allActs.includes('connect')) assert.ok(d.pick_up === 'fresh', `${name}: Connect GitHub is offered only when no app exists (pick_up=${d.pick_up})`);
|
|
272
281
|
}
|
|
273
282
|
assert.equal(seen['waiting on GitHub'].pick, 'reopen');
|
|
274
283
|
assert.equal(seen['link lapsed'].pick, 'fresh');
|
|
275
284
|
assert.equal(seen['conversion stopped, code young'].pick, 'convert');
|
|
276
285
|
assert.equal(seen['conversion stopped, code spent'].pick, 'operator');
|
|
277
286
|
assert.equal(seen['sign-in unconfirmed, re-asking'].pick, null);
|
|
278
|
-
assert.match(seen['sign-in unconfirmed, re-asking'].
|
|
287
|
+
assert.match(seen['sign-in unconfirmed, re-asking'].all, /Asking your project again, with the app GitHub already made\./);
|
|
279
288
|
assert.equal(seen['sign-in unconfirmed, window over'].pick, 'check');
|
|
280
|
-
|
|
281
|
-
assert.
|
|
289
|
+
const never = seen['signed in, install never confirmed, window over'];
|
|
290
|
+
assert.equal(never.f.gh[1].sub, 'GitHub stopped before the app was on acme/mercury.', 'the mockup\'s sentence, from the real read');
|
|
291
|
+
assert.equal(never.f.said.map((p) => p.t).join(''), 'One step stopped. Nothing is lost, and no second app will be made.', 'said once, in the sentence');
|
|
292
|
+
assert.ok(seen['installed, device flow off, window over'].allActs.includes('check-again'));
|
|
282
293
|
assert.equal(seen['all four ticked'].pick, null);
|
|
283
|
-
assert.
|
|
294
|
+
assert.ok(!seen['all four ticked'].f.gh.some((r) => r.state === 'bad') && !seen['all four ticked'].allActs.some((a) => /^pick-up/.test(a)));
|
|
284
295
|
t.diagnostic(Object.entries(seen).map(([k, v]) => `${k}: ${v.pick}`).join(' · '));
|
|
285
296
|
});
|
|
286
297
|
|
|
@@ -289,13 +300,13 @@ test('every state the setup can reach: the REAL status answer draws a sentence a
|
|
|
289
300
|
function actions({ pickUp, answer, manifestState = null, blockerStatus = 201 }) {
|
|
290
301
|
const calls = { fetched: [], polled: [], progress: [], started: 0, renders: 0 };
|
|
291
302
|
const sb = page({
|
|
292
|
-
|
|
303
|
+
instanceId: 42, manifestState, pollTimer: null, manifestStartedAt: 1700000000000, ghTab: null,
|
|
293
304
|
fetch: async (url, init) => {
|
|
294
305
|
calls.fetched.push({ url, init });
|
|
295
306
|
if (/\/blockers$/.test(url)) return { ok: blockerStatus < 300, status: blockerStatus, json: async () => ({}) };
|
|
296
307
|
return { ok: true, status: 200, json: async () => answer };
|
|
297
308
|
},
|
|
298
|
-
rememberManifest: () => {},
|
|
309
|
+
rememberManifest: () => {}, repaintFacts: () => { calls.renders++; }, paintFacts: () => { calls.renders++; },
|
|
299
310
|
pollManifest: (s) => calls.polled.push(s), startProgress: (id) => calls.progress.push(id),
|
|
300
311
|
startManifest: () => { calls.started++; },
|
|
301
312
|
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { strict as assert } from 'node:assert';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
|
+
import fs from 'node:fs';
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
const gh = require('../modules/lifecycle/github-push.js');
|
|
@@ -169,6 +170,51 @@ test('mergeBlockReason maps the common blocker states', () => {
|
|
|
169
170
|
assert.equal(gh.mergeBlockReason({ ...open, merged: true }), null);
|
|
170
171
|
});
|
|
171
172
|
|
|
173
|
+
// --- task 1004666: a check still running is waiting, not red --------------
|
|
174
|
+
//
|
|
175
|
+
// check-rollup maps a status context's state into `conclusion`, so PENDING and
|
|
176
|
+
// EXPECTED (posted but unfinished, or required but not yet reported) reached
|
|
177
|
+
// mergeBlockReason as conclusions. It counted anything outside SUCCESS/NEUTRAL/
|
|
178
|
+
// SKIPPED as failing, so a BLOCKED PR waiting on a third-party status read
|
|
179
|
+
// "failing_check:<context>" and the reconciler tried a regen-heal on it.
|
|
180
|
+
|
|
181
|
+
const ctx = (context, state) => ({ __typename: 'StatusContext', context, state, description: null });
|
|
182
|
+
|
|
183
|
+
test('1004666 a PENDING or EXPECTED status context on a BLOCKED PR reads as waiting', async () => {
|
|
184
|
+
for (const state of ['PENDING', 'EXPECTED']) {
|
|
185
|
+
const { fetchImpl } = mkFetch({ node: prNode({ mergeStateStatus: 'BLOCKED', rollup: 'PENDING', checks: [TRUST, ctx('ci/external', state)] }) });
|
|
186
|
+
const r = await gh.mergeIfGreen({ branch: 'claude/wt' }, DEPS(fetchImpl));
|
|
187
|
+
assert.equal(r.reason, 'blocked_required_review_or_pending_check', `${state} is not a failure`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('1004666 on an UNSTABLE PR a pending optional check is pending, not failing', () => {
|
|
192
|
+
const open = { pr_state: 'open', merged: false };
|
|
193
|
+
assert.equal(gh.mergeBlockReason({ ...open, merge_state: 'UNSTABLE', checks: [{ name: 'e2e', conclusion: 'PENDING' }] }), 'non_required_check_pending');
|
|
194
|
+
assert.equal(gh.mergeBlockReason({ ...open, merge_state: 'UNSTABLE', checks: [{ name: 'e2e', conclusion: 'TIMED_OUT' }] }), 'non_required_check_failing:e2e');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('1004666 every red conclusion still names its check, past a pending one', () => {
|
|
198
|
+
for (const state of ['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE']) {
|
|
199
|
+
const open = { pr_state: 'open', merged: false, merge_state: 'BLOCKED' };
|
|
200
|
+
assert.equal(gh.mergeBlockReason({ ...open, checks: [{ name: 'ci/external', conclusion: 'PENDING' }, { name: 'unit', conclusion: state }] }), 'failing_check:unit', state);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('1004666 the server and the laptop read red from ONE set', () => {
|
|
205
|
+
const rollup = require('../modules/lifecycle/check-rollup.js');
|
|
206
|
+
const land = fs.readFileSync(new URL('../scripts/gds/ship-land.js', import.meta.url), 'utf8');
|
|
207
|
+
const push = fs.readFileSync(new URL('../modules/lifecycle/github-push.js', import.meta.url), 'utf8');
|
|
208
|
+
assert.ok(land.includes("RED_CONCLUSIONS } = require('../../modules/lifecycle/check-rollup.js')"), 'ship-land imports the shared set');
|
|
209
|
+
assert.doesNotMatch(land, /const RED_CONCLUSIONS\s*=/, 'and no longer declares its own');
|
|
210
|
+
assert.doesNotMatch(push, /_BAD_CHECK|'SUCCESS', 'NEUTRAL', 'SKIPPED'/, 'the old allow-list reading is gone');
|
|
211
|
+
for (const c of ['PENDING', 'EXPECTED', 'SUCCESS', 'NEUTRAL', 'SKIPPED', 'STALE', null, undefined]) {
|
|
212
|
+
assert.equal(rollup.isRedCheck({ conclusion: c }), false, String(c));
|
|
213
|
+
}
|
|
214
|
+
assert.equal(rollup.isRedCheck({ conclusion: 'failure' }), true, 'case-insensitive like the old reader');
|
|
215
|
+
assert.equal(rollup.isRedCheck(null), false);
|
|
216
|
+
});
|
|
217
|
+
|
|
172
218
|
// --- task 1003988: a red PR names what failed, or says it cannot see -------
|
|
173
219
|
//
|
|
174
220
|
// Without Checks:read GitHub nulls every CheckRun node and keeps the rollup state,
|
package/tests/onboard.mjs
CHANGED
|
@@ -193,11 +193,12 @@ test('every render surface carries the terminal hall step (task 1002703)', () =>
|
|
|
193
193
|
const cli = read('scripts/gds/onboard.js');
|
|
194
194
|
assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
|
|
195
195
|
assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
|
|
196
|
+
// the one page (task 1004683): the hall is entered through the Enter Genesis door, the
|
|
197
|
+
// hall's founding mode, once every required fact of part 03 is done
|
|
196
198
|
const hubHtml = read('modules/public-landing/public/projects.html');
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
assert.match(
|
|
200
|
-
assert.match(enterHall.join(''), /bongos dev/, 'the wizard offers the local hall when no domain');
|
|
199
|
+
assert.match(hubHtml, /<a class="btn" id="enterGenesis" data-onboard-step="enter-hall" target="_blank" rel="noopener">Enter Genesis<\/a>/,
|
|
200
|
+
'the ?view=new wizard tags the enter-hall step on its Enter Genesis door');
|
|
201
|
+
assert.match(hubHtml, /\$\('enterGenesis'\)\.setAttribute\('href', 'https:\/\/' \+ inst\.domain\);/, 'which opens the project\'s own address');
|
|
201
202
|
});
|
|
202
203
|
|
|
203
204
|
test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
|
|
@@ -206,8 +207,10 @@ test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (t
|
|
|
206
207
|
const { readFileSync } = require('node:fs');
|
|
207
208
|
const path = require('node:path');
|
|
208
209
|
const src = readFileSync(path.join(import.meta.dirname, '..', 'modules/public-landing/public/projects.html'), 'utf8');
|
|
209
|
-
// gap 2 (idea 1000675): the checklist
|
|
210
|
-
|
|
210
|
+
// gap 2 (idea 1000675): the checklist follows the canonical plan. Since task 1004683 part
|
|
211
|
+
// 03 is a fixed list of facts the server checks, each GitHub fact named by the plan step
|
|
212
|
+
// it is (FACT_PLAN_STEP); the drift guard below holds that set equal to the plan's.
|
|
213
|
+
assert.match(src, /var FACT_PLAN_STEP = \{ approve: 'oauth-app', install: 'app-install', sign_in: 'first-sign-in', device_flow: 'device-flow' \};/, 'the facts name their plan steps');
|
|
211
214
|
// gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
|
|
212
215
|
assert.match(src, /\/github-app/, 'the done panel drives the GitHub-App manifest flow');
|
|
213
216
|
assert.match(src, /github-app\/status/, 'and polls its progress to reflect live state');
|
|
@@ -305,7 +308,10 @@ test('the web form (?view=new) declares EXACTLY the canonical human steps — dr
|
|
|
305
308
|
// keys are kebab-case literals; the charset excludes the page's DYNAMIC fallback
|
|
306
309
|
// tag (data-onboard-step="' + esc(s.key) + '"), which renders unknown plan steps
|
|
307
310
|
// at runtime and has no literal key to pin here (task 1002694).
|
|
308
|
-
|
|
311
|
+
// …plus the plan step each of part 03's facts is (task 1004683), set from one map
|
|
312
|
+
const facts = (src.match(/var FACT_PLAN_STEP = \{([^}]*)\};/) || [, ''])[1];
|
|
313
|
+
const found = [...src.matchAll(/data-onboard-step="([a-z0-9-]+)"/g)].map((m) => m[1])
|
|
314
|
+
.concat([...facts.matchAll(/: '([a-z0-9-]+)'/g)].map((m) => m[1]));
|
|
309
315
|
const uniq = [...new Set(found)].sort();
|
|
310
316
|
assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
|
|
311
317
|
}
|
|
@@ -523,7 +523,9 @@ function bootGate({ me = { github_login: 'kestrel' }, created = { id: 7, slug: '
|
|
|
523
523
|
test('the wizard opens the invite step only once the hall is up, and only for a signed-in owner of a project that exists (tasks 1002346, 1004503)', () => {
|
|
524
524
|
assert.ok(gateSlice().includes('function paintInviteGate(live)'), 'the gate slice is found');
|
|
525
525
|
assert.match(SCRIPT, /function renderDone\(\) \{[\s\S]*?paintInviteGate\(false\);/, 'the done panel paints the gate shut');
|
|
526
|
-
|
|
526
|
+
// since task 1004683: "is alive" — and with it the invite — opens once every required fact
|
|
527
|
+
// of part 03 is done, which includes the hall answering (last_verified_up)
|
|
528
|
+
assert.match(SCRIPT, /var on = !!\(ready && inst && hostOk\(inst\.domain\) && addressReady\(inst\) && inst\.last_verified_up === true\);[\s\S]{0,900}paintInviteGate\(on\);/, 'and opens it only on the verified Live');
|
|
527
529
|
assert.equal((SCRIPT.match(/setInviteSlug\('wizInvite'/g) || []).length, 2, 'the gate is the only caller: shut, or open on Live');
|
|
528
530
|
|
|
529
531
|
const before = bootGate();
|
|
@@ -577,8 +579,9 @@ test('the wizard’s invite step ships as real markup in part 03, with the alive
|
|
|
577
579
|
assert.match(panel, /data-invite-send="wizInvite"/, 'the sender');
|
|
578
580
|
assert.match(panel, /data-invite-skip="wizInvite"/, 'and the skip is a CONTROL, not "leave it and move on"');
|
|
579
581
|
assert.match(panel, /Optional/, 'the panel says so before it asks');
|
|
580
|
-
assert.ok(panel.indexOf('id="
|
|
581
|
-
'
|
|
582
|
+
assert.ok(panel.indexOf('id="ghFacts"') > 0 && panel.indexOf('id="ghFacts"') < panel.indexOf('id="done-h"') && panel.indexOf('id="done-h"') < panel.indexOf('id="wizInvite"') &&
|
|
583
|
+
panel.indexOf('id="wizInvite"') < panel.indexOf('id="enterGenesis"'),
|
|
584
|
+
'the mockup\'s order: the facts, then "is alive", then the invite, then the Enter Genesis door (tasks 1004648, 1004683)');
|
|
582
585
|
assert.match(panel, /Invite your first builders\. They become founding builders\./, 'in the mockup\'s words');
|
|
583
586
|
assert.match(panel, /<p class="samePath" id="wizInviteLater"><\/p>/, 'and the way back afterwards is a slot beside the modules one');
|
|
584
587
|
assert.match(SCRIPT, /\$\('wizInviteLater'\)\.innerHTML = 'You can invite builders any time from ' \+ projectPageLink\('the project’s own page'\) \+ '\.';/,
|
|
@@ -299,10 +299,10 @@ test('the catalog is read ONCE however often the section repaints', async () =>
|
|
|
299
299
|
|
|
300
300
|
test('the wizard’s done panel points at the project’s settings for its extras', () => {
|
|
301
301
|
assert.match(HUB, /<p class="samePath" id="doneModules"><\/p>/, 'the panel carries the slot');
|
|
302
|
-
assert.match(SCRIPT,
|
|
303
|
-
'and renderDone fills it with one sentence pointing at the manage page');
|
|
302
|
+
assert.match(SCRIPT, /var mods = \$\('doneModules'\);\n\s*mods\.textContent = '';\n\s*partsInto\(mods, \['Add or remove modules later in ', \{ t: 'the project’s settings', href: manageHref\(\) \}, '\.'\]\);/,
|
|
303
|
+
'and renderDone fills it with one sentence pointing at the manage page, drawn from nodes (task 1004683)');
|
|
304
304
|
assert.equal((SCRIPT.match(/doneModules/g) || []).length, 1, 'one line, written in one place');
|
|
305
|
-
assert.match(SCRIPT, /function
|
|
305
|
+
assert.match(SCRIPT, /function manageHref\(\) \{\s*return instanceId \? '\?view=manage&id=' \+ encodeURIComponent\(instanceId\)/,
|
|
306
306
|
'and the link names the instance just created, not the list');
|
|
307
307
|
});
|
|
308
308
|
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -2190,7 +2190,7 @@ test('the wizard folds at one width: no rail fold is left, and no 900px rule is
|
|
|
2190
2190
|
assert.match(STYLE, /@media \(max-width:560px\)\{\s*\.setRow\{grid-template-columns:minmax\(0,1fr\) auto;grid-template-areas:"k acts" "v v";/, 'part 02\'s rows stack under 560, Change beside the label');
|
|
2191
2191
|
});
|
|
2192
2192
|
|
|
2193
|
-
test('the wizard\'s own structures wear the world: hairline fields, cells of one band,
|
|
2193
|
+
test('the wizard\'s own structures wear the world: hairline fields, cells of one band, part 02\'s rows, part 03\'s checked facts (tasks 1003346, 1004683)', () => {
|
|
2194
2194
|
const at = STYLE.indexOf('the create wizard (task 1002763');
|
|
2195
2195
|
assert.ok(at > -1, 'the wizard block is still the page\'s own');
|
|
2196
2196
|
const block = STYLE.slice(at);
|
|
@@ -2204,13 +2204,12 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2204
2204
|
assert.match(block, /\.setRows\{margin:14px 0 0;border-top:1px solid var\(--rule\);\}/, 'part 02\'s rows hang from one hairline');
|
|
2205
2205
|
assert.match(block, /\.setRow\{display:grid;grid-template-columns:108px minmax\(0,1fr\) auto;[^}]*border-bottom:1px solid var\(--rule\);\}/, 'each row is label · value · Change on its own hairline');
|
|
2206
2206
|
assert.doesNotMatch(block, /\.review( li)?\{/, 'the ledger\'s rules are gone with renderReview');
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
assert.match(block, /\.
|
|
2210
|
-
assert.match(block,
|
|
2211
|
-
assert.match(block, /\.
|
|
2212
|
-
assert.
|
|
2213
|
-
assert.match(block, /\.doneHead \.tick\{display:none;\}/, 'the tick leaves; the rail says filed');
|
|
2207
|
+
// task 1004683 (docs/design/mocks/day-night-rooms/setup-status.html): part 03 is one list
|
|
2208
|
+
// of facts on hairlines — a dot, the fact, when it was checked — and no stage strip at all
|
|
2209
|
+
assert.match(block, /\.setFact\{display:grid;grid-template-columns:24px minmax\(0,1fr\) auto;[^}]*border-top:1px solid var\(--rule\);\}/, 'each fact is dot · fact · when, on its own hairline');
|
|
2210
|
+
assert.match(block, /\.setFact\.done \.d\{background:color-mix\(in srgb,var\(--ok\) 12%,transparent\);color:var\(--ok\);border-color:var\(--ok\);\}/, 'a verified fact wears the tick in the ok ink');
|
|
2211
|
+
assert.match(block, /\.setFact\.bad \.d\{[^}]*color:var\(--err\);/, 'a stopped fact wears the error ink');
|
|
2212
|
+
assert.doesNotMatch(block, /\.prog\b|\.humanSteps|\.doneHead|\.stepsHead|\.ghbtn/, 'the standup strip, the old checklist and their sheet are gone');
|
|
2214
2213
|
assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
|
|
2215
2214
|
assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
|
|
2216
2215
|
// the one exception is drawn, not chrome: the night room's moon glows in the window
|
|
@@ -2221,7 +2220,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2221
2220
|
assert.doesNotMatch(HUB, /data-back="|id="wizBack"/, 'no back door is left');
|
|
2222
2221
|
assert.match(HUB, /<div class="wizTop">\s*<span class="wizEyebrow" id="wizEyebrow">New project<\/span>\s*<\/div>/, 'the eyebrow alone in the top row');
|
|
2223
2222
|
assert.match(HUB, /<div class="createBar">\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>\s*<span class="wizRecap" id="wizRecap"><\/span>/, 'Create, with the one-line recap beside it');
|
|
2224
|
-
assert.match(HUB, /<h2 id="done-part-h"><span class="n">03<\/span>Connect GitHub <span class="state" id="ghState">opens once your project exists<\/span><\/h2>/, 'part 03 names itself and where it is');
|
|
2223
|
+
assert.match(HUB, /<h2 id="done-part-h"><span class="n">03<\/span><span id="donePartT">Connect GitHub<\/span> <span class="state" id="ghState">opens once your project exists<\/span><\/h2>/, 'part 03 names itself and where it is');
|
|
2225
2224
|
// what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
|
|
2226
2225
|
assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
|
|
2227
2226
|
assert.match(SCRIPT, /hosting_shape: 'cloud-host',/, 'the shape is unchanged');
|