Browse Source
- Basic data extraction (imageInput, annotation, nanoBanana, prompt, llmGenerate) - Multiple connections handling (images array, text last-wins) - Dynamic input mapping with inputSchema - Edge cases (no connections, null data, non-existent nodes) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
1 changed files with 432 additions and 0 deletions
@ -0,0 +1,432 @@ |
|||||
|
/** |
||||
|
* Integration tests for workflowStore |
||||
|
* |
||||
|
* Tests the integration functions that handle node connections and data flow: |
||||
|
* - getConnectedInputs: extracts data from connected nodes |
||||
|
* - validateWorkflow: checks workflow integrity |
||||
|
* - topological sort: ensures correct execution order |
||||
|
*/ |
||||
|
|
||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; |
||||
|
import { act } from "@testing-library/react"; |
||||
|
import { useWorkflowStore } from "../workflowStore"; |
||||
|
import type { WorkflowNode, WorkflowEdge } from "@/types"; |
||||
|
|
||||
|
// Mock the Toast hook
|
||||
|
vi.mock("@/components/Toast", () => ({ |
||||
|
useToast: { |
||||
|
getState: () => ({ |
||||
|
show: vi.fn(), |
||||
|
}), |
||||
|
}, |
||||
|
})); |
||||
|
|
||||
|
// Mock the logger
|
||||
|
vi.mock("@/utils/logger", () => ({ |
||||
|
logger: { |
||||
|
info: vi.fn(), |
||||
|
warn: vi.fn(), |
||||
|
error: vi.fn(), |
||||
|
startSession: vi.fn().mockResolvedValue(undefined), |
||||
|
endSession: vi.fn().mockResolvedValue(undefined), |
||||
|
getCurrentSession: vi.fn().mockReturnValue(null), |
||||
|
}, |
||||
|
})); |
||||
|
|
||||
|
// Mock localStorage for provider settings
|
||||
|
const mockLocalStorage: Record<string, string> = {}; |
||||
|
vi.stubGlobal("localStorage", { |
||||
|
getItem: vi.fn((key: string) => mockLocalStorage[key] || null), |
||||
|
setItem: vi.fn((key: string, value: string) => { |
||||
|
mockLocalStorage[key] = value; |
||||
|
}), |
||||
|
removeItem: vi.fn((key: string) => { |
||||
|
delete mockLocalStorage[key]; |
||||
|
}), |
||||
|
clear: vi.fn(() => { |
||||
|
Object.keys(mockLocalStorage).forEach((key) => delete mockLocalStorage[key]); |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// Helper to reset store state between tests
|
||||
|
function resetStore() { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
store.clearWorkflow(); |
||||
|
} |
||||
|
|
||||
|
// Helper to create a test node
|
||||
|
function createTestNode( |
||||
|
id: string, |
||||
|
type: string, |
||||
|
data: Record<string, unknown> = {}, |
||||
|
position = { x: 0, y: 0 } |
||||
|
): WorkflowNode { |
||||
|
return { |
||||
|
id, |
||||
|
type: type as WorkflowNode["type"], |
||||
|
position, |
||||
|
data: data as WorkflowNode["data"], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// Helper to create a test edge
|
||||
|
function createTestEdge( |
||||
|
source: string, |
||||
|
target: string, |
||||
|
sourceHandle: string | null = null, |
||||
|
targetHandle: string | null = null, |
||||
|
hasPause = false |
||||
|
): WorkflowEdge { |
||||
|
return { |
||||
|
id: `edge-${source}-${target}-${sourceHandle || "default"}-${targetHandle || "default"}`, |
||||
|
source, |
||||
|
target, |
||||
|
sourceHandle, |
||||
|
targetHandle, |
||||
|
data: hasPause ? { hasPause: true } : undefined, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
describe("workflowStore integration tests", () => { |
||||
|
beforeEach(() => { |
||||
|
resetStore(); |
||||
|
vi.clearAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
resetStore(); |
||||
|
}); |
||||
|
|
||||
|
describe("getConnectedInputs", () => { |
||||
|
describe("Basic data extraction scenarios", () => { |
||||
|
it("should extract image from imageInput node", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage = "data:image/png;base64,testImageData"; |
||||
|
|
||||
|
// Set up nodes and edges directly
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("imageInput-1", "imageInput", { image: testImage }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("imageInput-1", "nanoBanana-1", "image", "image")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.images).toContain(testImage); |
||||
|
expect(result.images).toHaveLength(1); |
||||
|
}); |
||||
|
|
||||
|
it("should extract image from annotation node (outputImage)", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage = "data:image/png;base64,annotatedImageData"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("annotation-1", "annotation", { outputImage: testImage }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("annotation-1", "nanoBanana-1", "image", "image")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.images).toContain(testImage); |
||||
|
expect(result.images).toHaveLength(1); |
||||
|
}); |
||||
|
|
||||
|
it("should extract image from nanoBanana node (outputImage)", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage = "data:image/png;base64,generatedImageData"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", { outputImage: testImage }), |
||||
|
createTestNode("nanoBanana-2", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("nanoBanana-1", "nanoBanana-2", "image", "image")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-2"); |
||||
|
|
||||
|
expect(result.images).toContain(testImage); |
||||
|
expect(result.images).toHaveLength(1); |
||||
|
}); |
||||
|
|
||||
|
it("should extract text from prompt node", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testPrompt = "A beautiful sunset over the ocean"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("prompt-1", "prompt", { prompt: testPrompt }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("prompt-1", "nanoBanana-1", "text", "text")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.text).toBe(testPrompt); |
||||
|
}); |
||||
|
|
||||
|
it("should extract text from llmGenerate node (outputText)", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testOutput = "Generated text from LLM"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("llmGenerate-1", "llmGenerate", { outputText: testOutput }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("llmGenerate-1", "nanoBanana-1", "text", "text")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.text).toBe(testOutput); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Multiple connections", () => { |
||||
|
it("should collect multiple images from different sources", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage1 = "data:image/png;base64,image1Data"; |
||||
|
const testImage2 = "data:image/png;base64,image2Data"; |
||||
|
const testImage3 = "data:image/png;base64,image3Data"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("imageInput-1", "imageInput", { image: testImage1 }), |
||||
|
createTestNode("imageInput-2", "imageInput", { image: testImage2 }), |
||||
|
createTestNode("annotation-1", "annotation", { outputImage: testImage3 }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [ |
||||
|
createTestEdge("imageInput-1", "nanoBanana-1", "image", "image"), |
||||
|
createTestEdge("imageInput-2", "nanoBanana-1", "image", "image"), |
||||
|
createTestEdge("annotation-1", "nanoBanana-1", "image", "image"), |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.images).toHaveLength(3); |
||||
|
expect(result.images).toContain(testImage1); |
||||
|
expect(result.images).toContain(testImage2); |
||||
|
expect(result.images).toContain(testImage3); |
||||
|
}); |
||||
|
|
||||
|
it("should use last connected text source (not array)", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const prompt1 = "First prompt"; |
||||
|
const prompt2 = "Second prompt"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("prompt-1", "prompt", { prompt: prompt1 }), |
||||
|
createTestNode("prompt-2", "prompt", { prompt: prompt2 }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [ |
||||
|
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"), |
||||
|
createTestEdge("prompt-2", "nanoBanana-1", "text", "text"), |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
// Should have text from one of the prompts (last one processed)
|
||||
|
expect(result.text).toBe(prompt2); |
||||
|
}); |
||||
|
|
||||
|
it("should handle mix of image and text connections", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage = "data:image/png;base64,imageData"; |
||||
|
const testPrompt = "Test prompt text"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("imageInput-1", "imageInput", { image: testImage }), |
||||
|
createTestNode("prompt-1", "prompt", { prompt: testPrompt }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [ |
||||
|
createTestEdge("imageInput-1", "nanoBanana-1", "image", "image"), |
||||
|
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"), |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.images).toContain(testImage); |
||||
|
expect(result.images).toHaveLength(1); |
||||
|
expect(result.text).toBe(testPrompt); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Dynamic input mapping", () => { |
||||
|
it("should map handle IDs to schema names when inputSchema is present", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage = "data:image/png;base64,imageData"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("imageInput-1", "imageInput", { image: testImage }), |
||||
|
createTestNode("generateVideo-1", "generateVideo", { |
||||
|
inputSchema: [ |
||||
|
{ name: "image_url", type: "image", required: true, label: "Image" }, |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
edges: [createTestEdge("imageInput-1", "generateVideo-1", "image", "image")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("generateVideo-1"); |
||||
|
|
||||
|
expect(result.dynamicInputs).toHaveProperty("image_url"); |
||||
|
expect(result.dynamicInputs.image_url).toBe(testImage); |
||||
|
}); |
||||
|
|
||||
|
it("should map multiple image handles to schema names", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage1 = "data:image/png;base64,firstFrame"; |
||||
|
const testImage2 = "data:image/png;base64,lastFrame"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("imageInput-1", "imageInput", { image: testImage1 }), |
||||
|
createTestNode("imageInput-2", "imageInput", { image: testImage2 }), |
||||
|
createTestNode("generateVideo-1", "generateVideo", { |
||||
|
inputSchema: [ |
||||
|
{ name: "first_frame", type: "image", required: true, label: "First Frame" }, |
||||
|
{ name: "last_frame", type: "image", required: false, label: "Last Frame" }, |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
edges: [ |
||||
|
createTestEdge("imageInput-1", "generateVideo-1", "image", "image-0"), |
||||
|
createTestEdge("imageInput-2", "generateVideo-1", "image", "image-1"), |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("generateVideo-1"); |
||||
|
|
||||
|
expect(result.dynamicInputs).toHaveProperty("first_frame", testImage1); |
||||
|
expect(result.dynamicInputs).toHaveProperty("last_frame", testImage2); |
||||
|
}); |
||||
|
|
||||
|
it("should map multiple text handles to schema names", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const prompt = "Create a video"; |
||||
|
const negativePrompt = "No blur"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("prompt-1", "prompt", { prompt }), |
||||
|
createTestNode("prompt-2", "prompt", { prompt: negativePrompt }), |
||||
|
createTestNode("generateVideo-1", "generateVideo", { |
||||
|
inputSchema: [ |
||||
|
{ name: "prompt", type: "text", required: true, label: "Prompt" }, |
||||
|
{ name: "negative_prompt", type: "text", required: false, label: "Negative Prompt" }, |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
edges: [ |
||||
|
createTestEdge("prompt-1", "generateVideo-1", "text", "text-0"), |
||||
|
createTestEdge("prompt-2", "generateVideo-1", "text", "text-1"), |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("generateVideo-1"); |
||||
|
|
||||
|
expect(result.dynamicInputs).toHaveProperty("prompt", prompt); |
||||
|
expect(result.dynamicInputs).toHaveProperty("negative_prompt", negativePrompt); |
||||
|
}); |
||||
|
|
||||
|
it("should not populate dynamicInputs when no inputSchema present", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
const testImage = "data:image/png;base64,imageData"; |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("imageInput-1", "imageInput", { image: testImage }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("imageInput-1", "nanoBanana-1", "image", "image")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(Object.keys(result.dynamicInputs)).toHaveLength(0); |
||||
|
// But images array should still be populated
|
||||
|
expect(result.images).toContain(testImage); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Edge cases", () => { |
||||
|
it("should return empty images array and null text when no connections", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [createTestNode("nanoBanana-1", "nanoBanana", {})], |
||||
|
edges: [], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.images).toEqual([]); |
||||
|
expect(result.text).toBeNull(); |
||||
|
expect(result.dynamicInputs).toEqual({}); |
||||
|
}); |
||||
|
|
||||
|
it("should handle source node with null output data", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("imageInput-1", "imageInput", { image: null }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("imageInput-1", "nanoBanana-1", "image", "image")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.images).toEqual([]); |
||||
|
}); |
||||
|
|
||||
|
it("should handle connection to non-existent source node", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [createTestNode("nanoBanana-1", "nanoBanana", {})], |
||||
|
edges: [createTestEdge("nonexistent-1", "nanoBanana-1", "image", "image")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
expect(result.images).toEqual([]); |
||||
|
expect(result.text).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it("should treat empty string as no value (falsy check in getSourceOutput)", () => { |
||||
|
const store = useWorkflowStore.getState(); |
||||
|
|
||||
|
useWorkflowStore.setState({ |
||||
|
nodes: [ |
||||
|
createTestNode("prompt-1", "prompt", { prompt: "" }), |
||||
|
createTestNode("nanoBanana-1", "nanoBanana", {}), |
||||
|
], |
||||
|
edges: [createTestEdge("prompt-1", "nanoBanana-1", "text", "text")], |
||||
|
}); |
||||
|
|
||||
|
const result = store.getConnectedInputs("nanoBanana-1"); |
||||
|
|
||||
|
// Empty string is treated as falsy/no value by getSourceOutput
|
||||
|
expect(result.text).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue