11.3.11 End-to-end conformance — drive the whole planning cadence as an EXTERNAL HTTP client with a real PAT
11.3's E2E, in the form the story actually has: the client is curl, so the acceptance is a conformance suite that drives the endpoints from outside — a real minted PAT in an Authorization header, real routes, real Postgres, no in-process shortcuts and no service imports to set up state the API itself can create.
This is the story's verification_recipe, automated. The vitest gate (11.3.10) proves the parts; this proves the JOURNEY — and it is the only test that can, because every seam it crosses is one no single route's unit test owns.
No acceptance video — no user-observable surface, so the story accepts on its tests alone, exactly as 11.1 and 11.2 did.
The walk
The journey step, end to end, as one ordered scenario:
list projects → read one → list its sprints → create a sprint → move a batch of backlog items into it → read the sprint's members → read the backlog and find them gone → start the sprint → read the baseline → ask what is ready → complete the sprint → find the unfinished work back in the backlog.
Every step is an HTTP call whose ONLY input is what a previous response returned — a projectKey from the project list, a sprintId from the create, MOTIR-<n> keys from the backlog. That constraint is the point: it proves the API is navigable by a client that started with nothing but a token, which is the actual claim this story makes. A step that needs a value no response carries is a finding, not a fixture to hard-code.
What only this level can catch
- The
Locationheader on create is followable — issue the create, then GET the URL it returned verbatim. - A cursor survives a round trip through a real client — page a collection to exhaustion using only
nextCursor, and confirm the union equals the collection with no skips or duplicates. - The rate-limit headers are present on every response, success and refusal alike, and a full paged scan of a realistic collection does not trip the 60/min budget (11.1's shipped seam test, re-run over this story's collections).
- Refusals are refusals, over HTTP: a
read-only token gets 403 on every sprint write and both membership moves; asprints:writetoken whose owner is not a sprint admin gets 403 too, with a distinguishablecode; a token from another workspace gets 404 on every path taking aprojectKeyorsprintId. - Nothing leaks an internal cuid for a work item anywhere in the journey's responses.
Acceptance criteria
- The whole walk above runs as one ordered scenario against real routes + real Postgres with a real minted PAT, using only values returned by previous responses.
- The suite drives the API as an EXTERNAL client — it does not import a service, a repository or Prisma to arrange or assert state that the API itself exposes. (Minting the token and creating the tenant fixture are the permitted exceptions, exactly as the shipped conformance suites do it.)
- Both refusal sweeps are asserted per endpoint, not once:
read-only → 403 on every write; a non-adminsprints:writeowner → 403; another workspace's resource → 404 on every keyed path. - A paged scan driven purely by
nextCursorreturns each row exactly once, over a collection larger than one page. - The
Locationheader returned by sprint create resolves to the created sprint. - Every response carries
x-request-idand theX-RateLimit-*headers; the journey completes inside the per-token budget. - Failures name the STEP that failed, not just an assertion — a conformance suite that fails as "expected 200, got 404" on line 300 costs more than it saves.
- ONE PR against
motir-core.
Context refs
tests/api/v1/conformance.test.ts— 11.1's shipped external-client harness: PAT minting, the real-route driver, and the fixture conventions to reuse.tests/api/v1/work-item-conformance.test.ts— 11.2.12's journey suite, the direct precedent for this card's shape.lib/apiTokens/— how a scoped token is minted for a test, including aread-only grant.docs/decisions/public-api-conventions.md§3 (the scope table), §4 (the status table), §6 (the 60/min budget + headers).- Blockers: every code subtask of this story — 11.3.2, 11.3.3, 11.3.4, 11.3.5, 11.3.6, 11.3.7, 11.3.8, 11.3.9. Parent story: 11.3.