Skip to content

test(api): pin the endpoints the data proxy calls - #671

Closed
alukach wants to merge 2 commits into
mainfrom
feat/api-data-proxy-contract
Closed

alukach wants to merge 2 commits into
mainfrom
feat/api-data-proxy-contract

Conversation

@alukach

@alukach alukach commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Follows #667, now merged, and the discussion there about keeping the data proxy's endpoints from drifting.

What

The data proxy is the one consumer this API has to stay compatible with. The CLI is being built against the API now, so it follows the spec rather than pinning it. This PR adds one test, which fails CI when a change reshapes an endpoint the proxy calls. It changes no route and nothing in the published spec.

  • data-proxy-contract.test.ts keeps PROXY_CALLS, the list of calls the proxy makes, taken from data.source.coop's src/source_api/cache.rs. It fails if one of them leaves the generated document.
  • The snapshot. For each call the test snapshots the operation's parameters, request body, statuses and 200 schema, with $refs resolved and descriptions stripped. Rewording an endpoint's docs doesn't trip it, but renaming trusted, removing a status, or changing limit's bounds does. The snapshot pins the 200 body and the status list, because those are all the proxy reads. It treats 200 as the only success, 404 as a missing bucket and 401/403 as a refusal, and never reads an error body.

Pinned here:

  • GET /products/{account_id}
  • GET /products/{account_id}/{product_id}
  • GET /products/{account_id}/{product_id}/permissions
  • POST /accounts/{account_id}/trusts/exchanges
  • POST /service-account-keys/exchanges

The proxy's sixth call, GET /data-connections/{data_connection_id}, isn't in the generated document until #657 lands. That PR, or a follow-up to it, adds one line to PROXY_CALLS.

PROXY_CALLS is kept by hand, so it can't notice the proxy starting to call something new. That half belongs on the proxy's side: source-cooperative/data.source.coop#256 checks its stub routes against the published spec.

When the test fails

An added optional field is safe, because the proxy's serde types ignore unknown fields, and jest -u accepts it. A removed or retyped field, a changed status, or a new required parameter needs the data.source.coop change first. The comment above the snapshot test says so.

What it doesn't catch

Behavior the spec doesn't express. Two examples are answering an unknown key with 200 {"active": false} rather than a 404, and saying no to a trust with a 403. Those stay with the route and operation tests that already assert them.

Snapshot size

The snapshot is 1,452 lines, almost all of it the Product schema with its embedded Account union, once for the product and once per item in the account's listing. Any change to Product or Account will show up here as a snapshot diff, which is the point: the proxy deserializes those bodies.

Docs, ADRs and llms.txt

No user-facing behavior changes, so docs.source.coop and llms.txt are unaffected. This is a guard in this repo's tests rather than a decision shared with the proxy, so no data.source.coop ADR changes; ADRs 005, 007, 013 and 014 still describe these endpoints.

Testing

  • npm run type-check passes.
  • npx jest --ci src/app/api/openapi: 36 tests, 5 snapshots pass; the snapshots are unchanged from the first commit.
  • Checked that the test bites: renaming trusted to trust in the trust exchange's answer schema fails it with a diff naming the field.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
source-coop-ui Ready Ready Preview Oct 16, 2026 2:33am UTC
source-cooperative Ready Ready Preview Oct 16, 2026 2:33am UTC

Request Review

@claude

claude Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 14s —— View job


❌ Changes requested — see findings below.

Blocking

  • src/app/api/openapi/data-proxy-contract.test.ts:34: shape drops every key named description, summary, example or examples at any depth. That includes property names inside a schema's properties map. Product very likely has a description property, and its snapshot is the biggest part of this PR. Removing or retyping that field, or changing its type, would therefore not change the snapshot. The test would pass on exactly the kind of change it exists to catch. required would still list the name, but nothing else would pin it. Check the snapshot for description under properties. Strip prose only when the key isn't a property name:
const PROSE = ["description", "summary", "example", "examples"];
const shape = (value: unknown, inProperties = false): unknown =>
  Array.isArray(value)
    ? value.map((v) => shape(v))
    : value && typeof value === "object"
      ? Object.fromEntries(
          Object.entries(value)
            .filter(([key]) => inProperties || !PROSE.includes(key))
            .map(([key, v]) => [key, shape(v, key === "properties" && !inProperties)])
        )
      : value;

After the fix, regenerate the snapshot (jest -u). It should gain the property-named entries, and the diff is worth reading.

Simplify (ponytail)

  • data-proxy-contract.test.ts:21-25: the Operation type and the cast in beforeAll could go. Type paths as any (or a one-line cast) instead.
  • data-proxy-contract.test.ts:28: with the fix above, the helper grows. If the proxy reads no prose fields, an alternative is to snapshot without stripping and accept the docs-reword churn. Keeping the stripper is the better choice, but note the tradeoff.

Docs

The description names the docs, llms.txt and ADRs 005, 007, 013 and 014, and says why they still hold. Nothing to add.


💰 Estimated review cost: $0.10 · 0m13s · 4 turns

alukach and others added 2 commits October 15, 2026 19:31
The data proxy is the one consumer the API has to stay compatible with. The operations it calls (data.source.coop's src/source_api/cache.rs) are marked with forDataProxy, which adds `x-consumers: ["data-proxy"]` and opens their description with a warning, and data-proxy-contract.test.ts snapshots each one's parameters, request body, statuses and 200 schema, references resolved and prose stripped. A change that reshapes what the proxy reads fails CI until the snapshot is updated on purpose, and the test also fails if the marked set stops matching the list of calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The x-consumers marker and the forDataProxy description it came with duplicated PROXY_CALLS: no renderer or tool reads an x- extension, and the test only used the marker to find the operations its own list already names. The test now looks those operations up directly and fails if one leaves the document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach
alukach force-pushed the feat/api-data-proxy-contract branch from 37565e7 to 41fcf39 Compare October 16, 2026 02:31
alukach added a commit that referenced this pull request Oct 16, 2026
Follows #667, now merged. Writes down the API conventions this round of
PRs settled on, so the next endpoint follows them without anyone
reconstructing them from the code.

## What

- **`API.md`** at the repository root, beside `CONTRIBUTING.md`:
- **One operation, thin adapters:** what an operation decides, what a
route and a server action may do, and the exceptions for routes no
session can call.
- **Documented where it's defined:** `registerPath` beside the handler,
the coverage test and its `UNREGISTERED` allowlist, published prose,
explicit `HEAD`, `bearer` and `optionalBearer`, `internal`, and
`deprecated`.
- **Resources and methods:** URL and field naming, and a table of what
each method means and answers. Deactivating is `PATCH { "disabled": true
}`, never `DELETE`, and one resource has one edit endpoint.
  - **Lists:** `{items, next_cursor}`, never a bare array.
- **Errors:** the shared shape and when each status applies, the 500
`internal` error and the trust exchange's `{trusted: false}` 403,
authorizing before validating where the permission doesn't depend on the
input, and answering 404 for things the caller may not know of.
- **Writes:** carry only the fields that changed, and what
`accountsTable.update` always writes.
- **Compatibility:** the data proxy is pinned by a test, the CLI follows
the reference, and anyone else gets a traffic check, then deprecation
rather than removal.
- **`CLAUDE.md`** (and `AGENTS.md`, which links to it) gets a short
**API** section telling an agent to read `API.md` before touching
`/api/v1`.
- **`CONTRIBUTING.md`**'s "Server Actions & API Routes" paragraph said
both call `isAuthorized()` themselves. It now describes them as adapters
over one operation and points at `API.md`.

## Where it lives

A root `API.md` rather than more of `CLAUDE.md`: these are conventions
for whoever writes an endpoint, person or agent, and much of it is
visible in the code once you know where to look, which is what
`CLAUDE.md` says it isn't for. `CLAUDE.md` carries the pointer, so
agents still load it at the right moment. `CONTRIBUTING.md` links to it.

## Merge order

The guide describes the API as it stands once the open PRs from this
round merge, and names things they add:

- `optionalBearer`, `internal` and `deprecated` endpoints (#667)
- `data-proxy-contract.test.ts` (#671)
- explicit `HEAD` (#672)
- `pageSchema` and `onePage` (#673)
- one `PATCH` per account (#674)
- `DELETE` that only deletes (#675)

Merge it after them, or trim the line that names whichever of them
doesn't land.

## Where the code doesn't follow the guide yet

The guide states the conventions, not the code's progress toward them.
Known gaps, tracked outside the docs:

- The `/data-connections` routes and the data-connection actions still
read tables and call `isAuthorized()` directly: #593.
- The credentials, profile-image and `createAccount` actions still call
`isAuthorized()` directly: #592 and #594.
- `updateProduct`, `deleteProduct` and `updateMembership` validate the
body before authorizing, so a caller who may not act sees field errors
instead of a 403.
- `accountsTable.update` always writes `name`, `type` and whole metadata
maps, so `updateAccount` can undo a concurrent edit.

## Docs, ADRs and `llms.txt`

Repository-internal guidance only: no user-facing flow changes, so
docs.source.coop and `llms.txt` are unaffected, and it records no
decision shared with the data proxy beyond the existing contract test,
so no data.source.coop ADR changes.

## Testing

Documentation only; no code changed. Each claim in `API.md` was checked
against the code after a review pass found several the code didn't hold
to (`route-coverage.test.ts`, `accountsTable.update`, `withApiSession`'s
error handling, the revocations and trust-exchange routes). Checked that
`AGENTS.md` still resolves to `CLAUDE.md` and that the prose in `API.md`
wraps at 80, as the repository's Markdown does.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@alukach

alukach commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Not sure this should be done in this codebase, rather the data proxy should have these tests

@alukach alukach closed this Oct 16, 2026

This branch was successfully deployed

2 active deployments
Preview – source-cooperative — 41fcf399 Deployed Oct 16, 2026 by vercel[bot]
Preview – source-coop-ui — 41fcf399 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant