@bongos/core 1.19.971 → 1.19.973

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 (77) hide show
  1. package/.bongos-core.json +116 -76
  2. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  3. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  4. package/docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md +1 -1
  5. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +107 -0
  6. package/docs/adr/README.md +1 -0
  7. package/docs/api/openapi.json +1 -1
  8. package/docs/api-reference.md +1 -1
  9. package/docs/architecture.md +6 -6
  10. package/docs/branding-contract.md +1 -1
  11. package/docs/canonical-permissions.md +24 -24
  12. package/docs/copy-inventory.md +193 -187
  13. package/docs/copy-registry.json +320 -276
  14. package/docs/design/projects-hub-direction-v2.md +1 -1
  15. package/docs/design/vanilla-hall-ui-redesign-scope.md +2 -2
  16. package/docs/file-map.md +7 -7
  17. package/docs/handoff-template.md +1 -1
  18. package/docs/module-api-changelog.md +4 -0
  19. package/docs/modules-contract.md +1 -1
  20. package/docs/onboarding/drachmae-and-karma.md +2 -2
  21. package/docs/packs/engineer.md +5 -5
  22. package/docs/packs/ideator.md +1 -1
  23. package/docs/recipes/README.md +1 -1
  24. package/docs/recipes/audit-replay.md +2 -2
  25. package/docs/recipes/gds-db-backup.md +3 -3
  26. package/docs/recipes/managed-settings-remote-control.md +9 -9
  27. package/docs/recipes/memory-sync-conflicts.md +2 -2
  28. package/docs/recipes/ops-gotchas.md +24 -5
  29. package/docs/recipes/overnight-builder.md +3 -3
  30. package/docs/recipes/parallel-goal-waves.md +3 -3
  31. package/docs/recipes/reranking-a-goals-board.md +2 -2
  32. package/docs/recipes/search-index-ingestion.md +1 -1
  33. package/docs/recipes/ship-and-regrade.md +2 -2
  34. package/docs/recipes/staging-environment.md +1 -1
  35. package/docs/recipes/windows-builders.md +1 -1
  36. package/modules/hall-ui/public/deploy.html +10 -5
  37. package/modules/hall-ui/public/deploy.js +199 -110
  38. package/modules/hall-ui/public/goals-page.js +6 -0
  39. package/modules/hall-ui/public/goals-render.js +16 -0
  40. package/modules/hall-ui/public/mine-lens.js +172 -0
  41. package/modules/hall-ui/public/project-settings.css +36 -0
  42. package/modules/hall-ui/public/work-summary.js +90 -0
  43. package/modules/hall-ui/public/work.css +50 -0
  44. package/modules/hall-ui/public/work.html +24 -0
  45. package/modules/hall-ui/public/work.js +38 -51
  46. package/modules/lifecycle/db-goals.js +20 -3
  47. package/modules/lifecycle/routes/goals.js +2 -2
  48. package/modules/provisioning/core-upgrade.js +61 -2
  49. package/modules/public-landing/public/contact.html +1 -1
  50. package/modules/public-landing/public/index.html +1 -1
  51. package/modules/public-landing/public/privacy.html +1 -1
  52. package/modules/public-landing/public/projects.html +1 -1
  53. package/modules/public-landing/public/terms.html +1 -1
  54. package/package-lock.json +2 -2
  55. package/package.json +1 -1
  56. package/scripts/gds/api-alias-caller-scan.js +47 -9
  57. package/scripts/gds/cloudbongos-token-isolation.conf +33 -0
  58. package/scripts/gds/fitness.js +3 -0
  59. package/scripts/gds/provision-core-upgrade.js +132 -1
  60. package/scripts/gds/run-unit-tests.js +14 -9
  61. package/scripts/gds/scope-roster-guard.js +118 -0
  62. package/scripts/hall-preview/server.js +5 -0
  63. package/src/bongos/module-scope-map.js +1 -0
  64. package/src/module-api.js +1 -1
  65. package/tests/api_alias_caller_ratchet.mjs +68 -2
  66. package/tests/core_upgrade_door.mjs +81 -0
  67. package/tests/core_upgrade_runner.mjs +132 -0
  68. package/tests/deploy_page_projects.mjs +234 -79
  69. package/tests/fitness_scope_roster.mjs +117 -0
  70. package/tests/goal_mine_lens.mjs +233 -0
  71. package/tests/goal_tier.mjs +30 -4
  72. package/tests/hall_mine_lens.mjs +381 -0
  73. package/tests/idea_spark_hall.mjs +101 -12
  74. package/tests/known_failing_ratchet.mjs +19 -9
  75. package/tests/landing_legal_pages.mjs +5 -1
  76. package/tests/projects_hub_pre_uat.mjs +12 -6
  77. package/tests/served_paths_shipped.mjs +36 -2
@@ -1,11 +1,16 @@
1
- // tests/deploy_page_projects.mjs — the deploy page now drives every hosted project,
2
- // not only the control plane's own row (task 1004046).
1
+ // tests/deploy_page_projects.mjs — the deploy page is about ONE project: the hall's own,
2
+ // or the one a ?project= scope names (task 1004171, narrowing task 1004046's fleet list).
3
3
  //
4
4
  // These EXECUTE deploy.js in a DOM stub and read what it painted. The sibling suite
5
5
  // (core_upgrade_door.mjs) asserts the page's invariants against its SOURCE, which is
6
- // right for "never computes a version comparison" but cannot see the thing that broke
7
- // when this page went multi-box: whether a click on one box arms that box and not
8
- // another. That is behaviour, so it is tested as behaviour.
6
+ // right for "never computes a version comparison" but cannot see what this page is for:
7
+ // WHICH box it renders, and whether a press lands on that box. That is behaviour, so it
8
+ // is tested as behaviour.
9
+ //
10
+ // WHY THE FLEET FIXTURE SURVIVES a task that removed the fleet listing: the scope lookup
11
+ // still resolves a slug through it, and the regression worth pinning is not "the roster
12
+ // read is gone" but "the roster is never LISTED". A harness with no fleet could not tell
13
+ // those two apart.
9
14
  import assert from 'node:assert/strict';
10
15
  import { test } from 'node:test';
11
16
  import fs from 'node:fs';
@@ -32,7 +37,7 @@ const FLEET = [
32
37
  { id: '35', slug: 'unreadable-one', status: 'active', hosting_shape: 'standalone' },
33
38
  ];
34
39
 
35
- function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200 } = {}) {
40
+ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200, scope = null } = {}) {
36
41
  const els = new Map();
37
42
  const posts = [];
38
43
  const gets = [];
@@ -87,7 +92,9 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
87
92
  const windowObj = {
88
93
  BongosClient: { createClient: () => ({ request }) },
89
94
  OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])), toast(m) { toasts.push(String(m)); } },
90
- location: { assign() {} },
95
+ // `search` is what decides which project the page is about. A stub without it is the
96
+ // shape the page has to survive, and the default here keeps that path exercised.
97
+ location: { assign() {}, search: scope ? `?project=${encodeURIComponent(scope)}` : '' },
91
98
  };
92
99
  // The page's poll interval, driven rather than slept through. Injecting the REAL
93
100
  // setTimeout made every case whose preview never converges sit out the page's full
@@ -96,7 +103,7 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
96
103
  windowObj.__deployPollMs = 0;
97
104
  windowObj.__deployPollTries = 3;
98
105
  windowObj.__deployMoveTries = moveTries;
99
- const sandbox = { window: windowObj, document: documentObj, console, setTimeout, clearTimeout, queueMicrotask, Date, Set, Map, Promise };
106
+ const sandbox = { window: windowObj, document: documentObj, console, setTimeout, clearTimeout, queueMicrotask, Date, Set, Map, Promise, decodeURIComponent };
100
107
  sandbox.globalThis = sandbox;
101
108
  vm.createContext(sandbox);
102
109
  vm.runInContext(SRC, sandbox, { filename: 'deploy.js' });
@@ -109,64 +116,88 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
109
116
  return { els, posts, gets, reads, toasts, settle, tick, panel: () => els.get('dp-panel').innerHTML, sub: () => els.get('dp-sub').textContent };
110
117
  }
111
118
 
112
- test('the platform AND every live hosted project get a row', async () => {
119
+ // ---- WHICH project the page is about (task 1004171) -------------------------------
120
+
121
+ test('unscoped, the page is the hall’s OWN project and nothing else', async () => {
113
122
  const b = boot();
114
123
  await b.settle();
115
124
  const html = b.panel();
116
- assert.match(html, /cloudbongos/, 'the platform is still here');
117
- assert.match(html, /mercury/, 'a standalone project');
118
- assert.match(html, /hoodify-finance/, 'a co-tenant project');
119
- assert.match(b.sub(), /3 hosted projects/);
125
+ assert.match(html, /cloudbongos/, 'the hall’s own project');
126
+ assert.doesNotMatch(html, /mercury/, 'and not another project');
127
+ assert.doesNotMatch(html, /hoodify-finance/);
128
+ assert.match(b.sub(), /^cloudbongos$/);
120
129
  });
121
130
 
122
- test('torn-down projects never appear — the page is about what is live', async () => {
131
+ test('unscoped, it never LISTS the fleet or probes a project it is not about', async () => {
132
+ // The defect this replaces: one hall’s page fanned a preview request at every live
133
+ // instance, including two belonging to other builders, and offered a move on each.
123
134
  const b = boot();
124
135
  await b.settle();
136
+ assert.ok(!b.gets.includes('/provisioning/fleet'), 'no roster read');
137
+ assert.ok(!b.gets.includes('/provisioning/instances'), 'and no fallback roster read');
138
+ assert.deepEqual(b.gets.filter((g) => /\/instances\/\w+\/core-upgrade$/.test(g)), [],
139
+ 'and no per-project probe');
140
+ assert.equal(b.reads.size, 0, 'no tenant box was touched at all');
141
+ });
142
+
143
+ test('?project= renders THAT project, by slug, and still only one row', async () => {
144
+ const b = boot({ scope: 'mercury' });
145
+ await b.settle();
125
146
  const html = b.panel();
126
- assert.doesNotMatch(html, /walk-lantern/);
127
- assert.doesNotMatch(html, /testccc/);
147
+ assert.match(html, /mercury/);
148
+ assert.doesNotMatch(html, /cloudbongos/, 'the self row is not added beside it');
149
+ assert.doesNotMatch(html, /hoodify-finance/, 'nor any other project');
150
+ assert.equal(b.sub(), 'mercury');
151
+ assert.ok(!b.gets.includes('/provisioning/control-plane'), 'and the self row is not even read');
128
152
  });
129
153
 
130
- test('a project whose state cannot be read is LISTED, not silently dropped', async () => {
131
- // A box missing from a deploy page reads as a box that needs no deploying.
132
- const b = boot();
154
+ test('a scoped project is offered its own move, on its own id', async () => {
155
+ const b = boot({ scope: 'mercury' });
156
+ await b.settle();
157
+ b.els.get('dp-preview-31')._on.click();
158
+ b.els.get('dp-confirm-31')._on.click();
159
+ await b.settle();
160
+ const moves = b.posts.filter((x) => !(x.body && x.body.dry_run));
161
+ assert.equal(moves.length, 1);
162
+ assert.equal(moves[0].path, '/provisioning/instances/31/core-upgrade');
163
+ assert.equal(moves[0].body.to, '1.19.871', 'the version it was SHOWN');
164
+ });
165
+
166
+ test('a ?project= nobody hosts is NAMED, not left blank', async () => {
167
+ // A deploy page that renders nothing reads as a project with nothing to deploy, which
168
+ // is the one wrong impression this page must never give.
169
+ const b = boot({ scope: 'not-a-project' });
170
+ await b.settle();
171
+ assert.match(b.panel(), /not-a-project/);
172
+ assert.match(b.panel(), /no project by that name is hosted here/);
173
+ });
174
+
175
+ test('a scoped project whose state cannot be read is LISTED, not silently dropped', async () => {
176
+ const b = boot({ scope: 'unreadable-one' });
133
177
  await b.settle();
134
178
  assert.match(b.panel(), /unreadable-one/);
135
179
  assert.match(b.panel(), /Could not read this project/);
136
180
  });
137
181
 
138
- test('opening the confirm on one project does not arm another', async () => {
139
- const b = boot();
182
+ test('a scope resolves through the roster the viewer can actually see', async () => {
183
+ // A holder of core.pin.move who lacks the fleet atom still arrives from their own
184
+ // project's hall, so the lookup falls back to their own projects rather than erroring.
185
+ const b = boot({ scope: 'mercury', fleetStatus: 403 });
140
186
  await b.settle();
141
- b.els.get('dp-preview-31')._on.click();
142
- const html = b.panel();
143
- assert.match(html, /id="dp-confirm-31"/, 'mercury is armed');
144
- assert.doesNotMatch(html, /id="dp-confirm-32"/, 'hoodify-finance is NOT');
145
- assert.match(html, /id="dp-preview-32"/, 'and is still at step one');
187
+ assert.match(b.panel(), /mercury/);
188
+ assert.ok(b.gets.includes('/provisioning/instances'), 'via the own-projects roster');
146
189
  });
147
190
 
148
- test('confirming sends THAT project’s id and the version it was shown', async () => {
191
+ test('a window with no location.search is the unscoped case, not a crash', async () => {
192
+ // The vm harness is the honest version of a browser that has not given us one; a page
193
+ // that threw here would take the whole deploy door down for an ordinary visit.
149
194
  const b = boot();
150
195
  await b.settle();
151
- b.els.get('dp-preview-32')._on.click();
152
- b.els.get('dp-confirm-32')._on.click();
153
- await b.settle();
154
- // Arming also asks for a dry run now (task 1004121), so the MOVE is the post that is
155
- // not one — separated by its payload rather than by its position, because "the second
156
- // post" would silently start asserting about the wrong request the next time the order
157
- // changes.
158
- const moves = b.posts.filter((x) => !(x.body && x.body.dry_run));
159
- assert.equal(moves.length, 1, 'exactly one move is sent');
160
- assert.equal(moves[0].path, '/provisioning/instances/32/core-upgrade');
161
- // Field-wise, not deepEqual: the body is built inside the vm realm, so its
162
- // prototype is not this realm's Object and deepStrictEqual rejects it on identity
163
- // alone — a harness artifact that says nothing about the payload.
164
- assert.equal(moves[0].body.to, '1.19.871');
165
- assert.deepEqual(Object.keys(moves[0].body), ['to'], 'and nothing else is sent');
166
- // And the move must never carry the flag that would turn it into a no-op.
167
- assert.equal(moves[0].body.dry_run, undefined, 'a move is not a dry run');
196
+ assert.match(b.panel(), /cloudbongos/);
168
197
  });
169
198
 
199
+ // ---- the one box's states ---------------------------------------------------------
200
+
170
201
  test('a box with nothing newer offers no button', async () => {
171
202
  const b = boot({ controlPlane: preview('1', 'cloudbongos', { behind: false, recommended_version: null }) });
172
203
  await b.settle();
@@ -195,43 +226,35 @@ test('a database that is current, or whose state is unknown, gets no "behind" fl
195
226
  }
196
227
  });
197
228
 
198
- test('an unenrolled control plane still lets the projects through', async () => {
199
- // The regression this pins: the platform's own missing row used to be the whole
200
- // page, so one un-run setup command would hide every project too.
229
+ test('an unenrolled control plane says so, in the server’s own words', async () => {
201
230
  const b = boot({ controlPlaneStatus: 404 });
202
231
  await b.settle();
203
- const html = b.panel();
204
- assert.match(html, /has no deploy door yet/, 'the server’s own sentence, naming the fix');
205
- assert.match(html, /mercury/, 'and the projects are still listed');
206
- });
207
-
208
- test('a viewer without the fleet atom falls back to their own projects', async () => {
209
- const b = boot({ fleetStatus: 403 });
210
- await b.settle();
211
- assert.match(b.panel(), /mercury/);
232
+ assert.match(b.panel(), /has no deploy door yet/, 'the server’s own sentence, naming the fix');
212
233
  });
213
234
 
214
- test('a move re-reads ONLY the box it moved, not every project again', async () => {
215
- // The grader's finding on the first round: refreshing one row by re-running the
216
- // whole page load fans a probe out at every hosted project, including ones the
217
- // owner never touched.
235
+ test('confirming sends the version it was shown, and nothing else', async () => {
218
236
  const b = boot();
219
237
  await b.settle();
220
- b.gets.length = 0;
221
- b.els.get('dp-preview-32')._on.click();
222
- b.els.get('dp-confirm-32')._on.click();
238
+ b.els.get('dp-preview-1')._on.click();
239
+ b.els.get('dp-confirm-1')._on.click();
223
240
  await b.settle();
224
- const previewReads = b.gets.filter((g) => g.endsWith('/core-upgrade'));
225
- // Since task 1004145 a move POLLS, so the count is no longer one — and the count was
226
- // only ever a proxy for the thing that matters. Asserted directly instead: every
227
- // re-read is the moved box, and no other box is touched.
228
- assert.ok(previewReads.length > 0, 'the moved box is re-read');
229
- assert.deepEqual([...new Set(previewReads)], ['/provisioning/instances/32/core-upgrade'],
230
- 'and it is the ONLY box re-read — no fan-out across the fleet');
231
- assert.ok(!b.gets.includes('/provisioning/fleet'), 'the fleet is not re-listed');
241
+ // Arming also asks for a dry run now (task 1004121), so the MOVE is the post that is
242
+ // not one — separated by its payload rather than by its position, because "the second
243
+ // post" would silently start asserting about the wrong request the next time the order
244
+ // changes.
245
+ const moves = b.posts.filter((x) => !(x.body && x.body.dry_run));
246
+ assert.equal(moves.length, 1, 'exactly one move is sent');
247
+ assert.equal(moves[0].path, '/provisioning/instances/1/core-upgrade');
248
+ // Field-wise, not deepEqual: the body is built inside the vm realm, so its
249
+ // prototype is not this realm's Object and deepStrictEqual rejects it on identity
250
+ // alone — a harness artifact that says nothing about the payload.
251
+ assert.equal(moves[0].body.to, '1.19.871');
252
+ assert.deepEqual(Object.keys(moves[0].body), ['to'], 'and nothing else is sent');
253
+ // And the move must never carry the flag that would turn it into a no-op.
254
+ assert.equal(moves[0].body.dry_run, undefined, 'a move is not a dry run');
232
255
  });
233
256
 
234
- test('a move on the platform row re-reads the control plane, not a tenant preview', async () => {
257
+ test('a move re-reads its own box from its own endpoint, and no other', async () => {
235
258
  const b = boot();
236
259
  await b.settle();
237
260
  b.gets.length = 0;
@@ -239,7 +262,8 @@ test('a move on the platform row re-reads the control plane, not a tenant previe
239
262
  b.els.get('dp-confirm-1')._on.click();
240
263
  await b.settle();
241
264
  assert.ok(b.gets.includes('/provisioning/control-plane'), 'the self row is read from its own endpoint');
242
- assert.ok(!b.gets.some((g) => /instances\/3\d\/core-upgrade/.test(g)), 'and no tenant is re-probed');
265
+ assert.ok(!b.gets.some((g) => /instances\/3\d\/core-upgrade/.test(g)), 'and no tenant is probed');
266
+ assert.ok(!b.gets.includes('/provisioning/fleet'), 'the fleet is not listed');
243
267
  });
244
268
 
245
269
  // ---- the pre-flight verdict on step two (task 1004121) ---------------------------
@@ -259,6 +283,82 @@ test('step two shows what the dry run found, not just a restatement of the actio
259
283
  assert.match(html, /database &quot;cloudbongos&quot; reachable|database "cloudbongos" reachable/, 'and shows what was checked');
260
284
  });
261
285
 
286
+ // ── what the move would CARRY (task 1004173) ─────────────────────────────────────
287
+
288
+ const CHANGES = {
289
+ from: '1.19.800', to: '1.19.871', more: 0,
290
+ entries: [
291
+ { text: 'Builders can now tick one box to see only their own work.' },
292
+ { id: '1004167', text: 'Groundwork for letting an owner make their code repository private.' },
293
+ ],
294
+ };
295
+
296
+ test('step two names what the move BRINGS IN, in the ships’ own words', async () => {
297
+ // The whole complaint: "I don't actually see what would change when I hit the button."
298
+ // The pre-flight answers whether the machinery will allow it, which is a different
299
+ // question, and answering it was being presented as the preview.
300
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
301
+ preflight: { target: '1.19.871', ok: true, lines: [], error: null, changes: CHANGES },
302
+ preflight_at: new Date().toISOString(),
303
+ }) });
304
+ await b.settle();
305
+ b.els.get('dp-preview-1')._on.click();
306
+ const html = b.panel();
307
+ assert.match(html, /What this brings in/, 'the question the owner asked is answered');
308
+ assert.match(html, /2\s+pieces of work landed between core 1\.19\.800\s+and 1\.19\.871/);
309
+ assert.match(html, /tick one box to see only their own work/, 'in the ship’s own sentence');
310
+ assert.match(html, /code repository private/);
311
+ assert.match(html, /nothing blocks this move/i, 'and the verdict is STILL there below it');
312
+ });
313
+
314
+ test('a capped list says how many it did not show', async () => {
315
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
316
+ preflight: { target: '1.19.871', ok: true, lines: [], error: null, changes: { ...CHANGES, more: 94 } },
317
+ preflight_at: new Date().toISOString(),
318
+ }) });
319
+ await b.settle();
320
+ b.els.get('dp-preview-1')._on.click();
321
+ assert.match(b.panel(), /and 94 more, not listed/);
322
+ });
323
+
324
+ test('A RANGE THE RUNNER COULD NOT READ SAYS SO — never an empty list', async () => {
325
+ // "Nothing changed" over ninety-one releases is the one wrong impression this section
326
+ // could give, and it is exactly the shape a silent failure takes.
327
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
328
+ preflight: { target: '1.19.871', ok: true, lines: [], error: null,
329
+ changes: { from: '1.19.800', to: '1.19.871', error: 'could not find those releases in the core history on this machine' } },
330
+ preflight_at: new Date().toISOString(),
331
+ }) });
332
+ await b.settle();
333
+ b.els.get('dp-preview-1')._on.click();
334
+ const html = b.panel();
335
+ assert.match(html, /Could not read what this would bring in/);
336
+ assert.match(html, /could not find those releases/, 'in the runner’s own words');
337
+ assert.doesNotMatch(html, /What this brings in/, 'and never dressed up as a list');
338
+ });
339
+
340
+ test('no change list at all renders nothing, rather than an empty heading', async () => {
341
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
342
+ preflight: { target: '1.19.871', ok: true, lines: ['database reachable'], error: null },
343
+ preflight_at: new Date().toISOString(),
344
+ }) });
345
+ await b.settle();
346
+ b.els.get('dp-preview-1')._on.click();
347
+ const html = b.panel();
348
+ assert.doesNotMatch(html, /What this brings in/);
349
+ assert.doesNotMatch(html, /Could not read what this would bring in/);
350
+ assert.match(html, /nothing blocks this move/i, 'the verdict still shows');
351
+ });
352
+
353
+ test('the wait names what it is waiting for, and that the machine is being asked', async () => {
354
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { preflight: null, preflight_at: null }) });
355
+ await b.settle();
356
+ b.els.get('dp-preview-1')._on.click();
357
+ const html = b.panel();
358
+ assert.match(html, /what it would bring in/, 'both halves of what is being fetched');
359
+ assert.match(html, /can take a couple of minutes/, 'and that the pause is expected');
360
+ });
361
+
262
362
  test('a BLOCKED move says so, in the upgrade’s own words, and still offers the button', async () => {
263
363
  const b = boot({ controlPlane: preview('1', 'cloudbongos', {
264
364
  preflight: { target: '1.19.871', ok: false, lines: [], error: 'working tree not clean — commit/stash first, or pass --force.' },
@@ -334,7 +434,7 @@ test('a verdict already in hand is NOT re-requested', async () => {
334
434
  // once — before the runner's tick — then fell silent for the whole upgrade.
335
435
 
336
436
  test('confirming a move says so IMMEDIATELY, naming the version it is taking', async () => {
337
- const b = boot();
437
+ const b = boot({ scope: 'hoodify-finance' });
338
438
  await b.settle();
339
439
  b.els.get('dp-preview-32')._on.click();
340
440
  b.els.get('dp-confirm-32')._on.click();
@@ -348,10 +448,9 @@ test('confirming a move says so IMMEDIATELY, naming the version it is taking', a
348
448
  test('the in-flight state survives the poll — it does not close under the reader', async () => {
349
449
  // refreshOne() returns a row to step one, which is right after a move and wrong
350
450
  // DURING one; without keepStep the poll wipes its own message on the first interval.
351
- // Observed after a poll has actually run, but before the bound is exhausted.
352
451
  // A long bound so the loop cannot exhaust mid-assertion: the state under test exists
353
452
  // only WHILE it polls.
354
- const b = boot({ moveTries: 500, instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
453
+ const b = boot({ scope: 'hoodify-finance', moveTries: 500, instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
355
454
  await b.settle();
356
455
  b.els.get('dp-preview-32')._on.click();
357
456
  b.els.get('dp-confirm-32')._on.click();
@@ -365,6 +464,7 @@ test('when the box lands on the new core, it CONFIRMS it', async () => {
365
464
  // The whole ask: "confirmation when it updates". A changed number is not a
366
465
  // confirmation — nothing tied it to the press.
367
466
  const b = boot({
467
+ scope: 'hoodify-finance',
368
468
  instanceOver: (inst, n) => (n >= 2
369
469
  ? { 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 } }
370
470
  : { open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }),
@@ -378,10 +478,65 @@ test('when the box lands on the new core, it CONFIRMS it', async () => {
378
478
  assert.doesNotMatch(html, /Moving to core/i, 'and the in-flight state is gone');
379
479
  });
380
480
 
481
+ // ── a failure is dated, and stops being the headline once the box moves on (task 1004172)
482
+
483
+ test('a live failure is DATED and says what the project is running now, in the present tense', async () => {
484
+ // The sentence this replaces said "was put back on 1.19.969" — a past-tense claim about
485
+ // a rollback, built by substituting the CURRENT reading into it. Present tense is a
486
+ // statement about the reading in hand, and true whenever the reading is.
487
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
488
+ last_move: { target_version: '1.19.871', state: 'error', at: new Date(Date.now() - 3 * 60 * 1000).toISOString(), error: 'working tree not clean', superseded: false },
489
+ }) });
490
+ await b.settle();
491
+ const html = b.panel();
492
+ assert.match(html, /did not land/i, 'the failure is stated');
493
+ assert.match(html, /attempted\s+\d+\s*(s|min|h) ago/i, 'and DATED — an undated failure reads as the present forever');
494
+ assert.match(html, /is still running core 1\.19\.800/i, 'present tense about the reading in hand');
495
+ assert.doesNotMatch(html, /was put back on/i, 'never a past-tense claim built from a current reading');
496
+ });
497
+
498
+ test('a failure the box has moved past is demoted to history, not painted as the current state', async () => {
499
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
500
+ last_move: { target_version: '1.19.700', state: 'error', at: new Date(Date.now() - 2 * 24 * 3600 * 1000).toISOString(), error: 'Postgres has no role "root"', superseded: true },
501
+ }) });
502
+ await b.settle();
503
+ const html = b.panel();
504
+ assert.match(html, /An earlier attempt did not land/i, 'it is framed as history');
505
+ assert.match(html, /has moved on since/i, 'and says the box recovered');
506
+ assert.match(html, /dp-settled--past/, 'and wears the history mark, not the danger one');
507
+ assert.doesNotMatch(html, /dp-settled--failed/, 'the live-failure treatment is NOT applied');
508
+ // "55h ago" is arithmetic the reader has to do before they can tell whether it is news.
509
+ assert.match(html, /failed 2 days ago/, 'past two days the age reads in days, not hours');
510
+ });
511
+
512
+ test('the runner’s shell output is folded behind a disclosure, never the headline', async () => {
513
+ // upgrade.js's dirty-tree refusal prints a whole `git status` — 33 lines on test4 —
514
+ // and names a fix addressed to whoever holds a shell, which the owner does not.
515
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
516
+ last_move: { target_version: '1.19.871', state: 'error', at: new Date().toISOString(), error: 'working tree not clean — commit/stash first', superseded: false },
517
+ }) });
518
+ await b.settle();
519
+ const html = b.panel();
520
+ assert.match(html, /<details class="dp-settled__detail"><summary>What the upgrade said<\/summary>/, 'folded');
521
+ assert.match(html, /working tree not clean/, 'and the words are still all there');
522
+ assert.ok(html.indexOf('did not land') < html.indexOf('<details'), 'the plain sentence comes first');
523
+ });
524
+
525
+ test('a failure with no recorded time says nothing about when, rather than inventing one', async () => {
526
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', {
527
+ last_move: { target_version: '1.19.871', state: 'error', at: null, error: 'x', superseded: false },
528
+ }) });
529
+ await b.settle();
530
+ const html = b.panel();
531
+ assert.match(html, /did not land/i);
532
+ assert.doesNotMatch(html, /attempted never/i, 'the "never" the age formatter returns must not reach the copy as a date');
533
+ });
534
+
381
535
  test('a FAILED move is reported, with the reason, instead of looking untouched', async () => {
382
536
  // An errored intent is not an OPEN intent, so before last_move existed the payload
383
537
  // after a rolled-back upgrade was identical to the payload before it.
384
538
  const b = boot({
539
+ scope: 'hoodify-finance',
385
540
  instanceOver: (inst, n) => (n >= 2
386
541
  ? { 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.' } }
387
542
  : { open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }),
@@ -393,12 +548,12 @@ test('a FAILED move is reported, with the reason, instead of looking untouched',
393
548
  const html = b.panel();
394
549
  assert.match(html, /did not land/i, 'the failure is stated');
395
550
  assert.match(html, /working tree not clean/, 'and carries the runner’s own reason');
396
- assert.match(html, /put back on 1\.19\.800/i, 'and says the project is still running');
551
+ assert.match(html, /is still running core 1\.19\.800/i, 'and says what the project is running now');
397
552
  });
398
553
 
399
554
  test('a poll that runs out says it is still working, not that it failed', async () => {
400
555
  // MOVE_TRIES is a bound on this page's patience, not on the runner's work.
401
- const b = boot({ instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
556
+ const b = boot({ scope: 'hoodify-finance', instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
402
557
  await b.settle();
403
558
  b.els.get('dp-preview-32')._on.click();
404
559
  b.els.get('dp-confirm-32')._on.click();
@@ -414,7 +569,7 @@ test('a queue that is REFUSED leaves no in-flight state to wait on', async () =>
414
569
  // `b.postFails`, a property the harness never read. So it asserted "no in-flight
415
570
  // state" on a row where no move had been attempted, which is true of every row on the
416
571
  // page. The grader caught it. It now refuses the POST for real and presses the button.
417
- const b = boot({ postStatus: 409 });
572
+ const b = boot({ scope: 'hoodify-finance', postStatus: 409 });
418
573
  await b.settle();
419
574
  b.els.get('dp-preview-32')._on.click();
420
575
  b.els.get('dp-confirm-32')._on.click();
@@ -0,0 +1,117 @@
1
+ // tests/fitness_scope_roster.mjs
2
+ //
3
+ // Task 1002900 — the scope roster in src/bongos/module-scope-map.js (MODULE_GLOBS) is
4
+ // maintained by hand, and nothing asserted that every carved module has a covering key.
5
+ // A module lands in modules/<key>/, nobody adds the entry, and any goal scoped to that
6
+ // work hits an authorize-NOTHING wall: goal-scope-check.check() denies with
7
+ // empty_scope / unknown_scope. It surfaces only when someone tries to set a goal's wall.
8
+ //
9
+ // Three hand-fixed instances before this check existed: task 1002690 (provisioning +
10
+ // public-landing), task 1002322 (platform-identity), and `agents` — the one this row
11
+ // was filed for, since added at module-scope-map.js:322.
12
+ //
13
+ // SUBSET, NOT EQUALITY. The roster legitimately carries keys with no modules/<key>/ dir,
14
+ // for two distinct reasons, and equality would fail on all five in this tree:
15
+ // - non-module SCOPES: kernel, methodology, devsecops
16
+ // - modules ABSENT from this instance: art-pipeline, game (ADR 0108)
17
+ // So the invariant is one-directional. These tests pin that direction explicitly,
18
+ // because getting it wrong turns a useful check into one nobody can keep green.
19
+ //
20
+ // Run: node --test tests/fitness_scope_roster.mjs
21
+
22
+ import { strict as assert } from 'node:assert';
23
+ import { test } from 'node:test';
24
+ import { createRequire } from 'node:module';
25
+ import path from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
+ const { discoveredModuleKeys } = require(path.join(REPO_ROOT, 'scripts/gds/fitness.js'));
31
+ // The check lives in its own file (the role-pack-guard.js precedent) so fitness.js stays
32
+ // under the size ratchet; module discovery stays in fitness.js, which owns it.
33
+ const { scanScopeRosterCoverage } = require(path.join(REPO_ROOT, 'scripts/gds/scope-roster-guard.js'));
34
+ const scopeMap = require(path.join(REPO_ROOT, 'src/bongos/module-scope-map.js'));
35
+
36
+ const nothingExists = () => false;
37
+ const allExist = () => true;
38
+
39
+ test('THE BUG: a discovered module with no roster key is a violation', () => {
40
+ const { violations } = scanScopeRosterCoverage({
41
+ discovered: ['lifecycle', 'agents'],
42
+ rostered: ['lifecycle'],
43
+ globs: {},
44
+ exists: allExist,
45
+ });
46
+ assert.equal(violations.length, 1);
47
+ assert.match(violations[0], /'agents'/, 'must name the uncovered module');
48
+ assert.match(violations[0], /empty_scope|unknown_scope/,
49
+ 'and must name the symptom, since that is how this is actually discovered');
50
+ });
51
+
52
+ test('SUBSET NOT EQUALITY: a rostered key with no module is NOT a fault', () => {
53
+ // kernel/methodology/devsecops are surfaces, not modules; art-pipeline/game may be
54
+ // absent from a given instance. Failing on these would make the check unkeepable.
55
+ const { violations } = scanScopeRosterCoverage({
56
+ discovered: ['lifecycle'],
57
+ rostered: ['lifecycle', 'kernel', 'methodology', 'devsecops', 'art-pipeline', 'game'],
58
+ globs: {},
59
+ exists: allExist,
60
+ });
61
+ assert.deepEqual(violations, []);
62
+ });
63
+
64
+ test('several uncovered modules are all reported, sorted', () => {
65
+ const { violations } = scanScopeRosterCoverage({
66
+ discovered: ['zeta', 'alpha', 'lifecycle'],
67
+ rostered: ['lifecycle'],
68
+ globs: {},
69
+ exists: allExist,
70
+ });
71
+ assert.equal(violations.length, 2, 'one line per uncovered module — not just the first');
72
+ assert.match(violations[0], /'alpha'/);
73
+ assert.match(violations[1], /'zeta'/);
74
+ });
75
+
76
+ test('a dead exact-file glob is a WARNING, never a failure', () => {
77
+ // The mirror-image rot: a roster entry naming a file that no longer exists cannot wall
78
+ // anything off, but it breaks no build either — so it is reported, not enforced.
79
+ const { violations, warnings } = scanScopeRosterCoverage({
80
+ discovered: [], rostered: [],
81
+ globs: { kernel: ['server.js'] },
82
+ exists: nothingExists,
83
+ });
84
+ assert.deepEqual(violations, [], 'a dead glob must not fail the build');
85
+ assert.equal(warnings.length, 1);
86
+ assert.match(warnings[0], /server\.js/);
87
+ assert.match(warnings[0], /walls nothing/);
88
+ });
89
+
90
+ test('directory and wildcard globs are exempt from the existence warning', () => {
91
+ // A directory glob may legitimately point at an optional module absent from this
92
+ // instance — warning on those would bury the real signal in noise.
93
+ const { warnings } = scanScopeRosterCoverage({
94
+ discovered: [], rostered: [],
95
+ globs: { game: ['modules/game/', 'src/**/game-*.js'] },
96
+ exists: nothingExists,
97
+ });
98
+ assert.deepEqual(warnings, []);
99
+ });
100
+
101
+ test('THE REAL TREE: every discovered module is rostered (the check must land green)', () => {
102
+ // Last, and paired with the fixtures above: alone this would pass against a scanner
103
+ // that always returns [].
104
+ const { violations } = scanScopeRosterCoverage({
105
+ discovered: discoveredModuleKeys(),
106
+ rostered: scopeMap.moduleKeys(),
107
+ globs: {},
108
+ exists: allExist,
109
+ });
110
+ assert.deepEqual(violations, [],
111
+ 'a module was carved without a MODULE_GLOBS key — any goal scoped to it denies everything');
112
+ });
113
+
114
+ test('agents specifically is rostered (the instance this row was filed for)', () => {
115
+ assert.ok(scopeMap.moduleKeys().includes('agents'));
116
+ assert.ok(discoveredModuleKeys().includes('agents'), 'and it is a real discovered module, not just a roster entry');
117
+ });