import assert from 'node:assert/strict'; import test from 'node:test'; import { loadScenarioCatalog } from './catalog.mjs'; import { createGateReport, validateGateReport, } from './report.mjs'; import { selectImpactScenarios } from './impact.mjs'; const COMMIT = 'a'.repeat(40); const ARTIFACT = 'b'.repeat(64); async function passingReport() { const catalog = await loadScenarioCatalog(); return createGateReport({ commitSha: COMMIT, branch: 'main', artifactSha256: ARTIFACT, artifact: { path: '.runtime/portal', kind: 'directory-tree' }, scenarios: catalog.map((scenario) => ({ id: scenario.id, name: scenario.name, status: 'passed', cleanup_status: 'not_required', evidence: ['fixture'], })), completedAt: new Date('2026-07-26T10:00:00.000Z'), }); } test('complete report bound to main commit and artifact passes validation', async () => { const report = await passingReport(); const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: ARTIFACT, now: new Date('2026-07-26T11:00:00.000Z'), }); assert.deepEqual(result, { valid: true, errors: [] }); }); test('unknown scenario and mismatched summary fail closed', async () => { const report = await passingReport(); report.scenarios[0].status = 'unknown'; const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: ARTIFACT, now: new Date('2026-07-26T11:00:00.000Z'), }); assert.equal(result.valid, false); assert.match(result.errors.join('\n'), /REL-01 is unknown/); assert.match(result.errors.join('\n'), /summary does not match/); }); test('non-exemptable scenario cannot be marked not_applicable', async () => { const report = await passingReport(); const scenario = report.scenarios.find((item) => item.id === 'COMP-09'); scenario.status = 'not_applicable'; scenario.exemption = { scenario_id: 'COMP-09', commit_sha: COMMIT, reason: 'fixture', changed_paths_checked: ['README.md'], dependency_evidence: ['none'], reviewed_by: 'reviewer', reviewed_at: '2026-07-26T10:00:00.000Z', }; report.summary.passed -= 1; report.summary.not_applicable += 1; const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: ARTIFACT, now: new Date('2026-07-26T11:00:00.000Z'), }); assert.equal(result.valid, false); assert.match(result.errors.join('\n'), /COMP-09 is never exemptable/); }); test('reviewed exemption is accepted only for an exemptable scenario', async () => { const report = await passingReport(); const scenario = report.scenarios.find((item) => item.id === 'SEARCH-04'); scenario.status = 'not_applicable'; scenario.exemption = { scenario_id: 'SEARCH-04', commit_sha: COMMIT, reason: 'GitHub Search is disabled and no shared gateway path changed', changed_paths_checked: ['docs/example.md'], dependency_evidence: ['impact-map.json'], reviewed_by: 'independent-reviewer', reviewed_at: '2026-07-26T10:00:00.000Z', }; report.exemptions = [scenario.exemption]; report.summary.passed -= 1; report.summary.not_applicable += 1; const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: ARTIFACT, now: new Date('2026-07-26T11:00:00.000Z'), }); assert.deepEqual(result, { valid: true, errors: [] }); }); test('expired report or artifact mismatch is rejected', async () => { const report = await passingReport(); const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: 'c'.repeat(64), now: new Date('2026-07-27T11:00:00.000Z'), }); assert.equal(result.valid, false); assert.match(result.errors.join('\n'), /artifact_sha256 does not match/); assert.match(result.errors.join('\n'), /report has expired/); }); test('impact report accepts only the selected core and affected scenarios', async () => { const catalog = await loadScenarioCatalog(); const selection = { ...selectImpactScenarios({ catalog, changedPaths: ['memory-v2-lifecycle.mjs'], }), base_commit: 'c'.repeat(40), }; const selected = new Set(selection.selected_ids); const report = createGateReport({ commitSha: COMMIT, branch: 'main', artifactSha256: ARTIFACT, artifact: { path: '.runtime/portal', kind: 'directory-tree' }, mode: 'impact', selection, scenarios: catalog .filter((scenario) => selected.has(scenario.id)) .map((scenario) => ({ id: scenario.id, name: scenario.name, status: 'passed', cleanup_status: 'not_required', evidence: ['fixture'], })), completedAt: new Date('2026-07-26T10:00:00.000Z'), }); const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: ARTIFACT, now: new Date('2026-07-26T11:00:00.000Z'), }); assert.deepEqual(result, { valid: true, errors: [] }); assert.ok(report.summary.required < catalog.length); }); test('impact report rejects missing core coverage and unmapped non-full paths', async () => { const catalog = await loadScenarioCatalog(); const selection = { ...selectImpactScenarios({ catalog, changedPaths: ['docs/local-dev.md'], }), base_commit: 'c'.repeat(40), }; selection.unmapped_paths = ['unknown-runtime.mjs']; selection.selected_ids = selection.selected_ids.filter((id) => id !== 'AUTH-05'); selection.selected_total = selection.selected_ids.length; const selected = new Set(selection.selected_ids); const report = createGateReport({ commitSha: COMMIT, branch: 'main', artifactSha256: ARTIFACT, artifact: { path: '.runtime/portal', kind: 'directory-tree' }, mode: 'impact', selection, scenarios: catalog .filter((scenario) => selected.has(scenario.id)) .map((scenario) => ({ id: scenario.id, name: scenario.name, status: 'passed', cleanup_status: 'not_required', evidence: ['fixture'], })), completedAt: new Date('2026-07-26T10:00:00.000Z'), }); const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: ARTIFACT, now: new Date('2026-07-26T11:00:00.000Z'), }); assert.equal(result.valid, false); assert.match(result.errors.join('\n'), /missing core scenarios: AUTH-05/); assert.match(result.errors.join('\n'), /unmapped runtime paths block the impact Gate/); }); test('policy version 2 rejects a full production impact strategy', async () => { const catalog = await loadScenarioCatalog(); const selection = { ...selectImpactScenarios({ catalog, changedPaths: ['memory-v2-lifecycle.mjs'], }), strategy: 'full', base_commit: 'c'.repeat(40), }; const selected = new Set(selection.selected_ids); const report = createGateReport({ commitSha: COMMIT, branch: 'main', artifactSha256: ARTIFACT, artifact: { path: '.runtime/portal', kind: 'directory-tree' }, mode: 'impact', selection, scenarios: catalog .filter((scenario) => selected.has(scenario.id)) .map((scenario) => ({ id: scenario.id, name: scenario.name, status: 'passed', cleanup_status: 'not_required', evidence: ['fixture'], })), completedAt: new Date('2026-07-26T10:00:00.000Z'), }); const result = validateGateReport(report, { expectedCommit: COMMIT, expectedArtifactSha256: ARTIFACT, now: new Date('2026-07-26T11:00:00.000Z'), }); assert.equal(result.valid, false); assert.match(result.errors.join('\n'), /impact strategy must be core or impact/); });