93 lines
6.4 KiB
JavaScript
93 lines
6.4 KiB
JavaScript
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}`)
|