@bongos/core 1.21.119 → 1.21.121

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.
@@ -0,0 +1,148 @@
1
+ // tests/github_sign_in_seen.mjs — the project's own "sign-in works" ticks its setup
2
+ // checklist (task 1004410, from a Discord #bugs report: "says that github wiring is not done.
3
+ // When in reality i did make it and was able to sign in via github to my account").
4
+ //
5
+ // Sign-in used to tick only from the one-click GitHub attempt reaching 'done', so a project
6
+ // whose sign-in was wired some other way — a hand-made OAuth app, or a GitHub tab that never
7
+ // came back — sat at "Waiting on GitHub" for good. Now the liveness sweep reads the project's
8
+ // /healthz answer, records 'github-sign-in-seen' once per attempt, and githubChecklist reads it.
9
+ // The page half (what the founder sees) is tests/setup_status_facts.mjs §6.
10
+ //
11
+ // Pinned here, DB-free over recording fakes and real loopback servers:
12
+ // * githubChecklist: the fact ticks sign-in; with no app of ours it is wired_elsewhere and
13
+ // connected; with one, the install keeps its own fact;
14
+ // * pickUpFor offers nothing to pick up once sign-in works elsewhere;
15
+ // * githubSetupRead reads the fact even when no attempt exists;
16
+ // * recordSignInSeen writes at most one row per attempt;
17
+ // * probeHealthz reports auth_configured only from a small JSON answer saying true;
18
+ // * sweepOnce records the fact only for an up project that says so.
19
+ //
20
+ // Run: node --test tests/github_sign_in_seen.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import http from 'node:http';
25
+ import path from 'node:path';
26
+ import { createRequire } from 'node:module';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const gs = require(path.join(ROOT, 'modules', 'provisioning', 'github-setup.js'));
32
+ const liveness = require(path.join(ROOT, 'modules', 'provisioning', 'pollers', 'liveness-sweep.js'));
33
+
34
+ const SEEN = { signInSeenAt: '2026-10-14T10:00:00.000Z' };
35
+
36
+ // ── the checklist ──────────────────────────────────────────────────────────────────────
37
+ test('the project saying sign-in works ticks sign-in, whatever the attempt row says', () => {
38
+ for (const status of [null, 'pending', 'expired', 'error', 'code_received']) {
39
+ const c = gs.githubChecklist(status, null, SEEN);
40
+ assert.equal(c.sign_in, 'done', `${status}: sign-in is the project's own answer`);
41
+ assert.equal(c.wired_elsewhere, true, `${status}: no app of ours, so it was wired another way`);
42
+ assert.equal(c.approve, 'done');
43
+ assert.equal(c.install, 'done');
44
+ assert.equal(c.connected, true, `${status}: the project is not held back`);
45
+ }
46
+ });
47
+
48
+ test('without that answer, nothing changes: a pending attempt still waits on GitHub', () => {
49
+ const c = gs.githubChecklist('pending', null, {});
50
+ assert.deepEqual(c, { approve: 'now', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false, wired_elsewhere: false });
51
+ });
52
+
53
+ test('with our app recorded, the answer ticks sign-in and the install keeps its own fact', () => {
54
+ const c = gs.githubChecklist('converting', 'kestrel-cloudbongos-com', SEEN);
55
+ assert.equal(c.sign_in, 'done');
56
+ assert.equal(c.install, 'now', 'the install is still GitHub\'s to confirm');
57
+ assert.equal(c.wired_elsewhere, false);
58
+ assert.equal(c.connected, false);
59
+ assert.equal(gs.githubChecklist('done', 'k', { ...SEEN, installedAt: SEEN.signInSeenAt }).connected, true);
60
+ });
61
+
62
+ test('nothing to pick up once sign-in works another way', () => {
63
+ const now = Date.now();
64
+ for (const [status, row] of [['pending', { state: 'S', attempts: 0 }], ['expired', { attempts: 0 }], ['error', { attempts: 2, created_at: new Date(now).toISOString(), github_code: 'c' }]]) {
65
+ const checklist = gs.githubChecklist(status, null, SEEN);
66
+ assert.equal(gs.pickUpFor(row, status, checklist, false, now), null, `${status}: no Reopen, Start fresh or Convert`);
67
+ }
68
+ // and the old answers stand when it does not
69
+ assert.equal(gs.pickUpFor({ state: 'S' }, 'pending', gs.githubChecklist('pending', null, {}), false, now), 'reopen');
70
+ });
71
+
72
+ // ── the read ───────────────────────────────────────────────────────────────────────────
73
+ test('githubSetupRead reads the fact for a project with no attempt at all', async () => {
74
+ const seen = [];
75
+ const pool = {
76
+ query: async (sql, params) => {
77
+ seen.push(params);
78
+ assert.match(sql, /FROM provisioning_events/);
79
+ return { rows: params[1].includes('github-sign-in-seen') ? [{ event: 'github-sign-in-seen', at: SEEN.signInSeenAt, detail: null }] : [] };
80
+ },
81
+ };
82
+ const r = await gs.githubSetupRead(pool, { id: 42, target_ref: 'you/kestrel' }, null, null);
83
+ assert.equal(seen[0][0], 42, 'read by the project\'s own id');
84
+ assert.equal(r.checklist.connected, true);
85
+ assert.equal(r.checklist.wired_elsewhere, true);
86
+ assert.equal(r.pick_up, null);
87
+ // a failed read is "nothing verified yet", never a 500
88
+ const broken = await gs.githubSetupRead({ query: async () => { throw new Error('db down'); } }, { id: 42 }, null, null);
89
+ assert.equal(broken.checklist.sign_in, 'todo');
90
+ });
91
+
92
+ test('recordSignInSeen writes at most one row per attempt, from the liveness sweep', async () => {
93
+ let sql = '', params = null;
94
+ await gs.recordSignInSeen({ query: async (s, p) => { sql = s; params = p; return { rows: [] }; } }, 42);
95
+ assert.deepEqual(params, [42, 'github-sign-in-seen']);
96
+ assert.match(sql, /INSERT INTO provisioning_events/);
97
+ assert.match(sql, /WHERE NOT EXISTS/, 'a five-minute sweep never grows the ledger');
98
+ assert.match(sql, /max\(m\.created_at\) FROM provisioning_oauth_manifests m WHERE m\.instance_id = \$1/, 'once per attempt, so a fresh attempt is seen afresh');
99
+ assert.match(sql, /'liveness'/);
100
+ });
101
+
102
+ // ── the probe and the sweep ────────────────────────────────────────────────────────────
103
+ function serve(body, type = 'application/json') {
104
+ const server = http.createServer((req, res) => { res.writeHead(200, { 'content-type': type }); res.end(body); });
105
+ return new Promise((r) => server.listen(0, '127.0.0.1', () => r(server)));
106
+ }
107
+ function close(server) { if (server.closeAllConnections) server.closeAllConnections(); server.close(); }
108
+
109
+ test('probeHealthz says auth_configured only from a small JSON answer saying true', async () => {
110
+ const cases = [
111
+ ['{"ok":true,"auth_configured":true}', true],
112
+ ['{"ok":true,"auth_configured":false}', false],
113
+ ['{"ok":true}', false],
114
+ ['{"ok":true,"auth_configured":"true"}', false],
115
+ ['ok', false],
116
+ [JSON.stringify({ ok: true, auth_configured: true, pad: 'x'.repeat(8000) }), false],
117
+ ];
118
+ for (const [body, want] of cases) {
119
+ const s = await serve(body);
120
+ try {
121
+ const r = await liveness.probeHealthz(`http://127.0.0.1:${s.address().port}/healthz`, { timeoutMs: 3000 });
122
+ assert.equal(r.up, true, 'any 2xx is still up');
123
+ assert.equal(r.authConfigured, want, body.slice(0, 50));
124
+ } finally { close(s); }
125
+ }
126
+ });
127
+
128
+ test('the sweep records the fact for an up project that says so, and for no other', async () => {
129
+ const yes = await serve('{"ok":true,"auth_configured":true}');
130
+ const no = await serve('{"ok":true,"auth_configured":false}');
131
+ const inserts = [];
132
+ const db = {
133
+ query: async (sql, params) => {
134
+ if (/FROM provisioning_instances/.test(sql) && /WHERE status = 'active'/.test(sql)) {
135
+ return { rows: [{ id: 1, slug: 'kestrel', domain: null, port: yes.address().port }, { id: 2, slug: 'tern', domain: null, port: no.address().port }] };
136
+ }
137
+ if (/^UPDATE provisioning_instances/.test(sql)) return { rows: [{ id: params[0], prev_up: true }] };
138
+ if (/INSERT INTO provisioning_events/.test(sql)) { inserts.push(params); return { rows: [] }; }
139
+ throw new Error(`unexpected SQL: ${sql}`);
140
+ },
141
+ };
142
+ try {
143
+ liveness.resetNoticeState();
144
+ const out = await liveness.sweepOnce({ pool: db, log: () => {}, timeoutMs: 3000, notify: () => {}, collectRunnerDrift: async () => [] });
145
+ assert.equal(out.up, 2);
146
+ assert.deepEqual(inserts, [[1, 'github-sign-in-seen']]);
147
+ } finally { close(yes); close(no); }
148
+ });
@@ -58,7 +58,7 @@ test('modules: the row is the oversight row — title with the mono key, clamped
58
58
  assert.match(js, /<span class="ov-row__title">\$\{escapeHtml\(m\.title \|\| m\.key\)\}<span class="ov-mono">\$\{escapeHtml\(m\.key\)\}<\/span>/, 'the key rides inside the title in mono');
59
59
  assert.match(js, /class="ov-row__sub mod-row__desc"/, 'the description is the sub line');
60
60
  assert.match(js, /<details class="mod-more"/, 'a details disclosure opens the rest');
61
- assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}<\/div>/, 'provenance, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill are the facts line');
61
+ assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}\$\{restartFact\(m\)\}<\/div>/, 'provenance, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill and (task 1004463) the waits-for-a-restart pill are the facts line');
62
62
  assert.match(js, /<div class="ov-row__right mod-row__right">\$\{rightHtml\(m\)\}<\/div>/, 'the right column is one function');
63
63
  const css = read('modules.css');
64
64
  assert.match(css, /\.mod-row__desc \{[^}]*-webkit-line-clamp: 2/, 'the description clamps to two lines');
@@ -462,3 +462,34 @@ test('the demo proof module mounts + serves through the loader (BV1.R52 / task 1
462
462
  assert.deepEqual(routes.sort(), ['GET /demo/health', 'POST /demo/ping']);
463
463
  loader._reset();
464
464
  });
465
+
466
+ // ---------------------------------------------------------------------------
467
+ // task 1004463 — a toggle says whether it waits for a restart. Routes mount and pollers
468
+ // start once, at boot, so restartPendingFor compares the toggle with what THIS process
469
+ // mounted; the Modules toggle used to claim "Live on this box now" regardless.
470
+ // ---------------------------------------------------------------------------
471
+
472
+ test('task 1004463: restartPendingFor names what waits for a restart, and nothing before boot', () => {
473
+ loader._reset();
474
+ const factory = "module.exports = () => function moduleRoute(req, res, next) { next(); };";
475
+ const poller = 'module.exports = { start() {}, stop() {} };';
476
+ const root = fixtureRoot({
477
+ pages: { manifest: validManifest('pages', { contributes: { routes: ['main'] } }), files: { 'routes/main.js': factory } },
478
+ jobs: { manifest: validManifest('jobs', { contributes: { routes: ['main'], pollers: ['tick'] } }), files: { 'routes/main.js': factory, 'pollers/tick.js': poller } },
479
+ remote: { manifest: validManifest('remote', { contributes: { pollers: ['elsewhere'] } }) },
480
+ quiet: { manifest: validManifest('quiet') },
481
+ });
482
+ const opts = { roots: [root], core: '1.0.0' };
483
+ assert.deepEqual(loader.restartPendingFor('pages', true, opts), [], 'a process that never booted is never told a restart is pending');
484
+ const bootOn = (k) => k === 'jobs';
485
+ loader.mountModuleRoutes({ use: () => {} }, { isEnabled: bootOn }, opts);
486
+ loader.startModulePollers({}, { isEnabled: bootOn }, opts);
487
+ assert.deepEqual(loader.restartPendingFor('pages', true, opts), ['routes'], 'switched on after boot: its pages wait');
488
+ assert.deepEqual(loader.restartPendingFor('pages', false, opts), [], 'back off: matches what is mounted');
489
+ assert.deepEqual(loader.restartPendingFor('jobs', false, opts), ['routes', 'pollers'], 'switched off after boot: pages and jobs keep running');
490
+ assert.deepEqual(loader.restartPendingFor('jobs', true, opts), []);
491
+ assert.deepEqual(loader.restartPendingFor('remote', true, opts), [], 'a poller with no file here never runs in this process');
492
+ assert.deepEqual(loader.restartPendingFor('quiet', true, opts), [], 'nothing mounted at boot, nothing to wait for');
493
+ assert.deepEqual(loader.restartPendingFor('nope', true, opts), []);
494
+ loader._reset();
495
+ });
@@ -0,0 +1,73 @@
1
+ // tests/module_toggle_restart.mjs — task 1004463.
2
+ //
3
+ // Measured on cloudbongos.com: switching a module on stored the override and the toggle
4
+ // said "Live on this box now", but the module's pages stayed 404 until the next restart,
5
+ // because routes mount (and pollers start) once, at boot. The toggle now asks the loader
6
+ // what still waits (restartPendingFor) and says so; GET /modules carries the same field
7
+ // so the hall can show "waits for a restart" on the row.
8
+ //
9
+ // Hermetic: setOverride is stubbed BEFORE the route file binds it, and the loader's
10
+ // answer is stubbed on the loader object the route calls through. The loader's own rule
11
+ // is tests/module_loader.mjs (task 1004463 case).
12
+ //
13
+ // Run: node --test tests/module_toggle_restart.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { test } from 'node:test';
17
+ import { createRequire } from 'node:module';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const overrides = require('../src/bongos/module-overrides.js');
21
+ overrides.setOverride = async (key, enabled) => ({ [key]: enabled, sources: { [key]: 'override' } });
22
+ const loader = require('../src/module-loader/loader.js');
23
+ // the route binds restartPendingFor at require time, so stub it BEFORE the route loads and
24
+ // let each case choose the answer through `pending`
25
+ let pending = () => [];
26
+ loader.restartPendingFor = (...a) => pending(...a);
27
+ const buildModulesRouter = require('../src/bongos/routes/modules.js');
28
+
29
+ function handlerFor(router, method, path) {
30
+ const layer = router.stack.find((l) => l.route && l.route.path === path && l.route.methods[method]);
31
+ assert.ok(layer, `no ${method.toUpperCase()} ${path} on the router`);
32
+ return layer.route.stack[layer.route.stack.length - 1].handle;
33
+ }
34
+ async function invoke(handler, req) {
35
+ const res = {
36
+ statusCode: 200, body: undefined,
37
+ json(b) { this.body = b; return this; },
38
+ status(c) { this.statusCode = c; return this; },
39
+ fail(code, opts) { this.body = { error: code, ...opts }; return this; },
40
+ };
41
+ let err = null;
42
+ await handler(req, res, (e) => { err = e; });
43
+ if (err) throw err;
44
+ return res;
45
+ }
46
+ const router = buildModulesRouter();
47
+ const key = buildModulesRouter.listModules()[0].key;
48
+ const toggle = (enabled) => invoke(handlerFor(router, 'post', '/modules/:key/enable'), { params: { key }, query: {}, body: { enabled }, builder: { id: 1 } });
49
+
50
+ test('a toggle that waits for a restart says so, instead of "live now"', async () => {
51
+ pending = () => ['routes', 'pollers'];
52
+ const on = await toggle(true);
53
+ assert.deepEqual(on.body.restart_pending, ['routes', 'pollers']);
54
+ assert.doesNotMatch(on.body.note, /Live on this box now/);
55
+ assert.equal(on.body.note, 'Saved, and it survives the next deploy. It turns on at the next restart of this server. Until then its pages do not open and its background jobs do not run.');
56
+ pending = () => ['routes'];
57
+ const off = await toggle(false);
58
+ assert.equal(off.body.note, 'Saved, and it survives the next deploy. It turns off at the next restart of this server. Until then its pages still open.');
59
+ });
60
+
61
+ test('a toggle with nothing waiting is still live now', async () => {
62
+ pending = () => [];
63
+ const r = await toggle(true);
64
+ assert.deepEqual(r.body.restart_pending, []);
65
+ assert.equal(r.body.note, 'Live on this box now, and durable — it survives the next deploy.');
66
+ });
67
+
68
+ test('GET /modules carries restart_pending on every row', async () => {
69
+ pending = (k) => (k === key ? ['routes'] : []);
70
+ const r = await invoke(handlerFor(router, 'get', '/modules'), { params: {}, query: {}, body: {} });
71
+ for (const m of r.body.modules) assert.ok(Array.isArray(m.restart_pending), `${m.key} carries restart_pending`);
72
+ assert.deepEqual(r.body.modules.find((m) => m.key === key).restart_pending, ['routes']);
73
+ });
@@ -86,7 +86,10 @@ test('the restart door appears ONLY where it has a reason to exist, and mirrors
86
86
  // task 1004182 folded into 'cloud-host'. A `!== 'dedicated'` test against a value no row
87
87
  // can hold is always true, so keeping it would have been a condition that reads like a
88
88
  // guard and guards nothing.
89
- assert.doesNotMatch(body, /hosting_shape/, 'no shape term survives — the one it excluded no longer exists');
89
+ // task 1004463: the ONE shape term is an inclusion — the platform's own row always has the
90
+ // door, because a module toggled in its hall waits for a restart
91
+ assert.match(body, /inst.last_verified_up === false || inst.hosting_shape === 'control-plane'/, 'the self row is the one healthy card with a door');
92
+ assert.doesNotMatch(body, /dedicated/, 'the shape it once excluded no longer exists');
90
93
  assert.match(body, /!inst\.open_intent/, 'the strip tells the story while work is in flight');
91
94
  });
92
95
 
@@ -167,9 +167,9 @@ await ta('dry-run runs no real work and observes nothing', async () => {
167
167
  assert.equal(writes.length, 0);
168
168
  });
169
169
 
170
- await ta('a co-tenant restarts on the co-hosting box; a cloud-host on the control plane', async () => {
170
+ await ta("a co-tenant restarts on the co-hosting box; a cloud-host and the platform's own row on the control plane", async () => {
171
171
  const seen = {};
172
- for (const shape of ['co-tenant', 'cloud-host']) {
172
+ for (const shape of ['co-tenant', 'cloud-host', 'control-plane']) {
173
173
  const box = [], control = [];
174
174
  const { deps } = recorder();
175
175
  deps.exec = (cmd) => { box.push(cmd); return /healthz/.test(cmd) ? { ok: true, stdout: '{}' } : { ok: true, stdout: '' }; };
@@ -179,6 +179,7 @@ await ta('a co-tenant restarts on the co-hosting box; a cloud-host on the contro
179
179
  }
180
180
  assert.ok(seen['co-tenant'].box > 0 && seen['co-tenant'].control === 0, 'a co-tenant runs on the box');
181
181
  assert.ok(seen['cloud-host'].control > 0 && seen['cloud-host'].box === 0, 'a cloud-host runs control-plane-local (task 2074) — restart it THERE');
182
+ assert.ok(seen['control-plane'].control > 0 && seen['control-plane'].box === 0, 'the self row IS this box (task 1004463)');
182
183
  });
183
184
 
184
185
  await ta('privileged mode escalates the restart', async () => {
@@ -89,14 +89,17 @@ t('RESTART_FROM is exactly {active} — every other state is already served by t
89
89
  }
90
90
  });
91
91
 
92
- t('RESTART_SHAPES is the runner-reachable pair, and the set is a subset of HOSTING_SHAPES', () => {
93
- assert.deepEqual([...provisioning.RESTART_SHAPES].sort(), ['cloud-host', 'co-tenant']);
92
+ t('RESTART_SHAPES is what the runner can reach, and the set is a subset of HOSTING_SHAPES', () => {
93
+ // task 1004463 added the control plane's own row: its hall's module toggles wait for a restart
94
+ assert.deepEqual([...provisioning.RESTART_SHAPES].sort(), ['cloud-host', 'co-tenant', 'control-plane']);
95
+ const G = require('../modules/provisioning/control-plane-guard.js');
96
+ assert.equal(G.intentAllowedForShape('restart', 'control-plane'), true, 'the self-row guard lets the restart through, so the route never promises one it refuses');
94
97
  for (const s of provisioning.RESTART_SHAPES) assert.ok(provisioning.HOSTING_SHAPES.includes(s), `${s} is a real shape`);
95
98
  // Was `!has('dedicated')`. Task 1004182 removed that value from HOSTING_SHAPES, which made
96
99
  // the assertion vacuously true — it would now pass against any set at all. Name the shapes
97
100
  // that still EXIST and are still unreachable, and assert they are real first, so this
98
101
  // cannot rot the same way twice.
99
- for (const unreachable of ['byo-host', 'control-plane']) {
102
+ for (const unreachable of ['byo-host']) {
100
103
  assert.ok(provisioning.HOSTING_SHAPES.includes(unreachable), `${unreachable} must be a real shape or this assertion is vacuous`);
101
104
  assert.ok(!provisioning.RESTART_SHAPES.has(unreachable), unreachable);
102
105
  }
@@ -92,8 +92,8 @@ t('the two gates agree: every settings-apply shape is one the self-row guard let
92
92
  // "a restart is coming" and then answer the owner with control_plane_protected.
93
93
  const G = require('../modules/provisioning/control-plane-guard.js');
94
94
  for (const s of provisioning.SETTINGS_APPLY_SHAPES) assert.equal(G.intentAllowedForShape('settings-apply', s), true, s);
95
- // and the restart door was NOT opened on the self row as a side effect
96
- assert.ok(!provisioning.RESTART_SHAPES.has('control-plane'), 'task 1004334 widened the settings push only');
95
+ // the restart door on the self row came later, deliberately (task 1004463)
96
+ assert.ok(provisioning.RESTART_SHAPES.has('control-plane'), 'task 1004463 opened the restart door on the self row');
97
97
  });
98
98
 
99
99
  await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carries the settings-apply action', async () => {
@@ -398,3 +398,36 @@ test('part 03 names its own classes: the map table already owns .facts / .fact',
398
398
  assert.match(PANEL, /<div class="setFacts" id="homeFacts"/);
399
399
  assert.match(FACTS, /textNode\('div', 'setFact ' \+ r\.state\)/);
400
400
  });
401
+
402
+ // ── 6. sign-in set up another way (task 1004410) ───────────────────────────────────────
403
+ // A founder whose project already answers "sign-in works" was told "Waiting on GitHub" for
404
+ // good: the step ticked only from the one-click attempt. The liveness sweep now records the
405
+ // project's own answer ('github-sign-in-seen'), and that fact is what the step names.
406
+ const seen = { event: 'github-sign-in-seen', at: ago(2), detail: null };
407
+ const elsewhereCases = {
408
+ 'a GitHub tab that never came back': attempt({ status: 'pending', attempts: 0, created_at: ago(1), expires_at: soon(29) }),
409
+ 'a GitHub link that lapsed': attempt({ status: 'pending', attempts: 0, created_at: ago(90), expires_at: ago(30) }),
410
+ 'no attempt at all (a hand-made OAuth app)': null,
411
+ };
412
+ for (const [label, manifest] of Object.entries(elsewhereCases)) {
413
+ test(`sign-in that works is never "waiting on GitHub": ${label}`, async () => {
414
+ db = JSON.parse(JSON.stringify({ inst: inst(), intent: null, manifest, events: [seen] }));
415
+ const v = (await page()).read();
416
+ assert.deepEqual(v.byKey, { address: 'done', server: 'done', answers: 'done', approve: 'done', install: 'done', sign_in: 'done', device_flow: 'todo' });
417
+ assert.equal(v.count, 'ready ✓');
418
+ assert.equal(v.said, 'Kestrel is ready. Command-line sign-in is optional; it ticks itself whenever you turn it on.');
419
+ const approve = v.facts.find((f) => f.key === 'approve');
420
+ assert.equal(approve.sub, 'Not needed. Sign-in was set up another way.', 'says why a step it skipped is done');
421
+ assert.deepEqual(approve.acts, [], 'no Connect, Reopen or Pick up for a step that is not needed');
422
+ assert.equal(v.aliveHidden, false, 'the project is alive');
423
+ });
424
+ }
425
+
426
+ test('with our app recorded, the project\'s answer ticks sign-in and leaves the install its own fact', async () => {
427
+ db = JSON.parse(JSON.stringify({ inst: inst(), intent: null, manifest: attempt({ status: 'converting', app_id: 5, app_slug: 'kestrel-cloudbongos-com', updated_at: ago(1) }), events: [seen] }));
428
+ const v = (await page()).read();
429
+ assert.equal(v.byKey.approve, 'done');
430
+ assert.equal(v.byKey.sign_in, 'done');
431
+ assert.equal(v.byKey.install, 'you', 'the install is still GitHub\'s to confirm');
432
+ assert.notEqual(v.facts.find((f) => f.key === 'approve').sub, 'Not needed. Sign-in was set up another way.');
433
+ });