@bongos/core 1.18.18 → 1.18.19

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.18.18",
6
- "core_contract": "1.18.18",
7
- "source_commit": "f404cbe8964656605d03988362a2172921c6a192",
8
- "source_ref": "f404cbe",
9
- "built_at": "2026-08-03T23:55:06.878Z",
5
+ "core_version": "1.18.19",
6
+ "core_contract": "1.18.19",
7
+ "source_commit": "ccda86b3b4fe68b8a59dd1d70bf73bfec9682642",
8
+ "source_ref": "HEAD",
9
+ "built_at": "2026-08-08T18:02:27.307Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 275,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1437,
20
- "tree_sha256": "720dc8403155a686d4a7da8050c790f784979b66a1524a8f396bdc402a3893f5",
20
+ "tree_sha256": "6e3052888f6a30b9440eeaefee370fa8f296a9345ab9377935c703346d150f3c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -3737,12 +3737,12 @@
3737
3737
  {
3738
3738
  "path": "package-lock.json",
3739
3739
  "mode": "0000644",
3740
- "sha256": "06ff1390b45223f783401dd9eeba77c6020129735324ce536e9dc907a009230a"
3740
+ "sha256": "e8bc8b5238e6c8d6fb3c4168cc93e4fcbad6cff43364b52d7e976466de3c235d"
3741
3741
  },
3742
3742
  {
3743
3743
  "path": "package.json",
3744
3744
  "mode": "0000644",
3745
- "sha256": "d5a5f3f66b15c82b29a0d786fd823b913ba9f86389698a5284dc05af7fc4efcf"
3745
+ "sha256": "04a15921eadb531ab832ada19fd934cd7ffa79ca138cb23832394511a2c8e016"
3746
3746
  },
3747
3747
  {
3748
3748
  "path": "public-docs/index.html",
@@ -5017,7 +5017,7 @@
5017
5017
  {
5018
5018
  "path": "scripts/migrate.sh",
5019
5019
  "mode": "0000644",
5020
- "sha256": "856aaf6ac485e60679309ccbda0d404b74e7e55655ef005583e30231f1f0bce4"
5020
+ "sha256": "28fc9fc9450ca6147d57db0a6f3ab38a5edab5f7dee22ec0340093d854c6c50a"
5021
5021
  },
5022
5022
  {
5023
5023
  "path": "scripts/migrate_cost_ledger.sh",
@@ -5352,7 +5352,7 @@
5352
5352
  {
5353
5353
  "path": "src/module-api.js",
5354
5354
  "mode": "0000644",
5355
- "sha256": "e1ce10a4ed04e5bcc3430cb046150b4fc54644669ae2a7a6ea266bb0434223d6"
5355
+ "sha256": "36c522ca8a10cf57bc03bc87987c8eacea995ab99bf8f6b21842c68ec8389943"
5356
5356
  },
5357
5357
  {
5358
5358
  "path": "src/module-loader/loader.js",
@@ -6467,7 +6467,7 @@
6467
6467
  {
6468
6468
  "path": "tests/migrate_root_resolution.mjs",
6469
6469
  "mode": "0000644",
6470
- "sha256": "7cfb077788e8eac8e40ba60f18a0100e76cfbc2637a9e690e4dc68ba599c2cc3"
6470
+ "sha256": "caf6f21d69cf7ab0521deb40a10450e6f2fc7c3baadcd4602dbfc3edcd224d70"
6471
6471
  },
6472
6472
  {
6473
6473
  "path": "tests/migration_allocation.mjs",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.18.18",
3
+ "version": "1.18.19",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.18.18",
9
+ "version": "1.18.19",
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.18.18",
3
+ "version": "1.18.19",
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",
@@ -51,6 +51,15 @@ fi
51
51
 
52
52
  DB="${PGDATABASE:-amazonprimea}"
53
53
 
54
+ # Test/ops seam (task 1002611): --print-migrations walks the REAL core-migration loop but reports
55
+ # each path instead of applying it, and touches NO DB. tests/migrate_root_resolution.mjs uses it to
56
+ # assert that a checkout whose path contains a space still yields whole, readable paths. Reporting
57
+ # through the real loop (rather than a copy of it in the test) is the point: if the loop ever reverts
58
+ # to an unquoted `for f in $MIGRATIONS`, this seam fragments the paths and the test fails. Also handy
59
+ # in the field — `npm run migrate -- --print-migrations` shows exactly what migrate would apply.
60
+ DRY_RUN=0
61
+ if [ "${1:-}" = "--print-migrations" ]; then DRY_RUN=1; fi
62
+
54
63
  # --- Instance-seed policy (task 1704 / ADR 0105) --------------------------
55
64
  # OTB/product scope-truth (versions, goals, done_when_criteria) lives in seed
56
65
  # migrations under migrations/instance/ (plus a few guarded blocks embedded in the
@@ -81,22 +90,40 @@ echo "instance-seed policy: app.seed_instance_data=$SEED_INSTANCE (migrations/in
81
90
 
82
91
  # Make sure schema_migrations exists before we query it. (001 creates
83
92
  # it; other migrations rely on it.)
84
- psql -d "$DB" -v ON_ERROR_STOP=1 -tAc "
85
- CREATE TABLE IF NOT EXISTS schema_migrations (
86
- version text PRIMARY KEY,
87
- applied_at timestamptz NOT NULL DEFAULT now()
88
- );
89
- " >/dev/null
93
+ if [ "$DRY_RUN" = 0 ]; then
94
+ psql -d "$DB" -v ON_ERROR_STOP=1 -tAc "
95
+ CREATE TABLE IF NOT EXISTS schema_migrations (
96
+ version text PRIMARY KEY,
97
+ applied_at timestamptz NOT NULL DEFAULT now()
98
+ );
99
+ " >/dev/null
90
100
 
91
- APPLIED=$(psql -d "$DB" -tA -c "SELECT version FROM schema_migrations ORDER BY version")
101
+ APPLIED=$(psql -d "$DB" -tA -c "SELECT version FROM schema_migrations ORDER BY version")
102
+ else
103
+ APPLIED=""
104
+ fi
92
105
 
93
106
  # Apply one migration file unless its filename stem is already recorded. Stem (not path) is the
94
107
  # key, so moving a file between migrations/ and migrations/game/ does NOT re-run it on prod.
108
+ #
109
+ # The two counters are what the no-op guard below reads: a run that LISTED core migrations but
110
+ # neither applied nor recognized a single one is the space-in-path failure (task 1002611) and must
111
+ # be fatal, not silent. Both loops that call this run in the CURRENT shell (herestring / plain glob,
112
+ # never a `... | while` pipeline), so these increments are visible to the caller.
113
+ MIGRATIONS_APPLIED=0
114
+ MIGRATIONS_ALREADY=0
95
115
  apply_migration() {
96
116
  local f="$1" version
97
117
  version="$(basename "$f" .sql)"
118
+ if [ "$DRY_RUN" = 1 ]; then
119
+ # Counted as applied so the no-op guard below is exercised by the seam too.
120
+ printf 'would-apply\t%s\n' "$f"
121
+ MIGRATIONS_APPLIED=$((MIGRATIONS_APPLIED + 1))
122
+ return
123
+ fi
98
124
  if echo "$APPLIED" | grep -q "^${version}$"; then
99
125
  echo "skip $version (already applied)"
126
+ MIGRATIONS_ALREADY=$((MIGRATIONS_ALREADY + 1))
100
127
  return
101
128
  fi
102
129
  echo "apply $version"
@@ -108,6 +135,7 @@ apply_migration() {
108
135
  psql -d "$DB" -v ON_ERROR_STOP=1 -tAc \
109
136
  "INSERT INTO schema_migrations (version) VALUES ('${version}') ON CONFLICT DO NOTHING" \
110
137
  >/dev/null
138
+ MIGRATIONS_APPLIED=$((MIGRATIONS_APPLIED + 1))
111
139
  }
112
140
 
113
141
  # --- Forward-safety notice (task 1002223 / audit H10) ---------------------
@@ -116,7 +144,7 @@ apply_migration() {
116
144
  # UNACKNOWLEDGED forward-unsafe core migration so an operator knows a rollback past it would strand
117
145
  # old code on new schema. Fail-closed enforcement is the unit gate (tests/migrations_forward_safe.mjs);
118
146
  # here it is advisory (`|| true`) so a lint hiccup can NEVER break a deploy.
119
- if [ -f "$CORE_ROOT/scripts/gds/lint-migrations.js" ]; then
147
+ if [ "$DRY_RUN" = 0 ] && [ -f "$CORE_ROOT/scripts/gds/lint-migrations.js" ]; then
120
148
  node "$CORE_ROOT/scripts/gds/lint-migrations.js" --warn --dir "$CORE_ROOT/migrations" || true
121
149
  fi
122
150
 
@@ -131,14 +159,66 @@ fi
131
159
  # are included only when SEED_INSTANCE=on; stem-keyed tracking means a file's directory never
132
160
  # affects whether it re-runs on OTB prod. Today CORE_ROOT == INSTANCE_ROOT (single checkout);
133
161
  # once the core installs from node_modules the two roots diverge and this still merges correctly.
162
+ #
163
+ # PATHS WITH SPACES (task 1002611): this list is NEWLINE-separated and must be read a line at a
164
+ # time. The old `for f in $MIGRATIONS` word-split it on IFS — which includes space — so any checkout
165
+ # whose absolute path contains a space ("/Users/.../Hermeslines Marketing/...") fragmented every
166
+ # path, the `[ -e ]` guard below silently dropped all of them, and ZERO core migrations ran.
167
+ # Reproduced on 1.18.18: 117 files listed, 234 loop iterations, 234 -e failures, schema_migrations
168
+ # empty. The module loop further down is correctly quoted, so module migrations then ran against an
169
+ # EMPTY schema and reported a bogus `relation "achievements" does not exist` from modules/economy —
170
+ # `achievements` is created by CORE 006_pms_v2.sql, which never ran. Hence the guard after the loop.
171
+ # `<<<` (bash — see the shebang) keeps the loop in the CURRENT shell, so the counters survive it; a
172
+ # `... | while read` pipeline would both subshell the counters away and interact badly with
173
+ # `set -euo pipefail`. `|| true` on each `ls` keeps an empty glob from tripping pipefail.
134
174
  MIGRATIONS=$( {
135
- ls "$CORE_ROOT"/migrations/*.sql 2>/dev/null
136
- if [ "$SEED_INSTANCE" = "on" ]; then ls "$INSTANCE_ROOT"/migrations/instance/*.sql 2>/dev/null; fi
175
+ ls "$CORE_ROOT"/migrations/*.sql 2>/dev/null || true
176
+ if [ "$SEED_INSTANCE" = "on" ]; then ls "$INSTANCE_ROOT"/migrations/instance/*.sql 2>/dev/null || true; fi
137
177
  } | awk -F/ '{ print $NF "\t" $0 }' | sort | cut -f2- )
138
- for f in $MIGRATIONS; do
139
- [ -e "$f" ] || continue
178
+ migrations_listed=0
179
+ migrations_unreadable=0
180
+ before_applied=$MIGRATIONS_APPLIED
181
+ before_already=$MIGRATIONS_ALREADY
182
+ while IFS= read -r f; do
183
+ # An empty $MIGRATIONS still yields one empty line through `<<<`, so skip blanks before -e.
184
+ [ -n "$f" ] || continue
185
+ migrations_listed=$((migrations_listed + 1))
186
+ if [ ! -e "$f" ]; then
187
+ migrations_unreadable=$((migrations_unreadable + 1))
188
+ echo "migrate.sh: listed migration is not a readable file, skipping: $f" >&2
189
+ continue
190
+ fi
140
191
  apply_migration "$f"
141
- done
192
+ done <<< "$MIGRATIONS"
193
+
194
+ # --- No-op guard (task 1002611) -------------------------------------------
195
+ # Silence is what made the space-in-path bug expensive to diagnose: core migrations were skipped
196
+ # without a word, and the first error the operator saw came from an unrelated MODULE migration
197
+ # hitting the empty schema. If the glob LISTED core migrations but we neither applied nor recognized
198
+ # a single one, the list itself is broken — stop here rather than let the module loop below produce a
199
+ # misleading error about a table that a core migration was supposed to create.
200
+ core_applied=$((MIGRATIONS_APPLIED - before_applied))
201
+ core_already=$((MIGRATIONS_ALREADY - before_already))
202
+ if [ "$migrations_listed" -gt 0 ] && [ "$core_applied" -eq 0 ] && [ "$core_already" -eq 0 ]; then
203
+ {
204
+ echo
205
+ echo "migrate.sh: FATAL — listed $migrations_listed core migration(s) but applied 0 and found 0 already applied."
206
+ echo " $migrations_unreadable of the $migrations_listed listed path(s) were not readable files."
207
+ echo " CORE_ROOT=$CORE_ROOT"
208
+ echo " Refusing to continue to module migrations: they would run against an empty schema and"
209
+ echo " fail with a misleading error (e.g. relation \"achievements\" does not exist) from a module,"
210
+ echo " when the real cause is that no core migration ran."
211
+ echo " Run 'npm run migrate -- --print-roots' to see the roots migrate resolved."
212
+ } >&2
213
+ exit 1
214
+ fi
215
+
216
+ # The --print-migrations seam stops here: the core list is what it verifies, and going further would
217
+ # need a DB. The summary line is the machine-readable assertion target.
218
+ if [ "$DRY_RUN" = 1 ]; then
219
+ printf 'listed=%d unreadable=%d\n' "$migrations_listed" "$migrations_unreadable"
220
+ exit 0
221
+ fi
142
222
  [ "$SEED_INSTANCE" = "on" ] || echo "instance seeds skipped (migrations/instance/) — non-owning / vanilla instance"
143
223
 
144
224
  # Module-owned migrations (modules/<key>/migrations/*.sql) — applied only when the module is
@@ -208,7 +288,12 @@ if [ "${OTB_POST_DEPLOY_CHECK:-0}" = "1" ]; then
208
288
  BASELINE_FLAG="--baseline-only"
209
289
  echo "post-deploy: no prior score ledger — running in baseline-only mode"
210
290
  fi
211
- if ! python3 modules/art-pipeline/pipeline/post_deploy_check.py $BASELINE_FLAG; then
291
+ # `${VAR:+"$VAR"}` expands to NOTHING when empty and to the quoted value otherwise. A bare
292
+ # $BASELINE_FLAG relied on word-splitting to vanish (the habit that caused task 1002611), and
293
+ # quoting it outright would instead pass an empty argv entry that argparse rejects. An array
294
+ # would need bash 4 — `set -u` + `"${arr[@]}"` on an empty array aborts under the bash 3.2 that
295
+ # macOS still ships, and this script's shebang takes whatever bash is on PATH.
296
+ if ! python3 modules/art-pipeline/pipeline/post_deploy_check.py ${BASELINE_FLAG:+"$BASELINE_FLAG"}; then
212
297
  echo "post-deploy: whole-world check FAILED — see output above" >&2
213
298
  exit 1
214
299
  fi
package/src/module-api.js CHANGED
@@ -348,8 +348,20 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
348
348
  // precisely because nothing compared live against main. The doorway surface
349
349
  // is byte-identical to 1.18.17 (governance is reached through the kernel
350
350
  // port, not a new export), so despite the size this is → PATCH. (task 1002521)
351
+ // 1.18.19 — SINGLE-FIX patch cut from the core-v1.18.18 tag, not from main: core
352
+ // migrations were silently skipped on any checkout whose absolute path
353
+ // contains a space. `for f in $MIGRATIONS` word-split the newline-separated
354
+ // list on IFS, fragmenting every path; the `[ -e "$f" ]` guard then dropped
355
+ // all of them and schema_migrations stayed empty. The module loop is quoted,
356
+ // so module migrations ran against an empty schema and the operator saw a
357
+ // misleading `relation "achievements" does not exist` from modules/economy
358
+ // instead of the real cause. Now read with a newline-safe herestring, and a
359
+ // no-op guard makes the skip FATAL rather than silent. No doorway change, no
360
+ // schema change, one script + its test → PATCH. Deliberately does NOT carry
361
+ // the unreleased 1.19 tree on main, so a blocked instance can take the fix
362
+ // alone. (task 1002611)
351
363
  // ---------------------------------------------------------------------------
352
- const CORE_VERSION = '1.18.18'; // go-live carrier for 171 unreleased commits + the deploy-divergence check (task 1002521); see changelog above
364
+ const CORE_VERSION = '1.18.19'; // single-fix patch off the 1.18.18 tag: migrations skipped on a path with a space (task 1002611); see changelog above
353
365
 
354
366
  // A namespaced logger so a module's log lines are attributable + consistent.
355
367
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -17,7 +17,9 @@
17
17
 
18
18
  import { strict as assert } from 'node:assert';
19
19
  import { execFileSync } from 'node:child_process';
20
- import { mkdtempSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
20
+ import {
21
+ mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, symlinkSync, realpathSync,
22
+ } from 'node:fs';
21
23
  import { tmpdir } from 'node:os';
22
24
  import { fileURLToPath } from 'node:url';
23
25
  import path from 'node:path';
@@ -93,5 +95,126 @@ t('CORE_ROOT is always the script-relative repo root, independent of the invocat
93
95
  assert.equal(realpathSync(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
94
96
  });
95
97
 
98
+ // ==========================================================================
99
+ // A CORE ROOT WHOSE PATH CONTAINS A SPACE (task 1002611)
100
+ // ==========================================================================
101
+ // `for f in $MIGRATIONS` word-split the newline-separated migration list on IFS — which includes
102
+ // space — so a checkout at "/Users/.../Hermeslines Marketing/..." fragmented every path, the
103
+ // `[ -e "$f" ]` guard dropped all of them, and ZERO core migrations ran. Reproduced on
104
+ // @bongos/core@1.18.18: 117 files listed, 234 iterations, 234 -e failures, schema_migrations empty.
105
+ //
106
+ // These exercise migrate.sh's `--print-migrations` seam, which walks the REAL loop and reports each
107
+ // path instead of applying it (DB-free, like --print-roots). Reporting through the real loop is the
108
+ // point: a copy of the loop inside this test would not catch a revert of the loop in the script.
109
+
110
+ // A synthetic CORE root: `scripts/migrate.sh` SYMLINKED to the real script (so the fixture can never
111
+ // drift from it), a stub instance-config that names the fixture as both roots, and n migration files.
112
+ // `spaced` puts a space in the fixture's own directory name — the whole point of the fixture.
113
+ function makeCore({ spaced, files = ['001_a.sql', '002_b.sql', '006_pms_v2.sql', '019_c.sql'] } = {}) {
114
+ const prefix = spaced ? 'r89 spaced core-' : 'r89-plain-core-';
115
+ const dir = realpathSync(mkdtempSync(path.join(tmpdir(), prefix)));
116
+ mkdirSync(path.join(dir, 'scripts'));
117
+ mkdirSync(path.join(dir, 'migrations'));
118
+ mkdirSync(path.join(dir, 'src'));
119
+ symlinkSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
120
+ // Pin both roots to the fixture explicitly, so the test does not lean on migrate.sh's
121
+ // node-unavailable fallback to decide what CORE_ROOT is.
122
+ writeFileSync(path.join(dir, 'src', 'instance-config.js'), [
123
+ "const path = require('node:path');",
124
+ "const ROOT = path.resolve(__dirname, '..');",
125
+ 'module.exports = {',
126
+ ' resolveCoreRoot: () => ROOT,',
127
+ ' resolveInstanceRootExplicit: () => ROOT,',
128
+ '};',
129
+ '',
130
+ ].join('\n'));
131
+ for (const f of files) writeFileSync(path.join(dir, 'migrations', f), '-- probe\n');
132
+ return dir;
133
+ }
134
+
135
+ // Run `<core>/scripts/migrate.sh --print-migrations` and parse the seam's output.
136
+ // Returns { applied: [paths], listed, unreadable, status, stderr }.
137
+ function printMigrations(coreDir, { script = 'migrate.sh' } = {}) {
138
+ const env = { ...process.env };
139
+ for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
140
+ delete env.INIT_CWD;
141
+ let stdout = '';
142
+ let stderr = '';
143
+ let status = 0;
144
+ try {
145
+ stdout = execFileSync('bash', [path.join(coreDir, 'scripts', script), '--print-migrations'],
146
+ { cwd: coreDir, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
147
+ } catch (e) {
148
+ status = e.status ?? 1;
149
+ stdout = e.stdout ?? '';
150
+ stderr = e.stderr ?? '';
151
+ }
152
+ const applied = [];
153
+ let listed = null;
154
+ let unreadable = null;
155
+ for (const line of stdout.split('\n')) {
156
+ const a = /^would-apply\t(.*)$/.exec(line);
157
+ if (a) { applied.push(a[1]); continue; }
158
+ const s = /^listed=(\d+) unreadable=(\d+)$/.exec(line);
159
+ if (s) { listed = Number(s[1]); unreadable = Number(s[2]); }
160
+ }
161
+ return { applied, listed, unreadable, status, stderr, stdout };
162
+ }
163
+
164
+ t('core root containing a space: every migration is listed as ONE whole, readable path', () => {
165
+ const core = makeCore({ spaced: true });
166
+ assert.ok(core.includes(' '), 'fixture path must contain a space to be meaningful');
167
+ const r = printMigrations(core);
168
+ assert.equal(r.status, 0, `migrate.sh --print-migrations must succeed; stderr:\n${r.stderr}`);
169
+ assert.equal(r.listed, 4, 'all four migrations must be listed');
170
+ assert.equal(r.unreadable, 0, 'no listed path may fail the -e test (fragmentation)');
171
+ assert.equal(r.applied.length, 4, 'all four must reach apply_migration');
172
+ for (const f of r.applied) {
173
+ assert.ok(existsSync(f), `listed path must exist on disk, got fragment: ${f}`);
174
+ assert.ok(f.startsWith(core), `listed path must be whole and under the core root: ${f}`);
175
+ }
176
+ });
177
+
178
+ t('core root containing a space: applies in basename order, unaffected by the space', () => {
179
+ const core = makeCore({ spaced: true });
180
+ const r = printMigrations(core);
181
+ assert.deepEqual(
182
+ r.applied.map((f) => path.basename(f)),
183
+ ['001_a.sql', '002_b.sql', '006_pms_v2.sql', '019_c.sql'],
184
+ 'the merge-and-sort-by-basename order must survive a spaced path',
185
+ );
186
+ });
187
+
188
+ t('a space-free core root behaves identically (the fixture itself is not what passes)', () => {
189
+ const spaced = printMigrations(makeCore({ spaced: true }));
190
+ const plain = printMigrations(makeCore({ spaced: false }));
191
+ assert.equal(plain.status, spaced.status);
192
+ assert.equal(plain.listed, spaced.listed);
193
+ assert.equal(plain.unreadable, spaced.unreadable);
194
+ assert.deepEqual(plain.applied.map((f) => path.basename(f)), spaced.applied.map((f) => path.basename(f)));
195
+ });
196
+
197
+ // The no-op guard, proven against the ACTUAL pre-fix code. Re-introducing the unquoted loop into a
198
+ // copy of the script must now FAIL LOUDLY rather than skip every core migration in silence and let
199
+ // the module loop below report a misleading `relation "achievements" does not exist`.
200
+ t('the pre-fix unquoted loop now fails loudly instead of silently skipping', () => {
201
+ const core = makeCore({ spaced: true });
202
+ const src = readFileSync(MIGRATE, 'utf8');
203
+ const mutant = src
204
+ .replace('while IFS= read -r f; do', 'for f in $MIGRATIONS; do')
205
+ .replace('done <<< "$MIGRATIONS"', 'done');
206
+ assert.notEqual(mutant, src, 'the mutation must apply — has the core loop been reshaped?');
207
+ assert.ok(mutant.includes('for f in $MIGRATIONS'), 'mutant must carry the unquoted loop');
208
+ writeFileSync(path.join(core, 'scripts', 'mutant.sh'), mutant);
209
+
210
+ const r = printMigrations(core, { script: 'mutant.sh' });
211
+ assert.notEqual(r.status, 0, 'the unquoted loop on a spaced path must NOT exit 0');
212
+ assert.match(r.stderr, /FATAL/, 'the failure must be loud');
213
+ assert.match(r.stderr, /applied 0 and found 0 already applied/, 'it must name the real cause');
214
+ assert.match(r.stderr, /Refusing to continue to module migrations/,
215
+ 'it must say it stopped before the module loop — the misleading-error path');
216
+ assert.equal(r.applied.length, 0, 'the unquoted loop applies nothing (that was the bug)');
217
+ });
218
+
96
219
  console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
97
220
  process.exit(failed ? 1 : 0);