From 19b1dc316ef45208b5113f8326044543ad585905 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 22 Apr 2026 16:57:31 -0700 Subject: [PATCH] test: enforce response-schema map covers every router-registered route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex flagged that validateResponse silently skips unmapped routes, so a new route or a path rename can drop runtime validation without anyone noticing. This closes the maintenance gap with four assertions that walk both createMemoryRouter and createAgentRouter's Express stacks and cross-check against MEMORY_RESPONSE_SCHEMAS / AGENT_RESPONSE_SCHEMAS: - Every memory route has a schema map entry. - Every agent route has a schema map entry. - No MEMORY_RESPONSE_SCHEMAS key points at a non-existent route (catches stale entries left behind when a route is deleted). - No AGENT_RESPONSE_SCHEMAS key points at a non-existent route. The test uses stub service / trustRepo instances — Express route registration doesn't invoke service methods, so the stack is fully populated without a live DB. Runs in ~4ms. Follow-up Codex recommended (deriving the map from the same registration source as the OpenAPI schemas) would make the gap structurally impossible instead of catching it in CI. Noted as a separate refactor — this test is the minimum bar. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../response-schema-coverage.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/routes/__tests__/response-schema-coverage.test.ts diff --git a/src/routes/__tests__/response-schema-coverage.test.ts b/src/routes/__tests__/response-schema-coverage.test.ts new file mode 100644 index 0000000..af8e448 --- /dev/null +++ b/src/routes/__tests__/response-schema-coverage.test.ts @@ -0,0 +1,85 @@ +/** + * @file Completeness gate for the response-schema map. + * + * The runtime response validator (`src/middleware/validate-response.ts`) + * silently skips routes that aren't in its route→schema map. That + * fail-open behavior means a future route rename or new route would + * quietly drop validation without failing CI. This test closes that + * maintenance gap: it walks the actual Express router stack for both + * createMemoryRouter and createAgentRouter, and asserts every + * registered route has an entry in the map (and vice-versa, so stale + * keys are caught too). + * + * Fix when this fails: add the route to + * `src/routes/response-schema-map.ts` with the schema it emits, or + * add the schema alongside the route in `src/schemas/responses.ts` + * first. + */ + +import { describe, it, expect } from 'vitest'; +import type { Router } from 'express'; +import { createMemoryRouter } from '../memories'; +import { createAgentRouter } from '../agents'; +import { + MEMORY_RESPONSE_SCHEMAS, + AGENT_RESPONSE_SCHEMAS, +} from '../response-schema-map'; +import type { MemoryService } from '../../services/memory-service.js'; +import type { AgentTrustRepository } from '../../db/agent-trust-repository.js'; + +interface RouteLayer { + route?: { + path: string; + methods: Record; + }; +} + +function enumerateRouteKeys(router: Router): string[] { + const keys: string[] = []; + // Express router internals: each layer with a `route` represents a + // concrete registered path. Middleware layers (CORS, validateResponse) + // have no `route` and are skipped. + const stack = (router as unknown as { stack: RouteLayer[] }).stack; + for (const layer of stack) { + if (!layer.route) continue; + for (const method of Object.keys(layer.route.methods)) { + keys.push(`${method} ${layer.route.path}`); + } + } + return keys.sort(); +} + +describe('response-schema map covers every router-registered route', () => { + it('every memory route has an entry in MEMORY_RESPONSE_SCHEMAS', () => { + // Stubs suffice — router registration doesn't invoke service methods. + const router = createMemoryRouter({} as unknown as MemoryService); + const routes = enumerateRouteKeys(router); + const missing = routes.filter((k) => !(k in MEMORY_RESPONSE_SCHEMAS)); + expect(missing).toEqual([]); + }); + + it('every agent route has an entry in AGENT_RESPONSE_SCHEMAS', () => { + const router = createAgentRouter({} as unknown as AgentTrustRepository); + const routes = enumerateRouteKeys(router); + const missing = routes.filter((k) => !(k in AGENT_RESPONSE_SCHEMAS)); + expect(missing).toEqual([]); + }); + + it('no MEMORY_RESPONSE_SCHEMAS key points at a non-existent route', () => { + const router = createMemoryRouter({} as unknown as MemoryService); + const routes = new Set(enumerateRouteKeys(router)); + const stale = Object.keys(MEMORY_RESPONSE_SCHEMAS).filter( + (k) => !routes.has(k), + ); + expect(stale).toEqual([]); + }); + + it('no AGENT_RESPONSE_SCHEMAS key points at a non-existent route', () => { + const router = createAgentRouter({} as unknown as AgentTrustRepository); + const routes = new Set(enumerateRouteKeys(router)); + const stale = Object.keys(AGENT_RESPONSE_SCHEMAS).filter( + (k) => !routes.has(k), + ); + expect(stale).toEqual([]); + }); +});