/* Unit tests for validation report evidence covering validation-report evidence (RFC-1296). Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1311: Compass header sweep — declares the validation suite's contract for test-file scaffold coverage. */ // @vitest-environment node /* * RFC-1296: validation-report.json must carry honest evidence — per-validator * diagnostic summaries, real dist facts, and reuse paths that copy previous * validator evidence (or refuse the pass). */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { computeDistFacts } from "./dist-facts.ts"; import { contractFullFromPrevious, validatorsFromPipelineReports } from "./validation-report.ts"; import type { PreviousValidationReport } from "./mission-materialization-commands.ts"; import type { KernelPipelineReport } from "@warpgogol/werkstatt-engine/kernel"; let tmp: string; beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), "vr-evidence-")); }); afterEach(() => { rmSync(tmp, { recursive: true, force: true }); }); function step( commandName: string, data: unknown, ok = true, exitCode = 0, ): KernelPipelineReport["steps"][number] { return { commandName, ok, exitCode, data } as KernelPipelineReport["steps"][number]; } function report(steps: KernelPipelineReport["steps"]): KernelPipelineReport { return { steps } as KernelPipelineReport; } describe("validation-report evidence (RFC-1296)", () => { it("AC-2: validator entry carries per-rule warning counts from step data", () => { const validators = validatorsFromPipelineReports( report([ step("typography.validate", { diagnostics: [ { ruleId: "TERMS-DRIFT-01", severity: "warning" }, { ruleId: "TERMS-DRIFT-01", severity: "warning" }, { ruleId: "TYPO-01", severity: "error" }, ], }), ]), report([ step("text.corpus.validate", { findings: [{ ruleId: "TERM-04", severity: "warning" }] }), ]), ); expect(validators).toHaveLength(2); const typo = validators[0]!; expect(typo.name).toBe("typography.validate"); expect(typo.pipeline).toBe("build.check"); expect(typo.warnings).toBe(2); expect(typo.errors).toBe(1); expect(typo.rules?.["TERMS-DRIFT-01"]).toEqual({ error: 0, warning: 2 }); expect(validators[1]!.pipeline).toBe("build.post"); expect(validators[1]!.rules?.["TERM-04"]).toEqual({ error: 0, warning: 1 }); }); it("AC-3: reuse path copies previous validators and stamps reusedFrom", () => { const previous: PreviousValidationReport = { schemaVersion: "1.0.0", missionId: "m000100", contractFull: { passed: true, validators: [ { name: "list.punctuation.validate", status: "pass", exitCode: 0, warnings: 3, rules: { "LP-03": { error: 0, warning: 3 } }, }, ], }, build: { succeeded: true, routeCount: 120, sitemapHash: "sha256:x", failedSteps: [] }, distributionReused: false, buildInputHash: null, fullBuildRan: true, validatedAt: "2026-10-14T10:00:00Z", }; const cf = contractFullFromPrevious(previous); expect(cf.passed).toBe(true); expect(cf.reusedFrom).toBe("2026-10-14T10:00:00Z"); expect(cf.validators[0]!.rules?.["LP-03"]).toEqual({ error: 0, warning: 3 }); }); it("AC-3b: reuse without previous report is not a pass", () => { const cf = contractFullFromPrevious(null); expect(cf.passed).toBe(false); expect(cf.reason).toBe("no-validator-evidence"); expect(cf.validators).toEqual([]); }); it("AC-3c: reuse with an empty validator list is not a pass", () => { const cf = contractFullFromPrevious({ contractFull: { passed: true, validators: [] }, validatedAt: "2026-10-14T10:00:00Z", } as unknown as PreviousValidationReport); expect(cf.passed).toBe(false); expect(cf.reason).toBe("no-validator-evidence"); }); it("AC-3d: reused failing validator evidence keeps passed:false", () => { const cf = contractFullFromPrevious({ contractFull: { passed: false, validators: [{ name: "x", status: "fail", exitCode: 1 }], }, validatedAt: "2026-10-14T10:00:00Z", } as unknown as PreviousValidationReport); expect(cf.passed).toBe(false); expect(cf.validators).toHaveLength(1); }); it("AC-4: computeDistFacts counts non-redirect pages and hashes the real sitemap", async () => { const dist = join(tmp, "dist"); mkdirSync(join(dist, "client", "a"), { recursive: true }); mkdirSync(join(dist, "client", "b"), { recursive: true }); mkdirSync(join(dist, "client", "stub"), { recursive: true }); writeFileSync(join(dist, "client", "a", "index.html"), "A"); writeFileSync(join(dist, "client", "b", "index.html"), "B"); writeFileSync( join(dist, "client", "stub", "index.html"), '', ); writeFileSync(join(dist, "client", "sitemap.xml"), ""); const facts = await computeDistFacts(dist); expect(facts.routeCount).toBe(2); expect(facts.sitemapHash).toMatch(/^sha256:/); expect(facts.sitemapHash).not.toBe("sha256:no-sitemap"); }); it("computeDistFacts falls back to dist root and reports no-sitemap", async () => { const dist = join(tmp, "dist2"); mkdirSync(join(dist, "x"), { recursive: true }); writeFileSync(join(dist, "x", "index.html"), "X"); const facts = await computeDistFacts(dist); expect(facts.routeCount).toBe(1); expect(facts.sitemapHash).toBe("sha256:no-sitemap"); }); });