import { describe, it, expect } from "vitest"; import { createActor } from "xstate"; import { pipelineMachine } from "../src/orchestrator/machine.js"; function runMachine(events: Array>) { const actor = createActor(pipelineMachine); actor.start(); for (const event of events) { actor.send(event as any); } const snapshot = actor.getSnapshot(); actor.stop(); return snapshot; } describe("pipelineMachine", () => { it("starts in idle", () => { const actor = createActor(pipelineMachine); actor.start(); expect(actor.getSnapshot().value).toBe("idle"); actor.stop(); }); it("happy path: idle → planning → implementing → reviewing → deploying → done", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "test", requirements: "build something" }, { type: "PLAN_READY", planDir: "/tmp/plans", sprintId: "SPRINT-001" }, { type: "IMPL_DONE", branch: "feature/sprint-001", commits: ["abc1234"] }, { type: "APPROVE", reviewArtifact: "/tmp/review.json" }, { type: "DEPLOY_DONE", deployArtifact: "/tmp/deploy.json" }, ]); expect(snapshot.value).toBe("done"); expect(snapshot.status).toBe("done"); }); it("REQUEST_CHANGES loops back to implementing (up to maxReviewRounds)", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "test", requirements: "" }, { type: "PLAN_READY", planDir: "/tmp", sprintId: "S1" }, { type: "IMPL_DONE", branch: "b", commits: ["c1"] }, { type: "REQUEST_CHANGES", issues: [{ severity: "major", message: "fix it" }] }, ]); expect(snapshot.value).toBe("implementing"); expect(snapshot.context.reviewRound).toBe(1); }); it("escalates after max review rounds exceeded", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "test", requirements: "" }, { type: "PLAN_READY", planDir: "/tmp", sprintId: "S1" }, // Round 1 (reviewRound: 0 → 1) { type: "IMPL_DONE", branch: "b", commits: ["c1"] }, { type: "REQUEST_CHANGES", issues: [{ severity: "major", message: "fix" }] }, // Round 2 (reviewRound: 1 → 2) { type: "IMPL_DONE", branch: "b", commits: ["c2"] }, { type: "REQUEST_CHANGES", issues: [{ severity: "major", message: "fix" }] }, // Round 3 (reviewRound: 2 → 3) { type: "IMPL_DONE", branch: "b", commits: ["c3"] }, { type: "REQUEST_CHANGES", issues: [{ severity: "major", message: "fix" }] }, // Round 4 — reviewRound=3, guard 3 < 3 = false → escalated { type: "IMPL_DONE", branch: "b", commits: ["c4"] }, { type: "REQUEST_CHANGES", issues: [{ severity: "major", message: "fix" }] }, ]); expect(snapshot.value).toBe("escalated"); expect(snapshot.context.lastError).toContain("review rounds"); }); it("retryable error goes to retrying, then back (if under limit)", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "test", requirements: "" }, { type: "ERROR", actor: "planner", reason: "timeout", retryable: true }, ]); // retrying has an always transition — if canRetry, goes to idle expect(snapshot.value).toBe("idle"); expect(snapshot.context.retryCount).toBe(1); }); it("non-retryable error goes to escalated", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "test", requirements: "" }, { type: "ERROR", actor: "planner", reason: "permission denied", retryable: false }, ]); expect(snapshot.value).toBe("escalated"); expect(snapshot.context.lastError).toBe("permission denied"); }); it("escalated → RESUME goes back to idle", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "test", requirements: "" }, { type: "ERROR", actor: "planner", reason: "fail", retryable: false }, { type: "RESUME" }, ]); expect(snapshot.value).toBe("idle"); expect(snapshot.context.lastError).toBeNull(); }); it("ABORT from any active state goes to aborted", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "test", requirements: "" }, { type: "ABORT", reason: "user cancelled" }, ]); expect(snapshot.value).toBe("aborted"); expect(snapshot.context.lastError).toBe("user cancelled"); }); it("context tracks projectName and requirements from REQUEST", () => { const snapshot = runMachine([ { type: "REQUEST", projectName: "arang", requirements: "Live2D avatar" }, ]); expect(snapshot.context.projectName).toBe("arang"); expect(snapshot.context.requirements).toBe("Live2D avatar"); }); });