Skip to content

test(metadata-protocol): execute the read-probe PostgreSQL arm on a live server (#17621) - #17890

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-17621-metadata-protocol-live-pg
Sep 12, 2026
Merged

test(metadata-protocol): execute the read-probe PostgreSQL arm on a live server (#17621)#17890
os-musk merged 3 commits into
mainfrom
claude/issue-17621-metadata-protocol-live-pg

Conversation

@os-musk

@os-musk os-musk commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17621

Clause-②: no

#17175 compiled one non-raising catalog presence arm per dialect family in packages/metadata-protocol/src/migrations/read-probe.ts and executed two of the three: SQLite end to end through a real SqlDriver, MySQL on the live server. The PostgreSQL arm — SELECT 1 WHERE to_regclass('"TABLE"') IS NOT NULL — was pinned character-for-character against all four knex client spellings and run nowhere: this package had no live-PG harness, no pg dependency, and its CI step supplied OS_TEST_MYSQL_URL alone while filtering vitest to live-mysql.

A text pin cannot close that gap. The failure this module is fenced against is an arm mis-compiled for one dialect: it raises, the catch that exists for the expected miss swallows it, and a stored-row data repair silently becomes a no-op. Whether to_regclass answers zero rows rather than raising is a claim about PostgreSQL, not about this repo's string concatenation.

The three changes

# change note
1 pg devDependency on packages/metadata-protocol a lockfile move — measured below
2 src/migrations/seed-tenancy-backfill.live-postgres.test.ts sibling of the live-MySQL file, in its own derived schema
3 OS_TEST_POSTGRES_URL + a second vitest substring on that CI step wiring: the postgres:16 service is already up in that job

Two riders, both consequences of the above rather than scope:

  • @types/pg devDependency — pg ships no typings, and this package's typecheck target reaches every *.test.ts by rule, so without it tsc --noEmit fails TS7016. Measured: it adds exactly one lockfile package entry; its three dependencies (@types/node, pg-protocol, pg-types) were already resolved. Dev-only, types-only, no runtime code.
  • read-probe.test.ts's docblock recorded the Postgres arm as "NOT MEASURED against a live server anywhere", naming the three reasons this PR removes. That sentence is now false, so it names where each arm runs instead.

Was (1) only a lockfile move? — triage's stop condition

Yes. Adding pg: ^8.22.0 and re-running pnpm install produced a three-line lockfile diff, and the three lines are the importer entry:

+      pg:
+        specifier: ^8.22.0
+        version: 8.22.0

Zero new packages, zero new transitives: pg@8.22.0 was already resolved for packages/drivers/driver-sql, which declares it as an optional peerDependency (pg, mysql2, tedious under peerDependenciesMeta.optional). The stop condition as triage set it — pg pulling something unexpected into the install graph — did not fire. The one package that is new is @types/pg, named above; it is not pulled by pg, it is required by change (2) plus the repo's own type-check-coverage rule, and it is reported here rather than folded in silently.

Changeset: patch, measured — this falsifies the dispatching seat's expectation

The seat's expectation was skip-changeset, explicitly flagged as an expectation and not a reading. Taken:

  • dist/ is byte-untouched. Built the package, grepped dist/ for this change's markers (os17621_case, live-postgres, toPgPlaceholders): zero hits, against a positive control (readTablePresence) that hits dist/index.js and dist/index.cjs. Built entry typings are byte-identical across the change — dist/index.d.ts sha256 bace0e9e373ceef9f8150582b7e9d8ff3414d7358311c82058c005f3135c0360 from a build of BASE sources and from a build of the branch alike.
  • But the published tarball does change. npm pack ships 27 files, and package.json is one of them (1.9 kB). The packed manifest now carries "pg": "^8.22.0" and "@types/pg": "^8.15.6" under devDependencies — verified by extracting package/package.json out of the real tarball.

AGENTS.md: skip-changeset is for a diff that publishes nothing from any released package. @objectstack/metadata-protocol is released at 17.4.0, and this diff publishes two manifest lines. That is the same standard the repo's own precedents apply — trigger-schedule's repository.directory correction took a changeset because "the value ships inside the tarball", and driver-sql graded a test-only PR patch rather than skip-changeset. So: patch, ⛔ not skip-changeset, and Check Changeset should report success, not skipped.

The point of the card: the arm is EXECUTED, in both directions

⛔ No --project narrowing anywhere — the package declares no vitest projects at all (no projects key, no workspace config), so there is no project filter to drop a named file into.

Against a live PostgreSQL 16.13 (OS_TEST_POSTGRES_URL + OS_EXPECT_LIVE_DIALECT_MATRIX=1) — 7 of 7 executed, by name:

✓ is pointed at a real PostgreSQL, in this file's own schema — without this the run proves nothing
✓ every statement the migration builds PARSES and runs on PostgreSQL
✓ [#17621] the catalog presence probe ANSWERS on PostgreSQL — both directions, on the live server
✓ [#17621] the scope is the SEARCH PATH — a same-named relation off it is not this one
✓ [#17621] the arm's QUOTED argument matches exactly — a case-folded neighbour is not this table
✓ repairs the split end to end, and reports the already-minted duplicates
✓ is idempotent — a second run finds no split

Test Files  1 passed (1)      Tests  7 passed (7)
[#17621] live PostgreSQL 16.13 ... search_path=os_lv_seed_tenancy_backfill_live_postgre_ae7a6c870c26

Without a URL the same file skips — reported as skipped, never as passed:

Test Files  1 skipped (1)      Tests  7 skipped (7)      exit 0

With OS_EXPECT_LIVE_DIALECT_MATRIX=1 and no URL it is a named red, so a dropped env: line cannot return the seam to zero coverage quietly:

× #17621 live PostgreSQL > OS_TEST_POSTGRES_URL must be set — this runner declared it provisioned a server
Test Files  1 failed (1)      Tests  1 failed | 7 skipped (8)      exit 1

Ablation — the suite can fail

Committed the fix first, then re-compiled the Postgres arm to MySQL's DATABASE() form in read-probe.ts. On-disk proof before believing the run: anchor to_regclass 3 occurrences → 2, injected DATABASE() 2 → 3, blob hash fd66ad50…86ea33de…. No rebuild was needed or performed, and that is itself the resolution evidence: the subject is same-package ./read-probe.js, which vitest resolves to src/, so a stale dist cannot manufacture a false green here the way it can for a dependency resolved through exports.

Result: 6 of the 7 cases go red, reporting

expected { verdict: 'unreadable', …(2) } to deeply equal { verdict: 'absent', probe: 'catalog' }
+   "detail": "function database() does not exist"

— exactly the mis-compiled-arm shape the fence exists to keep out of 'absent'. The one case that stays green is the provisioning/non-vacuity check, which does not exercise the arm; that is the correct partition. Restored with git checkout HEAD -- PATH and verified: blob hash back to fd66ad50…, git diff HEAD empty, git status --porcelain clean.

Verification

what reading
package suite, unnarrowed (pnpm --filter @objectstack/metadata-protocol test, live PG present) 177 passed / 2 skipped (179 files), 2538 passed / 12 skipped (2550 tests), exit 0. The 2 skipped files are the live-MySQL pair — no MySQL server in this container.
tiers none. This package declares no vitest projects, so there is no tier split to report and no --project filter was used.
pnpm --filter @objectstack/metadata-protocol typecheck exit 0. Proved it really compiles the new file: tsc --noEmit --listFiles names seed-tenancy-backfill.live-postgres.test.ts (1 hit), positive control read-probe.ts present, negative control packages/cli/src absent, 741 files total.
CI filter widening, measured vitest list --filesOnly live-mysql selects 3 files; ... live-mysql live-postgres selects the same 3 plus seed-tenancy-backfill.live-postgres.test.ts. Multiple positionals are OR-ed and each stays a substring.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack 100 derived commands, all run at the final head 66dff5cb02 with dist present. 100 exit 0. --ran reconciliation: 100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN — a derived zero, every line recorded its exit code. The command set re-derived at that head is identical to the one derived before the rework commit.
eslint . --no-inline-config --format json exit 0, 6671 files, 0 errors, 0 warnings — the whole repo population, not a narrowing.
check:nul-bytes red once, and fixed here: the editing tool materialised the unit-separator escapes in sequenceKeyHash into raw 0x1F bytes. Rewritten as escape text, gate green (8510 files scanned), and an independent grep -naP sweep over every changed file finds nothing.
check:live-db-isolation PASS — the new file is listed among the four live suites that derive their database, and it joins live-mysql-database.isolation.test.ts's auto-discovered population (9 cases green, distinctness now measured over 4 files rather than 3).

Three gates first answered exit 3 — PREREQUISITE NOT MET in this fresh worktree (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt); a full turbo run build over the workspace (72/72 tasks) was run and all three were then measured green. ⛔ None of those exit-3s was read as a pass.

Clause-②: no — the three falsification attempts

condition result
(a) diff moves an export line, or changes the built entry .d.ts negative, measured. No +/- line in the diff contains export. dist/index.d.ts byte-identical across a BASE-sources build and a branch build (same sha256).
(b) pg reaches the published dependency graph negative, measured. The packed manifest's dependencies are unchanged (7 workspace links + zod); no peerDependencies, no optionalDependencies. pg and @types/pg sit in devDependencies, which no consumer installs.
(c) scripts/pm/check-widening-tells.mjs --declaration no --diff FILE exit 0 — but read it as NOT MEASURED, not as a clean negative. Its own output: "0 judged against a declared surface, 5 NOT MEASURED … no declared surface covers it" for all five files. (a) and (b) carry the weight.

Rework — the install-time defect this PR shipped red with, and the verification hole behind it

The first push was 24-of-35 red in CI while origin/main was clean, and every red job died at the same place:

ERR_PNPM_OUTDATED_LOCKFILE
  specifiers in the lockfile don't match specifiers in package.json:
  * 1 dependencies were removed: @types/pg@^8.15.6

The lockfile was right and the manifest was wrong. @types/pg was added after the first commit on this branch; a later step of the dist/index.d.ts before/after measurement restored packages/metadata-protocol to HEAD, and HEAD at that moment predated the manifest edit. The pathspec did not cover pnpm-lock.yaml, so the lockfile kept the entry the manifest lost. Fixed by putting the one line back — the lockfile is byte-untouched, the mismatch closes from the manifest side.

Two things worth carrying, both about verification rather than about pg:

  • The restore check that missed it was git status --porcelain -- PATH coming back clean — which it was, because reverting to HEAD is clean. A clean status proves the restore ran; it does not prove the restore preserved the state the run was supposed to leave behind. Against an edit made after the commit being restored to, those two readings point opposite ways.
  • Nothing local could see the result. A warm node_modules never re-runs the frozen-lockfile check, so the defect existed only at install time on a clean tree while 100/100 derived gates and the full package suite stayed honestly green — the same family as [finding] vitest 的 --project 过滤器落空即静默成功 —— 点名一个 integration 文件跑 --project unit,报它是通过的文件、执行零个用例,并把它从文件计数里减掉 #17853, a verification that passes because it never executed the thing in question. ⇒ a diff touching package.json or pnpm-lock.yaml owes a clean-tree pnpm install --frozen-lockfile, and no amount of green elsewhere substitutes.

Measured on a genuinely clean tree (fresh worktree, no node_modules): exit 1 before the fix with CI's exact message, exit 0 after it, and git diff --name-only names package.json alone. Re-anchored at the fixed head 66dff5cb02: the live-PG suite is 7 passed (7) again, the skip direction 7 skipped (7) at exit 0, typecheck exit 0, the package suite 176 passed / 3 skipped (179 files) and 2531 passed / 19 skipped (2550 tests) with no live servers, all 100 derived gates exit 0, and eslint . --no-inline-config 6671 files with 0 findings.

The changeset stays patch, and the reasoning holds harder now: the packed package.json gains two dependency lines rather than one.


Authored by Claude Code — session session_01RuoNSXUbBoWHkNS4AknTrM (https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM). Attribution kept in prose here because this body was edited through raw REST, whose measured behaviour is to append its own footer block.

Acceptance notes

  • noted, not filed: the new file's per-file schema comes from currentLiveMysqlDatabase() — a MySQL-named resolver in a PostgreSQL suite. Deliberate, and the file's docblock says why: the derivation is dialect-neutral and already capped at PostgreSQL's 63-byte limit, and calling it by that name is what enrols the file in live-mysql-database.isolation.test.ts's population, which is discovered by reading each test file for that exact call. A dialect-neutral alias would buy a better name at the price of this file's distinctness never being measured. Renaming the resolver repo-wide is mechanical and belongs to whoever adds the next live suite to this package or generalises live-mysql-database.testkit.ts. 承接者: the next live-DB suite in packages/metadata-protocol.
  • noted, not filed: pr-automation.yml's documented skip-changeset clobber is already fixed on this tree — both labeler steps now route through scripts/pr-labels.mjs (POST + targeted DELETE, no whole-set PUT), so the "apply the label after the push settles" precaution is stale advice rather than a live hazard. Moot for this PR, which takes patch. 承接者: whoever next reads that precaution in a dispatch.

⛔ Draft, and left that way: landing is the PM seat's act. Not marked ready, auto-merge not armed.


Generated by Claude Code

… live server

#17175 compiled one catalog presence arm per dialect family and executed two of
the three: SQLite end to end through a real SqlDriver, MySQL on the live server.
The PostgreSQL arm was pinned character-for-character as TEXT and run nowhere —
this package had no live-PG harness, no `pg` dependency, and the CI leg supplied
`OS_TEST_MYSQL_URL` alone while filtering vitest to `live-mysql`.

A text pin cannot close that gap. The failure the module is fenced against is an
arm mis-compiled for one dialect: it raises, the `catch` that exists for the
expected miss swallows it, and a data repair silently becomes a no-op. Only a
server answers whether `to_regclass` comes back as ZERO ROWS rather than as an
exception when the relation is not there.

Three changes, the third of which is wiring rather than infrastructure — the
postgres service is already up in that job for the driver-sql leg:

- `pg` as a devDependency on `packages/metadata-protocol` (a lockfile move: the
  importer entry is the whole diff, `pg@8.22.0` is already resolved for
  driver-sql's optional peer);
- `seed-tenancy-backfill.live-postgres.test.ts`, the sibling of the live-MySQL
  file, in its own derived schema on the search_path;
- `OS_TEST_POSTGRES_URL` on that CI step and a second vitest substring.

`read-probe.test.ts`'s docblock recorded the arm as NOT MEASURED anywhere; that
sentence is now false, so it names where the arm runs instead.

Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM
Co-authored-by: Claude <noreply@anthropic.com>
…raw byte

`check:nul-bytes` found three raw 0x1F bytes in the new live-PostgreSQL suite's
`sequenceKeyHash`: the editing tool materialised the escape sequences into real
bytes while the file was being written — the exact slip that gate's header
records. Byte-identical at runtime; the live suite is green either way.

Also adds the `patch` changeset, graded from the measurement rather than
assumed: `dist/` is byte-untouched, but `package.json` ships in the tarball and
now carries `pg` and `@types/pg`, so `skip-changeset` does not apply.

Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 12, 2026
@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 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 c744c0af332c31748ab7efc87e87505277adae2dpackageMentionDocs.

…ile already pins

`pnpm install --frozen-lockfile` refused on a clean tree — "specifiers in the
lockfile don't match specifiers in package.json: 1 dependencies were removed:
@types/pg@^8.15.6" — so every CI job that installs died before running anything.

The lockfile was right and the manifest was wrong. `@types/pg` was added after
the first commit on this branch; a later step of the `dist/index.d.ts`
before/after measurement restored `packages/metadata-protocol` to HEAD, and HEAD
at that moment predated the manifest edit. The pathspec did not cover
`pnpm-lock.yaml`, so the lockfile kept the entry the manifest lost.

⚠️ The restore check that missed it was `git status --porcelain -- PKG` coming
back clean — which it was, because reverting to HEAD IS clean. A clean status
proves the restore ran; it does not prove the restore preserved the state the
run was supposed to leave behind. Against an edit made AFTER the commit being
restored to, the two readings point opposite ways.

⚠️ And nothing local could see the result: a warm `node_modules` never re-runs
the frozen-lockfile check, so the defect existed only at install time on a clean
tree while 100/100 derived gates and the full package suite stayed honestly
green. A diff touching `package.json` or `pnpm-lock.yaml` owes a clean-tree
`pnpm install --frozen-lockfile`; no amount of green elsewhere substitutes.

Measured here: refused before the fix with CI's exact message, exit 0 after it,
and the lockfile is byte-untouched — the mismatch closes from the manifest side.

Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM
Co-authored-by: Claude <noreply@anthropic.com>

os-musk commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

席位落地复核 —— Clause-②: no 成立,交付接受(含一次返工)

domain:engine 执行 PM 席 · session_01RuoNSXUbBoWHkNS4AknTrM · R2 · 所判 head 66dff5cb02bdabcfcccfc1ea0129393fa193cdfd

⚠️ ⛔ 本条不是同形契约复核记录 —— 申报 no,席内契约复核非必过,载体未挂也不该挂。

返工一轮:席位诊断正确,但机制是轮次给出的,比本席的猜测准

CI 曾 24/35 红,基线干净 ⇒ 是本 PR 的。原因:pnpm-lock.yamlpackages/metadata-protocol importer 声明了 @types/pg,而该包 package.json 没有 ⇒ --frozen-lockfile 拒装,每个要安装的 job 在跑任何东西之前就死了

本席把成因猜成「在权衡 open question 时把它从 manifest 拿掉了」—— 猜错。 轮次给出的真实机制:

@types/pg 是在本分支首个提交之后加的;后来本轮自己的 dist/index.d.ts 前后对比里跑了
git restore --source=HEAD --staged --worktree -- packages/metadata-protocol,而彼时的 HEAD 早于那次 manifest 编辑;pathspec 又不覆盖 pnpm-lock.yamllockfile 留住了 manifest 丢掉的东西

⭐⭐ 而最值钱的是它对为什么自查没抓到的分析:

当时的还原检查是 git status --porcelain -- PATH 干净 —— 它确实干净,因为回到 HEAD 本来就是干净的。
一个干净的 status 证明还原跑过了,⛔ 不证明还原保住了那次运行本该留下的状态。

⇒ 对一次「在被还原到的那个提交之后做的编辑」,这两个读数指向相反方向。⭐ 判,且这比本席给的框架更深。

修复与验证

  • 返工提交实测:1 文件 / 1 行 —— + "@types/pg": "^8.15.6",;lockfile 未动,缺口从 manifest 侧闭合。
  • ⭐ 轮次补了本席点名的那个空洞:干净树 pnpm install --frozen-lockfile —— 修复前 exit 1 并复现 CI 的逐字消息,修复后 exit 0。
  • CI 在 66dff5cb02:42 raw runs → 35 distinct names,32 success / 3 skipped,0 红 / 0 pending(⛔ 按 latest-run-per-name 计)。

⭐⭐ 本卡的要害:那条臂在 CI 上真的执行了,有服务端证据

本卡的控诉是「PostgreSQL 臂从未被执行过」。⇒ 「文件通过了」恰恰是不能结案的那种读数(没有 URL 时它 skip,而 skip 在文件计数里与 pass 无法区分)。

轮次给的是服务端证据:该 job 的 postgres:16 service container 日志里带着

ERROR: relation "os17621_absent_table" does not exist at character 15
STATEMENT: SELECT 1 FROM os17621_absent_table WHERE 1 = 0

本席独立复核了这条证据的关键前提:os17621_absent_table 在全树只出现在那一个新测试文件里(3 次,别无他处)⇒ 它出现在 CI 那台 postgres 的日志里,只能是那个测试打到了那台服务器。

并且三个方向都量过:live PG 7/7 逐名执行;无 URL 时 7 skipped / exit 0(报为 skip,⛔ 不是 pass);OS_EXPECT_LIVE_DIALECT_MATRIX=1 且无 URL 时具名变红 / exit 1

⛔ 本席的 changeset 预期被证伪,已接受

派发令里本席预期 skip-changeset(理由:测试文件、workflow、devDependency 都不进 dist)。⛔ 错。 轮次实测 package.json 本身就是 npm pack 发运的 27 个文件之一,而它现在多了依赖行 ⇒ 确实从已发布包发运了东西patch。⭐ 加回 @types/pg 后更成立:两行而非一行。

其余

  • ci.yml:step 更名、加 OS_TEST_POSTGRES_URL(与 driver-sql 腿同一个 URL、同一个既有服务)、vitest 过滤从 live-mysql 扩为 live-mysql live-postgres。⭐ 注释记了一条实测判断:单用更短的 live- 今天选中同一组,但会随下一个碰巧这么拼的无关文件静默扩大 ⇒ 两个显式子串。
  • 消融:把 PG 臂重编译成 MySQL 的 DATABASE() 形 ⇒ 7 例中 6 例红,verdict unreadable、detail function database() does not exist —— 正是 A SELECT ... WHERE 1 = 0 column-existence probe against _objectstack_sequences is logged at ERROR on a normal boot, before the table exists #17175 的围栏要挡在 absent 之外的那个形状;还原按 blob hash + git diff HEAD按状态证明。
  • 门禁:100 derived / 100 run / 全部 exit 0,三个 exit 3 补前置后重测,⛔ 未记作 pass。check:nul-bytes 曾红一次并已修(编辑工具把 sequenceKeyHash 里的 unit-separator 转义物化成了裸 0x1F 字节)。

domain:engine execution PM seat · session_01RuoNSXUbBoWHkNS4AknTrM · R2 · 2026-09-12


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants