Browse Source
- Extend WorkflowEdgeData with isLoop and loopCount fields - Add wouldCreateCycle tests (linear, diamond, self-loop, disconnected) - Add findLoopSubgraph tests (simple loop, partial loop, branch within loop) - Add copyLoopOutput tests (image, video, text, audio data types) - Tests fail as expected (RED phase)handoff-20260429-1057
2 changed files with 163 additions and 0 deletions
@ -0,0 +1,161 @@ |
|||
/** |
|||
* Loop Edge Utilities Tests |
|||
* |
|||
* Tests for cycle detection, loop subgraph identification, and loop data copying. |
|||
*/ |
|||
|
|||
import { describe, it, expect, vi } from "vitest"; |
|||
import { |
|||
wouldCreateCycle, |
|||
findLoopSubgraph, |
|||
copyLoopOutput, |
|||
} from "@/store/utils/executionUtils"; |
|||
import { WorkflowEdge, WorkflowNode, WorkflowNodeData } from "@/types"; |
|||
|
|||
// Helper to create minimal edge objects
|
|||
function makeEdge(source: string, target: string, data?: Record<string, unknown>): WorkflowEdge { |
|||
return { |
|||
id: `${source}-${target}`, |
|||
source, |
|||
target, |
|||
data: data || {}, |
|||
}; |
|||
} |
|||
|
|||
// Helper to create minimal node objects
|
|||
function makeNode(id: string, type: string, data: Partial<WorkflowNodeData> = {}): WorkflowNode { |
|||
return { |
|||
id, |
|||
type, |
|||
position: { x: 0, y: 0 }, |
|||
data: data as WorkflowNodeData, |
|||
}; |
|||
} |
|||
|
|||
describe("wouldCreateCycle", () => { |
|||
it("detects cycle in linear chain A→B→C when connecting C→A", () => { |
|||
const edges = [makeEdge("A", "B"), makeEdge("B", "C")]; |
|||
expect(wouldCreateCycle("C", "A", edges)).toBe(true); |
|||
}); |
|||
|
|||
it("returns false when no cycle: A→B, connecting A→C", () => { |
|||
const edges = [makeEdge("A", "B")]; |
|||
expect(wouldCreateCycle("A", "C", edges)).toBe(false); |
|||
}); |
|||
|
|||
it("detects cycle in diamond: A→B→D, A→C→D, connecting D→A", () => { |
|||
const edges = [ |
|||
makeEdge("A", "B"), |
|||
makeEdge("A", "C"), |
|||
makeEdge("B", "D"), |
|||
makeEdge("C", "D"), |
|||
]; |
|||
expect(wouldCreateCycle("D", "A", edges)).toBe(true); |
|||
}); |
|||
|
|||
it("detects self-loop: connecting A→A", () => { |
|||
const edges: WorkflowEdge[] = []; |
|||
expect(wouldCreateCycle("A", "A", edges)).toBe(true); |
|||
}); |
|||
|
|||
it("returns false for disconnected graph: A→B, C→D, connecting D→A", () => { |
|||
const edges = [makeEdge("A", "B"), makeEdge("C", "D")]; |
|||
expect(wouldCreateCycle("D", "A", edges)).toBe(false); |
|||
}); |
|||
}); |
|||
|
|||
describe("findLoopSubgraph", () => { |
|||
it("identifies simple loop A→B→C with loop edge C→A", () => { |
|||
const edges = [makeEdge("A", "B"), makeEdge("B", "C")]; |
|||
const result = findLoopSubgraph("C", "A", edges); |
|||
expect(result.sort()).toEqual(["A", "B", "C"].sort()); |
|||
}); |
|||
|
|||
it("excludes nodes outside loop: A→B→C→D with loop edge C→A", () => { |
|||
const edges = [makeEdge("A", "B"), makeEdge("B", "C"), makeEdge("C", "D")]; |
|||
const result = findLoopSubgraph("C", "A", edges); |
|||
expect(result.sort()).toEqual(["A", "B", "C"].sort()); |
|||
expect(result).not.toContain("D"); |
|||
}); |
|||
|
|||
it("includes branch within loop: A→B→C, B→D→C with loop edge C→A", () => { |
|||
const edges = [ |
|||
makeEdge("A", "B"), |
|||
makeEdge("B", "C"), |
|||
makeEdge("B", "D"), |
|||
makeEdge("D", "C"), |
|||
]; |
|||
const result = findLoopSubgraph("C", "A", edges); |
|||
expect(result.sort()).toEqual(["A", "B", "C", "D"].sort()); |
|||
}); |
|||
}); |
|||
|
|||
describe("copyLoopOutput", () => { |
|||
it("copies image from nanoBanana to imageInput", () => { |
|||
const sourceNode = makeNode("source", "nanoBanana", { |
|||
outputImage: "data:image/png;base64,abc123", |
|||
}); |
|||
const targetNode = makeNode("target", "imageInput", {}); |
|||
const updateNodeData = vi.fn(); |
|||
|
|||
copyLoopOutput(sourceNode, "image", targetNode, "image", updateNodeData); |
|||
|
|||
expect(updateNodeData).toHaveBeenCalledWith("target", { |
|||
image: "data:image/png;base64,abc123", |
|||
}); |
|||
}); |
|||
|
|||
it("copies video from generateVideo to videoInput", () => { |
|||
const sourceNode = makeNode("source", "generateVideo", { |
|||
outputVideo: "data:video/mp4;base64,xyz789", |
|||
}); |
|||
const targetNode = makeNode("target", "videoInput", {}); |
|||
const updateNodeData = vi.fn(); |
|||
|
|||
copyLoopOutput(sourceNode, "video", targetNode, "video", updateNodeData); |
|||
|
|||
expect(updateNodeData).toHaveBeenCalledWith("target", { |
|||
video: "data:video/mp4;base64,xyz789", |
|||
}); |
|||
}); |
|||
|
|||
it("copies text from llmGenerate to prompt", () => { |
|||
const sourceNode = makeNode("source", "llmGenerate", { |
|||
outputText: "Generated prompt text", |
|||
}); |
|||
const targetNode = makeNode("target", "prompt", {}); |
|||
const updateNodeData = vi.fn(); |
|||
|
|||
copyLoopOutput(sourceNode, "text", targetNode, "text", updateNodeData); |
|||
|
|||
expect(updateNodeData).toHaveBeenCalledWith("target", { |
|||
prompt: "Generated prompt text", |
|||
}); |
|||
}); |
|||
|
|||
it("copies audio from generateAudio to audioInput", () => { |
|||
const sourceNode = makeNode("source", "generateAudio", { |
|||
outputAudio: "data:audio/mp3;base64,audio123", |
|||
}); |
|||
const targetNode = makeNode("target", "audioInput", {}); |
|||
const updateNodeData = vi.fn(); |
|||
|
|||
copyLoopOutput(sourceNode, "audio", targetNode, "audio", updateNodeData); |
|||
|
|||
expect(updateNodeData).toHaveBeenCalledWith("target", { |
|||
audioFile: "data:audio/mp3;base64,audio123", |
|||
}); |
|||
}); |
|||
|
|||
it("does nothing when source value is null", () => { |
|||
const sourceNode = makeNode("source", "nanoBanana", { |
|||
outputImage: null, |
|||
}); |
|||
const targetNode = makeNode("target", "imageInput", {}); |
|||
const updateNodeData = vi.fn(); |
|||
|
|||
copyLoopOutput(sourceNode, "image", targetNode, "image", updateNodeData); |
|||
|
|||
expect(updateNodeData).not.toHaveBeenCalled(); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue