@bongos/core 1.19.789 → 1.19.791
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 +143 -53
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +6 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md +124 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +129 -1
- package/docs/api-reference.md +5 -2
- package/docs/copy-inventory.md +89 -72
- package/docs/copy-registry.json +272 -98
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/core-release-pipeline.md +12 -0
- package/migrations/core_241_core_upgrades_actor.sql +65 -0
- package/migrations/core_242_core_version_snapshot.sql +54 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/modules/government/catalog.js +8 -0
- package/modules/government/migrations/government_014_core_pin_move.sql +46 -0
- package/modules/hall-ui/public/deploy.html +64 -0
- package/modules/hall-ui/public/deploy.js +188 -0
- package/modules/hall-ui/public/hall-kit.js +7 -65
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/hall-visual.js +85 -0
- package/modules/hall-ui/public/index.html +1 -0
- package/modules/hall-ui/public/profile.html +1 -0
- package/modules/hall-ui/public/profile.js +2 -2
- package/modules/hall-ui/public/shell.js +4 -0
- package/modules/hall-ui/public/studio.css +1 -1
- package/modules/hall-ui/public/studio.html +1 -0
- package/modules/hall-ui/public/studio.js +4 -3
- package/modules/hall-ui/public/style.css +0 -16
- package/modules/hall-ui/public/task.html +1 -0
- package/modules/hall-ui/public/task.js +10 -13
- package/modules/provisioning/control-plane-guard.js +102 -0
- package/modules/provisioning/core-upgrade.js +289 -0
- package/modules/provisioning/migrations/provisioning_018_core_upgrade_intent.sql +98 -0
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/provisioning.js +8 -8
- package/modules/provisioning/routes/core-upgrade.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/accessibility-replay.js +195 -0
- package/scripts/gds/enroll-control-plane.js +101 -0
- package/scripts/gds/provision-core-upgrade.js +311 -0
- package/scripts/gds/provision.js +4 -2
- package/scripts/gds/upgrade.js +53 -14
- package/src/bongos/auth.js +19 -0
- package/src/bongos/serve-internal.js +21 -0
- package/src/module-api.js +1 -1
- package/tests/accessibility_replay.mjs +201 -0
- package/tests/auth_resolve_failure.mjs +4 -2
- package/tests/core_upgrade_door.mjs +408 -0
- package/tests/core_upgrade_runner.mjs +377 -0
- package/tests/fixtures/accessibility-replay.json +93 -0
- package/tests/hall_nav.mjs +4 -1
- package/tests/hall_page_gate_map.mjs +7 -0
- package/tests/hall_studio_world.mjs +19 -11
- package/tests/provisioning_disconnect.mjs +30 -5
- package/tests/provisioning_paid_shape_gate.mjs +14 -4
- package/tests/provisioning_restart.mjs +25 -9
- package/tests/provisioning_settings_apply.mjs +11 -2
- package/tests/provisioning_teardown_intent.mjs +7 -0
- package/tests/task_visuals.mjs +91 -27
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// tests/accessibility_replay.mjs
|
|
2
|
+
//
|
|
3
|
+
// The two-profile replay regression (task 1002517; clauses 5 and 8 of criterion
|
|
4
|
+
// `accessibility-fluency-profile`) — the proof the criterion named for itself
|
|
5
|
+
// and that had never been run.
|
|
6
|
+
//
|
|
7
|
+
// This is NOT a second copy of tests/session_accessibility.mjs. That file pins
|
|
8
|
+
// the scorer's arithmetic against fixtures it builds line by line. This one runs
|
|
9
|
+
// a fixed set of eight beginner questions through two real answer registers and
|
|
10
|
+
// asserts what the criterion actually claims: accessible prose clears a floor,
|
|
11
|
+
// an expert's prose is not scored for being an expert's prose, and the two
|
|
12
|
+
// verdicts come from the same corpus — so neither is a tautology.
|
|
13
|
+
//
|
|
14
|
+
// The assertion that keeps the rest honest is DISCRIMINATION (leg 3). A scorer
|
|
15
|
+
// that had gone blind — three detectors quietly matching nothing — produces a
|
|
16
|
+
// perfect 10 on the non-technical run and passes leg 1 looking exactly like a
|
|
17
|
+
// working one. Leg 3 reads the same engineer prose as a newcomer would and
|
|
18
|
+
// requires every detector to register hits and the score to fall through the
|
|
19
|
+
// floor. Nothing else here can pass while the detectors are dead.
|
|
20
|
+
//
|
|
21
|
+
// Run: node tests/accessibility_replay.mjs
|
|
22
|
+
|
|
23
|
+
import { strict as assert } from 'node:assert';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const H = require('../scripts/gds/accessibility-replay.js');
|
|
28
|
+
const acc = require('../modules/sessions/accessibility.js');
|
|
29
|
+
|
|
30
|
+
const corpus = H.loadCorpus();
|
|
31
|
+
const NEWCOMER = corpus.runs.non_technical.profile;
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
function test(name, fn) {
|
|
36
|
+
try {
|
|
37
|
+
fn();
|
|
38
|
+
passed++;
|
|
39
|
+
console.log(` ok ${name}`);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
failed++;
|
|
42
|
+
console.log(` FAIL ${name}`);
|
|
43
|
+
console.log(` ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const signalsOf = (r) => Object.fromEntries(r.result.signals.map((s) => [s.type, s]));
|
|
48
|
+
|
|
49
|
+
// ---- the floor is the scorer's own line, not a second number ---------------
|
|
50
|
+
|
|
51
|
+
test('FLOOR is bandFor()\'s workable boundary, so retuning the bands cannot move it silently', () => {
|
|
52
|
+
assert.equal(H.bandFor(H.FLOOR), 'workable', 'FLOOR sits exactly on the workable boundary');
|
|
53
|
+
assert.equal(H.bandFor(H.FLOOR - 0.1), 'dense', 'a tenth below the floor is already the dense band');
|
|
54
|
+
assert.equal(corpus.floor, H.FLOOR, 'the corpus records the same floor the harness enforces');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ---- the corpus is a fixed set, and both runs answer all of it -------------
|
|
58
|
+
|
|
59
|
+
test('the prompt set is fixed, non-trivial, and answered in full by both runs', () => {
|
|
60
|
+
assert.ok(corpus.prompts.length >= 8, 'at least eight beginner prompts');
|
|
61
|
+
const ids = corpus.prompts.map((p) => p.id);
|
|
62
|
+
assert.equal(new Set(ids).size, ids.length, 'prompt ids are unique');
|
|
63
|
+
for (const runName of ['non_technical', 'engineer']) {
|
|
64
|
+
const run = corpus.runs[runName];
|
|
65
|
+
for (const id of ids) {
|
|
66
|
+
const turns = run.replies[id];
|
|
67
|
+
assert.ok(Array.isArray(turns) && turns.length > 0, `${runName} answers ${id}`);
|
|
68
|
+
for (const t of turns) assert.ok(typeof t === 'string' && t.trim(), `${runName}/${id} turn is prose`);
|
|
69
|
+
}
|
|
70
|
+
for (const [field, value] of Object.entries(run.profile)) {
|
|
71
|
+
assert.ok(acc.PROFILE_VALUES[field]?.includes(value), `${runName} profile ${field}=${value} is a real value`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---- leg 1: the dimension recognises accessible prose ----------------------
|
|
77
|
+
|
|
78
|
+
test('leg 1 — the non-technical run clears the floor under the non-technical profile', () => {
|
|
79
|
+
const r = H.replay(corpus, 'non_technical');
|
|
80
|
+
assert.ok(r.result.score != null, 'the non-technical run is scored, not waived away');
|
|
81
|
+
assert.ok(
|
|
82
|
+
r.result.score >= H.FLOOR,
|
|
83
|
+
`non-technical run scored ${r.result.score}, floor is ${H.FLOOR}`
|
|
84
|
+
);
|
|
85
|
+
assert.equal(r.result.confidence, 'normal', 'the sample is big enough for the score to stand behind');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- leg 2: an expert's prose is not scored for being an expert's prose ----
|
|
89
|
+
|
|
90
|
+
test('leg 2 — the engineer run is not scored at all under the engineer profile', () => {
|
|
91
|
+
const r = H.replay(corpus, 'engineer');
|
|
92
|
+
assert.equal(r.result.score, null, 'an all-waived session scores null, never a number');
|
|
93
|
+
assert.equal(r.result.band, 'n/a');
|
|
94
|
+
assert.equal(r.result.signals.length, 3);
|
|
95
|
+
for (const s of r.result.signals) {
|
|
96
|
+
assert.equal(s.waived, true, `${s.type} is waived for this profile`);
|
|
97
|
+
assert.ok(/technical_level|toolchain_familiarity/.test(s.reason), `${s.type} names the setting that waived it`);
|
|
98
|
+
}
|
|
99
|
+
assert.equal(r.result.confidence, 'normal', 'the prose was read — the null is a waiver, not an empty sample');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('leg 2 holds when the same expert answers even more briefly', () => {
|
|
103
|
+
const full = H.replay(corpus, 'engineer');
|
|
104
|
+
const terse = H.replay(corpus, 'engineer', { variant: 'terse' });
|
|
105
|
+
assert.ok(terse.result.sampled_chars < full.result.sampled_chars, 'the terse variant really is shorter');
|
|
106
|
+
assert.equal(terse.result.score, null, 'still nothing to regress');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---- leg 3: the same prose, read as a newcomer would read it ---------------
|
|
110
|
+
|
|
111
|
+
test('leg 3 — the SAME engineer prose falls through the floor under the non-technical profile', () => {
|
|
112
|
+
const r = H.replay(corpus, 'engineer', { profile: NEWCOMER });
|
|
113
|
+
assert.ok(r.result.score != null, 'nothing is waived for a newcomer');
|
|
114
|
+
assert.ok(
|
|
115
|
+
r.result.score < H.FLOOR,
|
|
116
|
+
`engineer prose read as a newcomer scored ${r.result.score}, which should be under ${H.FLOOR}`
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('leg 3 — every detector registers hits, so a scorer that went blind cannot pass', () => {
|
|
121
|
+
const s = signalsOf(H.replay(corpus, 'engineer', { profile: NEWCOMER }));
|
|
122
|
+
for (const type of Object.keys(acc.SIGNAL_SPEC)) {
|
|
123
|
+
assert.ok(s[type], `${type} was scored`);
|
|
124
|
+
assert.ok(s[type].count > 0, `${type} found something in the engineer prose (found ${s[type].count})`);
|
|
125
|
+
assert.ok(s[type].evidence, `${type} can name what it found`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---- the perfect run is forgiveness, not avoidance -------------------------
|
|
130
|
+
|
|
131
|
+
test('the non-technical run scores well WITH a jargon term and implementation framing in it, not by dodging them', () => {
|
|
132
|
+
const prose = Object.values(corpus.runs.non_technical.replies).flat().join('\n');
|
|
133
|
+
const present = acc.JARGON_TERMS.filter((t) => new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(prose));
|
|
134
|
+
assert.ok(present.length > 0, 'the accessible prose does use at least one term from the jargon list');
|
|
135
|
+
assert.ok(/\barchitecture\b/i.test(prose), 'and frames something in architecture terms at least once');
|
|
136
|
+
|
|
137
|
+
const s = signalsOf(H.replay(corpus, 'non_technical'));
|
|
138
|
+
assert.equal(s.unexplained_jargon.count, 0, `"${present.join('", "')}" is used and DEFINED, so it is not a finding`);
|
|
139
|
+
assert.equal(s.implementation_over_outcome.count, 0, 'the architecture turn also names the cost, so it is not a finding');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---- leg 4: the metric reads density, not volume --------------------------
|
|
143
|
+
|
|
144
|
+
test('leg 4 — saying the same thing twice doubles the counts and leaves every rate, and the score, identical', () => {
|
|
145
|
+
const once = H.replay(corpus, 'engineer', { profile: NEWCOMER });
|
|
146
|
+
const twice = H.replay(corpus, 'engineer', { profile: NEWCOMER, variant: 'doubled' });
|
|
147
|
+
assert.equal(twice.result.sampled_chars, once.result.sampled_chars * 2, 'twice the prose');
|
|
148
|
+
|
|
149
|
+
const a = signalsOf(once);
|
|
150
|
+
const b = signalsOf(twice);
|
|
151
|
+
for (const type of Object.keys(acc.SIGNAL_SPEC)) {
|
|
152
|
+
assert.equal(b[type].count, a[type].count * 2, `${type} finds twice as much in twice the prose`);
|
|
153
|
+
assert.equal(b[type].rate, a[type].rate, `${type} rate is unchanged — the metric reads density, not volume`);
|
|
154
|
+
}
|
|
155
|
+
assert.equal(twice.result.score, once.result.score, 'so the score does not move with length');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('answering more briefly never costs points', () => {
|
|
159
|
+
for (const runName of ['non_technical', 'engineer']) {
|
|
160
|
+
const full = H.replay(corpus, runName);
|
|
161
|
+
const terse = H.replay(corpus, runName, { variant: 'terse' });
|
|
162
|
+
assert.ok(terse.result.sampled_chars < full.result.sampled_chars, `${runName} terse variant is shorter`);
|
|
163
|
+
if (full.result.score == null) {
|
|
164
|
+
assert.equal(terse.result.score, null, `${runName} stays unscored`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
assert.ok(
|
|
168
|
+
terse.result.score >= full.result.score,
|
|
169
|
+
`${runName} terse scored ${terse.result.score} against ${full.result.score} full — brevity must not cost`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ---- the two waivers are independent settings ------------------------------
|
|
175
|
+
|
|
176
|
+
test('an engineer who is new to the terminal still gets the click-path signal', () => {
|
|
177
|
+
const r = H.replay(corpus, 'engineer', { profile: H.ENGINEER_NEW_TO_TOOLCHAIN });
|
|
178
|
+
const s = signalsOf(r);
|
|
179
|
+
assert.equal(r.result.signals.filter((x) => x.waived).length, 2, 'technical_level waives two, not three');
|
|
180
|
+
assert.equal(s.terminal_where_ui_exists.waived, false, 'the click-path signal is waived by TOOLCHAIN familiarity, not by technical level');
|
|
181
|
+
assert.ok(s.terminal_where_ui_exists.count > 0, 'and it still finds the commands handed over');
|
|
182
|
+
assert.ok(r.result.score != null, 'so this profile still gets a number');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---- the harness is runnable, which is half of what the task asked for -----
|
|
186
|
+
|
|
187
|
+
test('the report renders and names every run', () => {
|
|
188
|
+
const out = H.formatReport(corpus);
|
|
189
|
+
assert.ok(out.includes(`floor ${H.FLOOR.toFixed(1)}`), 'the report states the floor it enforces');
|
|
190
|
+
assert.ok(out.includes('non-technical profile'), 'and both profiles appear');
|
|
191
|
+
assert.ok(out.includes('engineer profile'));
|
|
192
|
+
|
|
193
|
+
let json = '';
|
|
194
|
+
assert.equal(H.main(['--json'], (s) => { json += s; }), 0, 'the JSON mode exits clean');
|
|
195
|
+
const parsed = JSON.parse(json);
|
|
196
|
+
assert.equal(parsed.floor, H.FLOOR, 'and carries the floor a caller would check against');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(`Accessibility two-profile replay (clauses 5 + 8): ${passed} passed, ${failed} failed`);
|
|
201
|
+
if (failed > 0) process.exit(1);
|
|
@@ -96,16 +96,18 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
|
|
|
96
96
|
// requireGovernmentPage \ all FOUR delegate to runPageGate, guarded by
|
|
97
97
|
// requireProjectCuratePage / BV1.R107 (the /people curate gate; the
|
|
98
98
|
// requireBoardVotePage / ADR 0266 /board-room vote gate)
|
|
99
|
+
// requireCorePinPage / ADR 0293's /deploy door — the narrowest of them,
|
|
100
|
+
// on the archon-floor `core.pin.move` atom.
|
|
99
101
|
// Task 1003450 removed two more — requireNonXenosPage and the R113
|
|
100
102
|
// requireGovernmentManagePage — which had both ended up dispatched from no
|
|
101
103
|
// route at all; tests/hall_page_gate_map.mjs now fails if that recurs.
|
|
102
104
|
// Anything else async and router-mounted must be added with its own try/catch.
|
|
103
105
|
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
|
|
104
106
|
const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
|
|
105
|
-
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage'],
|
|
107
|
+
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireCorePinPage', 'requireGovernmentPage', 'requireProjectCuratePage'],
|
|
106
108
|
'a new async router middleware appeared — give it a try/catch and add it here');
|
|
107
109
|
// The page gates must actually delegate (that is WHY they are safe).
|
|
108
|
-
for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage', 'requireBoardVotePage']) {
|
|
110
|
+
for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage', 'requireBoardVotePage', 'requireCorePinPage']) {
|
|
109
111
|
const fn = src.slice(src.indexOf(`async function ${name}`));
|
|
110
112
|
assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
|
|
111
113
|
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// tests/core_upgrade_door.mjs — the owner's deploy door (task 1003159, ADR 0293).
|
|
2
|
+
//
|
|
3
|
+
// WHAT IS ACTUALLY AT RISK HERE. Enrolling the control plane as a row in
|
|
4
|
+
// provisioning_instances is what gives the deploy door something to point at —
|
|
5
|
+
// `provisioning_intents.instance_id` is NOT NULL, so an intent must name a row.
|
|
6
|
+
// The same row makes 'teardown' a button that would destroy the platform from
|
|
7
|
+
// inside the platform's own UI. `intentAllowedForShape` is the single place that
|
|
8
|
+
// decides, and these cases are what keep it honest.
|
|
9
|
+
//
|
|
10
|
+
// The guard is an ALLOW-list on purpose (ADR 0293 D2). A deny-list of today's
|
|
11
|
+
// destructive actions would silently admit tomorrow's, and the failure direction
|
|
12
|
+
// of "admitted by omission" is the platform deleting itself.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/core_upgrade_door.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const P = require('../modules/provisioning/provisioning.js');
|
|
24
|
+
// The guard is its own module, and callers import it DIRECTLY rather than through
|
|
25
|
+
// provisioning.js: that file sits on the oversized-file ratchet at its ceiling, so a
|
|
26
|
+
// re-export there costs a line the build will not give back. Importing the thing
|
|
27
|
+
// itself is also the honest shape - the guard is not provisioning's to own.
|
|
28
|
+
const G = require('../modules/provisioning/control-plane-guard.js');
|
|
29
|
+
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const repoRoot = join(__dirname, '..');
|
|
32
|
+
const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
33
|
+
|
|
34
|
+
let passed = 0;
|
|
35
|
+
let failed = 0;
|
|
36
|
+
const t = async (name, fn) => {
|
|
37
|
+
try { await fn(); passed += 1; console.log(` PASS ${name}`); }
|
|
38
|
+
catch (e) { failed += 1; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
console.log('\nthe vocabulary:');
|
|
42
|
+
|
|
43
|
+
await t('core-upgrade is a queueable intent action', () => {
|
|
44
|
+
assert.ok(P.INTENT_ACTIONS.includes('core-upgrade'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await t('control-plane is a hosting shape', () => {
|
|
48
|
+
assert.ok(P.HOSTING_SHAPES.includes('control-plane'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await t('the shape and the action both reach the DB constraints', () => {
|
|
52
|
+
// The JS list and the CHECK are two spellings of one vocabulary; a widening that
|
|
53
|
+
// lands in only one of them fails at insert time, in production, on the first
|
|
54
|
+
// real use. (The bug class task 1003913 shipped a fix for, in another file.)
|
|
55
|
+
const mig = read('modules', 'provisioning', 'migrations', 'provisioning_018_core_upgrade_intent.sql')
|
|
56
|
+
.replace(/^\s*--.*$/gm, ' ').replace(/\s+/g, ' ');
|
|
57
|
+
assert.match(mig, /CHECK \(action IN \([^)]*'core-upgrade'\)\)/, 'the intents CHECK admits the new action');
|
|
58
|
+
assert.match(mig, /CHECK \(hosting_shape IN \([^)]*'control-plane'\)\)/, 'the instances CHECK admits the new shape');
|
|
59
|
+
for (const a of P.INTENT_ACTIONS) {
|
|
60
|
+
assert.ok(mig.includes(`'${a}'`), `the widened CHECK must still carry '${a}' — a rewrite drops what it forgets`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
console.log('\nthe guard — what may be aimed at the platform itself:');
|
|
65
|
+
|
|
66
|
+
await t('TEARDOWN IS REFUSED on the control-plane row', () => {
|
|
67
|
+
// The case this whole file exists for.
|
|
68
|
+
assert.equal(G.intentAllowedForShape('teardown', 'control-plane'), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await t('every action not explicitly allowed is refused on the self row', () => {
|
|
72
|
+
const allowed = new Set(G.CONTROL_PLANE_ACTIONS);
|
|
73
|
+
for (const a of P.INTENT_ACTIONS) {
|
|
74
|
+
assert.equal(
|
|
75
|
+
G.intentAllowedForShape(a, 'control-plane'),
|
|
76
|
+
allowed.has(a),
|
|
77
|
+
`'${a}' on the control-plane row must be ${allowed.has(a) ? 'allowed' : 'refused'}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await t('the allow-list is exactly the two acts that cannot destroy the box', () => {
|
|
83
|
+
// Pinned by NAME so widening it is a deliberate edit someone has to argue for,
|
|
84
|
+
// not a side effect of adding an action to INTENT_ACTIONS.
|
|
85
|
+
assert.deepEqual([...G.CONTROL_PLANE_ACTIONS].sort(), ['core-upgrade', 'restart']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await t('an ordinary instance is untouched by the guard', () => {
|
|
89
|
+
for (const shape of ['co-tenant', 'dedicated', 'standalone']) {
|
|
90
|
+
for (const a of P.INTENT_ACTIONS) {
|
|
91
|
+
assert.equal(G.intentAllowedForShape(a, shape), true, `${a}/${shape} must be unaffected`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await t('an UNKNOWN shape is treated as ordinary, not as the control plane', () => {
|
|
97
|
+
// Deliberate: the guard protects one specific row. Failing closed on every
|
|
98
|
+
// unrecognised shape would break provisioning for a shape added later without
|
|
99
|
+
// touching this list, and that is a bigger blast radius than the thing guarded.
|
|
100
|
+
assert.equal(G.intentAllowedForShape('teardown', 'something-new'), true);
|
|
101
|
+
assert.equal(G.intentAllowedForShape('teardown', undefined), true);
|
|
102
|
+
assert.equal(G.intentAllowedForShape('teardown', null), true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
console.log('\nat most one self row:');
|
|
106
|
+
|
|
107
|
+
await t('the migration makes a second control-plane row impossible', () => {
|
|
108
|
+
// Two self rows would make "which one is me?" a question with no answer, and the
|
|
109
|
+
// door would point at whichever the query returned first.
|
|
110
|
+
const mig = read('modules', 'provisioning', 'migrations', 'provisioning_018_core_upgrade_intent.sql')
|
|
111
|
+
.replace(/\s+/g, ' ');
|
|
112
|
+
assert.match(mig, /CREATE UNIQUE INDEX IF NOT EXISTS provisioning_instances_one_control_plane_idx/);
|
|
113
|
+
assert.match(mig, /WHERE hosting_shape = 'control-plane'/, 'partial, so it constrains only the self row');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log('\nthe ledger records WHO (ADR 0293 D5):');
|
|
117
|
+
|
|
118
|
+
await t('core_241 adds an actor and a source, and binds the door to naming one', () => {
|
|
119
|
+
const mig = read('migrations', 'core_241_core_upgrades_actor.sql')
|
|
120
|
+
.replace(/^\s*--.*$/gm, ' ').replace(/\s+/g, ' ');
|
|
121
|
+
assert.match(mig, /ADD COLUMN IF NOT EXISTS requested_by_builder_id bigint REFERENCES builders\(id\) ON DELETE SET NULL/);
|
|
122
|
+
assert.match(mig, /source IS NULL OR source IN \('shell', 'owner-control'\)/);
|
|
123
|
+
assert.match(
|
|
124
|
+
mig,
|
|
125
|
+
/source IS DISTINCT FROM 'owner-control' OR requested_by_builder_id IS NOT NULL/,
|
|
126
|
+
'an upgrade through the DOOR must name its actor',
|
|
127
|
+
);
|
|
128
|
+
assert.match(mig, /NOT VALID/, 'without scanning history that predates the concept');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await t('the actor column is NULLABLE — the shell path must keep working', () => {
|
|
132
|
+
const mig = read('migrations', 'core_241_core_upgrades_actor.sql');
|
|
133
|
+
assert.doesNotMatch(
|
|
134
|
+
mig.replace(/^\s*--.*$/gm, ''),
|
|
135
|
+
/requested_by_builder_id bigint[^;]*NOT NULL/,
|
|
136
|
+
'a human at a shell has no builder to attribute; this column is not a gate on the CLI path',
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const CU = require('../modules/provisioning/core-upgrade.js');
|
|
141
|
+
|
|
142
|
+
const ACTIVE = Object.freeze({ id: 7, slug: 'cloudbongos', status: 'active', hosting_shape: 'control-plane', owner_builder_id: 3, core_version: '1.19.754' });
|
|
143
|
+
|
|
144
|
+
console.log('\nwhat the door refuses, before anything is queued:');
|
|
145
|
+
|
|
146
|
+
await t('a version that is not three numbers is refused', () => {
|
|
147
|
+
for (const bad of ['latest', '1.19', 'v1.19.757', '1.19.757-rc1', '', ' ', 'DROP TABLE']) {
|
|
148
|
+
const f = CU.coreUpgradeFault(ACTIVE, bad);
|
|
149
|
+
assert.ok(f, `'${bad}' must be refused`);
|
|
150
|
+
assert.equal(f.code, 'bad_target_version', `'${bad}'`);
|
|
151
|
+
assert.equal(f.status, 400);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await t('a well-formed version is admitted', () => {
|
|
156
|
+
assert.equal(CU.coreUpgradeFault(ACTIVE, '1.19.757'), null);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await t('the version ALREADY running is refused rather than restarting to change nothing', () => {
|
|
160
|
+
const f = CU.coreUpgradeFault(ACTIVE, '1.19.754', '1.19.754');
|
|
161
|
+
assert.ok(f);
|
|
162
|
+
assert.equal(f.code, 'already_on_version');
|
|
163
|
+
assert.match(f.message, /already runs core 1\.19\.754/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await t('an instance that is not ACTIVE has no settled pin to move', () => {
|
|
167
|
+
for (const status of ['provisioning', 'tearing_down', 'torn_down', 'error']) {
|
|
168
|
+
const f = CU.coreUpgradeFault({ ...ACTIVE, status }, '1.19.757');
|
|
169
|
+
assert.ok(f, status);
|
|
170
|
+
assert.equal(f.code, 'not_upgradable', status);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await t('a shape the platform does not manage is refused, and that check runs FIRST', () => {
|
|
175
|
+
// Ordering matters: the shape check protects the platform, the others protect
|
|
176
|
+
// the caller from a mistake. A dedicated droplet has no runner exec path, so
|
|
177
|
+
// queueing would be the F16 lie — a request that never lands.
|
|
178
|
+
//
|
|
179
|
+
// NB the shape refusal here is UPGRADE_SHAPES alone. This test used to be named
|
|
180
|
+
// for the self-row guard running first, which it never exercised — see the
|
|
181
|
+
// vacuity case below for why that call is not made at all.
|
|
182
|
+
const f = CU.coreUpgradeFault({ ...ACTIVE, hosting_shape: 'dedicated', status: 'torn_down' }, 'nonsense');
|
|
183
|
+
assert.ok(f);
|
|
184
|
+
assert.equal(f.code, 'core_upgrade_unavailable',
|
|
185
|
+
'the shape refusal wins over the state and format ones');
|
|
186
|
+
// And a dedicated droplet is refused even when everything else is perfect —
|
|
187
|
+
// the runner has no exec path to it, so queueing would be accepted and never land.
|
|
188
|
+
const clean = CU.coreUpgradeFault({ ...ACTIVE, hosting_shape: 'dedicated' }, '1.19.757');
|
|
189
|
+
assert.ok(clean, 'a dedicated droplet is refused on shape alone');
|
|
190
|
+
assert.equal(clean.code, 'core_upgrade_unavailable');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await t('no refusal message promises a duration (ADR 0190 §7)', () => {
|
|
194
|
+
const messages = [
|
|
195
|
+
CU.coreUpgradeFault({ ...ACTIVE, hosting_shape: 'dedicated' }, '1.19.757'),
|
|
196
|
+
CU.coreUpgradeFault({ ...ACTIVE, hosting_shape: 'control-plane', status: 'provisioning' }, '1.19.757'),
|
|
197
|
+
CU.coreUpgradeFault({ ...ACTIVE, status: 'torn_down' }, '1.19.757'),
|
|
198
|
+
CU.coreUpgradeFault(ACTIVE, 'latest'),
|
|
199
|
+
CU.coreUpgradeFault(ACTIVE, '1.19.754', '1.19.754'),
|
|
200
|
+
].map((f) => f.message);
|
|
201
|
+
for (const m of messages) {
|
|
202
|
+
assert.doesNotMatch(m, /\b(minute|second|hour|shortly|soon)\b/i, m);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
console.log('\nthe gate — the DONE-WHEN clause:');
|
|
207
|
+
|
|
208
|
+
await t('A NON-ARCHON IS REFUSED: the route gates on core.pin.move, an archon-floor system atom', () => {
|
|
209
|
+
// The task's explicit DONE-WHEN. Asserted at the DECLARATION rather than by
|
|
210
|
+
// driving express, because the gate IS the middleware list: a handler test would
|
|
211
|
+
// pass just as happily with the gate deleted.
|
|
212
|
+
// Comments stripped FIRST: the route's own header names requireRank to explain
|
|
213
|
+
// why it is not used, and a raw-text scan reads that prose as the forbidden call.
|
|
214
|
+
// Exactly the defect filed as task 1003832 against government_require_permission.
|
|
215
|
+
const route = read('modules', 'provisioning', 'routes', 'core-upgrade.js')
|
|
216
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
217
|
+
.replace(/^\s*\/\/.*$/gm, ' ')
|
|
218
|
+
.replace(/\s+/g, ' ');
|
|
219
|
+
assert.match(route, /api\.requirePermission\('core\.pin\.move'\)/, 'the atom gates the route');
|
|
220
|
+
assert.match(route, /api\.requireBuilder/, 'and an anonymous caller never reaches it');
|
|
221
|
+
assert.doesNotMatch(route, /requireRank/, 'BV1.R105: a modules/*/routes gate is an atom, never a rank');
|
|
222
|
+
|
|
223
|
+
const catalog = read('modules', 'government', 'catalog.js');
|
|
224
|
+
const entry = catalog.split('\n').find((l) => l.includes("key: 'core.pin.move'"));
|
|
225
|
+
assert.ok(entry, 'the atom is in the catalog');
|
|
226
|
+
assert.match(entry, /floor: 'archon'/, 'archon floor — deploy is a hard gate the owner holds');
|
|
227
|
+
assert.match(entry, /system: true/,
|
|
228
|
+
'system:true is what makes it UN-DELEGATABLE: the catalog invariant forbids granting a system key to metic');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await t('the grant migration gives it to archon and to NOBODY else', () => {
|
|
232
|
+
const mig = read('modules', 'government', 'migrations', 'government_014_core_pin_move.sql')
|
|
233
|
+
.replace(/^\s*--.*$/gm, ' ');
|
|
234
|
+
assert.match(mig, /SELECT 'archon', unnest\(ARRAY\[\s*'core\.pin\.move'/);
|
|
235
|
+
assert.doesNotMatch(mig, /SELECT 'metic'/, 'no metic grant — the absence is the point');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
console.log('\nthe web tier only ASKS (ADR 0111 §2 / ADR 0293 D1):');
|
|
239
|
+
|
|
240
|
+
await t('nothing in the door shells out, escalates, or reaches the control plane', () => {
|
|
241
|
+
// The rule that makes a stolen builder token unable to deploy. Asserted over both
|
|
242
|
+
// web-tier files, comments stripped so prose describing the runner is not read as
|
|
243
|
+
// the web tier doing it.
|
|
244
|
+
for (const f of [['modules', 'provisioning', 'core-upgrade.js'], ['modules', 'provisioning', 'routes', 'core-upgrade.js']]) {
|
|
245
|
+
const code = read(...f)
|
|
246
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
247
|
+
.replace(/^\s*\/\/.*$/gm, ' ');
|
|
248
|
+
for (const forbidden of ['child_process', 'spawnSync', 'execSync', 'sudo ', 'ssh ']) {
|
|
249
|
+
assert.ok(!code.includes(forbidden), `${f.join('/')} must not reference ${forbidden}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await t('the handler queues an intent carrying the TARGET, and records the actor', async () => {
|
|
255
|
+
const calls = [];
|
|
256
|
+
const provisioning = {
|
|
257
|
+
getInstanceById: async () => ACTIVE,
|
|
258
|
+
enqueueIntent: async (_db, id, action, requestedBy, targetVersion) => {
|
|
259
|
+
calls.push({ id, action, requestedBy, targetVersion });
|
|
260
|
+
return { intent: { id: 1, action, state: 'pending', target_version: targetVersion }, created: true };
|
|
261
|
+
},
|
|
262
|
+
recordEvent: async (_db, e) => { calls.push({ event: e.event, actor: e.actor }); },
|
|
263
|
+
};
|
|
264
|
+
const handler = CU.makeCoreUpgradeHandler({
|
|
265
|
+
provisioning, pool: {}, parseId: () => 7,
|
|
266
|
+
validateOrRespond: () => false, log: { error() {} },
|
|
267
|
+
});
|
|
268
|
+
let body = null;
|
|
269
|
+
const res = { json: (b) => { body = b; return res; }, fail: (code, o) => { body = { code, ...o }; return res; } };
|
|
270
|
+
await handler({ builder: { id: 42 }, params: { id: '7' }, body: { to: '1.19.757' } }, res);
|
|
271
|
+
|
|
272
|
+
const enqueued = calls.find((c) => c.action === 'core-upgrade');
|
|
273
|
+
assert.ok(enqueued, 'an intent was queued');
|
|
274
|
+
assert.equal(enqueued.targetVersion, '1.19.757', 'carrying the version the owner confirmed');
|
|
275
|
+
assert.equal(enqueued.requestedBy, 'api:archon:42', 'attributed to the caller');
|
|
276
|
+
assert.ok(calls.some((c) => c.event === 'core-upgrade' && c.actor === 'api:archon:42'), 'and audited');
|
|
277
|
+
assert.equal(body.queued, true);
|
|
278
|
+
assert.equal(body.target_version, '1.19.757');
|
|
279
|
+
assert.doesNotMatch(body.message, /\b(minute|second|hour)\b/i, 'no duration is promised');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await t('a SECOND request conflicts rather than reporting the first as done', async () => {
|
|
283
|
+
// Unlike a duplicate restart, which IS the thing asked for. This one names a
|
|
284
|
+
// target, and the open intent may name a different one — reporting that as
|
|
285
|
+
// success would tell the owner their version is coming when another is.
|
|
286
|
+
const provisioning = {
|
|
287
|
+
getInstanceById: async () => ACTIVE,
|
|
288
|
+
enqueueIntent: async () => ({ intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.756' }, created: false }),
|
|
289
|
+
recordEvent: async () => {},
|
|
290
|
+
};
|
|
291
|
+
const handler = CU.makeCoreUpgradeHandler({
|
|
292
|
+
provisioning, pool: {}, parseId: () => 7,
|
|
293
|
+
validateOrRespond: () => false, log: { error() {} },
|
|
294
|
+
});
|
|
295
|
+
let body = null;
|
|
296
|
+
const res = { json: (b) => { body = b; return res; }, fail: (code, o) => { body = { code, ...o }; return res; } };
|
|
297
|
+
await handler({ builder: { id: 42 }, params: { id: '7' }, body: { to: '1.19.757' } }, res);
|
|
298
|
+
assert.equal(body.code, 'intent_conflict');
|
|
299
|
+
assert.equal(body.status, 409);
|
|
300
|
+
assert.equal(body.details.target_version, '1.19.756', 'and it names the target already in flight');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await t('the handler does NOT read a core_version column, because there is none', () => {
|
|
304
|
+
// The first cut passed `inst.core_version || null`, which is always undefined —
|
|
305
|
+
// so the already-on-version refusal was unreachable from the route while reading
|
|
306
|
+
// as a live guard. Pinned from both ends: the schema has no such column, and the
|
|
307
|
+
// handler does not name it.
|
|
308
|
+
const migs = ['provisioning_001_tables.sql', 'provisioning_018_core_upgrade_intent.sql']
|
|
309
|
+
.map((f) => read('modules', 'provisioning', 'migrations', f)).join('\n');
|
|
310
|
+
assert.doesNotMatch(migs, /core_version/, 'no migration adds the column');
|
|
311
|
+
const code = read('modules', 'provisioning', 'core-upgrade.js')
|
|
312
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
313
|
+
.replace(/^\s*\/\/.*$/gm, ' ');
|
|
314
|
+
assert.doesNotMatch(code, /inst\.core_version/, 'and the handler does not pretend to read it');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await t('THE SELF-ROW GUARD IS VACUOUS FOR THIS ACTION, so neither door half may call it as a gate', () => {
|
|
318
|
+
// The failure this pins is a guard that reads as present and can never fire —
|
|
319
|
+
// the third instance of that shape on this one task. `intentAllowedForShape`
|
|
320
|
+
// answers TRUE for 'core-upgrade' against every shape there is: for a tenant
|
|
321
|
+
// shape unconditionally, and for the control plane because the action is
|
|
322
|
+
// permanently on CONTROL_PLANE_ACTIONS. A reader scanning either file would
|
|
323
|
+
// reasonably believe two gates stood there; only one ever did.
|
|
324
|
+
for (const shape of [...P.HOSTING_SHAPES, 'something-new', undefined, null]) {
|
|
325
|
+
assert.equal(G.intentAllowedForShape('core-upgrade', shape), true,
|
|
326
|
+
`the guard cannot refuse core-upgrade for shape '${shape}' — so calling it as a gate is dead code`);
|
|
327
|
+
}
|
|
328
|
+
// Neither half of the door may name it. If a later edit puts it back, this fails
|
|
329
|
+
// and points at the sentence above rather than at a mystery.
|
|
330
|
+
for (const f of [['modules', 'provisioning', 'core-upgrade.js'], ['scripts', 'gds', 'provision-core-upgrade.js']]) {
|
|
331
|
+
const src = read(...f).replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/^\s*\/\/.*$/gm, ' ');
|
|
332
|
+
assert.ok(!/intentAllowedForShape\s*\(/.test(src),
|
|
333
|
+
`${f.join('/')} must not call intentAllowedForShape — it can only ever return true here`);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
console.log('\nthe preview — step one of the two-step (ADR 0293 D4):');
|
|
338
|
+
|
|
339
|
+
const previewInst = { id: 7, slug: 'cloudbongos', hosting_shape: 'control-plane' };
|
|
340
|
+
|
|
341
|
+
await t('a box behind a published version reads as behind, and names what it would move to', () => {
|
|
342
|
+
const p = CU.previewFromSnapshot(previewInst,
|
|
343
|
+
{ served_version: '1.19.758', available: ['1.19.758', '1.19.762'], recommended: '1.19.762', observed_at: '2026-09-16T10:00:00Z' }, null);
|
|
344
|
+
assert.equal(p.served_version, '1.19.758');
|
|
345
|
+
assert.equal(p.recommended_version, '1.19.762');
|
|
346
|
+
assert.equal(p.behind, true);
|
|
347
|
+
assert.equal(p.reading, 'ok');
|
|
348
|
+
assert.equal(p.observed_at, '2026-09-16T10:00:00Z', 'the age of the reading is part of the answer, never hidden');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await t('a box already on the newest is NOT behind, and offers nothing', () => {
|
|
352
|
+
const p = CU.previewFromSnapshot(previewInst,
|
|
353
|
+
{ served_version: '1.19.762', available: ['1.19.762'], recommended: '1.19.762', observed_at: 'x' }, null);
|
|
354
|
+
assert.equal(p.behind, false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await t('A BOX THAT DID NOT ANSWER IS "no_answer", NEVER a confident "up to date"', () => {
|
|
358
|
+
// The failure this pins: collapsing an unread box into `behind:false` renders as
|
|
359
|
+
// "nothing to deploy" over a box whose version nobody actually knows.
|
|
360
|
+
const p = CU.previewFromSnapshot(previewInst,
|
|
361
|
+
{ served_version: null, available: ['1.19.762'], recommended: null, observed_at: 'x' }, null);
|
|
362
|
+
assert.equal(p.reading, 'no_answer');
|
|
363
|
+
assert.equal(p.served_version, null);
|
|
364
|
+
assert.equal(p.behind, false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await t('no snapshot at all reports observed_at:null rather than a confident-looking empty reading', () => {
|
|
368
|
+
const p = CU.previewFromSnapshot(previewInst, undefined, null);
|
|
369
|
+
assert.equal(p.observed_at, null);
|
|
370
|
+
assert.equal(p.reading, 'no_answer');
|
|
371
|
+
assert.equal(p.available_count, 0);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await t('an open intent is surfaced with its target, so the page can say what is already running', () => {
|
|
375
|
+
const p = CU.previewFromSnapshot(previewInst, { served_version: '1.19.758', available: [], recommended: null },
|
|
376
|
+
{ action: 'core-upgrade', state: 'running', target_version: '1.19.762' });
|
|
377
|
+
assert.deepEqual(p.open_intent, { action: 'core-upgrade', state: 'running', target_version: '1.19.762' });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
console.log('\nthe deploy page is gated on the same atom as its routes:');
|
|
381
|
+
|
|
382
|
+
await t('the page gate, the preview and the move all check core.pin.move — the shell can never drift wider than the button', () => {
|
|
383
|
+
const routes = read('modules', 'provisioning', 'routes', 'core-upgrade.js');
|
|
384
|
+
const gates = routes.match(/requirePermission\('([^']+)'\)/g) || [];
|
|
385
|
+
assert.ok(gates.length >= 3, 'all three routes carry a gate');
|
|
386
|
+
for (const g of gates) assert.match(g, /core\.pin\.move/, `every gate on this router is the same atom: ${g}`);
|
|
387
|
+
const auth = read('src', 'bongos', 'auth.js');
|
|
388
|
+
assert.match(auth, /requireCorePinPage: Object\.freeze\(\{ permission: 'core\.pin\.move'/, 'and the page gate is the same key');
|
|
389
|
+
const serve = read('src', 'bongos', 'serve-internal.js');
|
|
390
|
+
assert.match(serve, /const DEPLOY_PAGE_RE = \/\^\\\/builders\\\/deploy/, 'the page is matched');
|
|
391
|
+
assert.match(serve, /DEPLOY_PAGE_RE\.test\(norm\)\) \{\s*\n\s*return gdsAuth\.requireCorePinPage/, 'and dispatched to that gate');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await t('THE DEPLOY PAGE NEVER COMPUTES A VERSION COMPARISON — every number on it is a reading the server handed it', () => {
|
|
395
|
+
// The failure this pins is subtle and expensive: a page that decides for itself
|
|
396
|
+
// whether the box is behind will, one release later, disagree with the runner that
|
|
397
|
+
// actually performs the move — on the screen the owner trusts most.
|
|
398
|
+
const js = read('modules', 'hall-ui', 'public', 'deploy.js')
|
|
399
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
400
|
+
.replace(/^\s*\/\/.*$/gm, ' ');
|
|
401
|
+
assert.ok(!/localeCompare|semver|\.split\('\.'\)/.test(js), 'no version arithmetic on the client');
|
|
402
|
+
assert.ok(!/\bconst\s+\w*VERSION\w*\s*=\s*'/.test(js), 'no version literal baked into the page');
|
|
403
|
+
assert.match(js, /p\.recommended_version/, 'it sends the version the SERVER named');
|
|
404
|
+
assert.match(js, /p\.behind/, 'and asks the server whether there is anything to do');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
console.log(`\ncore_upgrade_door: ${passed} passed, ${failed} failed`);
|
|
408
|
+
if (failed) process.exit(1);
|