Skip to content

test(metadata-protocol): derive each live-MySQL suite's database from its own file, and enforce it repo-wide - #10915

Merged
os-elon merged 3 commits into
mainfrom
claude/issue-10382-live-mysql-per-file-db
Aug 21, 2026
Merged

test(metadata-protocol): derive each live-MySQL suite's database from its own file, and enforce it repo-wide#10915
os-elon merged 3 commits into
mainfrom
claude/issue-10382-live-mysql-per-file-db

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

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-protocol

land in the conformance database the CI job provisions — the same one, as each other

They do not, and never did. Both have created and use-d their own database since the day each landed — ad217b1928 and f6c904a18b, both 2026-08-18, two days before the card was filed:

const DB = 'os_metadata_protocol_9381'; // seed-tenancy-backfill.live-mysql.test.ts
const DB = 'os_metadata_protocol_9434'; // sys-setting-identity-index.live-mysql.test.ts

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:

[#9381] live MySQL 10.11.14-MariaDB-0ubuntu0.24.04.1
sql_mode=IGNORE_SPACE,ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,
NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

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_ERROR on the bare key probe, the CREATE INDEX refusal) 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 own testPath. It takes no argument on purpose: there is then no parameter a copy-paste can carry over from the file it was copied from.
  • The two live suites now call it instead of declaring a constant.
  • 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-isolation wired into lint.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:

  1. live-dialect-matrix.testkit.ts is not on @objectstack/driver-sql's public surface — the package exports . only, index.ts does not re-export it, and its own header says "Test-only: not exported from index.ts." Reaching it means publishing vitest-dependent test scaffolding to npm consumers, or importing another package's src/ — the thing check:cross-package-test-inputs polices.
  2. @objectstack/metadata-protocol does not depend on @objectstack/driver-sql at all. Adding it, even as a devDependency, drags knex, SqlDriver and that package's whole build into this one's test closure to obtain a pure string function, and inverts the layering.
  3. Most of that testkit is not this function: DialectCell carries knex configs, readServerZone takes a SqlDriver, and liveSchemaLedger() 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, deliberately

driver-sql names the database in the connection and explains at length why use was wrong there: knex's client.database() keeps returning the URL's database and binds that into columnInfo. None of that mechanism exists here — these files hold a raw mysql2 connection and nothing reads connection.config.database. use is 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 --fileParallelism alike.

16:32:34 40 Connect … 40 Query CREATE DATABASE IF NOT EXISTS `…`
41 Connect … 41 Query USE `…` -> DROP DATABASE IF EXISTS `…`
16:32:35 42 Connect … 42 Query CREATE DATABASE IF NOT EXISTS `…`
43 Connect … 43 Query USE `…` -> DROP DATABASE IF EXISTS `…`

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:

controlkindmeasured
live-mysql-database.isolation.test.tsablation — its subject does not exist before the change, so no pre-fix red run can existreplacing both calls with the literal 'conformance' reds it: expected 1 to be greater than or equal to 3
both suites pointed at the URL's own databasedefect control for the hazard, on a deliberately broken treehard red, deterministic, no concurrency needed: Error: Unknown database 'conformance' — the first suite's afterAll drops the database the URL points at, and the next handshake fails
scripts/check-live-db-isolation.mjsdefect control on the real pre-fix tree — it reads source, and the constants were there to readexit 1, naming all six sites, e.g. seed-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-mysql filter, runner CPU count, vitest's fileParallelism default, and which string an author typed.

Non-effects, predicted before each run and then compared

  • The rest of metadata-protocol must 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 9 it() blocks added.
  • driver-sql's own isolation scan must not be affected. Held: 17 passed | 3 skipped, unchanged; nothing in that package changed and its scan enumerates only its own directory.
  • No other gate's verdict moves. Held, except one that should have and did: check:entry-guard caught the new script exporting its detector while running on import, and it is fixed in 62ba73d43f rather 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-protocol has no vitest.config.*, so it aliases nothing to source; its cross-package deps go through dist (its KNOWN_UNALIASED_TEST_IMPORTS entry), while intra-package relative imports resolve to src. The testkit is unreachable from index.ts and therefore absent from the build:

$ node scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol 'currentLiveMysqlDatabase' --absent
✓ marker absent from all 22 built files

Restoration after every mutation was proven on disk, not assumed: git hash-object equal to git rev-parse HEAD:PATH for both files, git diff --exit-code 0, git status --porcelain empty.

Gates

Union derived with node scripts/pm/dispatch-gates.mjs (no path arguments) after the final commit and re-run at 62ba73d43f — 21 matched families plus the convention-triggered ones, every exit captured before any pipe. All green. The two that could have moved a ratchet:

check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 248.8s,
1908 raw tsc error(s) total, none above its recorded number.
check:entry-guard: 130 scripts/ file(s) — every entry guard goes through invoked-as.mjs;
88 export bindings, 78 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).

Suites at the same sha: metadata-protocol 131 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.json scripts block, and lint.yml. The new testkit is unreachable from src/index.ts and absent from dist (proven above), so no published artifact changes. Declared with the skip-changeset label.


Generated by Claude Code

… 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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 58563be069a70d09c4d9cc5f522bb27c5f8a78b2packageMentionDocs.

Which tree this was computed on

This run read content/docs from 028e6a15564a5a1eb444041201ea650dc6d64f18 — the merge of head 62ba73d43fe20552d4d6d0bedd5af2d2c975045c into base 58563be069a70d09c4d9cc5f522bb27c5f8a78b2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 21, 2026
@os-elonos-elon added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed ci/cd dependencies Pull requests that update a dependency file tests labels Aug 21, 2026 — with Claude
@os-elon
os-elon marked this pull request as ready for review August 21, 2026 17:19
@os-elon
os-elon enabled auto-merge August 21, 2026 17:19
@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32508203220 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 4.84s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit ee09d21Aug 21, 2026
45 checks passed
@os-elon
os-elon deleted the claude/issue-10382-live-mysql-per-file-db branch August 21, 2026 17:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-elon@claude