test(rest): pin the PARTIAL permission-store outage at the /meta doors with a per-read fault injector - #13614
Merged
Conversation
…s with a per-read fault injector [#13538] Adds census section 9. Sections 7 and 8 leave a gap between them: 7 re-derives the catch ARGUMENT at every resolveExecCtx site from source text, so a regression that changes no spelling is invisible to it by construction; 8 is behavioural but drives an UNCONDITIONAL rejection, i.e. a TOTAL outage. The case in between is the harmful one. When only the FIRST read fails, a swallowing site returns undefined, the handler proceeds with no tenant, and organizationIdForMetaRead is handed no organization -- so the door issues an env-wide read and answers an org-unscoped 200 carrying rows from outside the caller's organization. It is not a refusal, so nothing downstream notices. Section 9 changes section 8's CRITERION rather than adding a stricter source-text pin (the explicit non-fix). Two changes to the fault model, both measured in controls rather than assumed: - fault SELECTION per read (`failOrdinal`, 1-based, reset per driven request), so "the first read fails and the next succeeds" is expressible at all; and - an ORG-OVERRIDABLE driving type. Section 8 sweeps `object`, for which declaresOrgOverride is FALSE -- that type is env-wide BY DESIGN, healthy or faulted, so no fault model could make section 8 observe an org-scope difference on the type it drives. A control pins both halves. The load-bearing assertion is on the REQUEST the handler built, not on the status code: an org-unscoped read is the harm and a status is downstream of it. That is what survives a refactor which changes no spelling. Section 8 keeps its own subject -- a total outage is a real shape and still must not be served as success. rest-server.ts is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
…rtial-outage-fault-injector
… and correct the reading beside it [#13538] Ablation (collapse + swallow, both restored, rest-server.ts unchanged in the diff) recorded the list door directly: TOTAL outage -> status 503, sites [2821, 4388] PARTIAL (1st) -> status 200, sites [4388], read carried NO organizationId Site 2821 is resolveObjectMasker's guarded resolve, and that method early-returns unless metaType === 'object'. So what keeps section 8 green under a swallow is a second guarded resolve that only exists for `object` -- the same fixture choice that makes its org-scope question vacuous. It is NOT the three-resolves-in-the- handler reading recorded in the card and in section 8's own note: the handler's other two resolves sit behind metaTypeSingular(...) === 'app' and === 'dashboard' and never run for `object` at all. The conclusion those notes drew was right; the mechanism they named was not. Section 8's paragraph is left intact and a correction is added beside it, so the historical record stays readable. Re-deriving by SYMBOL rather than by line is what separated the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
Contributor
📓 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): |
This was referenced Aug 31, 2026
os-steve
marked this pull request as ready for review
August 31, 2026 03:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13538
Adds
§9topackages/rest/src/execctx-consumer-census.test.ts: a per-read fault injector that drives the/metadoors under a PARTIAL permission-store outage — only the chosen read fails — and pins that the answer is not an org-unscoped200.packages/rest/src/rest-server.tsis not in this diff. One file changed, +210 lines, test-only.The mandatory question: which existing pin could have caught this, and did not
§8. Not§7.§7re-derives the catch ARGUMENT at every site from source text. A regression that changes no spelling is invisible to it by construction, so it could never have caught this — and a stricter spelling assertion is the explicit non-fix the card rules out.§8is the one that could have. It is already behavioural, already drives these doors, already mounts them ISOLATED, and already records which sites each route touched — every part of the instrument except the fault MODEL. So what is owed is a changed criterion on§8's harness, not a new source-text case and not a new file.§8keeps its own subject: a total outage is a real shape and still must not be served as success.Two things in that model kept it away from the partial case, and
§9changes both:type: 'object', anddeclaresOrgOverride('object')is false — that type is env-wide BY DESIGN, healthy or faulted. The difference this card is about does not exist on the type§8sweeps, whatever fault model it were given.§9therefore drives an org-overridable type and asserts on the request the handler built, not on the status code. An org-unscoped read is the harm; a status is downstream of it. That is what survives a refactor which changes no spelling.Measured correction: why
§8really stays greenThe card, and
§8's own note, say the:typehandler resolves the context three times, so a later guarded resolve saves the door. Re-derived by symbol, that is not the operative mechanism. The handler's other two resolves sit behindmetaTypeSingular(...) === 'app'and=== 'dashboard'and never run forobjectat all.Ablation recorded the list door directly:
5032821, 43882004388organizationIdSite
2821isresolveObjectMasker's guarded resolve, and that method early-returns unlessmetaType === 'object'. So what actually keeps§8green under a swallow is a second guarded resolve that only exists forobject— the same fixture choice that makes its org-scope question vacuous. Two independent blind spots, both flowing from one fixture.The card's conclusion was right; the mechanism it named was not.
§8's paragraph is left intact and a correction is added beside it, so the historical record stays readable. This is the bounded in-place fix in this PR beyond the new section, and it is named here rather than left as a silent tidy-up.Ablation — reach of the new pin, with predictions declared first
Every leg mutated
rest-server.tsin this worktree only, under a trap, and restored; nothing from those legs is in the diff. Restoration was proved on both legs bygit hash-objectequalling the HEAD blob ANDgit diff HEADempty (an empty hash treated as FAILURE, not "nothing to compare"). Mutation was proved ON DISK, never from an editor exit code: thecatch(() => undefined)occurrence count moved16 -> 17, and the collapse leg's removed and injected texts were counted separately (0and1).No rebuild is required for these legs, and that is a property of the suite rather than an assumption: the census imports
RestServerthrough the relative specifier./rest-server.js, which vitest resolves to the TypeScript source beside it — never through a packageexportsmap todist/. There is positive proof inside the harness: the site recorder readsrest-server.ts:LINE:COLoff the stack, and the controls assert the site lists are non-empty, which is impossible if the executing module were a built artifact.:typefirst read§7red,§9red,§8red§7red,§9red,§8GREEN — prediction falsified, card confirmed§9GREEN;§2and§7CONTROL red on counting drift only§9red,§8green§9RED (both starred),§8GREEN,§7redLeg C is the one that matters. It is the configuration where
§8's accidental protection is gone, and there§9is the only thing that reddens on the behaviour:§7's signal is a spelling-and-count drift that a refactorer would "fix" by updating numbers, after which nothing would remain.Leg B is a falsification worth recording: a collapse on its own reintroduces no harm, because every site still re-raises — so
§9staying green there is correct, not a gap. What the collapse actually removes is§8's protection, which is why the decisive leg had to combine the two.Verification
Union run at
83a2f0900, the final commit, after mergingorigin/main.pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/execctx-consumer-census.test.ts— exit 0,Tests 28 passed (28)(20 to 25itblocks; the 5 added are§9).pnpm --filter @objectstack/rest run typecheck— exit 0. Note this is not the "typecheck excludes test files" blind spot: the package runscheck:test-typecheck, which compiles the test layer undertsconfig.test.jsonand says so.pnpm lint(repo-wideeslint . --no-inline-config, the derivation's known blind spot) — exit 0, run in full. No narrowing claimed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, stderr read: no stale-tree banner,--repoassertion holds. 26 families.23 PASS · 3 NOT MEASURED · 0 FAIL. Exit codes captured before any pipe. The three are refusals that say so in their own words, and are NOT folded into the pass list:
pnpm check:type-check-debt— "--re-measurecannot run: 26 workspace dependencies have no built type entry point on disk". Needs the closure built.pnpm check:dual-build-cjs-loads— "PREREQUISITE NOT MET ... this is NOT a pass: nothing was measured". Its own self-test passed (93 cases).node scripts/check-test-completeness.mjs— exits 3 and states "the local reading for this gate is NOT MEASURED ... it is not a red"; it needs a saved test-run log CI tees in.Changeset
skip-changeset, label applied on open. The diff is a single test file: it publishes nothing from any package and changes no user-visible behaviour. Stated here rather than assumed.Scope
/metadoors only. No assertion in§9mentions/ui/view, so it stays disjoint from the concurrent work onregisterUiEndpointsinrest-server.ts. The 21 sites converted by PR #13475 are not reopened.Generated by Claude Code