Skip to content

feat(http): validate the /me and usage response shapes at runtime - #291

Open
Davidson3556 wants to merge 1 commit into
TestSprite:mainfrom
Davidson3556:feat/validate-account-response-shapes
Open

feat(http): validate the /me and usage response shapes at runtime#291
Davidson3556 wants to merge 1 commit into
TestSprite:mainfrom
Davidson3556:feat/validate-account-response-shapes

Conversation

@Davidson3556

@Davidson3556 Davidson3556 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Extends the response-validation layer established by #266 to the account surfaces — the GET /me reads and the usage projection. This is the first of the incremental groups #277 asks for (reads / writes / account surfaces); the test and project shapes are left for follow-ups.

Part of #277. Tracked by #292.

All three /me callers go through the generic client.get, so all three still blind-cast the body:

caller shape what a drifted body did before
auth status / whoami MeResponse m.scopes.join(', ') and m.scopes.includes(...) are unguarded → raw TypeError (exit 1) in text mode, and exit 0 with a partial identity under --output json
usage / credits UsageResponse a string credits reached the Math.floor(credits / creditsPerRun) pre-flight arithmetic
doctor MeIdentity fully-optional projection, but its local interface had already drifted from the stubbed schema (v3Enabled existed on the command side only)

Changes:

  • New ME_RESPONSE_SCHEMA and USAGE_RESPONSE_SCHEMA in src/lib/response-schemas.ts.
  • Wires the previously-unwired ME_IDENTITY_SCHEMA into doctor, and adds the missing v3Enabled to it.
  • doctor's MeIdentity is now a type alias of the schema's wire type, so interface and schema cannot drift apart again.
  • Schemas are passed at the three client.get('/me', { schema }) call sites. init inherits validation through runWhoami, where the existing try/catch already degrades to a placeholder identity — so a drifted /me never fails the whole init.

Policy compliance (#266, unchanged)

  • Every object is looseObject: additive server fields pass and are preserved, so --output json stays byte-faithful. Covered by a test that sends an unknown orgName through whoami --output json.
  • env is validated as an open string via openWireLiteral, so a new deployment tier (sandbox, …) cannot hard-fail the CLI.
  • Fixture-evidence discipline for required vs optional:
    • RequireduserId, keyId, scopes, env. Every /me fixture in the suite supplies all four (auth.test.ts, init.test.ts, usage.test.ts, cli.subprocess.test.ts, test/mock-backend/fixtures.ts, lib/dry-run/samples.ts), and scopes is exactly the field whose absence crashes today.
    • Optional, no defaultemail, displayName, v3Enabled, credits, subPlan, creditsPerRun. These are the documented forward-compat fields the backend does not send yet; every renderer branch is already gated on presence, so absence must stay absence.
    • doctor deliberately keeps the fully-optional projection rather than reusing ME_RESPONSE_SCHEMA: OK_ME in doctor.test.ts is { userId, keyId } with no scopes/env, and a connectivity probe must not fail on a partial identity.

Deliberately not wired

The pre-write ping in runConfigure (auth setup) discards the /me body and only checks that the key was accepted. Validating there would let an unrelated /me field change block credential setup, which is the one path a user needs when everything else is broken.

Related issue

Closes #292 — the account-surfaces slice of #277.

#277 is an umbrella that lists seven shapes and says "one PR per coherent group of shapes is fine", so it stays open for the remaining groups rather than being closed by this PR. #292 scopes just the /me and usage surfaces and is assigned to me.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Documentation only
  • Build / CI / chore

Checklist

  • PR targets the main branch.
  • Commits follow Conventional Commits (feat(...), fix(...), docs(...), …).
  • npm run lint and npm run format:check pass.
  • npm run typecheck passes.
  • npm test passes and coverage stays at or above the 80% gate.
  • New behavior is covered by unit tests (mock-based; no network or credentials required).
  • No secrets, API keys, internal endpoints, or personal data are included.
  • User-facing changes are reflected in README.md / DOCUMENTATION.md where relevant. — n/a, no user-facing surface changes; the only visible difference is a crash becoming a typed envelope.

Notes for reviewers

Before / after

Against a local server returning a /me body without scopes:

Before (main) — text mode leaks an internal TypeError:

$ testsprite auth status
Error: Cannot read properties of undefined (reading 'join')
exit=1

Before (main) — JSON mode is worse, it silently succeeds:

$ testsprite auth status --output json
{
  "userId": "u-1",
  "keyId": "k-1",
  "env": "development"
}
exit=0

An agent reading error.code sees nothing and scopes is undefined.

After (this branch):

$ testsprite auth status
Error: Response shape mismatch from /me.
Retry; if it persists, report this requestId (the server returned an unexpected shape).
requestId: cli_c5ad1928-6125-4a87-b18d-3c8e7f158fb0
exit=1

$ testsprite auth status --output json
{
  "error": {
    "code": "INTERNAL",
    "message": "Response shape mismatch from /me.",
    "nextAction": "Retry; if it persists, report this requestId (the server returned an unexpected shape).",
    "requestId": "cli_9234c39f-81d6-4d58-a9f6-b15d408e7a43",
    "details": {
      "issues": [
        { "path": "scopes", "message": "Invalid key: Expected \"scopes\" but received undefined" }
      ]
    }
  }
}
exit=1

Gates

npm run typecheck, npm run lint, npm run format:check, npm test (2041 passed / 2 skipped, 58 files), npm run build, npm run test:e2e (60 passed / 1 skipped) — all green on this branch. The only format:check warning is the untracked local .claude/settings.local.json, which is not part of this change.

Follow-ups

Happy to take the remaining #277 groups in separate PRs: the read shapes (CliTest, CliTestCode, CliProject), the write shapes (CliCreateTestResponse), CliFailureSummary, and the leftover as T casts in the http.ts helpers. Also happy to split or reshape this one if you would rather see a single larger PR.

AI usage

Written with AI assistance (Claude Code). I reviewed every line, chose the required-vs-optional split from the fixtures cited above, and verified the before/after behavior and all gates locally.

Extends the TestSprite#266 validation layer to the account surfaces. `GET /me` is
read by three commands through the generic `client.get`, all of which
blind-cast the body:

- `auth status`/`whoami` renders `m.scopes.join(', ')` and computes
  missing scopes via `m.scopes.includes(...)` with no guard, so a `/me`
  body without `scopes` crashed with a raw `TypeError: Cannot read
  properties of undefined (reading 'join')` (exit 1). Under
  `--output json` it was worse: the renderer never ran, so the CLI
  printed the partial identity and exited 0, and an agent reading
  `scopes` got undefined.
- `usage` feeds `credits` / `creditsPerRun` into
  `Math.floor(credits / creditsPerRun)`, so a string balance reached the
  pre-flight arithmetic unchecked.
- `doctor`'s connectivity probe reads a fully-optional projection whose
  local interface had already drifted from the stubbed schema
  (`v3Enabled` existed on the command side only).

Adds `ME_RESPONSE_SCHEMA` and `USAGE_RESPONSE_SCHEMA`, wires the
previously-unwired `ME_IDENTITY_SCHEMA` into `doctor`, and aliases
doctor's `MeIdentity` to the schema's wire type so the two cannot drift
again. Drift now surfaces as the standard typed INTERNAL envelope naming
the mismatched field paths.

Follows the TestSprite#266 policy unchanged: every object is `looseObject` so
additive server fields pass through and reach `--output json` untouched;
`env` is validated as an open string via `openWireLiteral` so a new
deployment tier cannot hard-fail; only fields that every `/me` fixture in
the suite supplies (`userId`, `keyId`, `scopes`, `env`) are required, and
the genuinely absent-safe ones (`email`, `displayName`, `v3Enabled`,
`credits`, `subPlan`, `creditsPerRun`) stay optional with no default.

Refs TestSprite#277
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

✅ This PR is linked to an issue assigned to @Davidson3556 — thanks! The needs-issue label has been removed.

@github-actions github-actions Bot added the needs-issue PR not linked to an issue yet — please open one first and claim it (see CONTRIBUTING) label Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

GET /me response validation

Layer / File(s) Summary
Shared response schemas
src/lib/response-schemas.ts, src/lib/response-schemas.test.ts
Adds typed ME_RESPONSE_SCHEMA, ME_IDENTITY_SCHEMA, and USAGE_RESPONSE_SCHEMA with required-field, optional-field, additive-field, environment, and credit validation coverage.
Command schema adoption
src/commands/auth.ts, src/commands/doctor.ts, src/commands/usage.ts, src/commands/auth.test.ts, src/commands/usage.test.ts
Auth, doctor, and usage validate /me responses through the shared schemas; tests cover typed internal errors and preservation of additive JSON fields.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: ruili-testsprite

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: runtime validation for /me and usage response shapes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot removed the needs-issue PR not linked to an issue yet — please open one first and claim it (see CONTRIBUTING) label Jul 26, 2026
@zeshi-du

Copy link
Copy Markdown
Contributor

Approving the direction — this is the right implementation of #292 and it follows the policy #266 established: looseObject plus open wire literals, so a new server field never breaks the CLI. Wiring the previously-stubbed ME_IDENTITY_SCHEMA into doctor is a good catch that wasn't in the original ask.

It sat 17 days fully green with no review, which was a pure review-bandwidth failure on our side, and in the meantime main moved two releases so the branch now conflicts. Please rebase and I'll merge — nothing else is outstanding.

Splitting the account surfaces out as #292 so #277 could stay open for the remaining groups was exactly the right way to handle it. For anyone else reading: CliTest, CliTestCode, CliCreateTestResponse, CliProject, CliFailureSummary, and the two remaining raw as T casts in src/lib/http.ts are still unclaimed under #277.

@zeshi-du zeshi-du left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been green since July 26 and waited 18 days for a human to look at it — that's on us, not on you, and I'm sorry for the wait. There's also a mechanical complication that happened on our end while it waited; I own that too.

The rebase this needs (not a rejection)

GitHub currently shows this PR as conflicting with main. That's because two releases (0.5.0, then 0.6.0) shipped directly on top of the commit you branched from, and they touched the same shapes you did — the org/workspace fields on /me. Concretely:

  • MeIdentityWire / ME_IDENTITY_SCHEMA in src/lib/response-schemas.ts: main added organizations? / org? since you branched. Your PR independently adds v3Enabled?. Neither side has both — the shape needs all five fields (userId, keyId, v3Enabled, organizations, org) after the merge, not one branch winning. Worth knowing: this is the exact drift your commit message describes, and it is still there on main today — doctor.ts's hand-rolled MeIdentity interface already carries all five fields; the shared schema type is the piece still missing v3Enabled. The two interim releases didn't fix that, they just added more around it.
  • Import blocks in auth.ts / doctor.ts / usage.ts conflict mechanically — we both added an import next to the same line.
  • ME_RESPONSE_SCHEMA and USAGE_RESPONSE_SCHEMA are written against MeResponse / UsageResponse as they existed when you opened this. Both interfaces have since grown activeOrg, organizations, org, and v3Enabled. Because both schemas are looseObject this is not a crash risk — those fields ride through unvalidated like any other unnamed field. But closing exactly that gap is the point of this work, so it'd be worth widening both schemas to cover the four new fields while you're already in the file. Your call, not a merge condition.

I'm happy to push the rebase myself if you'd rather not untangle it, given that it's our release cadence that caused it.

What I checked and found correct

  • Policy compliance against #266 — this is the thing that would have been a real bug. Every new schema is looseObject, and env is validated as an open string via openWireLiteral, not a closed enum. A server adding a field or a new environment tier will not hard-fail the CLI.
  • The required-vs-optional split, against the fixtures you cited. I checked all six — auth.test.ts, init.test.ts, usage.test.ts, test/cli.subprocess.test.ts, test/mock-backend/fixtures.ts, lib/dry-run/samples.ts — and every one supplies userId / keyId / scopes / env. Requiring those four and leaving the rest optional matches what the fixtures actually show, not just what the interface claims.
  • The bug itself is real and still on main: runWhoami's unguarded m.scopes.join(', ') / m.scopes.includes(...). Your before/after isn't hypothetical.
  • Error path: reuses the existing INTERNAL-envelope machinery unchanged — exit 1, an actionable nextAction, the requestId, and up to three mismatched field paths, never the response body. getDetail('issues') in your new test resolves correctly against it.
  • Forward-compatibility coverage is present at both levels: schema-unit (extra key preserved, unknown env accepted) and command-integration (whoami --output json passes an additive field straight through). That's the case that has to work for looseObject to be worth anything, and it's tested rather than asserted.
  • Leaving the runConfigure pre-write ping unvalidated is the correct call — I confirmed that call site really does discard the body and rely only on the HTTP layer's exception path.
  • doctor's error handling: I traced checkConnectivity's catch block — an INTERNAL from the new schema wiring degrades to a normal fail check line, it doesn't crash doctor. One small non-blocking addition: no test pins that specific scenario, so today the safety is provable by reading the code rather than by a test that would go red if it regressed.
  • CHANGELOG: agreed this needs no entry — same call #266 made for the same kind of change.

Nothing here is a defect in what you wrote. The scope match to #292 is exact, the schema design follows the established rules, and the tests are real. Once the rebase lands, with organizations / org folded in alongside v3Enabled, this is good to merge from my side.

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.

Validate the /me and usage response shapes at runtime (account surfaces of #277)

2 participants