import { describe, expect, it } from "vitest"; import type { Settings, TaskDetail, WorkflowIr } from "@fusion/core"; import { WorkflowTaskRuntime, type WorkflowTaskRuntimeDeps } from "../workflow-task-runtime.js"; import type { WorkflowNodeResult } from "../workflow-graph-executor.js"; import type { PreparedWorktree, WorkflowRuntimePrimitives } from "../runtime-primitives.js"; const task = { id: "FN-9002" } as TaskDetail; const flagOff = { experimentalFeatures: {} } as unknown as Pick; function selectedIr(): WorkflowIr { return { version: "v1", name: "selected", nodes: [ { id: "start", kind: "start" }, { id: "prepare", kind: "prompt", config: { prompt: "prepare" } }, { id: "execute", kind: "prompt", config: { seam: "execute" } }, { id: "zend", kind: "end" }, ], edges: [ { from: "start", to: "prepare", condition: "success" }, { from: "prepare", to: "execute", condition: "success" }, { from: "execute", to: "zend", condition: "success" }, { from: "execute", to: "zend", condition: "failure" }, ], }; } function recordingPrimitives( calls: string[], overrides: Partial> & { prepareData?: PreparedWorktree | null; } = {}, observed: { prepared?: PreparedWorktree } = {}, ): WorkflowRuntimePrimitives { const prepared: PreparedWorktree = { worktreePath: "/tmp/fusion-worktree" }; return { prepareWorktree: async () => { calls.push("prepare-worktree"); return { outcome: overrides.prepare?.outcome ?? "success", value: overrides.prepare?.value, contextPatch: overrides.prepare?.contextPatch, data: overrides.prepare?.outcome === "failure" ? undefined : overrides.prepareData === null ? undefined : overrides.prepareData ?? prepared, }; }, readArtifact: async () => undefined, writeArtifact: async (_ctx, _task, key) => ({ outcome: "success", data: { key } }), runPlanningSession: async () => { calls.push("planning"); return { outcome: "success", data: { approved: true, artifactKeys: [] } }; }, runCodingSession: async (_ctx, _task, preparedWorktree) => { calls.push("execute"); observed.prepared = preparedWorktree; const override = overrides.execute; return { outcome: override?.outcome ?? "success", value: override?.value ?? "implemented", contextPatch: override?.contextPatch, data: { taskDone: override?.outcome !== "failure", modifiedFiles: [] }, }; }, runTaskStep: async () => ({ outcome: "success" }), resetTaskStep: async () => ({ ok: true }), runReview: async (_ctx, _task, input) => { calls.push(input.stepIndex === undefined ? "review" : "step-review"); return { outcome: "success", value: input.stepIndex === undefined ? "in-review" : "approve", data: { verdict: "APPROVE" }, }; }, runVerification: async () => ({ outcome: "success", data: { verdict: "skipped" } }), runWorkflowStep: async () => { calls.push("workflow-step"); const override = overrides.workflowStep; return { outcome: override?.outcome ?? "success", value: override?.value ?? "workflow-steps-passed", contextPatch: override?.contextPatch, data: { allPassed: override?.value !== "remediation-scheduled" }, }; }, updateSteps: async (_ctx, _task, steps) => ({ outcome: "success", data: { count: steps.length } }), transitionTask: async () => { calls.push("schedule"); return { outcome: "success" }; }, requestMerge: async () => { calls.push("merge"); return { outcome: "success", value: "merged", data: { status: "merged" } }; }, abortRun: async () => ({ outcome: "success" }), audit: () => undefined, }; } describe("WorkflowTaskRuntime", () => { it("requires execution wiring at the type boundary", () => { // @ts-expect-error WorkflowTaskRuntime is an execution entry point, so primitives are required. const missingPrimitives: WorkflowTaskRuntimeDeps = { store: { getTaskWorkflowSelection: () => undefined, getWorkflowDefinition: async () => undefined, }, runCustomNode: async () => ({ outcome: "success" }), }; expect(missingPrimitives).toBeDefined(); }); it("runs a selected workflow through the graph engine", async () => { const calls: string[] = []; const observed: { prepared?: PreparedWorktree } = {}; let workflowSelectionReads = 0; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => { workflowSelectionReads += 1; return { workflowId: "WF-001", stepIds: [] }; }, getWorkflowDefinition: async () => ({ ir: selectedIr() }), }, primitives: recordingPrimitives( calls, { prepare: { outcome: "success", contextPatch: { preparedKey: "from-prepare" } }, execute: { outcome: "success", contextPatch: { executeKey: "from-execute" } }, }, observed, ), runCustomNode: async (node) => { calls.push(`custom:${node.id}`); return { outcome: "success" }; }, }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("completed"); expect(calls).toEqual(["custom:prepare", "prepare-worktree", "execute"]); expect(result.visitedNodeIds).toEqual(["start", "prepare", "execute"]); expect(observed.prepared).toEqual({ worktreePath: "/tmp/fusion-worktree" }); expect(result.context.preparedKey).toBe("from-prepare"); expect(result.context.executeKey).toBe("from-execute"); expect(workflowSelectionReads).toBe(1); }); it("fails execute instead of skipping coding when prepare succeeds without worktree data", async () => { const calls: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-001", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: selectedIr() }), }, primitives: recordingPrimitives(calls, { prepare: { outcome: "success", value: "prepared-without-data" }, prepareData: null, }), runCustomNode: async (node) => { calls.push(`custom:${node.id}`); return { outcome: "success" }; }, }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("failed"); expect(calls).toEqual(["custom:prepare", "prepare-worktree"]); expect(result.visitedNodeIds).toEqual(["start", "prepare", "execute"]); }); it("resolves an unselected task to the built-in coding workflow instead of falling back", async () => { const calls: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => undefined, getWorkflowDefinition: async () => undefined, }, primitives: recordingPrimitives(calls), runCustomNode: async (node) => { calls.push(`custom:${node.id}`); return { outcome: "success" }; }, }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("completed"); expect(calls).toEqual(["planning", "prepare-worktree", "execute", "workflow-step", "review", "merge"]); expect(result.visitedNodeIds).toEqual(["start", "planning", "execute", "workflow-step", "review", "merge"]); }); it("stops the built-in workflow before review when workflow-step remediation is scheduled", async () => { const calls: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => undefined, getWorkflowDefinition: async () => undefined, }, primitives: recordingPrimitives(calls, { workflowStep: { outcome: "success", value: "remediation-scheduled" }, }), runCustomNode: async (node) => { calls.push(`custom:${node.id}`); return { outcome: "success" }; }, }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("completed"); expect(calls).toEqual(["planning", "prepare-worktree", "execute", "workflow-step"]); expect(result.visitedNodeIds).toEqual(["start", "planning", "execute", "workflow-step"]); }); it("fails selected workflow lookup misses instead of running the built-in workflow", async () => { const calls: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-MISSING", stepIds: [] }), getWorkflowDefinition: async () => undefined, }, primitives: recordingPrimitives(calls), runCustomNode: async () => ({ outcome: "success" }), }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("failed"); expect(result.reason).toContain("workflow-resolution-error: workflow-missing: WF-MISSING"); expect(calls).toEqual([]); }); it("fails corrupt selected workflow definitions instead of running the built-in workflow", async () => { const calls: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-CORRUPT", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: "not a workflow ir" }), }, primitives: recordingPrimitives(calls), runCustomNode: async () => ({ outcome: "success" }), }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("failed"); expect(result.reason).toContain("workflow-resolution-error:"); expect(calls).toEqual([]); }); it("forces only the graph executor flag while preserving other settings", async () => { let observedSettings: Pick | undefined; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-001", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: selectedIr() }), }, primitives: recordingPrimitives([]), runCustomNode: async () => ({ outcome: "success" }), handlers: { prompt: async (_node, context) => { observedSettings = context.settings; return { outcome: "success" }; }, }, }); const settings = { experimentalFeatures: { workflowColumns: true }, testMode: true, } as unknown as Settings; const result = await runtime.run(task, settings); expect(result.disposition).toBe("completed"); expect(observedSettings?.experimentalFeatures?.workflowGraphExecutor).toBe(true); expect(observedSettings?.experimentalFeatures?.workflowColumns).toBe(true); expect((observedSettings as Settings | undefined)?.testMode).toBe(true); }); it("uses a workflow-specific default run id", async () => { const observedRunIds: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-001", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: selectedIr() }), }, primitives: recordingPrimitives([]), runCustomNode: async () => ({ outcome: "success" }), branchPersistence: { loadBranchStates: (_taskId, runId) => { observedRunIds.push(runId); return []; }, }, }); await runtime.run(task, flagOff); expect(observedRunIds).toContain("FN-9002:WF-001"); }); it("uses the built-in workflow id in the default run id for unselected tasks", async () => { const observedRunIds: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => undefined, getWorkflowDefinition: async () => undefined, }, primitives: recordingPrimitives([]), runCustomNode: async () => ({ outcome: "success" }), branchPersistence: { loadBranchStates: (_taskId, runId) => { observedRunIds.push(runId); return []; }, }, }); await runtime.run(task, flagOff); expect(observedRunIds).toContain("FN-9002:builtin:coding"); }); it("surfaces graph failures as workflow-engine failures, not fallback", async () => { const calls: string[] = []; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-001", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: selectedIr() }), }, primitives: recordingPrimitives(calls, { execute: { outcome: "failure", value: "implementation-incomplete" } }), runCustomNode: async (node) => { calls.push(`custom:${node.id}`); return { outcome: "success" }; }, }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("failed"); expect(result.outcome).toBe("failure"); expect(calls).toEqual(["custom:prepare", "prepare-worktree", "execute"]); }); it("converts interpreter throws into workflow-engine failures", async () => { const badIr: WorkflowIr = { version: "v1", name: "bad", nodes: [ { id: "start", kind: "start" }, { id: "zend", kind: "end" }, ], edges: [{ from: "start", to: "ghost" }], }; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-001", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: badIr }), }, primitives: recordingPrimitives([]), runCustomNode: async () => ({ outcome: "success" }), }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("failed"); expect(result.reason).toMatch(/workflow-execution-error/); }); it("preserves graph node ids when the graph throws after seam and custom side effects", async () => { const cyclicIr: WorkflowIr = { version: "v1", name: "cyclic", nodes: [ { id: "start", kind: "start" }, { id: "do-execute", kind: "prompt", config: { seam: "execute" } }, { id: "loop", kind: "prompt", config: { prompt: "loop" } }, ], edges: [ { from: "start", to: "do-execute", condition: "success" }, { from: "do-execute", to: "loop", condition: "success" }, { from: "loop", to: "do-execute", condition: "success" }, ], }; const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-001", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: cyclicIr }), }, primitives: recordingPrimitives([]), runCustomNode: async () => ({ outcome: "success" }), }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("failed"); expect(result.reason).toMatch(/workflow-execution-error/); expect(result.visitedNodeIds).toEqual(["do-execute", "loop"]); }); it("diagnostic event failures do not affect execution", async () => { const runtime = new WorkflowTaskRuntime({ store: { getTaskWorkflowSelection: () => ({ workflowId: "WF-001", stepIds: [] }), getWorkflowDefinition: async () => ({ ir: selectedIr() }), }, primitives: recordingPrimitives([]), runCustomNode: async () => ({ outcome: "success" }), onEvent: () => { throw new Error("diagnostics failed"); }, }); const result = await runtime.run(task, flagOff); expect(result.disposition).toBe("completed"); }); });