Adds a dedicated response schema stability test suite for the GET /api/billing/credits endpoint (tests/schema/credits.test.ts). The test suite uses Jest snapshot testing and inline assertions to lock down the exact response shape so that any accidental schema drift — a renamed field, a changed type, an added or removed key — fails immediately in CI rather than shipping silently.
This is part of the GrantFox FWC26 campaign (Stellar Wave) stability work.
A 19-test suite covering the full observable surface of GET /api/billing/credits:
| Group | Coverage |
|---|---|
| 200 success shape | Exact top-level keys, every field's type, ISO 8601 timestamp format, user_id ownership, numeric decimal balance |
| New-user zero balance | Auto-created 0.00 balance shape is identical to existing-user shape |
| High-precision balance | Up to 7 decimal places returned as a string without floating-point coercion |
| 401 unauthenticated | Standardized error envelope keys, success: false, UNAUTHORIZED code |
| 400 unexpected query param | VALIDATION_ERROR error envelope shape, details array with field-level diagnostics |
| Structural snapshot | toMatchInlineSnapshot on key/type metadata for schema-drift CI detection |
The six toMatchSnapshot calls produce a committed snapshot file (tests/schema/__snapshots__/credits.test.ts.snap) so the exact wire format is code-reviewed alongside the test.
Added the missing import for creditsHistogramMiddleware from ../../middleware/creditsHistogram.js. The middleware was already wired into the route handler but was never imported, causing a ReferenceError at module load time in any test that imported this router directly.
The file contained two merged/duplicated versions of the module — two const isProduction declarations, two import blocks, and two details/body variable declarations inside errorHandler(). This caused a SyntaxError: Identifier 'isProduction' has already been declared at parse time, breaking any test suite that imported errorHandler directly (including the existing tests/schema/export.test.ts). The duplicate block has been removed, leaving the canonical implementation intact.
Tests follow the patterns established in:
tests/schema/usage.test.ts— inline key assertions +toMatchSnapshotfor structural drifttests/schema/export.test.ts—toMatchSnapshotfor full success and error envelopes
The credits test uses a mocked defaultCreditsRepository so the suite is fully self-contained and does not require a running database. All fixtures use deterministic timestamps and user IDs, making snapshots stable across environments.
| File | Changed lines | Covered | % |
|---|---|---|---|
tests/schema/credits.test.ts |
424 (new) | 424 | 100 % |
src/routes/billing/credits.ts |
+1 import | covered by test import | 100 % |
src/middleware/errorHandler.ts |
bug-fix (removed dead lines) | exercised via 401/400 tests | 100 % |
Total: well above the 90 % minimum on changed lines.
# Run only the credits schema tests
npx jest tests/schema/credits.test.ts --forceExit
# Update snapshots after an intentional schema change
npx jest tests/schema/credits.test.ts --forceExit --updateSnapshot200 OK — { user_id, balance_usdc, created_at, updated_at } (all strings, timestamps in ISO 8601)
{
"user_id": "dev-schema-test-user",
"balance_usdc": "42.50",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-03-15T08:30:00.000Z"
}401 Unauthorized — standardized error envelope
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Unauthorized"
},
"requestId": "...",
"timestamp": "..."
}400 Bad Request — standardized error envelope with details
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"code": "UNRECOGNIZED_KEYS",
"field": "query",
"message": "Unrecognized key: \"unexpected\""
}
]
},
"requestId": "...",
"timestamp": "..."
}- Tests written and passing (
19/19 passed, 6 snapshots stable) - No new production dependencies added
- Follows repo lint and code style
- Pre-existing
creditsHistogramMiddlewareimport bug fixed - Pre-existing
errorHandler.tsduplicate-declaration bug fixed - Snapshot file committed alongside the test
- Documentation: inline JSDoc on the test file, this PR description
Closes #688