@bongos/core 1.19.91 → 1.19.92

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.91",
6
- "core_contract": "1.19.91",
7
- "source_commit": "157eeaee12b32ba8b9e23f62cf8476cc689e3e07",
5
+ "core_version": "1.19.92",
6
+ "core_contract": "1.19.92",
7
+ "source_commit": "3458c7057c3f26cc4304f95dc80ea9b5d6928d7c",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-13T16:52:49.920Z",
9
+ "built_at": "2026-08-13T17:47:34.165Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 298,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1559,
20
- "tree_sha256": "a915672ef661add366011ce3030a79b196e486e67a437c4531c54de0b0bc3f87",
20
+ "tree_sha256": "475c679379a933a41a8c0c717e122af3bee2bfa56999f07d1e800961300c989e",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4112,12 +4112,12 @@
4112
4112
  {
4113
4113
  "path": "package-lock.json",
4114
4114
  "mode": "0000644",
4115
- "sha256": "9d710f118a6ae91aadb6111cde0ee0db020c018ce405b687236c84f75c8f69f1"
4115
+ "sha256": "64b1a11a2b4a426879abf792df6861e28ffa1539b32f0527b9044baa2c1533ef"
4116
4116
  },
4117
4117
  {
4118
4118
  "path": "package.json",
4119
4119
  "mode": "0000644",
4120
- "sha256": "1c7c2996a68dde5369dd07c387737614af9292244de4db2fa9fb0a70912e94aa"
4120
+ "sha256": "fe0b5e65bcb64aff0ccc7609387e31656d5eaa7a48441a48a3b46862b9000323"
4121
4121
  },
4122
4122
  {
4123
4123
  "path": "public-docs/index.html",
@@ -5387,7 +5387,7 @@
5387
5387
  {
5388
5388
  "path": "scripts/gds/upgrade.js",
5389
5389
  "mode": "0000644",
5390
- "sha256": "b539715de4893f8fa68c53c145ac2f54de39ccc5517f9e5ad17540c487dc24b0"
5390
+ "sha256": "04ee659f61c79a1dbc2302f047d1d7870352d0892c2df974f6f000184794d3f8"
5391
5391
  },
5392
5392
  {
5393
5393
  "path": "scripts/gds/validate-design.js",
@@ -5417,7 +5417,7 @@
5417
5417
  {
5418
5418
  "path": "scripts/migrate.sh",
5419
5419
  "mode": "0000644",
5420
- "sha256": "1c0ffd6822949c0aeb0bd89f48e12c2923c6d623228619729e9812056351fe9d"
5420
+ "sha256": "0768bf9c9d6368ae673e1e5ac161cfe9bae4d113129f11f8b89afeeef1f9dc1b"
5421
5421
  },
5422
5422
  {
5423
5423
  "path": "scripts/migrate_cost_ledger.sh",
@@ -5752,7 +5752,7 @@
5752
5752
  {
5753
5753
  "path": "src/module-api.js",
5754
5754
  "mode": "0000644",
5755
- "sha256": "d1f73a5d0f57613d96dd88738b593dd343e8330bc5452d414fd8752aa78da7f0"
5755
+ "sha256": "e334fc7dbd4247d7f9362c0304fa635f5470b058ebaf6d08e70a9276846531e7"
5756
5756
  },
5757
5757
  {
5758
5758
  "path": "src/module-loader/loader.js",
@@ -7007,7 +7007,7 @@
7007
7007
  {
7008
7008
  "path": "tests/migrate_root_resolution.mjs",
7009
7009
  "mode": "0000644",
7010
- "sha256": "eec69562f86d218a0f22411bbf6707d864e42e5f0b3f841692ca938d775a45ad"
7010
+ "sha256": "e5cfa24bb69d6fade0f2f8bffd668bbda9490aca335d301abcf6f79154b60690"
7011
7011
  },
7012
7012
  {
7013
7013
  "path": "tests/migration_allocation.mjs",
@@ -7762,7 +7762,7 @@
7762
7762
  {
7763
7763
  "path": "tests/upgrade.mjs",
7764
7764
  "mode": "0000644",
7765
- "sha256": "60e55591174f59238486d318fb92dc8fd8251eb73435d690e38ea83a2a5f235e"
7765
+ "sha256": "a565b3a5e93a41feccaa8b616272390ea27cf2d50950a96f04856d18caa67744"
7766
7766
  },
7767
7767
  {
7768
7768
  "path": "tests/upgrade_persist_pin.mjs",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.91",
3
+ "version": "1.19.92",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.91",
9
+ "version": "1.19.92",
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.91",
3
+ "version": "1.19.92",
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",
@@ -324,6 +324,42 @@ function runMigrate(instanceDir, run = spawnSync) {
324
324
  return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
325
325
  }
326
326
 
327
+ // WHICH database will migrate target? (task 1002755)
328
+ //
329
+ // The pre-flight probe below is only meaningful if it tests the SAME database the migration
330
+ // will later use. It used to be handed `process.env.PGDATABASE` — undefined on a normal box —
331
+ // so psql fell through to a database named after the OS user, failed for an unrelated reason,
332
+ // and soft-skipped with "psql failed for another reason". migrate then picked its own name and
333
+ // hit a database that did not exist. Two soft failures composing into a hard one, discovered
334
+ // only AFTER the pin had moved.
335
+ //
336
+ // So resolve it here, up front, by asking the core's OWN resolver in a child pointed at this
337
+ // instance — the same precedence migrate.sh uses (PGDATABASE wins, else the instance's branding
338
+ // pack) and the same function, so the probe and the migration cannot disagree. Spawned rather
339
+ // than required because requiring pool.js constructs a pg Pool in this process.
340
+ // Returns { db, ran }:
341
+ // { db: 'name', ran: true } — resolved
342
+ // { db: null, ran: true } — the resolver RAN and this instance names no database. Fatal:
343
+ // migrate cannot proceed and we know it before the pin moves.
344
+ // { db: null, ran: false } — the resolver could not run at all (no node_modules yet, a
345
+ // stubbed spawn in tests). NOT fatal: that is a different problem
346
+ // from "no database configured", and guessing which one it is would
347
+ // block bumps for the wrong reason. migrate.sh makes the same
348
+ // distinction and still fails loudly later, so the net holds.
349
+ function resolveInstanceDb({ instanceDir }, run = spawnSync) {
350
+ if (process.env.PGDATABASE) return { db: process.env.PGDATABASE, ran: true };
351
+ const poolPath = path.join(instanceDir, 'node_modules', CORE_PKG, 'src', 'bongos', 'pool');
352
+ // the module path rides as argv, never interpolated into JS source — a Windows-shaped path's
353
+ // backslashes are escape sequences inside a string literal
354
+ const r = run(process.execPath, ['-e', "process.stdout.write(require(process.argv[1]).instanceDbName() || '')", poolPath], {
355
+ cwd: instanceDir,
356
+ encoding: 'utf8',
357
+ env: { ...process.env, CLOUDBONGOS_INSTANCE_ROOT: instanceDir },
358
+ });
359
+ if (!r || r.error || r.status !== 0 || typeof r.stdout !== 'string') return { db: null, ran: false };
360
+ return { db: String(r.stdout).trim() || null, ran: true };
361
+ }
362
+
327
363
  // Regenerate the per-instance API artifacts (OpenAPI spec + api-reference + typed
328
364
  // client) from the NEW core's routes — the claude-materialize sibling (task 2051).
329
365
  // WHY: the routes + generators ship WITH core, and the generated spec/docs/client also
@@ -748,10 +784,17 @@ async function runUpgrade(opts, deps = {}) {
748
784
  // mistake, and finding it at migrate means unwinding a pin that never should have
749
785
  // been written (task 1002712).
750
786
  if (!opts.skipMigrate) {
751
- const dbId = (deps.preflightDbIdentity || preflightDbIdentity)({ instanceDir, db: process.env.PGDATABASE }, run);
787
+ const { db, ran } = (deps.resolveInstanceDb || resolveInstanceDb)({ instanceDir }, run);
788
+ if (ran && !db) {
789
+ return { ok: false, error: 'cannot determine which database to migrate.\n'
790
+ + ' Set PGDATABASE, or give this instance a db.database in config/branding.json.\n'
791
+ + ' Checked BEFORE the pin moves: migrate runs after the install, so a name that\n'
792
+ + ' cannot be resolved there costs a full rollback to discover (task 1002755).' };
793
+ }
794
+ const dbId = (deps.preflightDbIdentity || preflightDbIdentity)({ instanceDir, db }, run);
752
795
  if (!dbId.ok) return { ok: false, error: dbId.reason };
753
- if (dbId.skipped) log(` • db identity pre-check: ${dbId.skipped}`);
754
- else log(' ✓ pre-flight: database reachable as the current user');
796
+ if (dbId.skipped) log(` • db identity pre-check${db ? ` (${db})` : ''}: ${dbId.skipped}`);
797
+ else log(` ✓ pre-flight: database${db ? ` "${db}"` : ''} reachable as the current user`);
755
798
  }
756
799
 
757
800
  // 1b. mandatory post-restart health check (audit H9 / task 1002222, finding F5). A bump that
@@ -1005,7 +1048,7 @@ async function main(argv = process.argv.slice(2)) {
1005
1048
  module.exports = {
1006
1049
  runUpgrade, parseArgs, main, rollback,
1007
1050
  versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
1008
- vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth,
1051
+ vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity, resolveInstanceDb, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth,
1009
1052
  readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
1010
1053
  CORE_PKG, ARTIFACT,
1011
1054
  };
@@ -32,7 +32,13 @@ INVOKE_CWD="${INIT_CWD:-$PWD}"
32
32
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
33
33
  DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
34
34
  CORE_ROOT="$(cd "$DEFAULT_ROOT" && node -e "process.stdout.write(require('./src/instance-config').resolveCoreRoot())" 2>/dev/null || echo "$DEFAULT_ROOT")"
35
- INSTANCE_ROOT="$(cd "$INVOKE_CWD" && node -e "process.stdout.write(require('$DEFAULT_ROOT/src/instance-config').resolveInstanceRootExplicit() || '')" 2>/dev/null || true)"
35
+ # The module path rides in as argv rather than being interpolated into the JS source: a
36
+ # Windows-shaped DEFAULT_ROOT contains backslashes, which are ESCAPE SEQUENCES inside a JS
37
+ # string literal ("\U", "\r", "\D"), so interpolating corrupts the path, the require throws,
38
+ # and this silently falls back to DEFAULT_ROOT — precisely the F6 shape described above, just
39
+ # triggered by the path separator instead of the cwd. Fixed alongside task 1002755, which hit
40
+ # the identical bug one resolution further down.
41
+ INSTANCE_ROOT="$(cd "$INVOKE_CWD" && node -e "process.stdout.write(require(process.argv[1]).resolveInstanceRootExplicit() || '')" "$DEFAULT_ROOT/src/instance-config" 2>/dev/null || true)"
36
42
  INSTANCE_ROOT="${INSTANCE_ROOT:-$DEFAULT_ROOT}"
37
43
  # Run from the instance root so instance-relative paths resolve (the post-deploy art check below)
38
44
  # AND so the enabledModules() read finds the instance's config/modules.json (resolveInstanceRoot
@@ -49,17 +55,68 @@ if [ "${1:-}" = "--print-roots" ]; then
49
55
  exit 0
50
56
  fi
51
57
 
52
- DB="${PGDATABASE:-amazonprimea}"
53
-
54
58
  # Test/ops seam (added with the 1.18.19 space-in-path fix): --print-migrations walks the REAL core-migration loop but reports
55
59
  # each path instead of applying it, and touches NO DB. tests/migrate_root_resolution.mjs uses it to
56
60
  # assert that a checkout whose path contains a space still yields whole, readable paths. Reporting
57
61
  # through the real loop (rather than a copy of it in the test) is the point: if the loop ever reverts
58
62
  # to an unquoted `for f in $MIGRATIONS`, this seam fragments the paths and the test fails. Also handy
59
63
  # in the field — `npm run migrate -- --print-migrations` shows exactly what migrate would apply.
64
+ #
65
+ # Resolved BEFORE the database name below, because a dry run touches no DB (every psql call is
66
+ # guarded by DRY_RUN) and so must not require one.
60
67
  DRY_RUN=0
61
68
  if [ "${1:-}" = "--print-migrations" ]; then DRY_RUN=1; fi
62
69
 
70
+ # --- Which database? (task 1002755) ---------------------------------------
71
+ # SAME precedence as src/bongos/pool.js: an explicit PGDATABASE wins, else the instance's own
72
+ # branding pack names it (config/branding.json db.database, else the neutral starter). Resolved by
73
+ # CALLING pool.instanceDbName() rather than re-reading the config here, so the migrator and the
74
+ # running server can never disagree about which database this instance is.
75
+ #
76
+ # There is deliberately NO literal fallback. This script ships inside the core to EVERY instance,
77
+ # so a baked-in default is one host's database name pointed at everyone else's box: on a co-hosted
78
+ # machine it silently targets a stranger's data, and everywhere else it dies with a confusing
79
+ # "database ... does not exist" long after the caller has moved its version pin. pool.js already
80
+ # made exactly this call for the server ("NOT hard-defaulting to a host's DB name", pool.js:10-16);
81
+ # this is the migrator catching up, not a new policy.
82
+ # The module path rides in as argv, NOT interpolated into the JS source: a Windows-shaped
83
+ # CORE_ROOT contains backslashes, and inside a JS string literal those are escape sequences
84
+ # ("\U", "\r"), so interpolating silently corrupts the path and this resolution degrades to
85
+ # empty. argv values are never parsed as JS.
86
+ DB="${PGDATABASE:-}"
87
+ DB_RESOLVER_ERR=""
88
+ if [ -z "$DB" ]; then
89
+ # stderr is CAPTURED, not discarded: "the pack names no database" and "the resolver could not
90
+ # run at all" are different problems with different fixes, and collapsing both into an empty
91
+ # string sends the operator to edit a config file when the real answer is `npm install`.
92
+ # (pool.js requires pg, so an instance with no node_modules fails here rather than resolving.)
93
+ if DB_RESOLVED="$(node -e "process.stdout.write(require(process.argv[1]).instanceDbName() || '')" "$CORE_ROOT/src/bongos/pool" 2>&1)"; then
94
+ DB="$DB_RESOLVED"
95
+ else
96
+ DB_RESOLVER_ERR="$DB_RESOLVED"
97
+ fi
98
+ fi
99
+ if [ -z "$DB" ] && [ "$DRY_RUN" = 0 ]; then
100
+ echo "migrate: cannot determine which database to migrate." >&2
101
+ if [ -n "$DB_RESOLVER_ERR" ]; then
102
+ echo " the db-name resolver did not run: $(printf '%s' "$DB_RESOLVER_ERR" | head -n 1)" >&2
103
+ echo " if the core's dependencies are not installed yet, run: npm install" >&2
104
+ else
105
+ echo " Set PGDATABASE, or give this instance a db.database in config/branding.json." >&2
106
+ fi
107
+ echo " Refusing to guess: a baked-in default would target another instance's database." >&2
108
+ exit 2
109
+ fi
110
+
111
+ # Test/ops seam: --print-db reports the database this run WOULD migrate, and exits before any DB
112
+ # work. `bongos upgrade` calls it to learn the name up front, so its pre-flight can prove the DB is
113
+ # reachable BEFORE the version pin moves — otherwise an unresolvable name is only discovered at
114
+ # migrate time, and the pin has to be unwound (task 1002755).
115
+ if [ "${1:-}" = "--print-db" ]; then
116
+ printf '%s\n' "$DB"
117
+ exit 0
118
+ fi
119
+
63
120
  # --- Instance-seed policy (task 1704 / ADR 0105) --------------------------
64
121
  # OTB/product scope-truth (versions, goals, done_when_criteria) lives in seed
65
122
  # migrations under migrations/instance/ (plus a few guarded blocks embedded in the
package/src/module-api.js CHANGED
@@ -609,8 +609,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
609
609
  // landed since 1.19.89 with no explicit bump. run 31717551308. (task 1002620)
610
610
  // 1.19.91 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
611
611
  // landed since 1.19.90 with no explicit bump. run 31722841411. (task 1002620)
612
+ // 1.19.92 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
613
+ // landed since 1.19.91 with no explicit bump. run 31727411036. (task 1002620)
612
614
  // ---------------------------------------------------------------------------
613
- const CORE_VERSION = '1.19.91'; // CI auto-patch carrier (ADR 0161); see changelog above
615
+ const CORE_VERSION = '1.19.92'; // CI auto-patch carrier (ADR 0161); see changelog above
614
616
 
615
617
  // A namespaced logger so a module's log lines are attributable + consistent.
616
618
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -18,7 +18,7 @@
18
18
  import { strict as assert } from 'node:assert';
19
19
  import { execFileSync } from 'node:child_process';
20
20
  import {
21
- mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, symlinkSync, realpathSync,
21
+ mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, symlinkSync, copyFileSync, realpathSync,
22
22
  } from 'node:fs';
23
23
  import { tmpdir } from 'node:os';
24
24
  import { fileURLToPath } from 'node:url';
@@ -116,7 +116,15 @@ function makeCore({ spaced, files = ['001_a.sql', '002_b.sql', '006_pms_v2.sql',
116
116
  mkdirSync(path.join(dir, 'scripts'));
117
117
  mkdirSync(path.join(dir, 'migrations'));
118
118
  mkdirSync(path.join(dir, 'src'));
119
- symlinkSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
119
+ // Symlink so the fixture can never drift from the real script. Windows refuses symlinks without
120
+ // elevation (EPERM), so fall back to a COPY taken fresh from MIGRATE on each run — equally
121
+ // drift-proof, and it is what lets these cases run on a Windows checkout at all.
122
+ try {
123
+ symlinkSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
124
+ } catch (e) {
125
+ if (e.code !== 'EPERM' && e.code !== 'EACCES') throw e;
126
+ copyFileSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
127
+ }
120
128
  // Pin both roots to the fixture explicitly, so the test does not lean on migrate.sh's
121
129
  // node-unavailable fallback to decide what CORE_ROOT is.
122
130
  writeFileSync(path.join(dir, 'src', 'instance-config.js'), [
@@ -216,5 +224,95 @@ t('the pre-fix unquoted loop now fails loudly instead of silently skipping', ()
216
224
  assert.equal(r.applied.length, 0, 'the unquoted loop applies nothing (that was the bug)');
217
225
  });
218
226
 
227
+ // ==========================================================================
228
+ // WHICH DATABASE does migrate target? (task 1002755)
229
+ //
230
+ // scripts/migrate.sh:52 used to read `DB="${PGDATABASE:-amazonprimea}"` — one host instance's
231
+ // database name, baked into a script the core ships to EVERY instance. With no PGDATABASE in the
232
+ // environment, a co-hosted box silently aimed at a stranger's database and everywhere else died
233
+ // with a confusing "database ... does not exist" — after the caller had already moved its version
234
+ // pin. ADR 0149:17 had already named the divergence: the server pool resolves from the branding
235
+ // pack while migrate.sh defaulted to a literal, and "they only agree if PGDATABASE is set for both."
236
+ //
237
+ // These drive the `--print-db` seam, which resolves the name and exits before any psql — the
238
+ // --print-roots pattern. The fixture carries a REAL config/branding.json so the assertions
239
+ // exercise the actual resolver (pool.instanceDbName) rather than a stub of it.
240
+
241
+ // A core fixture that is ALSO an instance: adds the branding pack instanceDbName() reads.
242
+ // db.database === null models the neutral starter — an instance that has not named its database.
243
+ function makeInstance({ database } = {}) {
244
+ const dir = makeCore({ spaced: false });
245
+ mkdirSync(path.join(dir, 'config'), { recursive: true });
246
+ writeFileSync(
247
+ path.join(dir, 'config', 'branding.json'),
248
+ `${JSON.stringify({ db: { database: database ?? null } }, null, 2)}\n`,
249
+ );
250
+ // migrate.sh resolves the name through <CORE_ROOT>/src/bongos/pool. Re-export the REAL module
251
+ // so these cases exercise the actual resolver rather than a restatement of it — a stub here
252
+ // would pass happily while pool.js drifted. The real file resolves its own `pg` from the repo's
253
+ // node_modules (module resolution walks up from ITS path, not the fixture's), and its
254
+ // resolveInstanceRoot() falls back to cwd — which migrate.sh has already set to this fixture.
255
+ mkdirSync(path.join(dir, 'src', 'bongos'), { recursive: true });
256
+ writeFileSync(
257
+ path.join(dir, 'src', 'bongos', 'pool.js'),
258
+ `module.exports = require(${JSON.stringify(path.join(REPO_ROOT, 'src', 'bongos', 'pool.js'))});\n`,
259
+ );
260
+ return dir;
261
+ }
262
+
263
+ function printDb(coreDir, extraEnv = {}) {
264
+ const env = { ...process.env, ...extraEnv };
265
+ for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
266
+ if (!('PGDATABASE' in extraEnv)) delete env.PGDATABASE;
267
+ try {
268
+ const stdout = execFileSync('bash', [path.join(coreDir, 'scripts', 'migrate.sh'), '--print-db'],
269
+ { cwd: coreDir, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
270
+ return { status: 0, stdout: stdout.trim(), stderr: '' };
271
+ } catch (e) {
272
+ return { status: e.status ?? 1, stdout: String(e.stdout || '').trim(), stderr: String(e.stderr || '') };
273
+ }
274
+ }
275
+
276
+ t('--print-db reports the database named by the instance\'s own branding pack', () => {
277
+ const r = printDb(makeInstance({ database: 'acme_instance_db' }));
278
+ assert.equal(r.status, 0, r.stderr);
279
+ assert.equal(r.stdout, 'acme_instance_db');
280
+ });
281
+
282
+ t('an explicit PGDATABASE wins over the pack — same precedence as pool.js', () => {
283
+ const r = printDb(makeInstance({ database: 'from_the_pack' }), { PGDATABASE: 'from_the_env' });
284
+ assert.equal(r.status, 0, r.stderr);
285
+ assert.equal(r.stdout, 'from_the_env');
286
+ });
287
+
288
+ t('an instance that names no database is REFUSED, never defaulted', () => {
289
+ const r = printDb(makeInstance({ database: null }));
290
+ assert.equal(r.status, 2, 'must exit 2 — the seam refuses rather than printing an empty name');
291
+ assert.doesNotMatch(r.stdout, /\S/, 'nothing is printed as the database name');
292
+ assert.match(r.stderr, /cannot determine which database/i);
293
+ assert.match(r.stderr, /Refusing to guess/i);
294
+ // the regression itself: it must not reach for another instance's database
295
+ assert.doesNotMatch(`${r.stdout}${r.stderr}`, /amazonprimea/i);
296
+ });
297
+
298
+ t('--print-migrations stays DB-free — a dry run must not require a database name', () => {
299
+ // the seam walks the real loop and touches no psql, so an instance with no db.database
300
+ // must still be able to list what would be applied
301
+ const core = makeInstance({ database: null });
302
+ const r = printMigrations(core);
303
+ assert.equal(r.status, 0, `a dry run must not need a database: ${r.stderr}`);
304
+ assert.equal(r.listed, 4);
305
+ });
306
+
307
+ t('migrate.sh hardcodes no instance database name', () => {
308
+ // the static lock on the original defect — a literal default reintroduced anywhere in the
309
+ // script points every other instance at a database that is not theirs
310
+ const src = readFileSync(MIGRATE, 'utf8');
311
+ const code = src.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join('\n');
312
+ assert.doesNotMatch(code, /PGDATABASE:-\s*[A-Za-z]/,
313
+ 'PGDATABASE must have no literal fallback — resolve from the branding pack instead');
314
+ assert.doesNotMatch(code, /amazonprimea/i, 'no instance database name in executable lines');
315
+ });
316
+
219
317
  console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
220
318
  process.exit(failed ? 1 : 0);
package/tests/upgrade.mjs CHANGED
@@ -1033,6 +1033,64 @@ t('runUpgrade: a REFUSED bump still reports the non-blocking module findings', a
1033
1033
  assert.match(text, /author unreachable/);
1034
1034
  });
1035
1035
 
1036
+ // ── which database will migrate target? (task 1002755) ────────────────────
1037
+ // The pre-flight probe is only meaningful if it tests the SAME database migrate will use. It used
1038
+ // to be handed process.env.PGDATABASE — undefined on a normal box — so psql fell through to a
1039
+ // database named after the OS user, failed for an unrelated reason, and soft-skipped; migrate then
1040
+ // picked its own name and died on a database that did not exist, AFTER the pin had moved.
1041
+ // The three-way result is the point: "names no database" and "resolver could not run" are
1042
+ // different problems and only the first may block a bump.
1043
+
1044
+ t('resolveInstanceDb: an explicit PGDATABASE wins without spawning anything', () => {
1045
+ const prev = process.env.PGDATABASE;
1046
+ process.env.PGDATABASE = 'from_the_env';
1047
+ try {
1048
+ let spawned = false;
1049
+ const r = u.resolveInstanceDb({ instanceDir: '/x' }, () => { spawned = true; return { status: 0, stdout: 'other' }; });
1050
+ assert.deepEqual(r, { db: 'from_the_env', ran: true });
1051
+ assert.equal(spawned, false, 'the env answer needs no child process');
1052
+ } finally {
1053
+ if (prev === undefined) delete process.env.PGDATABASE; else process.env.PGDATABASE = prev;
1054
+ }
1055
+ });
1056
+
1057
+ t('resolveInstanceDb: reports the name the core resolver printed', () => {
1058
+ const prev = process.env.PGDATABASE;
1059
+ delete process.env.PGDATABASE;
1060
+ try {
1061
+ const r = u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ status: 0, stdout: 'acme_db\n' }));
1062
+ assert.deepEqual(r, { db: 'acme_db', ran: true });
1063
+ } finally { if (prev !== undefined) process.env.PGDATABASE = prev; }
1064
+ });
1065
+
1066
+ t('resolveInstanceDb: ran-but-empty is FATAL, unrunnable is not', () => {
1067
+ const prev = process.env.PGDATABASE;
1068
+ delete process.env.PGDATABASE;
1069
+ try {
1070
+ // the instance genuinely names no database — knowable, and a bump must stop
1071
+ assert.deepEqual(u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ status: 0, stdout: '' })),
1072
+ { db: null, ran: true });
1073
+ // the resolver could not run — a DIFFERENT problem; must not be reported as "no database"
1074
+ assert.deepEqual(u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ status: 1, stdout: '' })),
1075
+ { db: null, ran: false });
1076
+ assert.deepEqual(u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ error: new Error('ENOENT') })),
1077
+ { db: null, ran: false });
1078
+ } finally { if (prev !== undefined) process.env.PGDATABASE = prev; }
1079
+ });
1080
+
1081
+ t('runUpgrade: refuses BEFORE the pin moves when the instance names no database', async () => {
1082
+ const dir = mkdtempSync(join(tmpdir(), 'nodb-'));
1083
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '1.0.0' } }, null, 2));
1084
+ const res = await u.runUpgrade({ to: '1.0.1', instance: dir, registry: true, force: true }, {
1085
+ resolveInstanceDb: () => ({ db: null, ran: true }),
1086
+ log: () => {},
1087
+ });
1088
+ assert.equal(res.ok, false);
1089
+ assert.match(res.error, /cannot determine which database/i);
1090
+ // the pin must be untouched — that is the whole point of checking in pre-flight
1091
+ assert.match(readFileSync(join(dir, 'package.json'), 'utf8'), /"@bongos\/core": "1\.0\.0"/);
1092
+ });
1093
+
1036
1094
  await Promise.all(pending);
1037
1095
  console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
1038
1096
  process.exit(failed ? 1 : 0);