Repository navigation
test: cover query scoping, live feed revocation, and the unread fixture block - #128
Merged
Merged
Conversation
…re block Three gaps in what the permission surface is held to here. **No test asserted that a type-level grant scopes a query result** — the most load-bearing read behavior on the wire, and the one that crosses the worker boundary. Both query endpoints run on QueryWorkerPool, a thread with its own connection, so reach is re-derived there rather than inherited from the request thread that accepted the call; nothing else in the suite crosses that boundary, since by-id reads and writes stay on the main thread. tests/routes/queryScoping.test.ts covers read-own against read-any, a withdrawn grant on the very next query, undelete restoring it, a deleted group and a roster removal both narrowing reach, _grant records never enumerating for a grantee, both query encodings answering alike, and the anonymous view under a default grant. Revocation is the direction that matters: a cached grant set on the worker would answer a query the owner had already withdrawn, and no by-id test would notice. **The feed's authority cache was only tested across a resume gap.** A subscription is the one place authority is held across operations, so it is the one place a withdrawal has to arrive as an event to be seen at all. Three tests cover a connection that stays open: a type-level grant withdrawn mid-stream, a record shared mid-stream, and a roster change, which expires the resolved roles rather than the grant set. Each asserts a frame the subscriber may still see, so a broken connection cannot pass by going quiet. **deleteRecordSequenceFixtures was consumed by nothing.** The acceptance gate is per-block, so it only guards arrays this file imports — core can add a whole block and every test here still passes. The server satisfies the fixture (deleteAndReturn() already reads at destruction time); it was unverified, not wrong. Dispatched now, and a whole-package test finds the arrays by shape rather than by list, so the next unread block names itself in the failure instead of passing silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC
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.
Summary
Three gaps in what this server's permission surface is actually held to. Found by a verification pass driven from outside the process, by a wire-only client against a running server; these are the assertions that belong here rather than in core, because nothing in core can make them.
1. No test asserted that a type-level grant scopes a query result. The most load-bearing read behavior on the wire, and the one that crosses a thread boundary. Both query endpoints run on
QueryWorkerPool— a worker with its own connection to the same database — so a grant's reach is re-derived there rather than inherited from the request thread that accepted the call. Nothing else in the suite crosses that boundary: the route tests beside it exercise by-id reads and writes, which stay on the main thread, and core's own tests exerciseScopedStackin one process. Theread-own/read-anyreferences already intests/routes/records.test.tsare all about the_grantwrite fence, not about a grant scoping what a query returns.tests/routes/queryScoping.test.ts(10 tests) coversread-ownagainstread-any, a withdrawn grant on the very next query,undeleterestoring it, a deleted_groupand a roster removal both narrowing reach,_grantrecords never enumerating for a grantee, both query encodings answering with the same set, and the anonymous view under a default grant.Revocation is the direction that matters. A cached grant set on the worker would answer a query the owner has already withdrawn, and no by-id test would notice.
2. The feed's authority cache was only tested across a resume gap. A subscription is the one place authority is held across operations —
ScopedSubscriptionkeeps the grant set and resolved roster roles for the life of the connection — so it is the one place a withdrawal has to arrive as an event to be seen at all. The existingdrops a backlog frame whose grant was revoked during the gaptest covers a change made while nobody was listening; these cover the opposite case, where the connection is open the whole time and the cache is what could go stale:Each asserts a frame the subscriber may still see, so a connection that has simply broken cannot pass by going quiet.
3.
deleteRecordSequenceFixtureswas consumed by nothing.tests/conformance.test.tsdescribes itself as an acceptance gate whose coverage tests fail when core adds or renames a fixture — but that guarantee is per-block, so it only guards arrays this file imports. Core can add a whole block and every test here still passes. That is what happened to this one.The server does satisfy it:
deleteAndReturn()already reads at destruction time, so the fixture passes as written. It was unverified, not wrong.Dispatched now, and closed structurally: a final whole-package test discovers the fixture arrays by shape rather than by list, so anything shaped like a fixture set is found whether or not a block reads it, and an unhandled one names itself in the failure.
Docs
No behavior change — tests only. No route, status code, error code, auth rule or environment variable moved.
Verification
The +15 are 10 query-scoping, 3 feed, 1 sequence-fixture dispatch, 1 whole-package gate.
I checked the new gate fails when it should, rather than assuming it. Reverting just the sequence dispatch — the exact pre-PR state, where the block exists in core and nothing here reads it — produces:
The scoping and feed tests are non-vacuous by construction: each asserts an exact set or a specific frame index, so a leaked record or frame displaces the expected one and fails rather than passing unnoticed.
Notes for reviewers
Placement of
queryScoping.test.ts. It lives intests/routes/because it drivesGET /recordsandPOST /records/query, and that is where a reader looks for "what does a query return for whom" — but what it is really pinning is the worker boundary insrc/lib/queryWorker/. The module comment says so. Happy to move it totests/lib/if you would rather it sit with the mechanism than with the surface.The whole-package gate will fire on haverstack/core#320. That PR adds nine
errorResponseFixtures, so once it releases and this repo bumps@haverstack/conformance-fixtures, both theerrorResponseFixturesblock coverage and this new gate will fail until dispatch arms exist. That is the mechanism working as intended — landing the gate before the fixtures release is the right order — and the dispatch is the follow-up PR that release unblocks.Not covered here, and deliberately: the
_grantwrite fence's per-verb refusals, the reshare gate, and write-implies-read all already have tests, and the cross-implementation half of the overhaul's rules is going to core as fixtures (#320) rather than being duplicated here.Docs findings from the same pass are in #127, which is docs-only and independent of this.
🤖 Generated with Claude Code
https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC
Generated by Claude Code