@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 +11 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/migrate.sh +99 -14
- package/src/module-api.js +13 -1
- package/tests/migrate_root_resolution.mjs +124 -1
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.
|
|
6
|
-
"core_contract": "1.18.
|
|
7
|
-
"source_commit": "
|
|
8
|
-
"source_ref": "
|
|
9
|
-
"built_at": "2026-08-
|
|
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": "
|
|
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": "
|
|
3740
|
+
"sha256": "e8bc8b5238e6c8d6fb3c4168cc93e4fcbad6cff43364b52d7e976466de3c235d"
|
|
3741
3741
|
},
|
|
3742
3742
|
{
|
|
3743
3743
|
"path": "package.json",
|
|
3744
3744
|
"mode": "0000644",
|
|
3745
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/scripts/migrate.sh
CHANGED
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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);
|