test(plugin-security): three read-fault readings for the seams #15598 fenced out — measurement only, no behaviour change - #15998
Conversation
…ay when the read does not answer Nine readings, one per arm, driven against a real ObjectQL over a real SqlDriver. Measurement only — no source in this package changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…eck flagged Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift CheckNothing 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
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
Standing down on the red
Why it cannot be this PR's: this diff adds one new test file and changes no source in the repository — 403 lines, Mechanism (measured on #15980, filed as #15990): the self-test writes a stub Today's distribution, which is the sharpest evidence it is environmental — same defect, four PRs, and the re-runs disagree:
Same commit, same workflow, two runs, two verdicts. That is per-job launcher resolution, not any property of a diff. Severity bound: false-RED only, never false-green — a failing launcher makes every artifact look Fix status: #16009 is open against #15990 and takes the launcher dependency out of the fixture. Nothing to port into this PR before it lands. ⛔ No test skipped, disabled or quarantined; ⛔ no empty commit; ⛔ no registry widened. Generated by Claude Code |
|
Correction to this seat's own comment above: the "re-run cleared it" rows were WRONG, and the mechanism I gave was wrong too. 1. No re-run cleared anything. I reported #15996's and #15978's re-runs as having cleared the failure. They had not. Measured just now, their 2. The mechanism I described — "an ambient
That last point is what my "flaky per job" story could not explain and this one does: flaky ACROSS jobs, byte-identical WITHIN one. 3. The standing-down conclusion is unchanged and is now better supported: this failure is not this PR's, it can only false-RED, and #16009 takes the launcher dependency out of the fixture. What changes is that my evidence for it was partly wrong, and the corrected evidence is stronger. Generated by Claude Code |
Part of #15840
Measurement only. No source file in this repository changes — the diff is one new
test file, 403 lines. The card asked which of the three surviving seams has a declared
consumer contract to apply and which does not, and the honest answer is not one answer.
What was measured
A real
ObjectQLover a realSqlDriver(better-sqlite3:memory:), booted exactly asengine-find-bare-array.pin.test.tsboots it. The only thing standing in is the FAULT —one verb, for one object, made to throw or to answer an envelope. Every other call, and
every read whose answer is the subject, reaches the real engine untouched. The seam is a
Proxyover the real instance rather than a hand-built stand-in: a literal would be asecond, hand-maintained idea of the engine's call shape, which is the wrong instrument
when the engine's own answer is the thing being read.
Nine cases, and the three seams do not behave alike.
Reading 1 —
normalize-managed-by.ts:53tryFindThe only one of the three where all four conditions of AGENTS.md's read-seam invention
rule hold: a storage read in a
try, acatchthat logs at no level at all, a pathout returning an invented empty, and no discrimination on the error's type.
Measured: a catalog holding a row that genuinely needs healing, behind a read that cannot
answer, is byte-identical on both channels to a catalog that is already canonical —
the same
{ positions: 0, permissionSets: 0 }and, on a recording logger that capturesinfo/warn/debug/error, the same empty line list. The injection is self-proving:the fault fired exactly 4 times (once per legacy value the pass scans for) and the row it
should have healed is still stamped
system.Consumer reading:
security-plugin.tswraps the call intry/catchand reports[security] managed_by vocab normalization failed (non-fatal). That is the only handlingany consumer declares, and nothing reaches it — the fault is swallowed one frame
below, so the seam never throws. The returned counts are discarded by the only production
consumer; the pin asserts the
resolvesso this stays true by measurement.Reading 2 —
auto-org-admin-grant.ts:151tryFindReports, but at
debug, and the value it invents flows into a verdict.Measured on the revoke leg, which is where the invention costs something: with a
standing grant, no qualifying membership, and the
sys_user_permission_setread faulted,reconcileOrgAdminGrantanswers{ action: 'noop' }— the identical value a pair thatnever held a grant produces — while the grant the platform just decided to take away is
still in force. The two runs are separated only by a
debugline; nowarn, noerror.Measured on the installation-wide sweep: with the
sys_memberread faulted,backfillOrgAdminGrantsanswers{ scanned: 0, granted: 0, revoked: 0, skipped: 0 }andcloses with
info[security] org-admin grant backfill complete— the same summary andthe same log line as an installation that genuinely has no members. The member that WAS
there goes unreconciled.
Consumer reading:
security-plugin.tsdiscards both values and declares handling only fora throw; the seam never throws.
backfillOrgAdminGrantsis the one consumer that brancheson the value, and it branches on
actiononly.reason— the channel a read fault wouldhave to travel in — is read by nobody: zero hits across
packagesfor a non-testconsumer, with
res.actionfiring as the positive control on the same command and scope.Reading 3 —
claim-seed-ownership.ts:177idsFrom— the card's premise is partly falsified hereThe card's table records this site as "non-array answers
[], with no report at all".That is true of
idsFromread in isolation and not true of the pass it sits in.idsFromhas notryand nocatch. A read fault atreadPageis never seen by it: thethrow leaves
readPageand is caught byclaimSeedOwnership's own per-predicate handler,which reports at
warn, names the object, and states the consequence —claimSeedOwnership failed for probe_deal; those rows stay unowned and the next run will claim them. Measured, carrying the injected error's own message. That is a declareddisposition, in-file, and it is already the right one.
What is left at this site is the non-array arm alone, which #15598 measured as unreached
on the shipped engine. Driven anyway (the page read made to answer an envelope), it also
reports at
warn— but attributes a cause that did not happen:the write refused as over-sized but the predicate matched no rows to page, said while the envelope was in factcarrying the row.
What this PR deliberately does NOT do
⛔ It changes no seam's disposition. Two of the three open questions the card names —
whether an unreadable page should stop
claimSeedOwnership's pass, and whether anunreadable membership read should make
reconcileOrgAdminGrantskip or refuse — areundecidable from this tree: no consumer branches on the value, and none declares a
contract for the read not answering. Unlike #15598's seventh block, there is no
PermissionEvaluator.resolvePermissionSetshere to measure and apply. Choosing one wouldbe inventing the contract, which is the failure mode this card was filed to avoid.
⛔ No gate's population changes, and no gate is widened.
⛔ No
packages/spec/api-surfacemember, exported type or error code is touched:git diff origin/main...HEAD -- packages/spec/api-surface/is empty. Clause-② re-derivedfrom the diff: no.
The cost of the obvious repair, measured rather than argued
AGENTS.md's read-seam rule names "report the failure once" as a fix. Ablation on reading
1's site, run as a red-then-green pair: the silent
catchreplaced with alogger?.warn?.and the logger threaded to it. Mutation confirmed on disk before the run(
git hash-objectmovedfa327193to1b3c244f, the silent-catch grep went 1 to 0, theinjected marker went 0 to 1); the suite went red on exactly one assertion, the
toEqual([])on the log channel, which now saw 4 lines. Restored withgit checkout HEAD --against an absolute path and proved byte-identical (git diff HEADempty, hash back to
fa327193).So "report once" is really "report four times per boot" at this site — one per legacy
value scanned, not one per pass. That is a real input to the level question AGENTS.md
raises about training people to skim, and it is the kind of thing a uniform sweep across
the three sites would not have noticed.
The pin is therefore discriminating rather than merely green: it bites precisely on the
candidate repair, and only there.
Verification
Run at
4eb354f16, after the final commit.pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2— 100 testfiles, 1872 tests, all passed.
pnpm --filter @objectstack/plugin-security run typecheck— exit 0;check:test-typecheckreportsOK ... 0 file(s) / 0 error(s). The new file is provenin the program with
tsc --listFiles(1 hit), not assumed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-built path list): 53 families. 50 green, exit codes captured before any
pipe. Three report
PREREQUISITE NOT MET(exit 3, explicitly "not a pass and not afinding"):
check:dual-build-cjs-loads,check:published-readme-exports,check:type-check-debt— all three read whole-repodist/, and this worktree builtonly the plugin-security dependency closure. CI builds the repo and runs them.
check:engine-double-contract,check:test-source-alias,check:cross-package-test-inputs,check:published-files,check:nul-bytesandcheck:type-check-coverageare among the green ones.No changeset: nothing is published from any package by this diff (
check:published-filesgreen), so the PR carries
skip-changeset.Generated by Claude Code