Browse Source
- Test loop subgraph executes N times - Test prefix/suffix nodes execute once - Test regression safety (no loops) - Test loopCount: 1 behavior - Test abort signal stops loop mid-iterationhandoff-20260429-1057
1 changed files with 216 additions and 0 deletions
@ -0,0 +1,216 @@ |
|||||
|
/** |
||||
|
* Loop Edge Integration Tests |
||||
|
* |
||||
|
* Tests the runtime behavior of loop edges during workflow execution. |
||||
|
* These tests validate that loops execute N times with correct data flow. |
||||
|
*/ |
||||
|
|
||||
|
import { describe, it, expect, beforeEach, vi } from "vitest"; |
||||
|
import { useWorkflowStore } from "../workflowStore"; |
||||
|
import { WorkflowNode, WorkflowEdge, WorkflowNodeData } from "@/types"; |
||||
|
|
||||
|
// Test helpers
|
||||
|
function makeNode(id: string, type: string, data?: Partial<WorkflowNodeData>): WorkflowNode { |
||||
|
return { |
||||
|
id, |
||||
|
type, |
||||
|
position: { x: 0, y: 0 }, |
||||
|
data: data || {}, |
||||
|
} as WorkflowNode; |
||||
|
} |
||||
|
|
||||
|
function makeEdge( |
||||
|
source: string, |
||||
|
target: string, |
||||
|
opts?: { isLoop?: boolean; loopCount?: number; hasPause?: boolean } |
||||
|
): WorkflowEdge { |
||||
|
return { |
||||
|
id: `${source}-${target}`, |
||||
|
source, |
||||
|
target, |
||||
|
data: opts || {}, |
||||
|
} as WorkflowEdge; |
||||
|
} |
||||
|
|
||||
|
function setupStore(nodes: WorkflowNode[], edges: WorkflowEdge[]): void { |
||||
|
useWorkflowStore.setState({ nodes, edges }); |
||||
|
} |
||||
|
|
||||
|
describe("setLoopCount", () => { |
||||
|
beforeEach(() => { |
||||
|
// Reset store to clean state
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [], |
||||
|
edges: [], |
||||
|
groups: {}, |
||||
|
isRunning: false, |
||||
|
hasUnsavedChanges: false, |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("updates edge data loopCount", () => { |
||||
|
const edge = makeEdge("a", "b", { isLoop: true, loopCount: 3 }); |
||||
|
setupStore([], [edge]); |
||||
|
|
||||
|
useWorkflowStore.getState().setLoopCount(edge.id, 5); |
||||
|
|
||||
|
const updatedEdge = useWorkflowStore.getState().edges.find((e) => e.id === edge.id); |
||||
|
expect(updatedEdge?.data?.loopCount).toBe(5); |
||||
|
}); |
||||
|
|
||||
|
it("clamps loop count to minimum 1", () => { |
||||
|
const edge = makeEdge("a", "b", { isLoop: true, loopCount: 3 }); |
||||
|
setupStore([], [edge]); |
||||
|
|
||||
|
useWorkflowStore.getState().setLoopCount(edge.id, 0); |
||||
|
|
||||
|
const updatedEdge = useWorkflowStore.getState().edges.find((e) => e.id === edge.id); |
||||
|
expect(updatedEdge?.data?.loopCount).toBe(1); |
||||
|
}); |
||||
|
|
||||
|
it("clamps loop count to maximum 100", () => { |
||||
|
const edge = makeEdge("a", "b", { isLoop: true, loopCount: 3 }); |
||||
|
setupStore([], [edge]); |
||||
|
|
||||
|
useWorkflowStore.getState().setLoopCount(edge.id, 150); |
||||
|
|
||||
|
const updatedEdge = useWorkflowStore.getState().edges.find((e) => e.id === edge.id); |
||||
|
expect(updatedEdge?.data?.loopCount).toBe(100); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("executeWorkflow with loop edges", () => { |
||||
|
beforeEach(() => { |
||||
|
// Reset store to clean state
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [], |
||||
|
edges: [], |
||||
|
groups: {}, |
||||
|
isRunning: false, |
||||
|
hasUnsavedChanges: false, |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("executes loop subgraph exactly N times", async () => { |
||||
|
// Create A(imageInput) → B(prompt) → C(output) with loop edge C→A (loopCount: 3)
|
||||
|
const nodeA = makeNode("a", "imageInput", { image: "data:image/png;base64,test" }); |
||||
|
const nodeB = makeNode("b", "prompt", { prompt: "test prompt" }); |
||||
|
const nodeC = makeNode("c", "output"); |
||||
|
|
||||
|
const edges = [ |
||||
|
makeEdge("a", "b"), |
||||
|
makeEdge("b", "c"), |
||||
|
makeEdge("c", "a", { isLoop: true, loopCount: 3 }), |
||||
|
]; |
||||
|
|
||||
|
setupStore([nodeA, nodeB, nodeC], edges); |
||||
|
|
||||
|
// Spy on copyLoopOutput to count iterations
|
||||
|
const copyLoopOutputSpy = vi.spyOn( |
||||
|
await import("../utils/executionUtils"), |
||||
|
"copyLoopOutput" |
||||
|
); |
||||
|
|
||||
|
await useWorkflowStore.getState().executeWorkflow(); |
||||
|
|
||||
|
// copyLoopOutput should be called (loopCount - 1) times
|
||||
|
// because first iteration uses original input
|
||||
|
expect(copyLoopOutputSpy).toHaveBeenCalledTimes(2); // 3 iterations - 1 = 2 calls
|
||||
|
}); |
||||
|
|
||||
|
it("executes non-loop prefix and suffix nodes once", async () => { |
||||
|
// Create PREFIX(imageInput) → A(prompt) → B(prompt) → SUFFIX(output)
|
||||
|
// with loop edge B→A (loopCount: 3)
|
||||
|
const prefix = makeNode("prefix", "imageInput", { image: "data:image/png;base64,test" }); |
||||
|
const nodeA = makeNode("a", "prompt", { prompt: "loop start" }); |
||||
|
const nodeB = makeNode("b", "prompt", { prompt: "loop end" }); |
||||
|
const suffix = makeNode("suffix", "output"); |
||||
|
|
||||
|
const edges = [ |
||||
|
makeEdge("prefix", "a"), |
||||
|
makeEdge("a", "b"), |
||||
|
makeEdge("b", "suffix"), |
||||
|
makeEdge("b", "a", { isLoop: true, loopCount: 3 }), |
||||
|
]; |
||||
|
|
||||
|
setupStore([prefix, nodeA, nodeB, suffix], edges); |
||||
|
|
||||
|
// Spy on updateNodeData to track execution
|
||||
|
const updateNodeDataSpy = vi.spyOn(useWorkflowStore.getState(), "updateNodeData"); |
||||
|
|
||||
|
await useWorkflowStore.getState().executeWorkflow(); |
||||
|
|
||||
|
// Check that prefix and suffix nodes were updated less frequently than loop nodes
|
||||
|
// (This is a simplified check - in real execution, loop nodes would be updated more)
|
||||
|
const prefixUpdates = updateNodeDataSpy.mock.calls.filter((call) => call[0] === "prefix"); |
||||
|
const suffixUpdates = updateNodeDataSpy.mock.calls.filter((call) => call[0] === "suffix"); |
||||
|
const loopNodeUpdates = updateNodeDataSpy.mock.calls.filter( |
||||
|
(call) => call[0] === "a" || call[0] === "b" |
||||
|
); |
||||
|
|
||||
|
// Prefix and suffix should have fewer updates than loop nodes
|
||||
|
expect(prefixUpdates.length).toBeLessThan(loopNodeUpdates.length); |
||||
|
expect(suffixUpdates.length).toBeLessThan(loopNodeUpdates.length); |
||||
|
}); |
||||
|
|
||||
|
it("does not affect workflows with no loop edges", async () => { |
||||
|
// Standard A → B → C workflow
|
||||
|
const nodeA = makeNode("a", "imageInput", { image: "data:image/png;base64,test" }); |
||||
|
const nodeB = makeNode("b", "prompt", { prompt: "test" }); |
||||
|
const nodeC = makeNode("c", "output"); |
||||
|
|
||||
|
const edges = [makeEdge("a", "b"), makeEdge("b", "c")]; |
||||
|
|
||||
|
setupStore([nodeA, nodeB, nodeC], edges); |
||||
|
|
||||
|
// Should execute without errors
|
||||
|
await useWorkflowStore.getState().executeWorkflow(); |
||||
|
|
||||
|
// Verify workflow completed
|
||||
|
expect(useWorkflowStore.getState().isRunning).toBe(false); |
||||
|
}); |
||||
|
|
||||
|
it("treats loopCount:1 same as no loop", async () => { |
||||
|
// Create A(imageInput) → B(prompt) with loop edge B→A (loopCount: 1)
|
||||
|
const nodeA = makeNode("a", "imageInput", { image: "data:image/png;base64,test" }); |
||||
|
const nodeB = makeNode("b", "prompt", { prompt: "test" }); |
||||
|
|
||||
|
const edges = [makeEdge("a", "b"), makeEdge("b", "a", { isLoop: true, loopCount: 1 })]; |
||||
|
|
||||
|
setupStore([nodeA, nodeB], edges); |
||||
|
|
||||
|
// Spy on copyLoopOutput
|
||||
|
const copyLoopOutputSpy = vi.spyOn( |
||||
|
await import("../utils/executionUtils"), |
||||
|
"copyLoopOutput" |
||||
|
); |
||||
|
|
||||
|
await useWorkflowStore.getState().executeWorkflow(); |
||||
|
|
||||
|
// With loopCount=1, copyLoopOutput should never be called (no iterations to feedback)
|
||||
|
expect(copyLoopOutputSpy).toHaveBeenCalledTimes(0); |
||||
|
}); |
||||
|
|
||||
|
it("stops loop mid-iteration when aborted", async () => { |
||||
|
// Create loop with loopCount: 10
|
||||
|
const nodeA = makeNode("a", "imageInput", { image: "data:image/png;base64,test" }); |
||||
|
const nodeB = makeNode("b", "prompt", { prompt: "test" }); |
||||
|
|
||||
|
const edges = [makeEdge("a", "b"), makeEdge("b", "a", { isLoop: true, loopCount: 10 })]; |
||||
|
|
||||
|
setupStore([nodeA, nodeB], edges); |
||||
|
|
||||
|
// Start execution (don't await)
|
||||
|
const executionPromise = useWorkflowStore.getState().executeWorkflow(); |
||||
|
|
||||
|
// Abort after a short delay
|
||||
|
setTimeout(() => { |
||||
|
useWorkflowStore.getState().stopWorkflow(); |
||||
|
}, 10); |
||||
|
|
||||
|
await executionPromise; |
||||
|
|
||||
|
// Verify execution was stopped
|
||||
|
expect(useWorkflowStore.getState().isRunning).toBe(false); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue