import { readFile } from 'node:fs/promises'; import { isDeepStrictEqual } from 'node:util'; import { ApiV2OrganizationSourceRecordsRead200Response, ApiV2OrganizationSourceRecordsRead400Response, ApiV2OrganizationSourceRecordsRead404Response, V2OrganizationSourceRecordsList200Response, V2OrganizationSourceRecordsList400Response, } from './generated-zod/contract.ts'; const schemas = new Map([ ['list:200', V2OrganizationSourceRecordsList200Response], ['list:400', V2OrganizationSourceRecordsList400Response], ['detail:200', ApiV2OrganizationSourceRecordsRead200Response], ['detail:400', ApiV2OrganizationSourceRecordsRead400Response], ['detail:404', ApiV2OrganizationSourceRecordsRead404Response], ]); const requiredCases = new Set([ 'mixed-list', 'empty-list', 'invalid-list', 'invalid-detail', 'missing-detail', ...['budget_ubpandnubp', 'fns_sme_support_recipients', 'sro_membership_check'] .flatMap(source => [`${source}-list`, `${source}-detail`]), ]); const path = process.argv[2]; if (!path || process.argv.length !== 3) { throw new Error('Usage: bun run contract-check /absolute/runtime-fixtures.json'); } const fixtures = JSON.parse(await readFile(path, 'utf8')); if (fixtures.version !== 1 || !Array.isArray(fixtures.cases)) { throw new Error('Unsupported runtime fixture format'); } const seen = new Set(); for (const fixture of fixtures.cases) { if (seen.has(fixture.name)) throw new Error(`Duplicate case: ${fixture.name}`); seen.add(fixture.name); const schema = schemas.get(`${fixture.operation}:${fixture.status}`); if (!schema) throw new Error(`No generated response schema: ${fixture.name}`); const parsed = schema.safeParse(fixture.body); if (!parsed.success) { const issues = parsed.error.issues.slice(0, 5) .map(issue => `${issue.path.join('.') || ''}: ${issue.code}`); throw new Error(`${fixture.name} violates generated Zod: ${issues.join('; ')}`); } // A successful parse must also retain every arbitrary upstream/raw JSON key. if (!isDeepStrictEqual(parsed.data, fixture.body)) { throw new Error(`${fixture.name}: generated Zod changed or stripped response data`); } } for (const name of requiredCases) { if (!seen.has(name)) throw new Error(`Missing required runtime case: ${name}`); } const empty = fixtures.cases.find(fixture => fixture.name === 'empty-list').body; if (empty.data.length !== 0 || empty.meta.pagination.total_count !== 0 || empty.meta.pagination.total_pages !== 0) { throw new Error('Empty-list fixture must exercise empty arrays and zero counts'); } console.log(`Generated Zod runtime contract: ${seen.size} API responses passed`);