Skip to content

fix(sdk): normalize wire-vs-type drift in subdomains/faucet/usage#164

Merged
MajorTal merged 2 commits into
mainfrom
claude/relaxed-khayyam-a1be25
Apr 30, 2026
Merged

fix(sdk): normalize wire-vs-type drift in subdomains/faucet/usage#164
MajorTal merged 2 commits into
mainfrom
claude/relaxed-khayyam-a1be25

Conversation

@MajorTal
Copy link
Copy Markdown
Collaborator

Summary

Fixes the three SDK type-vs-runtime drifts called out in #163. All three were the result of unit-test mocks agreeing with the typed shape rather than the live gateway, so the drift shipped quietly.

  • subdomains.list was typed Promise<SubdomainSummary[]> but the gateway responds { subdomains: [...] }. The shipped list_subdomains MCP tool was throwing TypeError on iteration in production. SDK now unwraps the envelope.
  • UsageReport.lease_expires_at was typed as required string, but GET /projects/v1/admin/:id/usage doesn't return the field. Relaxed to ?: string | null (the field is also null for unleased accounts per the gateway's billing module). Gateway-side fix filed in run402-private#143 — once that ships, the type can tighten back up.
  • FaucetResult was typed in camelCase, but POST /faucet/v1 returns snake_case + an amount_usd_micros integer. Both request-faucet and init MCP tools were logging undefined for amount/token/transactionHash. Now normalized at the SDK boundary; amountUsdMicros: number added for callers that want the raw value.

Test mocks updated to mirror the actual wire shapes — so the next regression gets caught before it ships.

Why this happened

Every affected unit-test mock returned the typed shape, not the wire shape. Tests passed; reality didn't. Worth flagging that our integration tests don't catch this either: cli-integration.test.ts and cli-e2e.test.mjs use string-containment assertions (captured().includes("api_calls")) rather than schema/field validation, and the faucet has zero real-gateway test coverage at all (the integration test pre-funds the wallet to skip the faucet path). A contract-test harness would be a worthwhile follow-up.

Test plan

  • npm test — 483/484 unit + sync tests pass; one pre-existing failure (sync.test.ts "all SURFACE endpoints appear in llms.txt") is unrelated and reproduces on main
  • node --test --import tsx sdk/src/namespaces/allowance.test.ts sdk/src/namespaces/secrets.test.ts sdk/src/namespaces/projects.test.ts — affected SDK suites: 44/44 pass
  • node --test --import tsx src/tools/init.test.ts — MCP init suite: 10/10 pass
  • npm run build:sdk — clean
  • Smoke against live gateway: r.subdomains.list(id) returns array, r.allowance.faucet() returns populated camelCase fields, r.projects.getUsage(id).lease_expires_at is undefined

Files

  • sdk/src/namespaces/subdomains.ts — unwrap body.subdomains
  • sdk/src/namespaces/projects.types.tslease_expires_at?: string | null
  • sdk/src/namespaces/allowance.ts — normalize wire body to camelCase, add amountUsdMicros
  • sdk/src/namespaces/secrets.test.ts, projects.test.ts, allowance.test.ts, src/tools/init.test.ts — mocks now use wire shape

Closes #163.

MajorTal and others added 2 commits April 30, 2026 09:46
Three SDK methods declared one return shape and returned another at
runtime in 1.51.0, surfaced in run402#163:

- `subdomains.list` returned the gateway envelope `{ subdomains: [...] }`
  but was typed as `SubdomainSummary[]` — crashed the shipped
  `list_subdomains` MCP tool with TypeError on iteration.
- `getUsage` declared `lease_expires_at: string` but the gateway omits
  the field entirely. Relaxed to `?: string | null` here; gateway-side
  fix filed in run402-private#143.
- `allowance.faucet` declared camelCase `transactionHash`/`amount` but
  the gateway returns snake_case `transaction_hash`/`amount_usd_micros`,
  so callers saw `undefined` in log lines. Now normalized at the SDK
  boundary; `amountUsdMicros` added for callers wanting the raw number.

Test mocks updated to mirror the actual wire shapes so the next
regression gets caught before shipping — the previous mocks agreed with
the typed shape, not reality, which is how the drift slipped through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The e2e mocks for `GET /subdomains/v1` and `POST /faucet/v1` returned
the SDK's old typed shape rather than what the live gateway emits, so
they couldn't catch the drifts fixed in the previous commit. After the
SDK normalization, the subdomains mock no longer matched the unwrap
path and `subdomains list` failed in CI.

- `/subdomains/v1` GET → `{ subdomains: [...] }` (was a bare array).
- `/faucet/v1` POST → `{ transaction_hash, amount_usd_micros, ... }`
  (was `{ tx_hash, amount, ... }`).

Same principle as the SDK unit-test mock updates: tests verify reality,
not what the type says.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MajorTal MajorTal merged commit 29f6ca1 into main Apr 30, 2026
4 checks passed
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.

@run402/sdk type-vs-runtime drift: subdomains.list, projects.getUsage, allowance.faucet all return shapes that don't match their .d.ts

1 participant