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
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)

1 participant