Uh oh!
There was an error while loading. Please reload this page.
test(metadata-protocol): derive each live-MySQL suite's database from its own file, and enforce it repo-wide - #10915
Conversation
… its own file The two live-MySQL migration suites named their database with a hard-coded constant (os_metadata_protocol_9381 / _9434). They were distinct only because two authors typed two different strings, and each issues `drop database` in afterAll — so a third file copy-pasted from either would drop the database a running sibling is mid-test in. currentLiveMysqlDatabase() takes no argument, so there is nothing a copy-paste can carry over. Same derivation as #9350's driver-sql resolver (os_lv_ prefix, 34-char slug, 12 hex of sha256 over the workspace-relative path), which makes the two independent copies jointly injective on the one MySQL server CI provisions for both legs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
#9350's enforcement of per-file live database isolation is a source scan inside packages/drivers/driver-sql, so it was correctly green while two live suites in packages/metadata-protocol named their databases with hand-typed constants. A per-package scan cannot see the package that has not been written yet. check:live-db-isolation finds the live files itself — they are the ones reading OS_TEST_*_URL — and fails any whose database name reaches its DDL from a literal or from an identifier initialised with one. It reds on the pre-fix tree, which makes it the one control in this change that has a real defect run behind it. It deliberately does not check distinctness: that needs the derivation actually run, and lives in the two packages' own isolation suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
check:entry-guard caught this on the new file's first union run: it exports its detector so --self-test drives the real functions, which means it can be imported FOR those exports — and an unguarded top-level dispatch would then run the whole scan, and its process.exit, inside the importer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 028e6a15564a5a1eb444041201ea650dc6d64f18 && git checkout 028e6a15564a5a1eb444041201ea650dc6d64f18
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 62ba73d43fe20552d4d6d0bedd5af2d2c975045c && git checkout -B drift-repro 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 && git merge --no-ff 62ba73d43fe20552d4d6d0bedd5af2d2c975045c
node scripts/docs-audit/affected-docs.mjs --json 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32508203220 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10382
The card's central claim is false, and was false when it was filed
The card says the two live-MySQL suites in
packages/metadata-protocolThey do not, and never did. Both have created and
use-d their own database since the day each landed —ad217b1928andf6c904a18b, both 2026-08-18, two days before the card was filed:So "they can collide with each other" was not true either. What is true is the half the card's acceptance criterion actually names: those databases were hard-coded constants, distinct only because two authors happened to type two different strings. The property #9350 established for
packages/drivers/driver-sql— a live file's database derives from the file, not from a shared constant — did not hold here, and nothing anywhere would have gone red if a third file had copied one of the constants.That residual is what this PR closes.
The blocker the card called decisive is also stale, and was tested rather than accepted
The card's reason 3 was "a live MySQL cannot be run in an agent container … the change would be unverifiable except through CI". A MariaDB 10.11.14 was raised in this container instead (the recipe #10633 recorded, and #10836 documents), configured with MySQL 8's default
sql_mode, and every measurement below is from a real server:What the server covers here, and what it does not. Carrying #10633's caveat: MariaDB is a MySQL-protocol stand-in, not MySQL 8.0, and must never be used to assert MySQL error wording. This change is
create database if not exists+use— database creation and selection — which is exactly the class the stand-in is sound for. The suites' own MySQL-wording assertions (ER_PARSE_ERRORon the barekeyprobe, theCREATE INDEXrefusal) ran and passed here too, but this PR does not rely on them; they are CI's job on the real MySQL 8.What changed
live-mysql-database.testkit.ts(new) —currentLiveMysqlDatabase(), seeded from vitest's owntestPath. It takes no argument on purpose: there is then no parameter a copy-paste can carry over from the file it was copied from.live-mysql-database.isolation.test.ts(new) — the structural assertion the card asks for: distinct, file-derived, deterministic, identifier-safe, measured over the real on-disk file list rather than invented inputs.scripts/check-live-db-isolation.mjs(new) +check:live-db-isolationwired intolint.yml— the repo-wide enforcement sibling.Why a sibling resolver rather than exporting #9350's
A shared export would be better if it were reachable. It is not, for three independent reasons:
live-dialect-matrix.testkit.tsis not on@objectstack/driver-sql's public surface — the package exports.only,index.tsdoes not re-export it, and its own header says "Test-only: not exported fromindex.ts." Reaching it means publishing vitest-dependent test scaffolding to npm consumers, or importing another package'ssrc/— the thingcheck:cross-package-test-inputspolices.@objectstack/metadata-protocoldoes not depend on@objectstack/driver-sqlat all. Adding it, even as a devDependency, drags knex,SqlDriverand that package's whole build into this one's test closure to obtain a pure string function, and inverts the layering.DialectCellcarries knex configs,readServerZonetakes aSqlDriver, andliveSchemaLedger()hard-codes driver-sql's own directory.The copy is deliberately the same derivation — same
os_lv_prefix, same 34-character slug cap, same 12 hex of sha256 over the same key (the workspace-relative path). That is what makes the two independent copies jointly injective on the one MySQL server CI provisions for both legs: two live files can only collide by sharing a repo-relative path. Drift between the copies is what the new repo-wide gate watches.use, not the connection URL — the opposite of driver-sql's choice, deliberatelydriver-sql names the database in the connection and explains at length why
usewas wrong there: knex'sclient.database()keeps returning the URL's database and binds that intocolumnInfo. None of that mechanism exists here — these files hold a rawmysql2connection and nothing readsconnection.config.database.useis kept because it is what the files already do and what the card asked for; the reason it is safe is the absence of knex, not a disagreement with #9350.Evidence — this is an anti-vacuity card, so the controls are the point
A green live run proves nothing here, measured rather than asserted. On the pre-change tree the two suites are 10/10 green on the live server. They are also 10/10 green with the property deliberately broken (both constants pointed at one name that is not the URL's database) — four runs. The server's general query log says why: the two files never overlap, under default settings and under an explicit
--maxWorkers=2 --fileParallelismalike.Each file's whole run is ~150 ms while a second fork takes ~1 s to boot. This prediction missed — a collision was expected and did not occur — and the miss is reported rather than quietly corrected, because it strengthens the card's own argument: nothing observable distinguishes the broken configuration from the correct one.
The three controls, each named for what kind it is:
live-mysql-database.isolation.test.ts'conformance'reds it:expected 1 to be greater than or equal to 3Error: Unknown database 'conformance'— the first suite'safterAlldrops the database the URL points at, and the next handshake failsscripts/check-live-db-isolation.mjsseed-tenancy-backfill.live-mysql.test.ts:80 create database interpolates DB, which is the constant 'os_metadata_protocol_9381'The second row is why the change is worth landing despite the first: the hazard is dormant, not absent, and every input deciding which form you get is incidental — file count in the
live-mysqlfilter, runner CPU count, vitest'sfileParallelismdefault, and which string an author typed.Non-effects, predicted before each run and then compared
metadata-protocolmust not move. Held exactly: 130 files / 1788 tests without the new file, 131 / 1797 with it — a delta of +1 file and +9 tests, which is exactly the 9it()blocks added.check:entry-guardcaught the new script exporting its detector while running on import, and it is fixed in62ba73d43frather than exempted.src/dist reasoning, in both directions
The ablation target is reached by a relative import inside the package, so no rebuild is involved — and that is proven, not assumed.
metadata-protocolhas novitest.config.*, so it aliases nothing to source; its cross-package deps go throughdist(itsKNOWN_UNALIASED_TEST_IMPORTSentry), while intra-package relative imports resolve tosrc. The testkit is unreachable fromindex.tsand therefore absent from the build:Restoration after every mutation was proven on disk, not assumed:
git hash-objectequal togit rev-parse HEAD:PATHfor both files,git diff --exit-code0,git status --porcelainempty.Gates
Union derived with
node scripts/pm/dispatch-gates.mjs(no path arguments) after the final commit and re-run at62ba73d43f— 21 matched families plus the convention-triggered ones, every exit captured before any pipe. All green. The two that could have moved a ratchet:Suites at the same sha:
metadata-protocol131 passed | 2 skipped (1797 tests) with no server; 3 passed / 19 tests against the live MariaDB.Changeset
None, deliberately — this PR publishes nothing. It touches two test files, two new test-support files, a repo gate script, the root
package.jsonscripts block, andlint.yml. The new testkit is unreachable fromsrc/index.tsand absent fromdist(proven above), so no published artifact changes. Declared with theskip-changesetlabel.Generated by Claude Code