import { test, expect, describe, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, readFile, writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { shouldTransitiveSkip, loadImportedCacheHits, persistCacheHits, clearPipelineCacheHits, type PipelineRunState, } from "../runtime/execute-pipeline.ts"; import type { KernelCommandDefinition } from "@warpgogol/werkstatt-shared/kernel"; /* RFC-0687: Unit tests for transitive cache skip logic — shouldTransitiveSkip, loadImportedCacheHits, persistCacheHits, clearPipelineCacheHits. Covers all 10 acceptance criteria test cases (a)-(j). Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. Fixtures stay inline and minimal so each case states its own setup. RFC-1311: Compass header sweep — declares the transitive suite's contract for test-file scaffold coverage. */ // @ai-invariant fixtures must not couple to internals beyond the public surface under test function makeCommand(overrides: Partial = {}): KernelCommandDefinition { return { name: "test.validate", modulePath: "test", description: "Test validator", scope: "workspace", execute: () => undefined, ...overrides, }; } function makeRunState(commands: string[] = []): PipelineRunState { return { cacheHitCommands: new Set(commands), pipelineName: "build.check", }; } describe("RFC-0687: shouldTransitiveSkip", () => { test("(a) transitive skip fires when all upstream cached", () => { const cmd = makeCommand({ name: "validator.a", validatesOutputs: ["generator.x", "generator.y"], reads: ["src/**/*.ts"], }); const state = makeRunState(["generator.x", "generator.y"]); expect(shouldTransitiveSkip(cmd, state)).toBe(true); }); test("(b) no skip when upstream cache miss", () => { const cmd = makeCommand({ name: "validator.a", validatesOutputs: ["generator.x", "generator.y"], reads: ["src/**/*.ts"], }); const state = makeRunState(["generator.x"]); expect(shouldTransitiveSkip(cmd, state)).toBe(false); }); test("(c) no skip for cacheable: false validators", () => { const cmd = makeCommand({ name: "validator.a", validatesOutputs: ["generator.x"], cacheable: false, }); const state = makeRunState(["generator.x"]); expect(shouldTransitiveSkip(cmd, state)).toBe(false); }); test("(d) transitive skip through a chain of 2 validators", () => { const validatorA = makeCommand({ name: "validator.a", validatesOutputs: ["generator.g"], reads: ["src/**/*.ts"], }); const validatorB = makeCommand({ name: "validator.b", validatesOutputs: ["generator.g", "validator.a"], reads: ["src/**/*.ts"], }); const state = makeRunState(["generator.g", "validator.a"]); expect(shouldTransitiveSkip(validatorA, state)).toBe(true); expect(shouldTransitiveSkip(validatorB, state)).toBe(true); }); test("(e) --force disables transitive skip (empty cacheHitCommands)", () => { const cmd = makeCommand({ name: "validator.a", validatesOutputs: ["generator.x"], reads: ["src/**/*.ts"], }); const state = makeRunState([]); expect(shouldTransitiveSkip(cmd, state)).toBe(false); }); test("(h) no skip when validatesOutputs is empty or undefined", () => { const cmdWithEmpty = makeCommand({ name: "validator.a", validatesOutputs: [], reads: ["src/**/*.ts"], }); const cmdWithout = makeCommand({ name: "validator.b", reads: ["src/**/*.ts"], }); const state = makeRunState(["generator.x"]); expect(shouldTransitiveSkip(cmdWithEmpty, state)).toBe(false); expect(shouldTransitiveSkip(cmdWithout, state)).toBe(false); }); }); describe("RFC-0687: loadImportedCacheHits", () => { let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "rfc0687-")); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); test("(f) stale entries (>30 min) are ignored", async () => { const staleTimestamp = Date.now() - 31 * 60 * 1000; const data = { pipelines: { "build.prepare": { commands: ["stale.generator"], writtenAt: staleTimestamp, }, }, }; await mkdir(join(tempDir, ".cache"), { recursive: true }); await writeFile( join(tempDir, ".cache", "pipeline-cache-hits.json"), JSON.stringify(data), "utf8", ); const hits = await loadImportedCacheHits(tempDir, "build.check"); expect(hits.size).toBe(0); }); test("(g) cross-pipeline skip works — entries from other pipelines merged", async () => { const freshTimestamp = Date.now() - 5 * 60 * 1000; const data = { pipelines: { "build.prepare": { commands: ["generator.x", "generator.y"], writtenAt: freshTimestamp, }, }, }; await mkdir(join(tempDir, ".cache"), { recursive: true }); await writeFile( join(tempDir, ".cache", "pipeline-cache-hits.json"), JSON.stringify(data), "utf8", ); const hits = await loadImportedCacheHits(tempDir, "build.check"); expect(hits.has("generator.x")).toBe(true); expect(hits.has("generator.y")).toBe(true); expect(hits.size).toBe(2); }); test("(i) corrupt JSON falls back to empty set", async () => { await mkdir(join(tempDir, ".cache"), { recursive: true }); await writeFile( join(tempDir, ".cache", "pipeline-cache-hits.json"), "{ not valid json", "utf8", ); const hits = await loadImportedCacheHits(tempDir, "build.check"); expect(hits.size).toBe(0); }); test("missing file returns empty set", async () => { const hits = await loadImportedCacheHits(tempDir, "build.check"); expect(hits.size).toBe(0); }); test("current pipeline entries are excluded", async () => { const freshTimestamp = Date.now(); const data = { pipelines: { "build.check": { commands: ["self.generator"], writtenAt: freshTimestamp, }, "build.prepare": { commands: ["other.generator"], writtenAt: freshTimestamp, }, }, }; await mkdir(join(tempDir, ".cache"), { recursive: true }); await writeFile( join(tempDir, ".cache", "pipeline-cache-hits.json"), JSON.stringify(data), "utf8", ); const hits = await loadImportedCacheHits(tempDir, "build.check"); expect(hits.has("other.generator")).toBe(true); expect(hits.has("self.generator")).toBe(false); }); }); describe("RFC-0687: persistCacheHits", () => { let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "rfc0687-")); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); test("(j) persistCacheHits preserves entries for other pipelines", async () => { const initialData = { pipelines: { "build.prepare": { commands: ["prep.generator"], writtenAt: Date.now(), }, }, }; await mkdir(join(tempDir, ".cache"), { recursive: true }); await writeFile( join(tempDir, ".cache", "pipeline-cache-hits.json"), JSON.stringify(initialData), "utf8", ); await persistCacheHits(tempDir, "build.check", new Set(["check.validator"])); const raw = await readFile(join(tempDir, ".cache", "pipeline-cache-hits.json"), "utf8"); const data = JSON.parse(raw); expect(data.pipelines["build.prepare"].commands).toContain("prep.generator"); expect(data.pipelines["build.check"].commands).toContain("check.validator"); }); test("persistCacheHits creates file when missing", async () => { await persistCacheHits(tempDir, "build.check", new Set(["new.command"])); const raw = await readFile(join(tempDir, ".cache", "pipeline-cache-hits.json"), "utf8"); const data = JSON.parse(raw); expect(data.pipelines["build.check"].commands).toContain("new.command"); }); test("persistCacheHits overwrites same pipeline entry", async () => { const initialData = { pipelines: { "build.check": { commands: ["old.command"], writtenAt: Date.now() - 1000, }, }, }; await mkdir(join(tempDir, ".cache"), { recursive: true }); await writeFile( join(tempDir, ".cache", "pipeline-cache-hits.json"), JSON.stringify(initialData), "utf8", ); await persistCacheHits(tempDir, "build.check", new Set(["new.command"])); const raw = await readFile(join(tempDir, ".cache", "pipeline-cache-hits.json"), "utf8"); const data = JSON.parse(raw); expect(data.pipelines["build.check"].commands).toEqual(["new.command"]); }); }); describe("RFC-0687: clearPipelineCacheHits", () => { let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "rfc0687-")); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); test("clear empties all pipeline entries", async () => { const initialData = { pipelines: { "build.prepare": { commands: ["prep.generator"], writtenAt: Date.now(), }, "build.check": { commands: ["check.validator"], writtenAt: Date.now(), }, }, }; await mkdir(join(tempDir, ".cache"), { recursive: true }); await writeFile( join(tempDir, ".cache", "pipeline-cache-hits.json"), JSON.stringify(initialData), "utf8", ); await clearPipelineCacheHits(tempDir); const raw = await readFile(join(tempDir, ".cache", "pipeline-cache-hits.json"), "utf8"); const data = JSON.parse(raw); expect(Object.keys(data.pipelines)).toHaveLength(0); }); test("clear creates empty file when missing", async () => { await clearPipelineCacheHits(tempDir); const raw = await readFile(join(tempDir, ".cache", "pipeline-cache-hits.json"), "utf8"); const data = JSON.parse(raw); expect(data.pipelines).toEqual({}); }); });