@bongos/core 1.19.951 → 1.19.953

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.
Files changed (57) hide show
  1. package/.bongos-core.json +67 -62
  2. package/docs/copy-inventory.md +32 -24
  3. package/docs/copy-registry.json +100 -28
  4. package/docs/module-api-changelog.md +4 -0
  5. package/modules/hall-ui/public/atlas.html +1 -1
  6. package/modules/hall-ui/public/blockers.html +1 -1
  7. package/modules/hall-ui/public/board-room.html +1 -1
  8. package/modules/hall-ui/public/collab.html +1 -1
  9. package/modules/hall-ui/public/copy-desk.html +1 -1
  10. package/modules/hall-ui/public/deploy.html +3 -3
  11. package/modules/hall-ui/public/deploy.js +97 -5
  12. package/modules/hall-ui/public/diagrams.html +1 -1
  13. package/modules/hall-ui/public/drachmae.html +1 -1
  14. package/modules/hall-ui/public/gate.html +1 -1
  15. package/modules/hall-ui/public/goal-map.html +1 -1
  16. package/modules/hall-ui/public/goals.html +1 -1
  17. package/modules/hall-ui/public/government.html +1 -1
  18. package/modules/hall-ui/public/idea.html +1 -1
  19. package/modules/hall-ui/public/ideas.html +1 -1
  20. package/modules/hall-ui/public/index.html +1 -1
  21. package/modules/hall-ui/public/modules.html +1 -1
  22. package/modules/hall-ui/public/people.html +1 -1
  23. package/modules/hall-ui/public/primer.html +1 -1
  24. package/modules/hall-ui/public/profile.html +1 -1
  25. package/modules/hall-ui/public/project-settings.css +16 -0
  26. package/modules/hall-ui/public/project-settings.html +1 -1
  27. package/modules/hall-ui/public/ranks.html +1 -1
  28. package/modules/hall-ui/public/roadmap.html +1 -1
  29. package/modules/hall-ui/public/roster.html +1 -1
  30. package/modules/hall-ui/public/sessions.html +1 -1
  31. package/modules/hall-ui/public/settings.html +1 -1
  32. package/modules/hall-ui/public/shell.js +12 -3
  33. package/modules/hall-ui/public/sky.html +1 -1
  34. package/modules/hall-ui/public/studio.html +1 -1
  35. package/modules/hall-ui/public/style.css +6 -0
  36. package/modules/hall-ui/public/task.html +1 -1
  37. package/modules/hall-ui/public/thinking.html +1 -1
  38. package/modules/hall-ui/public/watch.html +1 -1
  39. package/modules/hall-ui/public/work.html +1 -1
  40. package/modules/provisioning/core-upgrade.js +35 -3
  41. package/modules/provisioning/migrations/provisioning_022_last_core_move_index.sql +25 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/scripts/gds/adopt-preflight.js +1 -1
  45. package/scripts/gds/device-flow-help.js +1 -1
  46. package/scripts/gds/init.js +5 -3
  47. package/scripts/gds/login.js +1 -1
  48. package/scripts/gds/setup.js +1 -1
  49. package/src/module-api.js +1 -1
  50. package/tests/adopt_preflight.mjs +1 -1
  51. package/tests/bongos_login.mjs +1 -1
  52. package/tests/core_update_banner.mjs +30 -2
  53. package/tests/core_upgrade_door.mjs +50 -0
  54. package/tests/deploy_page_projects.mjs +131 -7
  55. package/tests/discord_approvals.mjs +1 -1
  56. package/tests/module_api.mjs +1 -1
  57. package/tests/nav_permission_atoms.mjs +1 -1
@@ -12,7 +12,7 @@
12
12
  // • CI collisions — the repo's workflows vs core's expected unit/secret-scan/
13
13
  // gate-review checks.
14
14
  // • IGNORED incoming files — a pre-existing .gitignore rule that would swallow what
15
- // adopt is about to commit (task 1003400). This is the check that was missing when
15
+ // adopt is about to commit (task 1003881). This is the check that was missing when
16
16
  // Cloud Bongos was adopted onto a repo whose .gitignore already carried `.claude/`:
17
17
  // the skills, hooks and settings.json were materialized onto disk and then dropped
18
18
  // by `git add`, so the adopt commit landed 8 files with no methodology surface at
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  // scripts/gds/device-flow-help.js — the one explanation of a Device-Flow refusal, shared
4
- // by `bongos setup` and `bongos login` (task 1003400).
4
+ // by `bongos setup` and `bongos login` (task 1003881).
5
5
  //
6
6
  // WHY IT EXISTS AT ALL. GitHub creates an App from a manifest with Device Flow OFF and
7
7
  // exposes no API to switch it on — only a checkbox in the App's settings. So on every
@@ -497,7 +497,8 @@ async function interview({ adopt = false } = {}) {
497
497
  // ---------------------------------------------------------------------------
498
498
 
499
499
  // PURE (exported for tests) — the root START-HERE guide (task 1002404, goal
500
- // 1000052). A freshly-CLONED instance must tell a human what to run FIRST: the
500
+ // 1000052; task 1003881 made it LAYER onto an existing README instead of
501
+ // skipping one — see layerReadme below). A freshly-CLONED instance must tell a human what to run FIRST: the
501
502
  // `bongos` CLI ships INSIDE the not-yet-installed @bongos/core package, so
502
503
  // `npm install` has to come before any `bongos` command (including `bongos dev`).
503
504
  // `bongos init` runs npm install during scaffolding, but a later `git clone` of
@@ -637,8 +638,9 @@ function writeConfigs(spec, { dir, force, dryRun, coreDep, log = console.log })
637
638
  log(` ✓ wrote ${path.relative(dir, modulesPath)}`);
638
639
  log(` ✓ wrote ${path.relative(dir, hierarchyPath)}`);
639
640
  log(` ✓ wrote ${path.relative(dir, pkgPath)} (pins ${CORE_PKG} → ${pinNote})`);
640
- // README START-HERE (task 1002404): mint it when absent, else APPEND the marked
641
- // section. Never a blind overwrite — an owner's own README content always survives.
641
+ // README START-HERE (task 1002404, layering added by task 1003881): mint it when
642
+ // absent, else APPEND the marked section. Never a blind overwrite — an owner's own
643
+ // README content always survives, which is why the guide stopped being skipped.
642
644
  layerReadme(branding, { dir, dryRun: false, log });
643
645
  return { brandingPath, modulesPath, hierarchyPath, pkgPath, readmePath, wrote: true };
644
646
  }
@@ -23,7 +23,7 @@
23
23
  const readline = require('node:readline');
24
24
  const { spawnSync } = require('node:child_process');
25
25
  const { saveSession, loadStoredSession, listStoredSessions } = require('./cli-lib');
26
- // The device-flow refusal detector + its plain-language cure (task 1003400). A leaf
26
+ // The device-flow refusal detector + its plain-language cure (task 1003881). A leaf
27
27
  // module, NOT setup.js: setup requires this file for runDeviceFlow, so requiring it
28
28
  // back would close a require cycle. Both callers depend on the leaf instead.
29
29
  const { isDeviceFlowDisabled, deviceFlowFallbackLines } = require('./device-flow-help');
@@ -274,7 +274,7 @@ async function promptOnboardingConsent() {
274
274
  }
275
275
 
276
276
  // ── W1: the manifest-born App has Device Flow OFF (task 1003051) ─────────────
277
- // The detector + the guidance moved to ./device-flow-help (task 1003400) so
277
+ // The detector + the guidance moved to ./device-flow-help (task 1003881) so
278
278
  // `bongos login` can use the same words without closing a require cycle back
279
279
  // through this file (setup requires login for runDeviceFlow). Re-exported here
280
280
  // because both are part of setup.js's tested public surface.
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.951'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.953'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -101,7 +101,7 @@ t('preflightAdopt: a clean repo → no blocking; every category reports', () =>
101
101
  } finally { rmSync(dir, { recursive: true, force: true }); }
102
102
  });
103
103
 
104
- // ---- ignored-incoming: the check that was missing (task 1003400) ----------
104
+ // ---- ignored-incoming: the check that was missing (task 1003881) ----------
105
105
  //
106
106
  // The failure it exists to stop: adopt onto a repo whose .gitignore already carries
107
107
  // `.claude/`. The skills/hooks/settings are written to disk, `git add` drops them, the
@@ -45,7 +45,7 @@ test('pollDecision maps the /access-requests/status body to a loop action', () =
45
45
  assert.equal(pollDecision({}), 'wait');
46
46
  });
47
47
 
48
- // ---- the Device-Flow dead end (task 1003400) -------------------------------
48
+ // ---- the Device-Flow dead end (task 1003881) -------------------------------
49
49
  //
50
50
  // Device Flow is OFF on every GitHub App created from a manifest and GitHub offers no
51
51
  // API to switch it on, so terminal sign-in is broken from birth on a fresh instance —
@@ -202,7 +202,7 @@ test('a real request through the assembled router returns the verdict, not a 500
202
202
 
203
203
  // ── the banner ────────────────────────────────────────────────────────────────
204
204
 
205
- function boot({ permissions, update, status = 200, dismissed = null, provisioning = true } = {}) {
205
+ function boot({ permissions, update, status = 200, dismissed = null, provisioning = true, pathname = '/settings' } = {}) {
206
206
  const els = new Map();
207
207
  const inserted = [];
208
208
  const stored = {};
@@ -227,7 +227,7 @@ function boot({ permissions, update, status = 200, dismissed = null, provisionin
227
227
  addEventListener() {}, dispatchEvent() {},
228
228
  get cookie() { return ''; }, set cookie(_v) {},
229
229
  };
230
- const location = { hostname: 'h', origin: 'https://h', pathname: '/settings', hash: '', assign() {}, replace() {} };
230
+ const location = { hostname: 'h', origin: 'https://h', pathname, hash: '', assign() {}, replace() {} };
231
231
  const windowObj = {
232
232
  __BRANDING__: {}, __MODULES__: { enabled: { provisioning } },
233
233
  BongosClient: {
@@ -334,3 +334,31 @@ test('the banner renders from the projection /me ACTUALLY sends, not one written
334
334
  'gates on is outside NAV_PERMISSION_ATOMS and the feature is dead for everyone (task 1004118)',
335
335
  );
336
336
  });
337
+
338
+ // ── the banner must not offer the page you are on (task 1004145) ──────────────
339
+ // The owner pressed this on /deploy and reported "I clicked update, but I didn't see
340
+ // anything happen". Nothing had: it is an <a href="/deploy"> styled as a button and
341
+ // labelled with the verb, so on /deploy it navigates to the current page and does
342
+ // nothing at all. No request was ever made — verified against the live box, which had
343
+ // no core-upgrade POST in three hours and no queued intent from that press.
344
+
345
+ test('on /deploy the banner points at the control instead of linking to /deploy', async () => {
346
+ const b = boot({ pathname: '/deploy', permissions: { 'core.pin.move': true }, update: BEHIND });
347
+ await new Promise((r) => setTimeout(r, 0));
348
+ const html = b.banner().innerHTML;
349
+ assert.doesNotMatch(html, /href="\/deploy"/, 'no link to the page already open');
350
+ assert.match(html, /control below/i, 'it says where the real control is');
351
+ assert.match(html, /1\.19\.871/, 'and still announces the release');
352
+ });
353
+
354
+ test('everywhere else the banner still offers the link', async () => {
355
+ const b = boot({ pathname: '/settings', permissions: { 'core.pin.move': true }, update: BEHIND });
356
+ await new Promise((r) => setTimeout(r, 0));
357
+ assert.match(b.banner().innerHTML, /href="\/deploy"/, 'the action is real from any other page');
358
+ });
359
+
360
+ test('a trailing slash is still /deploy', async () => {
361
+ const b = boot({ pathname: '/deploy/', permissions: { 'core.pin.move': true }, update: BEHIND });
362
+ await new Promise((r) => setTimeout(r, 0));
363
+ assert.doesNotMatch(b.banner().innerHTML, /href="\/deploy"/);
364
+ });
@@ -697,5 +697,55 @@ await t('every strict body schema in this module declares each field its handler
697
697
  'under strict, a field the schema does not name is a 400 BEFORE the handler runs, so reading it is unreachable code and a broken endpoint');
698
698
  });
699
699
 
700
+
701
+ console.log('\nthe last move, whatever became of it (task 1004145):');
702
+
703
+ const SNAP = { served_version: '1.19.900', available: ['1.19.900', '1.19.949'], recommended: '1.19.949', observed_at: new Date().toISOString() };
704
+
705
+ await t('a FAILED move is visible — the payload is no longer identical to before it', () => {
706
+ // open_intent is `state IN ('pending','running')` by the index's own definition, so an
707
+ // errored intent is not open. Before last_move, the payload after a rolled-back
708
+ // upgrade matched the payload before it exactly, and the owner's only signal was a
709
+ // number that never changed — indistinguishable from never having pressed.
710
+ const failed = { action: 'core-upgrade', state: 'error', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: 'working tree not clean — commit/stash first, or pass --force.' };
711
+ const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, failed);
712
+ assert.equal(p.open_intent, null, 'it is not an OPEN intent');
713
+ assert.equal(p.last_move.state, 'error');
714
+ assert.equal(p.last_move.target_version, '1.19.949');
715
+ assert.match(p.last_move.error, /working tree not clean/, 'and the reason travels');
716
+ });
717
+
718
+ await t('a SUCCESSFUL move is attributable, not merely finished', () => {
719
+ const done = { action: 'core-upgrade', state: 'done', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: null };
720
+ const p = CU.previewFromSnapshot({ ...ACTIVE }, { ...SNAP, served_version: '1.19.949', recommended: null }, null, done);
721
+ assert.equal(p.last_move.state, 'done');
722
+ assert.equal(p.last_move.target_version, p.served_version, 'the move names the version the box now serves');
723
+ assert.equal(p.last_move.error, null);
724
+ });
725
+
726
+ await t('a long refusal is truncated — it reaches a DOM', () => {
727
+ // The dirty-tree refusal carries a whole `git status`; test4's ran to dozens of lines.
728
+ const huge = { action: 'core-upgrade', state: 'error', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: 'x'.repeat(5000) };
729
+ const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, huge);
730
+ assert.ok(p.last_move.error.length <= 600, `bounded — got ${p.last_move.error.length}`);
731
+ });
732
+
733
+ await t('no move ever made is null, not an invented success', () => {
734
+ const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, null);
735
+ assert.equal(p.last_move, null);
736
+ });
737
+
738
+ await t('lastCoreMove reads MOVES only — a preview refusal is not a failed deploy', async () => {
739
+ // Folding core-upgrade-preflight in here would let a dry run's refusal render as
740
+ // "the move did not land". The preview reports itself through `preflight`.
741
+ const seen = [];
742
+ const pool = { query: async (sql, params) => { seen.push({ sql, params }); return { rows: [] }; } };
743
+ await CU.lastCoreMove(pool, 41);
744
+ assert.match(seen[0].sql, /action = 'core-upgrade'/, 'scoped to the move action');
745
+ assert.doesNotMatch(seen[0].sql, /preflight/, 'and never widened to the preview');
746
+ assert.doesNotMatch(seen[0].sql, /state IN/, 'and not filtered to open states — the point is the terminal ones');
747
+ assert.deepEqual(seen[0].params, [41]);
748
+ });
749
+
700
750
  console.log(`\ncore_upgrade_door: ${passed} passed, ${failed} failed`);
701
751
  if (failed) process.exit(1);
@@ -32,10 +32,15 @@ const FLEET = [
32
32
  { id: '35', slug: 'unreadable-one', status: 'active', hosting_shape: 'standalone' },
33
33
  ];
34
34
 
35
- function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200 } = {}) {
35
+ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200 } = {}) {
36
36
  const els = new Map();
37
37
  const posts = [];
38
38
  const gets = [];
39
+ // How many times each box has been re-read. A move POLLS, so a test of the move has to
40
+ // be able to change what the server says between reads — otherwise the only reachable
41
+ // assertion is "it asked", never "it noticed".
42
+ const reads = new Map();
43
+ const toasts = [];
39
44
  const makeEl = () => ({
40
45
  _on: {}, _html: '', hidden: false, textContent: '', disabled: false,
41
46
  addEventListener(ev, fn) { this._on[ev] = fn; },
@@ -48,7 +53,13 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
48
53
  const request = async (method, url, opts) => {
49
54
  const p = url.replace('/api/bongos', '');
50
55
  if (method === 'GET') gets.push(p);
51
- if (method === 'POST') { posts.push({ path: p, body: opts && opts.body }); return { ok: true, status: 200, data: { message: 'Queued.' } }; }
56
+ if (method === 'POST') {
57
+ posts.push({ path: p, body: opts && opts.body });
58
+ if (postStatus !== 200) {
59
+ return { ok: false, status: postStatus, data: { error: { code: 'intent_conflict', message: 'A core-upgrade run is currently running for this project.' } } };
60
+ }
61
+ return { ok: true, status: 200, data: { message: 'Queued.' } };
62
+ }
52
63
  if (p === '/provisioning/control-plane') {
53
64
  if (controlPlaneStatus !== 200) {
54
65
  return { ok: false, status: controlPlaneStatus, data: { error: { code: 'control_plane_not_enrolled', message: 'This platform has no deploy door yet. Run enroll-control-plane.js once.' } } };
@@ -66,13 +77,16 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
66
77
  if (!inst || inst.slug === 'unreadable-one') {
67
78
  return { ok: false, status: 500, data: { error: { code: 'core_upgrade_preview_failed', message: 'internal error' } } };
68
79
  }
69
- return { ok: true, status: 200, data: { preview: preview(inst.id, inst.slug, { hosting_shape: inst.hosting_shape }) } };
80
+ const n = (reads.get(inst.id) || 0) + 1;
81
+ reads.set(inst.id, n);
82
+ const over = instanceOver ? instanceOver(inst, n) : {};
83
+ return { ok: true, status: 200, data: { preview: preview(inst.id, inst.slug, { hosting_shape: inst.hosting_shape, ...over }) } };
70
84
  }
71
85
  return { ok: false, status: 404, data: { error: { code: 'not_found' } } };
72
86
  };
73
87
  const windowObj = {
74
88
  BongosClient: { createClient: () => ({ request }) },
75
- OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])), toast() {} },
89
+ OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])), toast(m) { toasts.push(String(m)); } },
76
90
  location: { assign() {} },
77
91
  };
78
92
  // The page's poll interval, driven rather than slept through. Injecting the REAL
@@ -81,13 +95,18 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
81
95
  // flaky against a torn-down sandbox.
82
96
  windowObj.__deployPollMs = 0;
83
97
  windowObj.__deployPollTries = 3;
98
+ windowObj.__deployMoveTries = moveTries;
84
99
  const sandbox = { window: windowObj, document: documentObj, console, setTimeout, clearTimeout, queueMicrotask, Date, Set, Map, Promise };
85
100
  sandbox.globalThis = sandbox;
86
101
  vm.createContext(sandbox);
87
102
  vm.runInContext(SRC, sandbox, { filename: 'deploy.js' });
88
103
  documentObj._ready();
89
104
  const settle = async () => { for (let i = 0; i < 40; i++) await new Promise((r) => setTimeout(r, 0)); };
90
- return { els, posts, gets, settle, panel: () => els.get('dp-panel').innerHTML, sub: () => els.get('dp-sub').textContent };
105
+ // A SHORT advance, for observing a state that exists only while the poll is running.
106
+ // `settle` drains the whole loop (POLL_MS is 0 here), so an assertion about the
107
+ // in-flight row has to look before the loop finishes, not after it.
108
+ const tick = async (n = 3) => { for (let i = 0; i < n; i++) await new Promise((r) => setTimeout(r, 0)); };
109
+ return { els, posts, gets, reads, toasts, settle, tick, panel: () => els.get('dp-panel').innerHTML, sub: () => els.get('dp-sub').textContent };
91
110
  }
92
111
 
93
112
  test('the platform AND every live hosted project get a row', async () => {
@@ -189,8 +208,12 @@ test('a move re-reads ONLY the box it moved, not every project again', async ()
189
208
  b.els.get('dp-confirm-32')._on.click();
190
209
  await b.settle();
191
210
  const previewReads = b.gets.filter((g) => g.endsWith('/core-upgrade'));
192
- assert.deepEqual(previewReads, ['/provisioning/instances/32/core-upgrade'],
193
- 'exactly one preview re-read, and it is the moved box');
211
+ // Since task 1004145 a move POLLS, so the count is no longer one — and the count was
212
+ // only ever a proxy for the thing that matters. Asserted directly instead: every
213
+ // re-read is the moved box, and no other box is touched.
214
+ assert.ok(previewReads.length > 0, 'the moved box is re-read');
215
+ assert.deepEqual([...new Set(previewReads)], ['/provisioning/instances/32/core-upgrade'],
216
+ 'and it is the ONLY box re-read — no fan-out across the fleet');
194
217
  assert.ok(!b.gets.includes('/provisioning/fleet'), 'the fleet is not re-listed');
195
218
  });
196
219
 
@@ -290,3 +313,104 @@ test('a verdict already in hand is NOT re-requested', async () => {
290
313
  assert.equal(b.posts.filter((x) => x.body && x.body.dry_run).length, 0, 'no check requested');
291
314
  assert.match(b.panel(), /nothing blocks this move/i, 'the held verdict is shown');
292
315
  });
316
+
317
+ // ── the press has to show something (task 1004145) ────────────────────────────
318
+ // The owner pressed Upgrade and reported "I didn't see anything happen". Two reasons:
319
+ // the banner's Upgrade is a link to /deploy (fixed in shell.js), and move() refreshed
320
+ // once — before the runner's tick — then fell silent for the whole upgrade.
321
+
322
+ test('confirming a move says so IMMEDIATELY, naming the version it is taking', async () => {
323
+ const b = boot();
324
+ await b.settle();
325
+ b.els.get('dp-preview-32')._on.click();
326
+ b.els.get('dp-confirm-32')._on.click();
327
+ await b.tick();
328
+ const html = b.panel();
329
+ assert.match(html, /Moving to core/i, 'the row reports the move, rather than snapping back');
330
+ assert.match(html, /1\.19\.871/, 'and names the target');
331
+ assert.match(html, /restarts as part of this/i, 'and warns what it costs');
332
+ });
333
+
334
+ test('the in-flight state survives the poll — it does not close under the reader', async () => {
335
+ // refreshOne() returns a row to step one, which is right after a move and wrong
336
+ // DURING one; without keepStep the poll wipes its own message on the first interval.
337
+ // Observed after a poll has actually run, but before the bound is exhausted.
338
+ // A long bound so the loop cannot exhaust mid-assertion: the state under test exists
339
+ // only WHILE it polls.
340
+ const b = boot({ moveTries: 500, instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
341
+ await b.settle();
342
+ b.els.get('dp-preview-32')._on.click();
343
+ b.els.get('dp-confirm-32')._on.click();
344
+ await b.tick(8);
345
+ assert.ok(b.reads.get('32') >= 1, 'at least one poll has landed');
346
+ assert.match(b.panel(), /Moving to core/i, 'still reporting after a refresh');
347
+ assert.doesNotMatch(b.panel(), /id="dp-preview-32"/, 'and has not reverted to step one');
348
+ });
349
+
350
+ test('when the box lands on the new core, it CONFIRMS it', async () => {
351
+ // The whole ask: "confirmation when it updates". A changed number is not a
352
+ // confirmation — nothing tied it to the press.
353
+ const b = boot({
354
+ instanceOver: (inst, n) => (n >= 2
355
+ ? { served_version: '1.19.871', behind: false, open_intent: null, last_move: { target_version: '1.19.871', state: 'done', at: new Date().toISOString(), error: null } }
356
+ : { open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }),
357
+ });
358
+ await b.settle();
359
+ b.els.get('dp-preview-32')._on.click();
360
+ b.els.get('dp-confirm-32')._on.click();
361
+ await b.settle();
362
+ const html = b.panel();
363
+ assert.match(html, /Now running core 1\.19\.871/i, 'the move is confirmed, not merely finished');
364
+ assert.doesNotMatch(html, /Moving to core/i, 'and the in-flight state is gone');
365
+ });
366
+
367
+ test('a FAILED move is reported, with the reason, instead of looking untouched', async () => {
368
+ // An errored intent is not an OPEN intent, so before last_move existed the payload
369
+ // after a rolled-back upgrade was identical to the payload before it.
370
+ const b = boot({
371
+ instanceOver: (inst, n) => (n >= 2
372
+ ? { open_intent: null, last_move: { target_version: '1.19.871', state: 'error', at: new Date().toISOString(), error: 'working tree not clean — commit/stash first, or pass --force.' } }
373
+ : { open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }),
374
+ });
375
+ await b.settle();
376
+ b.els.get('dp-preview-32')._on.click();
377
+ b.els.get('dp-confirm-32')._on.click();
378
+ await b.settle();
379
+ const html = b.panel();
380
+ assert.match(html, /did not land/i, 'the failure is stated');
381
+ assert.match(html, /working tree not clean/, 'and carries the runner’s own reason');
382
+ assert.match(html, /put back on 1\.19\.800/i, 'and says the project is still running');
383
+ });
384
+
385
+ test('a poll that runs out says it is still working, not that it failed', async () => {
386
+ // MOVE_TRIES is a bound on this page's patience, not on the runner's work.
387
+ const b = boot({ instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
388
+ await b.settle();
389
+ b.els.get('dp-preview-32')._on.click();
390
+ b.els.get('dp-confirm-32')._on.click();
391
+ for (let i = 0; i < 6; i++) await b.settle();
392
+ assert.ok(b.toasts.some((t) => /Still working on core 1\.19\.871/i.test(t)),
393
+ `it says where things stand — got ${JSON.stringify(b.toasts)}`);
394
+ assert.doesNotMatch(b.panel(), /did not land/i, 'and never claims a failure it did not see');
395
+ });
396
+
397
+ test('a queue that is REFUSED leaves no in-flight state to wait on', async () => {
398
+ // THIS TEST USED TO PASS WITHOUT CLICKING ANYTHING. Its setup read
399
+ // `b.els.get('dp-confirm-32')._on = el._on` — a self-assignment no-op — and set
400
+ // `b.postFails`, a property the harness never read. So it asserted "no in-flight
401
+ // state" on a row where no move had been attempted, which is true of every row on the
402
+ // page. The grader caught it. It now refuses the POST for real and presses the button.
403
+ const b = boot({ postStatus: 409 });
404
+ await b.settle();
405
+ b.els.get('dp-preview-32')._on.click();
406
+ b.els.get('dp-confirm-32')._on.click();
407
+ await b.settle();
408
+ // Counted on the MOVE specifically: arming the row also POSTs a dry_run pre-flight,
409
+ // so a bare posts.length would have been satisfied by the preview alone — the same
410
+ // class of mistake this test is being fixed for.
411
+ const moves = b.posts.filter((x) => !(x.body && x.body.dry_run));
412
+ assert.equal(moves.length, 1, `the press really happened — posts: ${JSON.stringify(b.posts)}`);
413
+ assert.doesNotMatch(b.panel(), /Moving to core/i, 'nothing was queued, so nothing is watched');
414
+ assert.ok(b.toasts.some((t) => /currently running/i.test(t)), 'and the refusal is reported');
415
+ assert.match(b.panel(), /id="dp-confirm-32"/, 'the button comes back so it can be retried');
416
+ });
@@ -170,7 +170,7 @@ await t('a DB error returns {error}, never throws', async () => {
170
170
  assert.equal(r.outcome, 'error');
171
171
  });
172
172
 
173
- // ---- the outbound half must not outlive the inbound half (task 1003400) ----
173
+ // ---- the outbound half must not outlive the inbound half (task 1003881) ----
174
174
  //
175
175
  // The 👍 this line invites is applied ONLY by discord-approvals.js, which lives in the
176
176
  // `discord` module. onboarding (which posts) is default-ON; discord is default-OFF; and
@@ -48,7 +48,7 @@ const PUBLISHED_SURFACE = [
48
48
  'artistGate', // added by task 1003575 (ADR 0241): the artist gate — off | advisory | strict, plus the pure verdict helpers over it. Two modules ask it two different questions ("file this review?" and "does an open one hold the deploy?") and both must resolve the knob through this one reader, never branding().project
49
49
  'readApplicantProfileFromHub', // added by task 1002972 (privacy spec D7): the reviewer queue's LIVE per-render read of one applicant's hub profile view. NARROW by design — the underlying call is authenticated with this instance's hub client secret, and loadIdpConfig stays OFF the doorway so no module ever holds the credential that speaks for the whole project
50
50
  'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
51
- 'isModuleEnabled', // task 1003400: is ANOTHER module present? onboarding's Discord approve/decline control is only actionable when the `discord` module's reaction handler is loaded, so the posting half has to be able to ask
51
+ 'isModuleEnabled', // task 1003881: is ANOTHER module present? onboarding's Discord approve/decline control is only actionable when the `discord` module's reaction handler is loaded, so the posting half has to be able to ask
52
52
  'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
53
53
  'on', 'emit', 'emitAsync', 'listEvents', 'seamSnapshot',
54
54
  'contribute', 'contributions', 'collectContributions', 'listContributionPoints',
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = '8a93e0b4ca715b9c';
90
+ const SHELL_SHA = '02de48ca2ae07995';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);