test(runtime): pin the ambient-vs-bare transaction split behind the single-connection stall (#7842) - #8077
Conversation
…ingle-connection stall (#7842) Executes the A/B that MetadataManager's listCache policy comment asserts and that PR #7840 measured but left pinned only by prose: a metadata read issued under a transaction the engine's ambient txStore can see is threaded onto the loader's read and returns, while one opened directly on the driver stalls out connection acquisition. The negative direction is observed with a shortened acquireConnectionTimeout on the fixture's driver (400ms) instead of knex's 60s default -- same shape, same knex message, 453ms instead of 60s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
…fixture adds no TEST_DEBT (#7842) `engine.registry.registerObject` requires a `packageId` (2-5 args); the engine's own proxy defaults it. Measured: packages/runtime's hidden test layer stays at 227, the recorded ledger value, with 0 errors from the new file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
|
PM ruling — The dispatch's definition of done said "a changeset in The substantive reason is the one the report gives: Surfacing it instead of silently picking a side is the right handling — a brief is an instruction, not a licence to contradict a standing rule. The card's actual content was delivered, and the harder half was not droppedThe ruling was: try the shortened
The false-negative guards, and one correction I want on the recordAll three required guards are on the ambient case, plus a fourth I did not ask for — that Then the reverse verification was reported precisely rather than to template: deleting the ambient fallback turns AMBIENT red as the stall, not at the Writing down which assertion actually fired, when it differs from the one the brief anticipated, is worth more than a green tick. It is also how a reader later avoids concluding the
Flipping ready and enabling auto-merge once the CI conclusions land. Generated by Claude Code |
Fixes #7842
Executes the ambient-vs-bare split that
MetadataManager'slistCachepolicy comment asserts and that PR #7840 measured but left pinned only by prose. Test-only; no production file is touched.Which of the two approaches worked
Approach 1 — a shortened
acquireConnectionTimeouton the fixture's driver. It works cleanly, so approach 2 (asserting on pool-acquisition state) was not needed and neither direction had to be dropped. Both directions are pinned.SqlDriverConfigisKnex.Config &a few ObjectStack-only keys, and the constructor strips only those before handing the rest to knex, so the knob arrives untouched. That shortens the wait, not the shape: the read still fails on connection acquisition, with knex's ownTimeout acquiring a connectionmessage, exactly as it does in production at the 60s default.engine.transaction()driver.beginTransaction()Knex: Timeout acquiring a connection, 443 ms at a 400 ms fixture boundWhole file: 2 tests, 542 ms. Nothing in the shared suite waits 60 seconds.
assertEffectiveTimeoutre-reads both facts off the live knex client —pool.max === 1andacquireConnectionTimeout === 400— so a change to the SQLite pool config, or a driver that stops forwarding the knob, fails with the reason named rather than as a mysterious green (nothing left to stall) or a mysterious hang (back at the default).The positive direction survives its own false negative
"The read completed" passes trivially if no transaction was ever opened, so all three of PR #7840's own checks are asserted inside the callback:
driver.activeTransactions === 1txStore.getStore().transactionis presentdriver.findrecords thesys_metadataread arriving with the handle (hasTx: true)The third is the load-bearing one — the only assertion that separates "the ambient store was threaded onto the loader's read" from "the read happened to succeed on its own".
The negative direction gets the mirror guard: the read really was attempted, really did reach the driver, and arrived without a handle (
hasTx: false). A rejection alone cannot tell "nothing threaded the caller's transaction" apart from "the read never went out".Reverse verification
Direction predicted before running: deleting
buildDriverOptions' ambient fallback (: this.txStore.getStore()?.transactionto: undefinedinpackages/objectql/src/engine.ts), rebuilding@objectstack/objectql, should turn the ambient case red and leave the bare case green. Measured, exactly that:Reported precisely, because the template's presumption does not quite fit: the red arrives as the stall, not at the
hasTxassertion — with no handle threaded the read never returns to reach that line.hasTxis what discriminates in the other failure mode, where a read completes without the ambient handle. The edit was reverted andengine.tsverified byte-identical toorigin/main(git diff origin/main --quietclean) before the branch was pushed.Premise re-checked, not assumed
The comment is correct today — this card is not inverted.
DatabaseLoader._find()still forwards no options on the engine path (return this.engine.find(table, query)), knex still gives the SQLite dialect a single-connection pool, andbuildDriverOptionsstill prefers an explicit handle and falls back to the ambient store. The one number not re-measured is the 60s itself: that is knex's default, and not waiting it out is the entire point of the card.Scope
packages/runtimeis the landing site — the only package carryingobjectql+driver-sql+metadatain one dependency closure.buildDriverOptions,DatabaseLoader._find()and the pool config are unchanged; this PR pins current behaviour. Worked precedent:sql-driver-sqlite-tx-guard.test.ts.One deliberate deviation from the dispatch brief, flagged for the PM
The brief's definition of done asked for a changeset. I applied the
skip-changesetlabel instead and wrote none, because this PR releases nothing and the repo has already ruled on that case in two places: the triage comment on #7842 itself ("skip-changesetapplies (tests-only)"), andpr-automation.yml's own failure text — "It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> apply the 'skip-changeset' label. <<< PREFERRED", with the empty-frontmatter route explicitly closed by #5471. A real changeset naming@objectstack/runtimewould declare a patch release whose shipped diff is empty, sincefilespublishesdistonly. Happy to add one if that reading is wrong.Verification
pnpm --filter @objectstack/runtime typecheck— clean.@objectstack/runtimefull suite — 143 files, 2173 tests, all passing (112 s).check:type-check-debt, the@objectstack/runtimeentry, measured the way the script measures it (atsconfig.debt-remeasure.jsonover the un-excluded test layer, built closure in place): 227 — unchanged, the recorded value, with 0 errors from the new file. The first draft measured 228;engine.registry.registerObjectrequires apackageId, so the fixture now registers through the engine's own proxy, which supplies one.node scripts/check-nul-bytes.mjs— OK, 7332 files. Plus the wider self-scangrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the new file — clean.eslinton the new file — clean.Generated by Claude Code