@bongos/core 1.19.972 → 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 +74 -64
- 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/architecture.md +6 -6
- package/docs/branding-contract.md +1 -1
- package/docs/canonical-permissions.md +24 -24
- package/docs/copy-inventory.md +45 -38
- package/docs/copy-registry.json +130 -67
- 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 +2 -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/project-settings.css +36 -0
- 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/provision-core-upgrade.js +132 -1
- package/scripts/gds/run-unit-tests.js +14 -9
- package/scripts/hall-preview/server.js +5 -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/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
|
@@ -836,5 +836,137 @@ await t('and a silent failure STILL refuses rather than inventing a pass', async
|
|
|
836
836
|
assert.match(v.error, /said nothing/);
|
|
837
837
|
});
|
|
838
838
|
|
|
839
|
+
|
|
840
|
+
console.log('\nwhat the move would CARRY, not just whether it is allowed (task 1004173):');
|
|
841
|
+
|
|
842
|
+
// The two merge shapes a real core history holds, as `git log --merges
|
|
843
|
+
// --format=%s%x1f%b%x1e` emits them. The second shape is the COMMON one: measured over
|
|
844
|
+
// 1.19.881→1.19.972, 98 of 114 usable summaries came from the CI auto-merge's body.
|
|
845
|
+
const REC = (subject, body = '') => `${subject}\x1f${body}\x1e`;
|
|
846
|
+
const LOG = [
|
|
847
|
+
REC('Merge scoperoster-1002900: The build now catches a missing registration at the moment it happens.'),
|
|
848
|
+
REC('Merge pull request #1108 from an-owner/task-1004167', 'task 1004167: Groundwork for letting an owner make their code repository private.\n'),
|
|
849
|
+
REC('Merge origin/main into scoperoster-1002900 (pre-stage freshness, task 1068)'),
|
|
850
|
+
REC('Merge remote-tracking branch \'origin/main\' into errshape-1003658'),
|
|
851
|
+
REC('Merge pull request #1107 from an-owner/task-1001621'),
|
|
852
|
+
REC('chore: regen repo-map after merge'),
|
|
853
|
+
].join('');
|
|
854
|
+
|
|
855
|
+
await t('BOTH merge shapes are read — the CI auto-merge carries its summary in the BODY', () => {
|
|
856
|
+
const out = R.parseChangeRecords(LOG);
|
|
857
|
+
assert.equal(out.length, 2, `two usable summaries — got ${JSON.stringify(out)}`);
|
|
858
|
+
assert.match(out[0].text, /catches a missing registration/, 'the hand-landed shape, from the subject');
|
|
859
|
+
assert.equal(out[1].id, '1004167', 'and the CI shape, from the body, with its task id');
|
|
860
|
+
assert.match(out[1].text, /make their code repository private/);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
await t('the mechanics of landing are DROPPED — they are not the work', () => {
|
|
864
|
+
// Over a real range these outnumber the summaries roughly ten to one, so listing them
|
|
865
|
+
// buries every sentence the owner wants.
|
|
866
|
+
const out = R.parseChangeRecords(LOG).map((e) => e.text).join(' | ');
|
|
867
|
+
assert.doesNotMatch(out, /pre-stage freshness/);
|
|
868
|
+
assert.doesNotMatch(out, /remote-tracking/);
|
|
869
|
+
assert.doesNotMatch(out, /regen repo-map/);
|
|
870
|
+
assert.doesNotMatch(out, /pull request/, 'a merge with no body summary contributes nothing');
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
await t('a branch merged twice says its sentence once', () => {
|
|
874
|
+
const dup = LOG + REC('Merge scoperoster-1002900: The build now catches a missing registration at the moment it happens.');
|
|
875
|
+
assert.equal(R.parseChangeRecords(dup).length, 2);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
await t('a paragraph-long summary is bounded — it reaches a DOM', () => {
|
|
879
|
+
const out = R.parseChangeRecords(REC(`Merge x-1: ${'y'.repeat(5000)}`));
|
|
880
|
+
assert.ok(out[0].text.length <= 400, `bounded — got ${out[0].text.length}`);
|
|
881
|
+
assert.match(out[0].text, /…$/, 'and says it was cut');
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// --- the range, and every way it can fail to resolve ------------------------------
|
|
885
|
+
|
|
886
|
+
const changeDeps = ({ shas = { '1.19.900': 'a'.repeat(40), '1.19.949': 'b'.repeat(40) }, log = LOG, logOk = true } = {}) => {
|
|
887
|
+
const ran = [];
|
|
888
|
+
return {
|
|
889
|
+
ran,
|
|
890
|
+
selfInstanceRoot: () => '/srv/core',
|
|
891
|
+
exec: (cmd) => {
|
|
892
|
+
ran.push(cmd);
|
|
893
|
+
const m = /--grep='release: core ([0-9.]+) '/.exec(cmd);
|
|
894
|
+
if (m) return { ok: true, stdout: `${shas[m[1]] || ''}\n` };
|
|
895
|
+
if (!logOk) return { ok: false, softFailed: true, stdout: '' };
|
|
896
|
+
return { ok: true, stdout: log };
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
await t('the range resolves through the release commits and lists what landed between', () => {
|
|
902
|
+
const d = changeDeps();
|
|
903
|
+
const r = R.changesBetween('1.19.900', '1.19.949', d);
|
|
904
|
+
assert.equal(r.error, undefined);
|
|
905
|
+
assert.equal(r.from, '1.19.900');
|
|
906
|
+
assert.equal(r.to, '1.19.949');
|
|
907
|
+
assert.equal(r.entries.length, 2);
|
|
908
|
+
assert.ok(d.ran.some((c) => /--merges/.test(c) && /a{40}\.\.b{40}/.test(c)), 'the log is asked for the resolved range');
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
await t('IT READS THE CONTROL PLANE’S OWN CHECKOUT, whichever instance is previewed', () => {
|
|
912
|
+
// A tenant's directory is a checkout of the TENANT's repo with the core vendored in
|
|
913
|
+
// from npm — no core history in it, and the wrong history if you squint. Under
|
|
914
|
+
// PROVISION_REMOTE_HOST `exec` is the remote box, so the executor matters too.
|
|
915
|
+
const d = changeDeps();
|
|
916
|
+
d.controlExec = d.exec;
|
|
917
|
+
d.exec = () => { throw new Error('the instance executor must not be used for core history'); };
|
|
918
|
+
const r = R.changesBetween('1.19.900', '1.19.949', d);
|
|
919
|
+
assert.equal(r.entries.length, 2);
|
|
920
|
+
for (const c of d.ran) assert.match(c, /git -C \/srv\/core /, 'always the control plane’s own core checkout');
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
await t('A RANGE THAT CANNOT BE RESOLVED IS AN ERROR, NEVER AN EMPTY LIST', () => {
|
|
924
|
+
// An empty list reads as "nothing changed", which over ninety-one releases is the one
|
|
925
|
+
// wrong impression this section could give — and it is the shape a missing checkout,
|
|
926
|
+
// a shallow clone or an unknown release all take.
|
|
927
|
+
const missing = R.changesBetween('1.19.900', '9.9.9', changeDeps());
|
|
928
|
+
assert.equal(missing.entries, undefined, 'no list at all');
|
|
929
|
+
assert.match(missing.error, /could not find those releases/);
|
|
930
|
+
|
|
931
|
+
const unreadable = R.changesBetween('1.19.900', '1.19.949', changeDeps({ logOk: false }));
|
|
932
|
+
assert.equal(unreadable.entries, undefined);
|
|
933
|
+
assert.match(unreadable.error, /could not read the core history/);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
await t('a version that is not three numbers never reaches the shell', () => {
|
|
937
|
+
const d = changeDeps();
|
|
938
|
+
const r = R.changesBetween("1.19.900'; rm -rf /; echo '", '1.19.949', d);
|
|
939
|
+
assert.match(r.error, /could not work out which releases/);
|
|
940
|
+
assert.deepEqual(d.ran, [], 'and nothing was run at all');
|
|
941
|
+
assert.match(R.changesBetween(null, '1.19.949', changeDeps()).error, /could not work out/, 'an unknown starting point too');
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
await t('a long range is capped, and says how many it did not list', () => {
|
|
945
|
+
const many = Array.from({ length: R.CHANGE_CAP + 5 }, (_, i) => REC(`Merge b-${i}: summary number ${i}`)).join('');
|
|
946
|
+
const r = R.changesBetween('1.19.900', '1.19.949', changeDeps({ log: many }));
|
|
947
|
+
assert.equal(r.entries.length, R.CHANGE_CAP);
|
|
948
|
+
assert.equal(r.more, 5, 'the remainder is counted, not silently dropped');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
await t('the dry run records the list beside the verdict, keyed to where the box IS', async () => {
|
|
952
|
+
// Both answers are about ONE move, so they are stored together — and `from` is stored
|
|
953
|
+
// with them because the list is only true of that starting point.
|
|
954
|
+
const d = makeDeps({ served: '1.19.762' });
|
|
955
|
+
d.queries.length = 0;
|
|
956
|
+
d.db = { query: async (sql, params) => {
|
|
957
|
+
d.queries.push({ sql, params });
|
|
958
|
+
if (/SELECT served_version/.test(sql)) return { rows: [{ served_version: '1.19.900' }] };
|
|
959
|
+
return { rows: [] };
|
|
960
|
+
} };
|
|
961
|
+
d.preflightUpgrade = () => ({ target: '1.19.949', ok: true, lines: [], error: null });
|
|
962
|
+
d.changesBetween = (from, to) => ({ from, to, entries: [{ text: 'a thing landed' }], more: 0 });
|
|
963
|
+
await R.preflightCoreUpgrade(inst(), d, intent({ target_version: '1.19.949' }));
|
|
964
|
+
const write = d.queries.find((q) => /INSERT INTO provisioning_core_version_snapshot/.test(q.sql));
|
|
965
|
+
const stored = JSON.parse(String(write.params[1]));
|
|
966
|
+
assert.equal(stored.changes.from, '1.19.900', 'the version the box is on right now');
|
|
967
|
+
assert.equal(stored.changes.to, '1.19.949');
|
|
968
|
+
assert.equal(stored.changes.entries[0].text, 'a thing landed');
|
|
969
|
+
});
|
|
970
|
+
|
|
839
971
|
console.log(`\ncore_upgrade_runner: ${passed} passed, ${failed} failed`);
|
|
840
972
|
if (failed) process.exit(1);
|
|
@@ -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();
|