@bongos/core 1.19.924 → 1.19.925

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 CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.924",
6
- "core_contract": "1.19.924",
7
- "source_commit": "c6cacd0f4e168f1f47817662b3eb47afa9b0e325",
5
+ "core_version": "1.19.925",
6
+ "core_contract": "1.19.925",
7
+ "source_commit": "930b42e46feac9ae2474f05b89fc6f34e34411f9",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-20T22:26:38.017Z",
9
+ "built_at": "2026-09-20T22:35:59.969Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 511,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2849,
20
- "tree_sha256": "c69c8cf54442568a4116f2e35e4ec1e9e72abbe2fac0bc4e258966118ad5aceb",
20
+ "tree_sha256": "10cb3a93d6b8e5f9825e7c3be73d087762b6fd943ed12823c7289f19939249f8",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2677,7 +2677,7 @@
2677
2677
  {
2678
2678
  "path": "docs/module-api-changelog.md",
2679
2679
  "mode": "0000644",
2680
- "sha256": "7fd498d8715f13993c9a6063ec9add31cdc9ee13b1892af2c366e5c8314dc233"
2680
+ "sha256": "7ada2ca77bedfca9dd58ce88e09d04a5449c08198adb55619285a95239a88481"
2681
2681
  },
2682
2682
  {
2683
2683
  "path": "docs/modules-contract.md",
@@ -7917,12 +7917,12 @@
7917
7917
  {
7918
7918
  "path": "package-lock.json",
7919
7919
  "mode": "0000644",
7920
- "sha256": "71c16e136b5801d5f4e6c1a7f51259a4806e4455a56667d938f741165fb606c2"
7920
+ "sha256": "37fbca7af4b1aa573094974c42fa895a9eec301c3af4fe8c2ce168753ce47ccc"
7921
7921
  },
7922
7922
  {
7923
7923
  "path": "package.json",
7924
7924
  "mode": "0000644",
7925
- "sha256": "5ffcd517937622f074ddafa5e24c388f82e70025be47e2b2903ecb51fca516d1"
7925
+ "sha256": "d667850fddfe0681b9e05312d9558a616f99be4f3eb399a8c6558d7e5745fc5f"
7926
7926
  },
7927
7927
  {
7928
7928
  "path": "public-docs/index.html",
@@ -8867,7 +8867,7 @@
8867
8867
  {
8868
8868
  "path": "scripts/gds/provision-core-upgrade.js",
8869
8869
  "mode": "0000644",
8870
- "sha256": "1c17b46ebe04bf19abfcfe40a0b3029c052362155b90b6445ea2b105cae7e621"
8870
+ "sha256": "05292d556595bb3ce88a3e0b76c10c7cab2fef7f9d7e1af1e0b3610d48830692"
8871
8871
  },
8872
8872
  {
8873
8873
  "path": "scripts/gds/provision-disconnect.js",
@@ -8892,7 +8892,7 @@
8892
8892
  {
8893
8893
  "path": "scripts/gds/provision-repo.js",
8894
8894
  "mode": "0000644",
8895
- "sha256": "dd912cf4446f572257b0a9cc7380a331b8ae7e3d834ccff828fddc13ae5bcb19"
8895
+ "sha256": "ef448bfb12437b2a3b83d7d726daff6a38b2a5a2db52096cc0150171d73547ce"
8896
8896
  },
8897
8897
  {
8898
8898
  "path": "scripts/gds/provision-teardown.js",
@@ -9922,7 +9922,7 @@
9922
9922
  {
9923
9923
  "path": "src/module-api.js",
9924
9924
  "mode": "0000644",
9925
- "sha256": "d846a0cb8672ac4215442311fbc5a8c6bcd00f99e085e98b485095a2573007c5"
9925
+ "sha256": "d365390e6290c57d21c149731cc048b379887d6f3146ba02dafdf89ee869739d"
9926
9926
  },
9927
9927
  {
9928
9928
  "path": "src/module-loader/catalog.js",
@@ -10837,7 +10837,7 @@
10837
10837
  {
10838
10838
  "path": "tests/core_upgrade_runner.mjs",
10839
10839
  "mode": "0000644",
10840
- "sha256": "c818ad655da58837122dc40a8d64b0ca89eb643aebba094ed674546cbaa3b1f5"
10840
+ "sha256": "994af91fda0df0ceb8b6e3eb80dd8bd23595a127a0e1d2e0381cb55e97beb557"
10841
10841
  },
10842
10842
  {
10843
10843
  "path": "tests/cosmos_token_contrast.mjs",
@@ -2322,5 +2322,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2322
2322
  landed since 1.19.922 with no explicit bump. run 35541278656. (task 1002620)
2323
2323
  1.19.924 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2324
2324
  landed since 1.19.923 with no explicit bump. run 35541674047. (task 1002620)
2325
+ 1.19.925 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2326
+ landed since 1.19.924 with no explicit bump. run 35542154729. (task 1002620)
2325
2327
  ---------------------------------------------------------------------------
2326
2328
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.924",
3
+ "version": "1.19.925",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.924",
9
+ "version": "1.19.925",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.924",
3
+ "version": "1.19.925",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -30,7 +30,7 @@
30
30
 
31
31
  const path = require('path');
32
32
  const { CONFIG, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, REPO_ROOT } = require('./provision-config.js');
33
- const { APP_USER, dbName, instanceUser, standaloneRoot } = require('./provision-repo.js');
33
+ const { APP_USER, dbName, instanceUser, selfInstanceRoot, standaloneRoot } = require('./provision-repo.js');
34
34
  const { isValidSlug } = require('../../modules/provisioning/provisioning.js');
35
35
 
36
36
  // The shapes whose box this runner can actually reach. Mirrors UPGRADE_SHAPES in the
@@ -96,7 +96,19 @@ function targetFault(to, available, installed, channel, channelAllows) {
96
96
  if (!available.includes(to)) {
97
97
  return `core ${to} is not published yet — nothing was changed. A release tag appears several minutes before the package itself does; try again once it lands.`;
98
98
  }
99
- if (installed && channelAllows && !channelAllows(installed, to, channel)) {
99
+ // AN UNREADABLE `installed` IS A FAULT, NOT A FREE PASS (task 1004123).
100
+ //
101
+ // This guard used to be written `if (installed && …)`, so a null installed version
102
+ // skipped the channel check entirely rather than failing it. That is backwards: not
103
+ // knowing where an instance is today is the one state in which "is this jump
104
+ // permitted" cannot be answered, and the channel exists precisely to refuse jumps
105
+ // nobody has looked at. It also hid the defect that motivated this: the control
106
+ // plane read its installed version from the wrong directory for weeks and got null
107
+ // every time, and every press sailed past this line.
108
+ if (!installed) {
109
+ return `could not read the core version installed at this instance — nothing was changed. An upgrade that cannot say where it is starting from cannot be bounded by the '${channel}' channel.`;
110
+ }
111
+ if (channelAllows && !channelAllows(installed, to, channel)) {
100
112
  return `core ${to} is outside this project's '${channel}' update channel — nothing was changed. Change the channel first if this is deliberate.`;
101
113
  }
102
114
  return null;
@@ -156,7 +168,8 @@ async function coreUpgradeInstance(inst, deps, intent) {
156
168
 
157
169
  // ---- 2. is the target real, and permitted -----------------------------------------
158
170
  const channelMod = deps.updateChannel || require('./update-channel.js');
159
- const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : REPO_ROOT;
171
+ const selfRoot = deps.selfInstanceRoot || selfInstanceRoot;
172
+ const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : selfRoot();
160
173
  const readInstalled = deps.readInstalledCoreVersion || require('./upgrade.js').readInstalledCoreVersion;
161
174
  const installed = readInstalled(instanceDir);
162
175
  // THE PLATFORM DEFAULT, NOT A PER-INSTANCE CHANNEL — and saying so beats implying
@@ -190,7 +203,7 @@ async function coreUpgradeInstance(inst, deps, intent) {
190
203
  : '';
191
204
  // Built by the SAME function the preview uses, so "what you were shown" and "what runs"
192
205
  // cannot drift apart into two hand-maintained flag lists (task 1004121).
193
- const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, requestedBy });
206
+ const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, requestedBy, selfRoot });
194
207
 
195
208
  if (!apply) {
196
209
  // A --dry-run runner tick used to only PRINT this command. It now runs the CLI's own
@@ -269,18 +282,31 @@ function parsePreflight(out) {
269
282
  // HERE rather than left to instanceUser()'s own throw: that throw is correct today, but
270
283
  // a call site whose safety depends on a helper's internals is one refactor away from a
271
284
  // shell-injection path (and the apply leg's own guard sits at the route, not here).
272
- function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '' } = {}) {
285
+ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', selfRoot = selfInstanceRoot } = {}) {
273
286
  if (!isValidSlug(inst && inst.slug)) {
274
287
  throw new Error(`upgradeInvocation: refusing to build a shell command from an invalid slug ${JSON.stringify(inst && inst.slug)}`);
275
288
  }
276
- const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : REPO_ROOT;
289
+ // WHERE THE CLI LIVES AND WHERE THE TARGET LIVES ARE DIFFERENT PLACES, TWICE OVER.
290
+ //
291
+ // `upgrade.js` ships in the CORE, and a provisioned instance root holds no scripts/
292
+ // directory at all — so the CLI runs from REPO_ROOT and NAMES its target (task
293
+ // 1004053, which fixed every standalone tenant).
294
+ //
295
+ // The second half is that REPO_ROOT is not the instance either (task 1004123). It
296
+ // names the copy of the core that is EXECUTING, which coincides with the instance
297
+ // only while the core is a checkout run in place. Once the control plane's runner
298
+ // moved into its vendored core the two diverged silently — REPO_ROOT still resolved,
299
+ // just to node_modules/@bongos/core — and the door aimed at the package instead of
300
+ // the project. So ask, rather than assume; in the legacy shape the answer is
301
+ // REPO_ROOT and the command below is byte-identical to what it always was.
302
+ const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : selfRoot();
277
303
  // The control plane is not a provisioned tenant and has no `bongos-` account; every
278
304
  // other shape runs as its own, which is also its Postgres login role (task 1004119).
279
305
  const runAs = inst.hosting_shape === 'control-plane' ? APP_USER : instanceUser(inst);
280
306
  const sudoP = privileged ? `sudo -u ${runAs} env PGDATABASE=${dbName(inst)} ` : '';
281
307
  const cli = path.posix.join('scripts', 'gds', 'upgrade.js');
282
308
  const shared = [`${sudoP}node ${cli}`, '--registry', `--to ${to}`,
283
- instanceDir === REPO_ROOT ? '' : `--instance ${instanceDir}`];
309
+ instanceDir === REPO_ROOT ? '' : `--instance ${instanceDir}`]; // REPO_ROOT is where the CLI runs (runDir below)
284
310
  const tail = dryRun
285
311
  ? ['--dry-run']
286
312
  : [`--service ${inst.slug}`,
@@ -297,7 +323,10 @@ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, reque
297
323
  function preflightUpgrade(inst, to, deps) {
298
324
  const { exec, privileged } = deps;
299
325
  const boxExec = inst.hosting_shape === 'standalone' ? (deps.controlExec || exec) : exec;
300
- const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, dryRun: true });
326
+ // A distinct local name: `deps.selfInstanceRoot || selfInstanceRoot` with a const of
327
+ // the same name would TDZ-shadow the import and crash every uninjected call.
328
+ const selfRoot = deps.selfInstanceRoot || selfInstanceRoot;
329
+ const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, dryRun: true, selfRoot });
301
330
  const r = boxExec(cmd, { cwd: runDir, allowFail: true });
302
331
  const out = `${(r && r.stdout) || ''}\n${(r && r.stderr) || ''}`;
303
332
  const { checks, error } = parsePreflight(out);
@@ -353,7 +382,8 @@ async function snapshotCoreVersions(inst, deps) {
353
382
  if (!UPGRADE_SHAPES.has(inst.hosting_shape) || inst.status !== 'active') return { ok: true, skipped: true };
354
383
  const boxExec = inst.hosting_shape === 'standalone' ? (deps.controlExec || exec) : exec;
355
384
  const channelMod = deps.updateChannel || require('./update-channel.js');
356
- const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : REPO_ROOT;
385
+ const selfRoot = deps.selfInstanceRoot || selfInstanceRoot;
386
+ const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : selfRoot();
357
387
  try {
358
388
  const probe = boxExec(versionCmd(inst), { allowFail: true });
359
389
  const served = parseServedVersion(probe && !probe.softFailed ? (probe.stdout || '') : null);
@@ -91,6 +91,31 @@ const CORE_PKG_DIR = 'node_modules/@bongos/core';
91
91
  // one constant so the backup dir owner + the backup unit's User= can't drift from it.
92
92
  const APP_USER = 'lars';
93
93
  function standaloneRoot(inst, { base = CONFIG.standaloneBase } = {}) { return path.posix.join(base, inst.slug); }
94
+ // WHERE THE INSTANCE THIS CORE SERVES ACTUALLY LIVES (task 1004123).
95
+ //
96
+ // REPO_ROOT is `path.resolve(__dirname, '..', '..')` — it points at whatever copy of
97
+ // the core is EXECUTING, which is not the same place as the instance being upgraded.
98
+ // Two shapes, and the code had only ever met the first:
99
+ //
100
+ // • a core CHECKOUT run in place (`/home/lars/cloudbongos`) — the instance root IS
101
+ // the checkout, so REPO_ROOT was right by coincidence;
102
+ // • a VENDORED core (`<instance>/node_modules/@bongos/core`) — REPO_ROOT is the
103
+ // package, and the instance is three directories up.
104
+ //
105
+ // The live control plane became the second shape when its runner unit was re-homed
106
+ // (`WorkingDirectory=/home/lars/cloudbongos-instance`, ExecStart into the vendored
107
+ // core), and nothing noticed: REPO_ROOT still resolved, just to the wrong tree. So
108
+ // `readInstalledCoreVersion` answered null instead of 1.19.917, every guard that
109
+ // reads `installed` silently stopped guarding, and an upgrade would have pinned and
110
+ // npm-installed INSIDE the core package while the instance's own pin stayed put.
111
+ //
112
+ // This is a string question, not a filesystem one: the vendored layout is the only
113
+ // way a core ends up under `node_modules/`, so the suffix IS the answer. Keeping it
114
+ // pure means the control-plane path is testable without a box.
115
+ function selfInstanceRoot(repoRoot = REPO_ROOT) {
116
+ const suffix = path.sep + CORE_PKG_DIR.split('/').join(path.sep);
117
+ return repoRoot.endsWith(suffix) ? repoRoot.slice(0, -suffix.length) : repoRoot;
118
+ }
94
119
  // Sync the instance's own repo (host content + the core pin). Mirrors deploy-instance.sh
95
120
  // step 1 — assumes the repo is already cloned at standaloneRoot (initial clone is a
96
121
  // precondition, like the runbook's `git pull`). Run with cwd = standaloneRoot.
@@ -1107,6 +1132,7 @@ module.exports = {
1107
1132
  safeVersionLabel,
1108
1133
  scaffoldStandaloneRepo,
1109
1134
  seedFirstVersionCmd,
1135
+ selfInstanceRoot,
1110
1136
  standaloneInstallCmd,
1111
1137
  standaloneMigrateCmd,
1112
1138
  standalonePullCmd,
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.924'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.925'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -36,6 +36,7 @@ const R = require('../scripts/gds/provision-core-upgrade.js');
36
36
  const U = require('../scripts/gds/upgrade.js');
37
37
  const P = require('../modules/provisioning/provisioning.js');
38
38
  const G = require('../modules/provisioning/control-plane-guard.js');
39
+ const RP = require('../scripts/gds/provision-repo.js');
39
40
 
40
41
  let passed = 0;
41
42
  let failed = 0;
@@ -553,5 +554,98 @@ await t('a preview of nothing is refused rather than cached as a pass', async ()
553
554
  assert.equal(d.queries.filter((q) => /INSERT INTO provisioning_core_version_snapshot/.test(q.sql)).length, 0);
554
555
  });
555
556
 
557
+ console.log('\nwhere the instance actually is (task 1004123):');
558
+
559
+ // The vendored layout the live control plane runs in: the core is a PACKAGE inside the
560
+ // instance, so the directory the core executes from is three levels below the directory
561
+ // that must be upgraded.
562
+ const VENDORED = '/home/lars/cloudbongos-instance/node_modules/@bongos/core';
563
+ const INSTANCE = '/home/lars/cloudbongos-instance';
564
+ const LEGACY = '/home/lars/cloudbongos';
565
+
566
+ await t('selfInstanceRoot walks out of a vendored core, and leaves a plain checkout alone', () => {
567
+ assert.equal(RP.selfInstanceRoot(VENDORED), INSTANCE, 'the package is not the instance');
568
+ assert.equal(RP.selfInstanceRoot(LEGACY), LEGACY, 'a checkout run in place IS the instance');
569
+ });
570
+
571
+ await t('a path that merely CONTAINS the package name is not mistaken for one', () => {
572
+ // Guarding the string test itself: only a trailing segment means "I am the vendored
573
+ // core", and an instance that happens to live under a similarly-named directory must
574
+ // not be truncated into nonsense.
575
+ const decoy = '/srv/node_modules/@bongos/core-tools';
576
+ assert.equal(RP.selfInstanceRoot(decoy), decoy);
577
+ assert.equal(RP.selfInstanceRoot('/srv/node_modules/@bongos/core/sub'), '/srv/node_modules/@bongos/core/sub');
578
+ });
579
+
580
+ await t('the runner reads the installed version from the INSTANCE, not from the core package', async () => {
581
+ // The defect this replaces: readInstalledCoreVersion was handed REPO_ROOT, which in
582
+ // the vendored shape holds no node_modules/@bongos/core at all — so it answered null
583
+ // on the live control plane for as long as the shape has existed.
584
+ const asked = [];
585
+ const d = { ...makeDeps({ served: '1.19.762' }), selfInstanceRoot: () => INSTANCE };
586
+ d.readInstalledCoreVersion = (dir) => { asked.push(dir); return '1.19.760'; };
587
+ await R.coreUpgradeInstance(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos', port: 3002 }), d, intent());
588
+ assert.deepEqual(asked, [INSTANCE], 'asked the instance root exactly once');
589
+ });
590
+
591
+ await t('the CLI runs where it lives and NAMES the instance it is aimed at', async () => {
592
+ const d = { ...makeDeps({ served: '1.19.762' }), selfInstanceRoot: () => INSTANCE };
593
+ await R.coreUpgradeInstance(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos', port: 3002 }), d, intent());
594
+ const cmd = upgradeCall(d);
595
+ assert.ok(cmd.includes(`--instance ${INSTANCE} `),
596
+ `without this the upgrade pins and installs inside the core package and the real pin never moves — got: ${cmd}`);
597
+ });
598
+
599
+ await t('the legacy shape still omits --instance, because there is nothing to disambiguate', async () => {
600
+ const d = { ...makeDeps({ served: '1.19.762' }), selfInstanceRoot: () => require('../scripts/gds/provision-config.js').REPO_ROOT };
601
+ await R.coreUpgradeInstance(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos', port: 3002 }), d, intent());
602
+ assert.doesNotMatch(upgradeCall(d), /--instance /, 'a checkout run in place is its own instance');
603
+ });
604
+
605
+ await t('the version snapshot asks the registry from the instance, not from inside the package', async () => {
606
+ // Weakest of the three sites — `recommended` is derived from the box's own /version,
607
+ // so aiming this one wrongly changes no answer today. It is guarded anyway: npm reads
608
+ // its configuration from the directory it runs in, and an unguarded line is a line
609
+ // that regresses quietly. Found by mutating each of the three sites in turn; this was
610
+ // the one no test noticed.
611
+ const cwds = [];
612
+ const d = makeDeps({ served: '1.19.760' });
613
+ d.selfInstanceRoot = () => INSTANCE;
614
+ d.updateChannel = { ...d.updateChannel, listAvailableVersions: ({ cwd }) => { cwds.push(cwd); return { ok: true, versions: ['1.19.760', '1.19.762'] }; } };
615
+ await R.snapshotCoreVersions(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos', port: 3002 }), d);
616
+ assert.deepEqual(cwds, [INSTANCE]);
617
+ });
618
+
619
+ await t('the PREVIEW aims at the instance too — a dry run of the wrong directory is the lie', async () => {
620
+ // This is the shape that made the defect dangerous rather than merely broken: dry-run
621
+ // against the core package and upgrade.js reports `(none) → <target>` with green
622
+ // pre-flight ticks, so the owner is shown a clean bill of health for a run that will
623
+ // restart production and roll back. Both legs share one builder; both must aim.
624
+ const box = { id: 41, slug: 'cloudbongos', port: 3002, hosting_shape: 'control-plane', db_name: 'cloudbongos' };
625
+ const selfRoot = () => INSTANCE;
626
+ const move = R.upgradeInvocation(box, '1.19.922', { privileged: true, selfRoot });
627
+ const peek = R.upgradeInvocation(box, '1.19.922', { privileged: true, dryRun: true, selfRoot });
628
+ assert.ok(peek.cmd.includes(`--instance ${INSTANCE} `), `the preview names its target — got: ${peek.cmd}`);
629
+ assert.equal(move.instanceDir, peek.instanceDir, 'and it is the same target the move would take');
630
+ assert.equal(peek.instanceDir, INSTANCE);
631
+ });
632
+
633
+ await t('an unreadable installed version is a REFUSAL, not a skipped channel check', () => {
634
+ // Fail-closed. This is the guard that would have caught the directory bug on the
635
+ // first press instead of after four live restarts.
636
+ const f = R.targetFault('1.19.760', ['1.19.758', '1.19.760'], null, 'patch', () => true);
637
+ assert.match(f, /could not read the core version installed/);
638
+ assert.match(f, /cannot be bounded by the 'patch' channel/);
639
+ });
640
+
641
+ await t('the runner refuses, and runs NOTHING, when it cannot see where the instance is', async () => {
642
+ const d = makeDeps({ served: '1.19.762' });
643
+ d.readInstalledCoreVersion = () => null;
644
+ const r = await R.coreUpgradeInstance(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos', port: 3002 }), d, intent());
645
+ assert.equal(r.ok, false);
646
+ assert.match(r.error, /could not read the core version installed/);
647
+ assert.equal(d.ranWith.filter((c) => /upgrade\.js/.test(c.cmd)).length, 0, 'the box was not touched');
648
+ });
649
+
556
650
  console.log(`\ncore_upgrade_runner: ${passed} passed, ${failed} failed`);
557
651
  if (failed) process.exit(1);