/* Unit tests for execute pipeline covering aggregateCollectErrors; shouldTransitiveSkip; pipeline cache-hits persistence. 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 execute suite's contract for test-file scaffold coverage. */ // @ai-invariant fixtures must not couple to kernel internals beyond the public surface under test import { test, expect, describe, beforeEach, afterEach } from "vitest"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { aggregateCollectErrors, applyWarnOnly, shouldTransitiveSkip, loadImportedCacheHits, persistCacheHits, clearPipelineCacheHits, mergeCollectErrorsOption, type PipelineRunState, } from "../execute-pipeline.ts"; import type { StepExecutionResult } from "../pipeline-scheduler.ts"; import type { KernelExecutionReport, KernelCommandDefinition, } from "@warpgogol/werkstatt-shared/kernel"; function okReport(command: string): KernelExecutionReport { return { commandName: command, exitCode: 0, ok: true, summary: "ok", metadata: {} as never, logs: [], timing: { durationMs: 10, exceededTimeout: false }, filesModified: [], }; } function failReport(command: string): KernelExecutionReport { return { commandName: command, exitCode: 1, ok: false, summary: "fail", metadata: {} as never, logs: [], timing: { durationMs: 10, exceededTimeout: false }, filesModified: [], }; } function makeResult( stepIndex: number, report: KernelExecutionReport, dependencySkipped = false, ): StepExecutionResult { return { stepIndex, report, dependencySkipped }; } const mockCommand: KernelCommandDefinition = { name: "test.cmd", description: "test", scope: "workspace", execute: async () => ({ exitCode: 0, ok: true, summary: "ok" }), }; describe("aggregateCollectErrors", () => { test("returns undefined when collectErrors is false", () => { const results = [makeResult(0, failReport("a"))]; expect(aggregateCollectErrors(results, false)).toBeUndefined(); }); test("returns undefined when no failures", () => { const results = [makeResult(0, okReport("a"))]; expect(aggregateCollectErrors(results, true)).toBeUndefined(); }); test("returns failed steps when collectErrors is true", () => { const results = [ makeResult(0, okReport("a")), makeResult(1, failReport("b")), makeResult(2, failReport("c")), ]; const result = aggregateCollectErrors(results, true); expect(result).toBeDefined(); expect(result!.failedSteps).toEqual(["b", "c"]); expect(result!.ok).toBe(false); }); test("excludes dependency-skipped steps from failures", () => { const results = [makeResult(0, failReport("a")), makeResult(1, failReport("b"), true)]; const result = aggregateCollectErrors(results, true); expect(result!.failedSteps).toEqual(["a"]); }); test("uses exitCode from first failure", () => { const results = [ makeResult(0, { ...failReport("a"), exitCode: 42 }), makeResult(1, failReport("b")), ]; const result = aggregateCollectErrors(results, true); expect(result!.exitCode).toBe(42); }); }); describe("shouldTransitiveSkip", () => { const runState: PipelineRunState = { cacheHitCommands: new Set(["gen.a", "gen.b"]), pipelineName: "test", }; test("returns false when cacheable is false", () => { const cmd = { ...mockCommand, cacheable: false, validatesOutputs: ["gen.a"] }; expect(shouldTransitiveSkip(cmd, runState)).toBe(false); }); test("returns false when validatesOutputs is absent", () => { expect(shouldTransitiveSkip(mockCommand, runState)).toBe(false); }); test("returns false when validatesOutputs is empty", () => { const cmd = { ...mockCommand, validatesOutputs: [] }; expect(shouldTransitiveSkip(cmd, runState)).toBe(false); }); test("returns true when all validatesOutputs are cache hits", () => { const cmd = { ...mockCommand, validatesOutputs: ["gen.a", "gen.b"] }; expect(shouldTransitiveSkip(cmd, runState)).toBe(true); }); test("returns false when not all validatesOutputs are cache hits", () => { const cmd = { ...mockCommand, validatesOutputs: ["gen.a", "gen.c"] }; expect(shouldTransitiveSkip(cmd, runState)).toBe(false); }); }); describe("pipeline cache-hits persistence", () => { let tmpDir: string; beforeEach(() => { tmpDir = mkdtempSync(join(process.cwd(), "tmp-cache-hits-")); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); test("loadImportedCacheHits returns empty set when file does not exist", async () => { const hits = await loadImportedCacheHits(tmpDir, "pipeline-a"); expect(hits.size).toBe(0); }); test("persistCacheHits writes and loadImportedCacheHits reads back", async () => { await persistCacheHits(tmpDir, "pipeline-a", new Set(["cmd.1", "cmd.2"])); const hits = await loadImportedCacheHits(tmpDir, "pipeline-b"); expect(hits.has("cmd.1")).toBe(true); expect(hits.has("cmd.2")).toBe(true); }); test("loadImportedCacheHits excludes current pipeline", async () => { await persistCacheHits(tmpDir, "pipeline-a", new Set(["cmd.1"])); const hits = await loadImportedCacheHits(tmpDir, "pipeline-a"); expect(hits.size).toBe(0); }); test("loadImportedCacheHits handles corrupt JSON gracefully", async () => { const dir = join(tmpDir, ".cache"); const { mkdirSync } = await import("node:fs"); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "pipeline-cache-hits.json"), "not json{"); const hits = await loadImportedCacheHits(tmpDir, "pipeline-a"); expect(hits.size).toBe(0); }); test("persistCacheHits preserves entries for other pipelines", async () => { await persistCacheHits(tmpDir, "pipeline-a", new Set(["cmd.a"])); await persistCacheHits(tmpDir, "pipeline-b", new Set(["cmd.b"])); const hits = await loadImportedCacheHits(tmpDir, "pipeline-c"); expect(hits.has("cmd.a")).toBe(true); expect(hits.has("cmd.b")).toBe(true); }); test("clearPipelineCacheHits empties the file", async () => { await persistCacheHits(tmpDir, "pipeline-a", new Set(["cmd.1"])); await clearPipelineCacheHits(tmpDir); const hits = await loadImportedCacheHits(tmpDir, "pipeline-b"); expect(hits.size).toBe(0); }); test("loadImportedCacheHits skips stale entries", async () => { const dir = join(tmpDir, ".cache"); const { mkdirSync } = await import("node:fs"); mkdirSync(dir, { recursive: true }); const staleData = { pipelines: { "pipeline-a": { commands: ["cmd.1"], writtenAt: Date.now() - 31 * 60 * 1000, }, }, }; writeFileSync(join(dir, "pipeline-cache-hits.json"), JSON.stringify(staleData)); const hits = await loadImportedCacheHits(tmpDir, "pipeline-b"); expect(hits.size).toBe(0); }); }); describe("mergeCollectErrorsOption — RFC-1144 AC-6", () => { const baseOptions = { workspaceRoot: "/tmp/ws", pipelineName: "build.post", }; test("declaration collectErrors: true enables the mode without the flag", () => { const merged = mergeCollectErrorsOption(baseOptions, { name: "build.post", steps: [], collectErrors: true, }); expect(merged.collectErrors).toBe(true); }); test("CLI flag alone still enables the mode (OR-semantics)", () => { const merged = mergeCollectErrorsOption( { ...baseOptions, collectErrors: true }, { name: "build.post", steps: [] }, ); expect(merged.collectErrors).toBe(true); }); test("neither opt-in leaves the mode off", () => { const merged = mergeCollectErrorsOption(baseOptions, { name: "build.post", steps: [], }); expect(merged.collectErrors).toBeUndefined(); }); test("declaration without collectErrors does not mutate options identity", () => { const merged = mergeCollectErrorsOption(baseOptions, { name: "build.post", steps: [], }); expect(merged).toBe(baseOptions); }); }); describe("applyWarnOnly — RFC-1184", () => { const warnStep = { command: "gate.new", warnOnly: true }; const blockingStep = { command: "gate.old" }; test("failing warnOnly step → ok=true, warned=true, exitCode preserved", () => { const report = failReport("gate.new"); applyWarnOnly(report, warnStep); expect(report.ok).toBe(true); expect(report.warned).toBe(true); expect(report.exitCode).toBe(1); // failure preserved for diagnostics expect(report.summary).toMatch(/^\[warn-only\]/); }); test("default (no warnOnly) failing step stays blocking", () => { const report = failReport("gate.old"); applyWarnOnly(report, blockingStep); expect(report.ok).toBe(false); expect(report.warned).toBeUndefined(); }); test("warnOnly passing step is not marked warned", () => { const report = okReport("gate.new"); applyWarnOnly(report, warnStep); expect(report.ok).toBe(true); expect(report.warned).toBeUndefined(); }); test("warned step does not enter collectErrors failedSteps — pipeline may pass", () => { const report = failReport("gate.new"); applyWarnOnly(report, warnStep); const results = [makeResult(0, report), makeResult(1, okReport("next"))]; expect(aggregateCollectErrors(results, true)).toBeUndefined(); }); test("mixed: warned failure + real failure — only the real one fails the pipeline", () => { const warnedReport = failReport("gate.new"); applyWarnOnly(warnedReport, warnStep); const results = [ makeResult(0, warnedReport), makeResult(1, failReport("gate.old")), makeResult(2, okReport("tail")), ]; const collected = aggregateCollectErrors(results, true); expect(collected).toBeDefined(); expect(collected!.failedSteps).toEqual(["gate.old"]); }); test("warned step does not taint dependents (ok=true → completed, not failed)", () => { // The scheduler reads report.ok only — applyWarnOnly flips it before the // report returns, so dependents see "completed", never "failed". This is // the contract collectErrors/dependency-skip builds on. const report = failReport("gate.new"); applyWarnOnly(report, warnStep); expect(report.ok).toBe(true); }); });