Browse Source

test(20-01): add topological sort integration tests

- Execution order (linear chain, multiple dependencies, parallel branches)
- Cycle detection
- Pause edge handling (stop, resume, pausedAtNodeId)
- Locked group handling (skip locked, execute unlocked)
- Start from specific node
- Execution state management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
addeeeb1a4
  1. 411
      src/store/__tests__/workflowStore.integration.test.ts

411
src/store/__tests__/workflowStore.integration.test.ts

@ -705,4 +705,415 @@ describe("workflowStore integration tests", () => {
});
});
});
describe("executeWorkflow (topological sort)", () => {
// Track execution order via updateNodeData calls
let executionOrder: string[];
beforeEach(() => {
executionOrder = [];
// Mock fetch for API calls
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, image: "data:image/png;base64,generated" }),
text: () => Promise.resolve(""),
}));
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe("Execution order tests", () => {
it("should execute linear chain A -> B -> C in order", async () => {
// Set up: imageInput -> prompt -> nanoBanana
// Only nanoBanana actually "executes" something visible
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }, { x: 0, y: 0 }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}, { x: 100, y: 0 }),
createTestNode("output-1", "output", {}, { x: 200, y: 0 }),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
createTestEdge("nanoBanana-1", "output-1", "image", "image"),
],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Check that the workflow completed (isRunning should be false)
expect(useWorkflowStore.getState().isRunning).toBe(false);
// Check that nanoBanana node was updated (status should be complete or loading at some point)
// The node should have been processed
const nanoBananaNode = useWorkflowStore.getState().nodes.find(n => n.id === "nanoBanana-1");
expect(nanoBananaNode).toBeDefined();
});
it("should execute multiple dependencies A, B -> C correctly", async () => {
// Two prompts feeding into one nanoBanana
useWorkflowStore.setState({
nodes: [
createTestNode("imageInput-1", "imageInput", { image: "data:image/png;base64,test" }, { x: 0, y: 0 }),
createTestNode("prompt-1", "prompt", { prompt: "test prompt" }, { x: 0, y: 100 }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}, { x: 200, y: 50 }),
],
edges: [
createTestEdge("imageInput-1", "nanoBanana-1", "image", "image"),
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Workflow should complete successfully
expect(useWorkflowStore.getState().isRunning).toBe(false);
expect(useWorkflowStore.getState().currentNodeId).toBeNull();
});
it("should throw error on cycle detection", async () => {
// Create a cycle: A -> B -> A
useWorkflowStore.setState({
nodes: [
createTestNode("nanoBanana-1", "nanoBanana", { prompt: "test" }),
createTestNode("nanoBanana-2", "nanoBanana", { prompt: "test" }),
],
edges: [
createTestEdge("nanoBanana-1", "nanoBanana-2", "image", "image"),
createTestEdge("nanoBanana-2", "nanoBanana-1", "image", "image"),
],
});
const store = useWorkflowStore.getState();
// Execute workflow - should handle cycle internally
await store.executeWorkflow();
// After cycle detection, workflow should stop running
expect(useWorkflowStore.getState().isRunning).toBe(false);
});
it("should handle parallel branches that merge", async () => {
// Two parallel paths that merge:
// prompt-1 -> nanoBanana-1 --|
// |-> output-1
// prompt-2 -> nanoBanana-2 --|
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "path 1" }, { x: 0, y: 0 }),
createTestNode("prompt-2", "prompt", { prompt: "path 2" }, { x: 0, y: 200 }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}, { x: 200, y: 0 }),
createTestNode("nanoBanana-2", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}, { x: 200, y: 200 }),
createTestNode("output-1", "output", {}, { x: 400, y: 100 }),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
createTestEdge("prompt-2", "nanoBanana-2", "text", "text"),
createTestEdge("nanoBanana-1", "output-1", "image", "image"),
createTestEdge("nanoBanana-2", "output-1", "image", "image"),
],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Workflow should complete
expect(useWorkflowStore.getState().isRunning).toBe(false);
// Both nanoBanana nodes should have been processed before output
const outputNode = useWorkflowStore.getState().nodes.find(n => n.id === "output-1");
expect(outputNode).toBeDefined();
});
});
describe("Pause edge handling", () => {
it("should stop execution at node with incoming pause edge", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}),
createTestNode("output-1", "output", {}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
createTestEdge("nanoBanana-1", "output-1", "image", "image", true), // Pause edge
],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Should be paused at output node
expect(useWorkflowStore.getState().pausedAtNodeId).toBe("output-1");
expect(useWorkflowStore.getState().isRunning).toBe(false);
});
it("should set pausedAtNodeId correctly", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text", true), // Pause edge
],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
expect(useWorkflowStore.getState().pausedAtNodeId).toBe("nanoBanana-1");
});
it("should resume from paused node when startFromNodeId matches pausedAtNodeId", async () => {
// First, run until pause
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text", true), // Pause edge
],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Should be paused
expect(useWorkflowStore.getState().pausedAtNodeId).toBe("nanoBanana-1");
// Now resume from the paused node
await store.executeWorkflow("nanoBanana-1");
// After resuming, pausedAtNodeId should be cleared
expect(useWorkflowStore.getState().pausedAtNodeId).toBeNull();
expect(useWorkflowStore.getState().isRunning).toBe(false);
});
});
describe("Locked group handling", () => {
it("should skip nodes in locked groups", async () => {
useWorkflowStore.setState({
nodes: [
{ ...createTestNode("prompt-1", "prompt", { prompt: "test" }), groupId: "group-1" },
{ ...createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}), groupId: "group-1" },
createTestNode("output-1", "output", {}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
createTestEdge("nanoBanana-1", "output-1", "image", "image"),
],
groups: {
"group-1": {
id: "group-1",
name: "Locked Group",
color: "neutral" as const,
position: { x: 0, y: 0 },
size: { width: 400, height: 400 },
locked: true, // Locked!
},
},
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Workflow should complete
expect(useWorkflowStore.getState().isRunning).toBe(false);
// The locked nodes should not have made API calls
// Since prompt and nanoBanana are in locked group, they should be skipped
// Only output should execute (but it has no image from skipped nanoBanana)
});
it("should execute non-locked group nodes normally", async () => {
useWorkflowStore.setState({
nodes: [
{ ...createTestNode("prompt-1", "prompt", { prompt: "test" }), groupId: "group-1" },
{ ...createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}), groupId: "group-1" },
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
],
groups: {
"group-1": {
id: "group-1",
name: "Unlocked Group",
color: "neutral" as const,
position: { x: 0, y: 0 },
size: { width: 400, height: 400 },
locked: false, // Not locked
},
},
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Workflow should complete and nodes should execute
expect(useWorkflowStore.getState().isRunning).toBe(false);
});
it("should only skip nodes in the locked group, not other nodes", async () => {
useWorkflowStore.setState({
nodes: [
// Locked group nodes
{ ...createTestNode("prompt-1", "prompt", { prompt: "locked prompt" }), groupId: "group-locked" },
// Unlocked nodes
createTestNode("prompt-2", "prompt", { prompt: "unlocked prompt" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}),
],
edges: [
createTestEdge("prompt-2", "nanoBanana-1", "text", "text"),
],
groups: {
"group-locked": {
id: "group-locked",
name: "Locked Group",
color: "neutral" as const,
position: { x: 0, y: 0 },
size: { width: 200, height: 200 },
locked: true,
},
},
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Workflow should complete
expect(useWorkflowStore.getState().isRunning).toBe(false);
// The unlocked nodes should have executed
// (nanoBanana-1 should have status updated)
});
});
describe("Start from specific node", () => {
it("should skip nodes before startFromNodeId in execution order", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
createTestNode("nanoBanana-1", "nanoBanana", {
outputImage: "data:image/png;base64,existingImage",
aspectRatio: "1:1",
resolution: "1MP",
model: "nano-banana",
}),
createTestNode("output-1", "output", {}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
createTestEdge("nanoBanana-1", "output-1", "image", "image"),
],
});
const store = useWorkflowStore.getState();
// Start from output node - should skip prompt and nanoBanana
await store.executeWorkflow("output-1");
expect(useWorkflowStore.getState().isRunning).toBe(false);
});
});
describe("Execution state management", () => {
it("should set isRunning to true during execution", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
],
edges: [],
});
const store = useWorkflowStore.getState();
// Start workflow but don't await yet
const promise = store.executeWorkflow();
// Wait for workflow to complete
await promise;
// After completion, isRunning should be false
expect(useWorkflowStore.getState().isRunning).toBe(false);
});
it("should ignore execution request if already running", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
],
edges: [],
isRunning: true, // Already running
});
const store = useWorkflowStore.getState();
// This should return immediately without doing anything
await store.executeWorkflow();
// Should still be running (our mock state)
expect(useWorkflowStore.getState().isRunning).toBe(true);
});
it("should clear currentNodeId after execution completes", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
],
edges: [],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
expect(useWorkflowStore.getState().currentNodeId).toBeNull();
});
});
});
});

Loading…
Cancel
Save