import { describe, expect, it } from "vitest"; import { ActionQueue, assertStepTransition, canTransitionStep, DynamicActionQueueImpl, isActiveStepStatus, isTerminalStepStatus, } from "./action-queue.js"; describe("Outcome-Driven Action Queue State Machine (packages/core/src/action-queue.ts)", () => { describe("Tier 1: Feature Coverage (Core Lifecycle & Interface Contracts)", () => { it("T1.1: initializes with empty steps and activeStepId === null", () => { const q = new DynamicActionQueueImpl(); expect(q.steps).toEqual([]); expect(q.activeStepId).toBeNull(); expect(q.getActiveStep()).toBeNull(); expect(q.isComplete()).toBe(false); expect(q.hasFailed()).toBe(false); }); it("T1.2: synthesizeInput decomposes multi-part instructions into sequential steps with criteria", () => { const q = new DynamicActionQueueImpl(); const created = q.synthesizeInput( "Install dependencies. Run test suite. Build production bundle.", ); expect(created.length).toBe(3); expect(q.steps.length).toBe(3); expect(q.steps[0]!.id).toBe("step-1"); expect(q.steps[0]!.intent).toBe("Install dependencies"); expect(q.steps[0]!.outcomeCriteria).toBe( "Verify Install dependencies completed successfully", ); expect(q.steps[0]!.status).toBe("pending"); expect(q.steps[1]!.intent).toBe("Run test suite"); expect(q.steps[2]!.intent).toBe("Build production bundle"); expect(q.steps.every((s) => s.status === "pending")).toBe(true); }); it("T1.3: advance transitions first pending step to running with startedAt timestamp", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Run audit then deploy"); const active = q.advance(); expect(active).not.toBeNull(); expect(active!.id).toBe("step-1"); expect(q.activeStepId).toBe("step-1"); expect(q.steps[0]!.status).toBe("running"); expect(q.steps[0]!.startedAt).toBeGreaterThan(0); expect(q.steps[1]!.status).toBe("pending"); expect(q.steps[1]!.startedAt).toBeUndefined(); }); it("T1.4: sequential advance completes active step with completedAt and starts next step", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Step A; Step B"); q.advance(); // Starts step A expect(q.steps[0]!.status).toBe("running"); q.advance(); // Completes step A, starts step B expect(q.steps[0]!.status).toBe("completed"); expect(q.steps[0]!.completedAt).toBeGreaterThan(0); expect(q.steps[1]!.status).toBe("running"); expect(q.steps[1]!.startedAt).toBeGreaterThan(0); expect(q.activeStepId).toBe(q.steps[1]!.id); q.advance(); // Completes step B expect(q.steps[1]!.status).toBe("completed"); expect(q.steps[1]!.completedAt).toBeGreaterThan(0); expect(q.activeStepId).toBeNull(); expect(q.isComplete()).toBe(true); }); it("T1.5: cancelActive marks active running step cancelled with timestamp and clears activeStepId", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Long running command; Step 2"); q.advance(); expect(q.steps[0]!.status).toBe("running"); const cancelled = q.cancelActive("user_barge_in"); expect(cancelled).not.toBeNull(); expect(cancelled!.status).toBe("cancelled"); expect(cancelled!.completedAt).toBeGreaterThan(0); expect(q.activeStepId).toBeNull(); expect(q.steps[0]!.status).toBe("cancelled"); expect(q.steps[1]!.status).toBe("pending"); }); it("T1.6: updateStepStatus transitions step status and syncs activeStepId and timestamps", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Step 1"); q.updateStepStatus("step-1", "running"); expect(q.steps[0]!.status).toBe("running"); expect(q.activeStepId).toBe("step-1"); expect(q.steps[0]!.startedAt).toBeGreaterThan(0); q.updateStepStatus("step-1", "completed"); expect(q.steps[0]!.status).toBe("completed"); expect(q.activeStepId).toBeNull(); expect(q.steps[0]!.completedAt).toBeGreaterThan(0); }); }); describe("Tier 2: Boundary & Corner Cases", () => { it("T2.1: synthesizeInput with empty or whitespace-only string is clean no-op", () => { const q = new DynamicActionQueueImpl(); expect(q.synthesizeInput("")).toEqual([]); expect(q.synthesizeInput(" \n\t ")).toEqual([]); expect(q.steps.length).toBe(0); }); it("T2.2: advance on empty or fully completed queue is a safe no-op", () => { const q = new DynamicActionQueueImpl(); expect(() => q.advance()).not.toThrow(); expect(q.advance()).toBeNull(); expect(q.activeStepId).toBeNull(); q.synthesizeInput("Single step"); q.advance(); // running q.advance(); // completed expect(q.advance()).toBeNull(); expect(q.activeStepId).toBeNull(); }); it("T2.3: cancelActive when no step is active is a safe no-op", () => { const q = new DynamicActionQueueImpl(); expect(q.cancelActive("no_active")).toBeNull(); expect(q.activeStepId).toBeNull(); q.synthesizeInput("Pending step"); expect(q.cancelActive("no_active")).toBeNull(); expect(q.activeStepId).toBeNull(); }); it("T2.4: failed step halts automatic advance until retry or cancellation", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Failing step; Subsequent step"); q.advance(); q.updateStepStatus("step-1", "failed"); expect(q.hasFailed()).toBe(true); const advanced = q.advance(); expect(advanced).toBeNull(); expect(q.activeStepId).toBeNull(); expect(q.steps[1]!.status).toBe("pending"); }); it("T2.5: state transition validator strictly enforces allowed lifecycle transitions", () => { expect(canTransitionStep("pending", "running")).toBe(true); expect(canTransitionStep("pending", "cancelled")).toBe(true); expect(canTransitionStep("pending", "completed")).toBe(false); expect(canTransitionStep("pending", "failed")).toBe(false); expect(canTransitionStep("running", "completed")).toBe(true); expect(canTransitionStep("running", "failed")).toBe(true); expect(canTransitionStep("running", "cancelled")).toBe(true); expect(canTransitionStep("running", "pending")).toBe(false); expect(canTransitionStep("completed", "running")).toBe(false); expect(canTransitionStep("cancelled", "completed")).toBe(false); expect(canTransitionStep("failed", "running")).toBe(true); // retry allowed expect(canTransitionStep("failed", "cancelled")).toBe(true); // abandon allowed expect(() => assertStepTransition("completed", "running")).toThrow( "Illegal step transition from completed to running", ); }); it("T2.6: updateStepStatus throws error when step does not exist", () => { const q = new DynamicActionQueueImpl(); expect(() => q.updateStepStatus("nonexistent", "running")).toThrow("Step not found"); }); }); describe("Tier 3: Dynamic Mutation & Reordering", () => { it("T3.1: reorderSteps repositions steps by index", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("A; B; C"); q.reorderSteps(0, 2); expect(q.steps.map((s) => s.intent)).toEqual(["B", "C", "A"]); }); it("T3.2: reorderSteps throws RangeError on invalid index bounds", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("A; B"); expect(() => q.reorderSteps(-1, 1)).toThrow(RangeError); expect(() => q.reorderSteps(0, 5)).toThrow(RangeError); }); it("T3.3: insertStep adds a new step at specific index or end", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("First; Third"); const inserted = q.insertStep({ intent: "Second" }, 1); expect(inserted.intent).toBe("Second"); expect(inserted.id).toBe("step-3"); expect(q.steps.map((s) => s.intent)).toEqual(["First", "Second", "Third"]); }); it("T3.4: removeStep removes a step from the queue and clears activeStepId if active", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("A; B"); q.advance(); // step-1 running expect(q.activeStepId).toBe("step-1"); const removedActive = q.removeStep("step-1"); expect(removedActive).toBe(true); expect(q.activeStepId).toBeNull(); expect(q.steps.length).toBe(1); const removedNonExistent = q.removeStep("step-99"); expect(removedNonExistent).toBe(false); }); it("T3.5: retryStep resets failed step to pending and clears completedAt timestamp", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Failed command"); q.advance(); q.updateStepStatus("step-1", "failed"); const retried = q.retryStep("step-1"); expect(retried.status).toBe("pending"); expect(retried.completedAt).toBeUndefined(); expect(q.hasFailed()).toBe(false); // Can now advance q.advance(); expect(q.steps[0]!.status).toBe("running"); }); it("T3.6: retryStep throws when called on non-failed step", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Pending command"); expect(() => q.retryStep("step-1")).toThrow( 'Cannot retry step with status "pending"; only failed steps can be retried', ); }); }); describe("Tier 4: Serialization & Helpers", () => { it("T4.1: toJSON and fromJSON preserve complete state and timestamp fidelity", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Inspect; Test; Deploy"); q.advance(); // step-1 running const snapshot = q.toJSON(); const restored = DynamicActionQueueImpl.fromJSON(snapshot); expect(restored.steps.length).toBe(3); expect(restored.activeStepId).toBe("step-1"); expect(restored.steps[0]!.status).toBe("running"); expect(restored.steps[0]!.startedAt).toBe(q.steps[0]!.startedAt); expect(restored.steps[1]!.status).toBe("pending"); }); it("T4.2: native JSON.stringify and JSON.parse round-trip preserves state", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Analyze; Build"); q.advance(); const jsonStr = JSON.stringify(q); const parsed = JSON.parse(jsonStr); expect(parsed.steps.length).toBe(2); expect(parsed.activeStepId).toBe("step-1"); }); it("T4.3: helper queries (getPendingSteps, getStep, isActiveStepStatus, isTerminalStepStatus)", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("S1; S2; S3"); expect(q.getPendingSteps().length).toBe(3); q.advance(); expect(q.getPendingSteps().length).toBe(2); expect(q.getStep("step-1")?.status).toBe("running"); expect(isActiveStepStatus(q.getStep("step-1")!.status)).toBe(true); q.cancelActive(); expect(isTerminalStepStatus(q.getStep("step-1")!.status)).toBe(true); }); it("T4.4: ActionQueue alias points to DynamicActionQueueImpl", () => { expect(ActionQueue).toBe(DynamicActionQueueImpl); const q = new ActionQueue(); expect(q).toBeInstanceOf(DynamicActionQueueImpl); }); }); describe("Tier 5: Real-World Scenario Hardening & Adversarial Stress", () => { it("T5.1: complex speech transcript with conjunctions decomposes cleanly", () => { const transcript = "First review the pull request, then run security scan, and build docker image"; const q = new DynamicActionQueueImpl(); q.synthesizeInput(transcript); expect(q.steps.length).toBe(3); expect(q.steps[0]!.intent).toContain("First review the pull request"); expect(q.steps[1]!.intent).toContain("run security scan"); expect(q.steps[2]!.intent).toContain("build docker image"); }); it("T5.2: multiple sequential syntheses append monotonically numbered steps", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Task 1; Task 2"); expect(q.steps.map((s) => s.id)).toEqual(["step-1", "step-2"]); q.synthesizeInput("Task 3; Task 4"); expect(q.steps.map((s) => s.id)).toEqual(["step-1", "step-2", "step-3", "step-4"]); }); it("T5.3: custom outcome criteria template applies to all synthesized steps", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("Lint files; Run vitest", { criteriaTemplate: (intent) => `Ensure 0 errors for ${intent}`, }); expect(q.steps[0]!.outcomeCriteria).toBe("Ensure 0 errors for Lint files"); expect(q.steps[1]!.outcomeCriteria).toBe("Ensure 0 errors for Run vitest"); }); it("T5.4: clear resets queue completely", () => { const q = new DynamicActionQueueImpl(); q.synthesizeInput("A; B; C"); q.advance(); expect(q.steps.length).toBe(3); expect(q.activeStepId).not.toBeNull(); q.clear(); expect(q.steps.length).toBe(0); expect(q.activeStepId).toBeNull(); }); }); });