@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.
- package/.bongos-core.json +116 -76
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +107 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/architecture.md +6 -6
- package/docs/branding-contract.md +1 -1
- package/docs/canonical-permissions.md +24 -24
- package/docs/copy-inventory.md +193 -187
- package/docs/copy-registry.json +320 -276
- package/docs/design/projects-hub-direction-v2.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +2 -2
- package/docs/file-map.md +7 -7
- package/docs/handoff-template.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/packs/engineer.md +5 -5
- package/docs/packs/ideator.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/gds-db-backup.md +3 -3
- package/docs/recipes/managed-settings-remote-control.md +9 -9
- package/docs/recipes/memory-sync-conflicts.md +2 -2
- package/docs/recipes/ops-gotchas.md +24 -5
- package/docs/recipes/overnight-builder.md +3 -3
- package/docs/recipes/parallel-goal-waves.md +3 -3
- package/docs/recipes/reranking-a-goals-board.md +2 -2
- package/docs/recipes/search-index-ingestion.md +1 -1
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/windows-builders.md +1 -1
- package/modules/hall-ui/public/deploy.html +10 -5
- package/modules/hall-ui/public/deploy.js +199 -110
- package/modules/hall-ui/public/goals-page.js +6 -0
- package/modules/hall-ui/public/goals-render.js +16 -0
- package/modules/hall-ui/public/mine-lens.js +172 -0
- package/modules/hall-ui/public/project-settings.css +36 -0
- package/modules/hall-ui/public/work-summary.js +90 -0
- package/modules/hall-ui/public/work.css +50 -0
- package/modules/hall-ui/public/work.html +24 -0
- package/modules/hall-ui/public/work.js +38 -51
- package/modules/lifecycle/db-goals.js +20 -3
- package/modules/lifecycle/routes/goals.js +2 -2
- package/modules/provisioning/core-upgrade.js +61 -2
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api-alias-caller-scan.js +47 -9
- package/scripts/gds/cloudbongos-token-isolation.conf +33 -0
- package/scripts/gds/fitness.js +3 -0
- package/scripts/gds/provision-core-upgrade.js +132 -1
- package/scripts/gds/run-unit-tests.js +14 -9
- package/scripts/gds/scope-roster-guard.js +118 -0
- package/scripts/hall-preview/server.js +5 -0
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +68 -2
- package/tests/core_upgrade_door.mjs +81 -0
- package/tests/core_upgrade_runner.mjs +132 -0
- package/tests/deploy_page_projects.mjs +234 -79
- package/tests/fitness_scope_roster.mjs +117 -0
- package/tests/goal_mine_lens.mjs +233 -0
- package/tests/goal_tier.mjs +30 -4
- package/tests/hall_mine_lens.mjs +381 -0
- package/tests/idea_spark_hall.mjs +101 -12
- package/tests/known_failing_ratchet.mjs +19 -9
- package/tests/landing_legal_pages.mjs +5 -1
- package/tests/projects_hub_pre_uat.mjs +12 -6
- package/tests/served_paths_shipped.mjs +36 -2
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// tests/deploy_page_projects.mjs — the deploy page
|
|
2
|
-
//
|
|
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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])), toast(m) { toasts.push(String(m)); } },
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
117
|
-
assert.
|
|
118
|
-
assert.
|
|
119
|
-
assert.match(b.sub(),
|
|
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('
|
|
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.
|
|
127
|
-
assert.doesNotMatch(html, /
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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('
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
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.
|
|
221
|
-
b.els.get('dp-
|
|
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
|
-
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
assert.
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
|
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 "cloudbongos" 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, /
|
|
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
|
+
});
|