import fs from 'node:fs' const read = p => JSON.parse(fs.readFileSync(p, 'utf8')) const scenarios = read('contracts/testing/kbx.test-scenarios.json') const fixtures = read('contracts/testing/kbx.test-fixtures.json') const screens = read('generated/screen-manifest.json') const apis = read('generated/api-manifest.json') const permissions = read('generated/permission-manifest.json') const problems = new Set(read('generated/problem-manifest.json').types ?? []) const screenIds = new Set(screens.map ? screens.map(x=>x.id) : (screens.screens ?? []).map(x=>x.id)) const apiMap = new Map((apis.operations ?? []).map(x => [x.id, x])) const permissionIds = new Set((permissions.permissions ?? []).map(x => x.id)) const fixtureSetIds = new Set((fixtures.fixtureSets ?? []).map(x => x.id)) const fixtureRefs = new Set(Object.keys(fixtures.refs ?? {})) const errors = [] if (!fixtures.syntheticOnly || !scenarios.principles?.syntheticFixturesOnly || !scenarios.principles?.noProductionData) errors.push('test contracts must declare synthetic-only/no-production-data policy') if (fixtures.fixedReferenceClock !== scenarios.principles?.fixedReferenceClock) errors.push('scenario and fixture reference clocks differ') const ids = new Set() for (const s of scenarios.scenarios ?? []) { if (ids.has(s.id)) errors.push(`duplicate scenario id: ${s.id}`) ids.add(s.id) if (!/^scenario\.[a-z0-9.-]+$/.test(s.id)) errors.push(`invalid scenario id: ${s.id}`) if (!screenIds.has(s.screenId)) errors.push(`${s.id}: unknown screenId ${s.screenId}`) if (!['e2e','integration','contract'].includes(s.kind)) errors.push(`${s.id}: invalid kind ${s.kind}`) if (!['database-reset','tenant-reset','ui-only'].includes(s.isolation)) errors.push(`${s.id}: invalid isolation ${s.isolation}`) if (!(s.assertions?.length > 0)) errors.push(`${s.id}: assertions required`) if (!(s.evidence?.length > 0)) errors.push(`${s.id}: evidence required`) for (const setId of s.fixtureSets ?? []) if (!fixtureSetIds.has(setId)) errors.push(`${s.id}: unknown fixture set ${setId}`) for (const p of s.requiredPermissions ?? []) if (!permissionIds.has(p)) errors.push(`${s.id}: unknown permission ${p}`) for (const opId of s.apiOperations ?? []) { const op = apiMap.get(opId) if (!op) { errors.push(`${s.id}: unknown api operation ${opId}`); continue } if (!(s.steps ?? []).some(step => step.operationId === opId)) errors.push(`${s.id}: API ${opId} declared but no step uses it`) if (op.idempotency === 'required') { const uses = (s.steps ?? []).filter(step => step.operationId === opId) if (!uses.some(step => typeof step.idempotencyKeyRef === 'string')) errors.push(`${s.id}: required-idempotency API ${opId} needs idempotencyKeyRef`) } } for (const step of s.steps ?? []) { if (step.operationId && !apiMap.has(step.operationId)) errors.push(`${s.id}/${step.id}: unknown step operation ${step.operationId}`) if (step.fixtureRef && !fixtureRefs.has(step.fixtureRef)) errors.push(`${s.id}/${step.id}: unknown fixtureRef ${step.fixtureRef}`) if (step.idempotencyKeyRef && !fixtureRefs.has(step.idempotencyKeyRef)) errors.push(`${s.id}/${step.id}: unknown idempotencyKeyRef ${step.idempotencyKeyRef}`) if (step.problemType && !problems.has(step.problemType)) errors.push(`${s.id}/${step.id}: unknown problemType ${step.problemType}`) const raw = JSON.stringify(step) if (/https?:\/\//i.test(raw) || /\/api\//i.test(raw)) errors.push(`${s.id}/${step.id}: raw URL/API route is forbidden; use operationId`) } } for (const set of fixtures.fixtureSets ?? []) { for (const ref of set.refs ?? []) if (!fixtureRefs.has(ref)) errors.push(`${set.id}: unknown ref ${ref}`) } // Simple synthetic-data guard: real-looking Korean mobile numbers must use 0000 exchange in canonical fixtures. const fixtureText = JSON.stringify(fixtures) for (const m of fixtureText.matchAll(/010-(\d{4})-(\d{4})/g)) if (m[1] !== '0000') errors.push(`fixture contains non-canonical phone pattern: ${m[0]}`) for (const [ref, value] of Object.entries(fixtures.refs ?? {})) { const serialized = JSON.stringify(value) if (/\b\d{13}\b/.test(serialized)) errors.push(`${ref}: production-like 13-digit barcode forbidden in canonical fixture`) if (value.kind === 'customer' && !String(value.code ?? '').startsWith('TEST-')) errors.push(`${ref}: customer code must start TEST-`) if (value.kind === 'order' && !String(value.orderNo ?? '').startsWith('TEST-')) errors.push(`${ref}: orderNo must start TEST-`) } const golden = ['OMS-ORD-001','OMS-ORD-002','OMS-ORD-003','WMS-PICK-001','COMMON-OPS-001','COMMON-REC-001','ERP-INV-MOVE-001','ERP-PRICE-001','ERP-INV-001','ERP-MST-ITEM-001'] for (const screenId of golden) if (!(scenarios.scenarios ?? []).some(s => s.screenId === screenId)) errors.push(`canonical scenario missing for ${screenId}`) const seed = fs.readFileSync('tests/fixtures/postgres/20-seed-v18.sql','utf8') const reset = fs.readFileSync('tests/fixtures/postgres/10-reset-v18.sql','utf8') for (const [name,text] of [['seed',seed],['reset',reset]]) { if (!text.includes('KBX_SCENARIO_TEST_ONLY')) errors.push(`${name} SQL missing environment guard`) if (/\btruncate\b/i.test(text)) errors.push(`${name} SQL must not truncate arbitrary host data`) } const generated = read('generated/test-scenario-manifest.json') if (generated.scenarios.length !== scenarios.scenarios.length) errors.push('generated scenario manifest count mismatch') const generatedFixtures = read('generated/test-fixture-manifest.json') if (!generatedFixtures.syntheticOnly) errors.push('generated fixture manifest lost syntheticOnly') const tsGenerated = fs.readFileSync('packages/kbx-contracts/src/generated/testScenarioCatalog.ts','utf8') const csGenerated = fs.readFileSync('backend/Shared/Testing/Generated/KbxTestScenarioCatalog.g.cs','utf8') if (!tsGenerated.includes(generated.sourceSha256) || !csGenerated.includes(generated.sourceSha256)) errors.push('scenario source SHA parity missing in TS/C# generated catalogs') if (!tsGenerated.includes(generated.fixtureSourceSha256) || !csGenerated.includes(generated.fixtureSourceSha256)) errors.push('fixture source SHA parity missing in TS/C# generated catalogs') if (errors.length) { console.error('test governance FAIL') for (const e of errors) console.error(`- ${e}`) process.exit(1) } console.log(`test governance PASS: scenarios=${scenarios.scenarios.length}, fixtureSets=${fixtures.fixtureSets.length}, refs=${fixtureRefs.size}, goldenCoverage=${golden.length}/${golden.length}`)