Skip to content

test: cover query scoping, live feed revocation, and the unread fixture block - #128

Merged
cuibonobo merged 1 commit into
mainfrom
claude/server-permission-test-coverage
Sep 19, 2026
Merged

cuibonobo merged 1 commit into
mainfrom
claude/server-permission-test-coverage

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

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 exercise ScopedStack in one process. The read-own/read-any references already in tests/routes/records.test.ts are all about the _grant write fence, not about a grant scoping what a query returns.

tests/routes/queryScoping.test.ts (10 tests) 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 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 — ScopedSubscription keeps 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 existing drops a backlog frame whose grant was revoked during the gap test 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:

  • a type-level grant withdrawn mid-stream
  • a record shared mid-stream, which starts delivery
  • a roster change, which expires the resolved roles rather than the grant set — a separate invalidation path

Each asserts a frame the subscriber may still see, so a connection that has simply broken cannot pass by going quiet.

3. deleteRecordSequenceFixtures was consumed by nothing. tests/conformance.test.ts describes 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

pnpm run format:check   ✓
pnpm run lint           ✓
pnpm typecheck          ✓
pnpm test               ✓  570 passed across 29 files (was 555 / 28)

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:

FAIL  tests/conformance.test.ts > fixture package coverage
      > every exported fixture array is dispatched by a block above
AssertionError: expected [ Array(1) ] to deeply equal []
+   "deleteRecordSequenceFixtures: hard-delete-under-concurrent-write"

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 in tests/routes/ because it drives GET /records and POST /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 in src/lib/queryWorker/. The module comment says so. Happy to move it to tests/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 the errorResponseFixtures block 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 _grant write 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

…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
@cuibonobo
cuibonobo merged commit f1f0cab into main Sep 19, 2026
8 checks passed
@cuibonobo
cuibonobo deleted the claude/server-permission-test-coverage branch September 19, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants