Spaces: integration tests against a permissioned-data PDS #93

Closed
opened 2026-08-20 21:08:47 +00:00 by claude-bot · 2 comments
Collaborator

#89 covers the permissioned data protocol with 212 unit tests, but every one of them stubs the HTTP layer — the cryptographic constructions are pinned against the reference implementation's own outputs, and the wire shape against hand-written JSON. Nothing has ever talked to a real space host.

No PDS release serves com.atproto.space.* yet; it lives on bluesky-social/atproto#5187. Once a container image exists, add integration tests under tests/ATProtoNet.IntegrationTests/ behind a RequiresSpacesFact gate, following the RequiresJetstreamFact pattern in TestInfrastructure.cs so they skip in CI rather than fail.

Worth covering, because these are the parts a stub cannot check:

  • The credential exchange end to end: getDelegationToken on the user's PDS, then getSpaceCredential on the authority with a real DPoP proof — including that the server accepts the htu the SDK sends, which a stub by definition cannot tell us.
  • Rejection paths: a replayed delegation token, a proof signed by the wrong key, a credential presented to a host it was not addressed to, and SpaceDeleted on renewal after deleteSpace.
  • A round-trip of the repo CAR: write records through createRecord, fetch with getRepo, and verify the server's commit and index with SpaceRepoCar.Verify. This is the highest-value one — it checks that the SDK's LtHash, commit context encoding, MAC, and canonical DAG-CBOR ordering all agree with a real implementation's, rather than with a reimplementation of the same spec.
  • Incremental sync across a real oplog, including the fallback when since is no longer serviceable.
  • simplespace policy enforcement: a non-member refused under member-list, and an app refused under #allowList.

The reference implementation's own suite (packages/pds/tests/space/) is a good map of what is worth asserting.


Filed by Claude while working on #89 (run).

#89 covers the permissioned data protocol with 212 unit tests, but every one of them stubs the HTTP layer — the cryptographic constructions are pinned against the reference implementation's own outputs, and the wire shape against hand-written JSON. Nothing has ever talked to a real space host. No PDS release serves `com.atproto.space.*` yet; it lives on [bluesky-social/atproto#5187](https://github.com/bluesky-social/atproto/pull/5187). Once a container image exists, add integration tests under `tests/ATProtoNet.IntegrationTests/` behind a `RequiresSpacesFact` gate, following the `RequiresJetstreamFact` pattern in `TestInfrastructure.cs` so they skip in CI rather than fail. Worth covering, because these are the parts a stub cannot check: - The **credential exchange** end to end: `getDelegationToken` on the user's PDS, then `getSpaceCredential` on the authority with a real DPoP proof — including that the server accepts the `htu` the SDK sends, which a stub by definition cannot tell us. - **Rejection paths**: a replayed delegation token, a proof signed by the wrong key, a credential presented to a host it was not addressed to, and `SpaceDeleted` on renewal after `deleteSpace`. - A **round-trip of the repo CAR**: write records through `createRecord`, fetch with `getRepo`, and verify the server's commit and index with `SpaceRepoCar.Verify`. This is the highest-value one — it checks that the SDK's LtHash, commit context encoding, MAC, and canonical DAG-CBOR ordering all agree with a real implementation's, rather than with a reimplementation of the same spec. - **Incremental sync** across a real oplog, including the fallback when `since` is no longer serviceable. - `simplespace` **policy enforcement**: a non-member refused under `member-list`, and an app refused under `#allowList`. The reference implementation's own suite (`packages/pds/tests/space/`) is a good map of what is worth asserting. --- _Filed by Claude while working on #89 ([run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/314))._
Author
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/319); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-93/run-319.md) — updating every ~5 minutes while the task runs.
Author
Collaborator

Pushed claude/issue-93 — 26 integration tests that talk to a real space host, plus the docs to stand one up.

The host

No PDS release serves com.atproto.space.* yet, so rather than write the tests blind I built one from atproto#5187 (permissioned-data, still open/WIP) and ran everything against it — pnpm install && pnpm build, then the oauth-provider-ui bundle (the PDS won't boot without it), then a TestNetworkNoAppView on localhost:2583 with its PLC on :2582. 26 passed; the same suite skips cleanly with no environment set, so CI is unaffected. docs/testing-spaces.md documents the exact recipe.

What landed

[RequiresSpacesFact] follows the RequiresJetstreamFact pattern — gated on ATPROTO_TEST_SPACES=true, plus ATPROTO_PDS_ADMIN_PASSWORD and ATPROTO_PLC_URL. SpaceNetworkFixture provisions three accounts (authority / member / outsider) and deletes them afterwards; each test creates its own space.

Suite Covers
SpaceCredentialTests The exchange end to end, including that the server accepts the htu the SDK signs — and the refusals: replayed delegation token, token for another space, proof from another key, proof for another host, bearer presentation, SpaceDeleted on renewal after deleteSpace.
SpaceRepoSyncTests The CAR round trip — createRecordgetRepoSpaceRepoCar.Verify against the server's own commit and index (LtHash, commit-context encoding, MAC, canonical DAG-CBOR order all had to agree with a real implementation). Then oplog replay, cursor resumption, divergence → full recovery, and the fallback when since isn't serviceable.
SimpleSpacePolicyTests Non-member refused under member-list, app refused under #allowList, the attestation retry firing, revocation at next renewal, repo boundary between co-located accounts.

What the real server changed about my expectations

Four tests failed on the first run, all because a stub had let me assume wrongly:

  • A replayed delegation token comes back as JwtReplayed, not InvalidDelegationToken — the test now accepts either, since the protocol requires the refusal, not a name for it.
  • A credential sent as a bearer token is a 400, not a 401; the test asserts the refusal plus a control that the same credential works under DPoP.
  • listRepoOps for a member who never wrote answers 200 {ops: [], commit: undefined} rather than RepoNotFound.
  • A malformed client attestation makes the WIP server 500 rather than answering InvalidClientAttestation, so that test asserts only that the retry fires with the right audience.

Two findings outside this task are filed as their own issues: the SDK's DID-document key extraction accepts only Multikey (upstream accepts the legacy EcdsaSecp256k1VerificationKey2019 form too — the fixture carries a workaround that should go away), and SpaceSyncer reporting Partial forever for a repo the account never wrote to.


⏱ 19m 9s · 106 turns · tokens: 209.8k in / 77.4k out (+11294.4k cache-read)
📜 Full session transcript
→ Opened PR: #97
→ Filed follow-up issue #98: DID document key extraction accepts only Multikey, not the legacy verification-method types
→ Filed follow-up issue #99: SpaceSyncer reports Partial forever for a repo the account never wrote to

Pushed **`claude/issue-93`** — 26 integration tests that talk to a real space host, plus the docs to stand one up. ## The host No PDS release serves `com.atproto.space.*` yet, so rather than write the tests blind I built one from [atproto#5187](https://github.com/bluesky-social/atproto/pull/5187) (`permissioned-data`, still open/WIP) and ran everything against it — `pnpm install && pnpm build`, then the `oauth-provider-ui` bundle (the PDS won't boot without it), then a `TestNetworkNoAppView` on `localhost:2583` with its PLC on `:2582`. **26 passed**; the same suite skips cleanly with no environment set, so CI is unaffected. `docs/testing-spaces.md` documents the exact recipe. ## What landed `[RequiresSpacesFact]` follows the `RequiresJetstreamFact` pattern — gated on `ATPROTO_TEST_SPACES=true`, plus `ATPROTO_PDS_ADMIN_PASSWORD` and `ATPROTO_PLC_URL`. `SpaceNetworkFixture` provisions three accounts (authority / member / outsider) and deletes them afterwards; each test creates its own space. | Suite | Covers | | --- | --- | | `SpaceCredentialTests` | The exchange end to end, including that the server accepts the `htu` the SDK signs — and the refusals: replayed delegation token, token for another space, proof from another key, proof for another host, bearer presentation, `SpaceDeleted` on renewal after `deleteSpace`. | | `SpaceRepoSyncTests` | The CAR round trip — `createRecord` → `getRepo` → `SpaceRepoCar.Verify` against the server's own commit and index (LtHash, commit-context encoding, MAC, canonical DAG-CBOR order all had to agree with a real implementation). Then oplog replay, cursor resumption, divergence → full recovery, and the fallback when `since` isn't serviceable. | | `SimpleSpacePolicyTests` | Non-member refused under `member-list`, app refused under `#allowList`, the attestation retry firing, revocation at next renewal, repo boundary between co-located accounts. | ## What the real server changed about my expectations Four tests failed on the first run, all because a stub had let me assume wrongly: - A replayed delegation token comes back as `JwtReplayed`, not `InvalidDelegationToken` — the test now accepts either, since the protocol requires the refusal, not a name for it. - A credential sent as a bearer token is a 400, not a 401; the test asserts the refusal plus a control that the same credential works under DPoP. - `listRepoOps` for a member who never wrote answers `200 {ops: [], commit: undefined}` rather than `RepoNotFound`. - A malformed client attestation makes the WIP server 500 rather than answering `InvalidClientAttestation`, so that test asserts only that the retry fires with the right audience. Two findings outside this task are filed as their own issues: the SDK's DID-document key extraction accepts only `Multikey` (upstream accepts the legacy `EcdsaSecp256k1VerificationKey2019` form too — the fixture carries a workaround that should go away), and `SpaceSyncer` reporting `Partial` forever for a repo the account never wrote to. --- ⏱ 19m 9s · 106 turns · tokens: 209.8k in / 77.4k out (+11294.4k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-93/run-319.md) → Opened PR: https://git.grandiras.net/Grandiras/ATProto.NET/pulls/97 → Filed follow-up issue [#98](https://git.grandiras.net/Grandiras/ATProto.NET/issues/98): DID document key extraction accepts only `Multikey`, not the legacy verification-method types → Filed follow-up issue [#99](https://git.grandiras.net/Grandiras/ATProto.NET/issues/99): SpaceSyncer reports `Partial` forever for a repo the account never wrote to
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/ATProto.NET#93
No description provided.