/* Unit tests for pipeline scheduler covering buildSchedule; executeScheduledSteps; executeScheduledSteps — RFC-1144 collect-errors. 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 pipeline 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 } from "vitest"; import { buildSchedule, executeScheduledSteps, ScheduleError } from "../pipeline-scheduler.ts"; import { applyWarnOnly } from "../execute-pipeline.ts"; import type { KernelExecutionReport, KernelPipelineStep } from "@warpgogol/werkstatt-shared/kernel"; function okReport(command: string): KernelExecutionReport { return { commandName: command, exitCode: 0, ok: true, summary: `${command}: OK`, metadata: {} as never, logs: [], timing: { durationMs: 10, exceededTimeout: false }, filesModified: [], }; } function failReport(command: string): KernelExecutionReport { return { commandName: command, exitCode: 1, ok: false, summary: `${command}: FAIL`, metadata: {} as never, logs: [], timing: { durationMs: 10, exceededTimeout: false }, filesModified: [], }; } function steps(...cmds: string[]): KernelPipelineStep[] { return cmds.map((cmd) => ({ command: cmd })); } describe("buildSchedule", () => { test("empty steps returns empty schedule", () => { expect(buildSchedule([])).toEqual([]); }); test("single step has no dependencies", () => { const schedule = buildSchedule(steps("a")); expect(schedule).toHaveLength(1); expect(schedule[0]!.dependencies.size).toBe(0); }); test("two sequential steps: second depends on first", () => { const schedule = buildSchedule(steps("a", "b")); expect(schedule[1]!.dependencies).toEqual(new Set([0])); }); test("explicit dependsOn: [] removes implicit dependency", () => { const schedule = buildSchedule([{ command: "a" }, { command: "b", dependsOn: [] }]); expect(schedule[1]!.dependencies.size).toBe(0); }); test("explicit dependsOn resolves command names to indices", () => { const schedule = buildSchedule([ { command: "a" }, { command: "b" }, { command: "c", dependsOn: ["a"] }, ]); expect(schedule[2]!.dependencies).toEqual(new Set([0])); }); test("throws on duplicate command names", () => { expect(() => buildSchedule(steps("a", "a"))).toThrow(ScheduleError); expect(() => buildSchedule(steps("a", "a"))).toThrow(/Duplicate command name/); }); test("throws on missing dependency reference", () => { expect(() => buildSchedule([{ command: "a", dependsOn: ["nonexistent"] }])).toThrow( /not in the pipeline/, ); }); test("throws on forward reference", () => { expect(() => buildSchedule([{ command: "a", dependsOn: ["b"] }, { command: "b" }])).toThrow( /forward reference/, ); }); test("skipped steps do not create implicit dependencies", () => { const schedule = buildSchedule([ { command: "a" }, { command: "b", skip: true }, { command: "c" }, ]); expect(schedule[2]!.dependencies).toEqual(new Set([0])); }); }); describe("executeScheduledSteps", () => { test("executes all steps sequentially with concurrency=1", async () => { const schedule = buildSchedule(steps("a", "b", "c")); const executed: string[] = []; const results = await executeScheduledSteps(schedule, 1, async (s) => { executed.push(s.step.command); return okReport(s.step.command); }); expect(results).toHaveLength(3); expect(executed).toEqual(["a", "b", "c"]); expect(results.every((r) => !r.dependencySkipped)).toBe(true); }); test("aborts on failure with concurrency=1", async () => { const schedule = buildSchedule(steps("a", "b", "c")); const results = await executeScheduledSteps(schedule, 1, async (s) => { if (s.step.command === "b") return failReport(s.step.command); return okReport(s.step.command); }); expect(results).toHaveLength(3); expect(results[1]!.report.ok).toBe(false); expect(results[2]!.dependencySkipped).toBe(true); }); test("parallel execution with concurrency > 1", async () => { const schedule = buildSchedule([{ command: "a" }, { command: "b" }, { command: "c" }]); const results = await executeScheduledSteps(schedule, 3, async (s) => { return okReport(s.step.command); }); expect(results).toHaveLength(3); expect(results.every((r) => r.report.ok)).toBe(true); }); test("skips dependents of failed step with concurrency > 1", async () => { const schedule = buildSchedule([ { command: "a" }, { command: "b", dependsOn: ["a"] }, { command: "c", dependsOn: ["b"] }, ]); const results = await executeScheduledSteps(schedule, 2, async (s) => { if (s.step.command === "a") return failReport(s.step.command); return okReport(s.step.command); }); expect(results).toHaveLength(3); expect(results[0]!.report.ok).toBe(false); expect(results[1]!.dependencySkipped).toBe(true); expect(results[2]!.dependencySkipped).toBe(true); }); test("independent steps continue when a step fails", async () => { const schedule = buildSchedule([{ command: "a" }, { command: "b", dependsOn: [] }]); const results = await executeScheduledSteps(schedule, 2, async (s) => { if (s.step.command === "a") return failReport(s.step.command); return okReport(s.step.command); }); expect(results).toHaveLength(2); expect(results[1]!.report.ok).toBe(true); expect(results[1]!.dependencySkipped).toBe(false); }); test("results are sorted by stepIndex", async () => { const schedule = buildSchedule(steps("a", "b", "c")); const results = await executeScheduledSteps(schedule, 3, async (s) => { return okReport(s.step.command); }); expect(results.map((r) => r.stepIndex)).toEqual([0, 1, 2]); }); }); describe("executeScheduledSteps — RFC-1144 collect-errors", () => { test("AC-1: read-only steps run to completion after a failed dependency", async () => { const schedule = buildSchedule(steps("a", "b", "c")); schedule[1]!.readOnly = true; schedule[2]!.readOnly = true; const executed: string[] = []; const results = await executeScheduledSteps( schedule, 1, async (s) => { executed.push(s.step.command); if (s.step.command === "a") return failReport(s.step.command); return okReport(s.step.command); }, { collectErrors: true }, ); expect(executed).toEqual(["a", "b", "c"]); expect(results).toHaveLength(3); expect(results[1]!.dependencySkipped).toBe(false); expect(results[1]!.report.ok).toBe(true); expect(results[2]!.dependencySkipped).toBe(false); }); test("AC-2: read-only step that ran after a failed dependency is annotated", async () => { const schedule = buildSchedule(steps("a", "b")); schedule[1]!.readOnly = true; const seen = new Map(); await executeScheduledSteps( schedule, 1, async (s) => { seen.set(s.step.command, s.ranAfterFailedDependency); if (s.step.command === "a") return failReport(s.step.command); return okReport(s.step.command); }, { collectErrors: true }, ); expect(seen.get("a")).toBeUndefined(); expect(seen.get("b")).toBe(true); }); test("AC-3: mutating step still skips after a failed dependency under collect-errors", async () => { const schedule = buildSchedule(steps("a", "b", "c")); schedule[1]!.readOnly = true; // b read-only — runs tainted // c has no readOnly — treated as mutating (conservative default) const executed: string[] = []; const results = await executeScheduledSteps( schedule, 1, async (s) => { executed.push(s.step.command); if (s.step.command === "a") return failReport(s.step.command); return okReport(s.step.command); }, { collectErrors: true }, ); expect(executed).toEqual(["a", "b"]); expect(results[2]!.dependencySkipped).toBe(true); }); test("AC-4: without collectErrors, read-only steps skip like everything else", async () => { const schedule = buildSchedule(steps("a", "b")); schedule[1]!.readOnly = true; const executed: string[] = []; const results = await executeScheduledSteps(schedule, 1, async (s) => { executed.push(s.step.command); if (s.step.command === "a") return failReport(s.step.command); return okReport(s.step.command); }); expect(executed).toEqual(["a"]); expect(results[1]!.dependencySkipped).toBe(true); }); test("transitive taint: read-only step that ran-tainted still skips mutating dependents", async () => { const schedule = buildSchedule([ { command: "a" }, { command: "b", dependsOn: ["a"] }, { command: "c", dependsOn: ["b"] }, { command: "d", dependsOn: ["b"] }, ]); schedule[1]!.readOnly = true; // b read-only — runs tainted, passes schedule[3]!.readOnly = true; // d read-only — runs annotated // c is mutating — must skip even though b passed const executed: string[] = []; const seen = new Map(); const results = await executeScheduledSteps( schedule, 2, async (s) => { executed.push(s.step.command); seen.set(s.step.command, s.ranAfterFailedDependency); if (s.step.command === "a") return failReport(s.step.command); return okReport(s.step.command); }, { collectErrors: true }, ); expect(executed.sort()).toEqual(["a", "b", "d"]); expect(seen.get("b")).toBe(true); expect(seen.get("d")).toBe(true); expect(results[2]!.dependencySkipped).toBe(true); // c skipped — mutating on tainted dep expect(results[3]!.dependencySkipped).toBe(false); // d ran — read-only }); test("read-only step that ran-tainted and failed still taints dependents", async () => { const schedule = buildSchedule(steps("a", "b", "c")); schedule[1]!.readOnly = true; schedule[2]!.readOnly = true; const results = await executeScheduledSteps( schedule, 1, async (s) => { if (s.step.command === "a" || s.step.command === "b") return failReport(s.step.command); return okReport(s.step.command); }, { collectErrors: true }, ); // b ran-tainted and failed — c (read-only) still runs, annotated. expect(results[1]!.report.ok).toBe(false); expect(results[2]!.dependencySkipped).toBe(false); expect(results[2]!.report.ok).toBe(true); }); }); describe("executeScheduledSteps — RFC-1184 warnOnly", () => { test("AC-2: failing warnOnly step completes, dependents run, report carries warned", async () => { const schedule = buildSchedule([ { command: "gate.new", warnOnly: true }, { command: "dependent.step" }, ]); const ran: string[] = []; const results = await executeScheduledSteps(schedule, 1, async (s) => { ran.push(s.step.command); const report = s.step.command === "gate.new" ? failReport("gate.new") : okReport(s.step.command); // Production wiring: applyWarnOnly runs inside the executor callback // before the report returns — the scheduler only ever reads ok. applyWarnOnly(report, s.step); return report; }); expect(ran).toEqual(["gate.new", "dependent.step"]); expect(results.every((r) => !r.dependencySkipped)).toBe(true); const warnResult = results.find((r) => r.stepIndex === 0)!; expect(warnResult.report.ok).toBe(true); expect(warnResult.report.warned).toBe(true); // Failure evidence preserved for diagnostics even though ok flipped. expect(warnResult.report.exitCode).toBe(1); }); test("AC-4: non-warnOnly failure still skips dependents and stays failed", async () => { const schedule = buildSchedule([{ command: "gate.old" }, { command: "dependent.step" }]); const results = await executeScheduledSteps(schedule, 1, async (s) => { const report = s.step.command === "gate.old" ? failReport("gate.old") : okReport(s.step.command); applyWarnOnly(report, s.step); return report; }); expect(results[0]!.report.ok).toBe(false); expect(results[0]!.report.warned).toBeUndefined(); expect(results[1]!.dependencySkipped).toBe(true); }); });