import { describe, it, expect } from "vitest"; import { getConnectedInputsPure, validateWorkflowPure } from "../connectedInputs"; import type { WorkflowNode, WorkflowEdge } from "@/types"; function makeNode(id: string, type: string, data: Record = {}): WorkflowNode { return { id, type, position: { x: 0, y: 0 }, data } as WorkflowNode; } function makeEdge(source: string, target: string, targetHandle?: string): WorkflowEdge { return { id: `${source}-${target}`, source, target, sourceHandle: "image", targetHandle: targetHandle || "image", } as WorkflowEdge; } describe("getConnectedInputsPure", () => { it("should return empty arrays when no edges connect to node", () => { const nodes = [makeNode("a", "prompt")]; const result = getConnectedInputsPure("a", nodes, []); expect(result.images).toEqual([]); expect(result.videos).toEqual([]); expect(result.audio).toEqual([]); expect(result.text).toBeNull(); expect(result.dynamicInputs).toEqual({}); expect(result.easeCurve).toBeNull(); }); it("should extract image from imageInput source", () => { const nodes = [ makeNode("img", "imageInput", { image: "data:image/png;base64,abc" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("img", "gen", "image")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.images).toEqual(["data:image/png;base64,abc"]); }); it("should extract text from prompt source", () => { const nodes = [ makeNode("p", "prompt", { prompt: "hello world" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("p", "gen", "text")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("hello world"); expect(result.textItems).toEqual(["hello world"]); expect(result.batchTextItems).toEqual([]); }); it("should collect multiple connected text inputs without overwriting earlier text", () => { const nodes = [ makeNode("p1", "prompt", { prompt: "first prompt" }), makeNode("p2", "prompt", { prompt: "second prompt" }), makeNode("gen", "generateVideo"), ]; const edges = [ makeEdge("p1", "gen", "text"), makeEdge("p2", "gen", "text"), ]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("first prompt"); expect(result.textItems).toEqual(["first prompt", "second prompt"]); expect(result.batchTextItems).toEqual([]); }); it("should extract image from annotation output", () => { const nodes = [ makeNode("ann", "annotation", { outputImage: "data:image/png;base64,xyz" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("ann", "gen", "image")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.images).toEqual(["data:image/png;base64,xyz"]); }); it("should extract image from nanoBanana output", () => { const nodes = [ makeNode("nb", "nanoBanana", { outputImage: "data:image/png;base64,nb" }), makeNode("out", "output"), ]; const edges = [makeEdge("nb", "out", "image")]; const result = getConnectedInputsPure("out", nodes, edges); expect(result.images).toEqual(["data:image/png;base64,nb"]); }); it("should extract video from generateVideo source", () => { const nodes = [ makeNode("vid", "generateVideo", { outputVideo: "data:video/mp4;base64,vid" }), makeNode("out", "output"), ]; const edges = [makeEdge("vid", "out", "image")]; const result = getConnectedInputsPure("out", nodes, edges); expect(result.videos).toEqual(["data:video/mp4;base64,vid"]); }); it("should extract text from llmGenerate source", () => { const nodes = [ makeNode("llm", "llmGenerate", { outputText: "generated text" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("llm", "gen", "text")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("generated text"); }); it("should extract full array JSON from array node default text output", () => { const nodes = [ makeNode("arr", "array", { outputText: "[\"one\",\"two\"]", outputItems: ["one", "two"] }), makeNode("gen", "nanoBanana"), ]; const edges = [{ id: "arr-gen", source: "arr", target: "gen", sourceHandle: "text", targetHandle: "text", }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("[\"one\",\"two\"]"); expect(result.textItems).toEqual(["[\"one\",\"two\"]"]); expect(result.batchTextItems).toEqual([]); }); it("should expose array batch items separately from ordinary multi-text inputs", () => { const nodes = [ makeNode("arr", "array", { outputText: "[\"one\",\"two\"]", outputItems: ["one", "two"], batchMode: true }), makeNode("gen", "nanoBanana"), ]; const edges = [{ id: "arr-gen-batch", source: "arr", target: "gen", sourceHandle: "text", targetHandle: "text", }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("one"); expect(result.textItems).toEqual(["one", "two"]); expect(result.batchTextItems).toEqual(["one", "two"]); }); it("should extract indexed item from array node dynamic output handle", () => { const nodes = [ makeNode("arr", "array", { outputText: "[\"one\",\"two\"]", outputItems: ["one", "two"] }), makeNode("gen", "nanoBanana"), ]; const edges = [{ id: "arr-gen-item", source: "arr", target: "gen", sourceHandle: "text-1", targetHandle: "text", }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("two"); }); it("should extract indexed item from array node edge metadata index", () => { const nodes = [ makeNode("arr", "array", { outputText: "[\"one\",\"two\",\"three\"]", outputItems: ["one", "two", "three"] }), makeNode("gen", "nanoBanana"), ]; const edges = [{ id: "arr-gen-meta", source: "arr", target: "gen", sourceHandle: "text", targetHandle: "text", data: { arrayItemIndex: 2 }, }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("three"); }); it("should wrap out-of-bounds arrayItemIndex via modulo", () => { const nodes = [ makeNode("arr", "array", { outputText: "[\"one\",\"two\"]", outputItems: ["one", "two"] }), makeNode("gen", "nanoBanana"), ]; const edges = [{ id: "arr-gen-stale", source: "arr", target: "gen", sourceHandle: "text", targetHandle: "text", data: { arrayItemIndex: 3 }, // index 3 on 2-item array wraps to index 1 }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("two"); }); it("should return null for out-of-bounds arrayItemIndex on empty array", () => { const nodes = [ makeNode("arr", "array", { outputText: "[]", outputItems: [] }), makeNode("gen", "nanoBanana"), ]; const edges = [{ id: "arr-gen-empty", source: "arr", target: "gen", sourceHandle: "text", targetHandle: "text", data: { arrayItemIndex: 0 }, }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBeNull(); }); it("should wrap large arrayItemIndex correctly", () => { const nodes = [ makeNode("arr", "array", { outputText: "[\"a\",\"b\",\"c\"]", outputItems: ["a", "b", "c"] }), makeNode("gen", "nanoBanana"), ]; const edges = [{ id: "arr-gen-large", source: "arr", target: "gen", sourceHandle: "text", targetHandle: "text", data: { arrayItemIndex: 7 }, // 7 % 3 = 1 }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("b"); }); it("should extract text from promptConstructor outputText", () => { const nodes = [ makeNode("pc", "promptConstructor", { outputText: "constructed", template: "tmpl" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("pc", "gen", "text")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("constructed"); }); it("should fallback to template when promptConstructor has no outputText", () => { const nodes = [ makeNode("pc", "promptConstructor", { outputText: null, template: "tmpl" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("pc", "gen", "text")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.text).toBe("tmpl"); }); it("should skip source nodes with null output", () => { const nodes = [ makeNode("img", "imageInput", { image: null }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("img", "gen", "image")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.images).toEqual([]); }); it("should handle multiple image inputs", () => { const nodes = [ makeNode("img1", "imageInput", { image: "data:image/png;base64,a" }), makeNode("img2", "imageInput", { image: "data:image/png;base64,b" }), makeNode("gen", "nanoBanana"), ]; const edges = [ makeEdge("img1", "gen", "image"), makeEdge("img2", "gen", "image"), ]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.images).toEqual(["data:image/png;base64,a", "data:image/png;base64,b"]); }); it("should populate dynamicInputs with schema mapping", () => { const nodes = [ makeNode("img", "imageInput", { image: "data:image/png;base64,a" }), makeNode("gen", "nanoBanana", { inputSchema: [{ name: "image_url", type: "image" }], }), ]; const edges = [makeEdge("img", "gen", "image-0")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.dynamicInputs).toEqual({ image_url: "data:image/png;base64,a" }); }); it("should populate dynamicInputs with video schema mapping", () => { const nodes = [ makeNode("vid", "videoInput", { video: "data:video/mp4;base64,v" }), makeNode("gen", "generateVideo", { inputSchema: [{ name: "video_urls", type: "video" }], }), ]; const edges = [{ id: "vid-gen", source: "vid", target: "gen", sourceHandle: "video", targetHandle: "video-0", }] as WorkflowEdge[]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.videos).toEqual(["data:video/mp4;base64,v"]); expect(result.dynamicInputs).toEqual({ video_urls: "data:video/mp4;base64,v" }); }); it("should extract easeCurve data", () => { const nodes = [ makeNode("ec", "easeCurve", { bezierHandles: [0.25, 0.1, 0.25, 1.0], easingPreset: "ease-in-out", outputDuration: 8, outputVideo: null, }), makeNode("vs", "videoStitch"), ]; const edges = [{ id: "ec-vs", source: "ec", target: "vs", sourceHandle: "easeCurve", targetHandle: "easeCurve", }] as WorkflowEdge[]; const result = getConnectedInputsPure("vs", nodes, edges); expect(result.easeCurve).toEqual({ bezierHandles: [0.25, 0.1, 0.25, 1.0], easingPreset: "ease-in-out", outputDuration: 8, }); }); it("should extract capturedImage from glbViewer source", () => { const nodes = [ makeNode("glb", "glbViewer", { capturedImage: "data:image/png;base64,snap" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("glb", "gen", "image")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.images).toEqual(["data:image/png;base64,snap"]); }); it("should return empty images when glbViewer has no capture", () => { const nodes = [ makeNode("glb", "glbViewer", { capturedImage: null }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("glb", "gen", "image")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.images).toEqual([]); }); it("should extract audio from audioInput source", () => { const nodes = [ makeNode("aud", "audioInput", { audioFile: "data:audio/wav;base64,abc" }), makeNode("gen", "nanoBanana"), ]; const edges = [makeEdge("aud", "gen", "audio")]; const result = getConnectedInputsPure("gen", nodes, edges); expect(result.audio).toEqual(["data:audio/wav;base64,abc"]); }); }); describe("validateWorkflowPure", () => { it("should fail for empty workflow", () => { const result = validateWorkflowPure([], []); expect(result.valid).toBe(false); expect(result.errors).toContain("Workflow is empty"); }); it("should pass for valid workflow", () => { const nodes = [ makeNode("p", "prompt"), makeNode("gen", "nanoBanana"), makeNode("out", "output"), ]; const edges = [ makeEdge("p", "gen", "text"), makeEdge("gen", "out"), ]; const result = validateWorkflowPure(nodes, edges); expect(result.valid).toBe(true); expect(result.errors).toEqual([]); }); it("should detect missing text input on nanoBanana", () => { const nodes = [makeNode("gen", "nanoBanana")]; const result = validateWorkflowPure(nodes, []); expect(result.valid).toBe(false); expect(result.errors[0]).toContain("missing text input"); }); it("should detect missing input on generateVideo", () => { const nodes = [makeNode("vid", "generateVideo")]; const result = validateWorkflowPure(nodes, []); expect(result.valid).toBe(false); expect(result.errors[0]).toContain("missing input"); }); it("should accept video input on generateVideo", () => { const nodes = [makeNode("src", "videoInput"), makeNode("vid", "generateVideo")]; const edges = [{ id: "src-vid", source: "src", target: "vid", sourceHandle: "video", targetHandle: "video", }] as WorkflowEdge[]; const result = validateWorkflowPure(nodes, edges); expect(result.valid).toBe(true); }); it("should detect missing image input on annotation without manual image", () => { const nodes = [makeNode("ann", "annotation", { sourceImage: null })]; const result = validateWorkflowPure(nodes, []); expect(result.valid).toBe(false); expect(result.errors[0]).toContain("missing image input"); }); it("should pass annotation with manual image", () => { const nodes = [makeNode("ann", "annotation", { sourceImage: "data:image/png;base64,x" })]; const result = validateWorkflowPure(nodes, []); expect(result.valid).toBe(true); }); it("should detect missing image input on output", () => { const nodes = [makeNode("out", "output")]; const result = validateWorkflowPure(nodes, []); expect(result.valid).toBe(false); expect(result.errors[0]).toContain("missing image input"); }); it("should accept text-0 handle for nanoBanana", () => { const nodes = [makeNode("p", "prompt"), makeNode("gen", "nanoBanana")]; const edges = [makeEdge("p", "gen", "text-0")]; const result = validateWorkflowPure(nodes, edges); expect(result.valid).toBe(true); }); it("should accept saved local prompt on nanoBanana without a text edge", () => { const nodes = [makeNode("gen", "nanoBanana", { inputPrompt: "remote saved prompt" })]; const result = validateWorkflowPure(nodes, []); expect(result.valid).toBe(true); }); });