Browse Source

Merge pull request #42 from shrimbly/fix/workflow-execution-guard

fix: set isRunning guard before async logger call
handoff-20260429-1057
Willie 5 months ago
committed by GitHub
parent
commit
57faec8261
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 131
      src/store/__tests__/workflowStore.integration.test.ts
  2. 10
      src/store/workflowStore.ts

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

@ -2307,4 +2307,135 @@ describe("workflowStore integration tests", () => {
});
});
});
describe("Race condition prevention", () => {
let mockFetch: ReturnType<typeof vi.fn>;
beforeEach(() => {
mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, image: "data:image/png;base64,generated" }),
text: () => Promise.resolve(""),
});
vi.stubGlobal("fetch", mockFetch);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("should only execute nodes once when executeWorkflow is called concurrently", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1K",
model: "nano-banana",
}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
],
});
const store = useWorkflowStore.getState();
// Fire two calls back-to-back without awaiting the first
const p1 = store.executeWorkflow();
const p2 = store.executeWorkflow();
await Promise.all([p1, p2]);
// Only one execution should have reached fetch (one nanoBanana node)
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it("should set isRunning synchronously before any await", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1K",
model: "nano-banana",
}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
],
});
const store = useWorkflowStore.getState();
expect(useWorkflowStore.getState().isRunning).toBe(false);
// Call without awaiting — isRunning should be true synchronously
const promise = store.executeWorkflow();
expect(useWorkflowStore.getState().isRunning).toBe(true);
await promise;
});
it("should only execute once when regenerateNode is called concurrently", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "test" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1K",
model: "nano-banana",
inputImages: [],
outputImage: "data:image/png;base64,previous",
}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
],
});
const store = useWorkflowStore.getState();
// Fire two calls back-to-back — second should be blocked by isRunning
const p1 = store.regenerateNode("nanoBanana-1");
const p2 = store.regenerateNode("nanoBanana-1");
await Promise.all([p1, p2]);
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it("should execute each node exactly once with multiple disconnected nodes", async () => {
useWorkflowStore.setState({
nodes: [
createTestNode("prompt-1", "prompt", { prompt: "a" }),
createTestNode("prompt-2", "prompt", { prompt: "b" }),
createTestNode("prompt-3", "prompt", { prompt: "c" }),
createTestNode("nanoBanana-1", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1K",
model: "nano-banana",
}),
createTestNode("nanoBanana-2", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1K",
model: "nano-banana",
}),
createTestNode("nanoBanana-3", "nanoBanana", {
aspectRatio: "1:1",
resolution: "1K",
model: "nano-banana",
}),
],
edges: [
createTestEdge("prompt-1", "nanoBanana-1", "text", "text"),
createTestEdge("prompt-2", "nanoBanana-2", "text", "text"),
createTestEdge("prompt-3", "nanoBanana-3", "text", "text"),
],
});
const store = useWorkflowStore.getState();
await store.executeWorkflow();
// Exactly 3 fetch calls — one per nanoBanana node
expect(mockFetch).toHaveBeenCalledTimes(3);
});
});
});

10
src/store/workflowStore.ts

@ -882,12 +882,12 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
return;
}
// Start logging session
await logger.startSession();
const isResuming = startFromNodeId === get().pausedAtNodeId;
set({ isRunning: true, pausedAtNodeId: null });
// Start logging session
await logger.startSession();
logger.info('workflow.start', 'Workflow execution started', {
nodeCount: nodes.length,
edgeCount: edges.length,
@ -1791,14 +1791,14 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
return;
}
set({ isRunning: true, currentNodeId: nodeId });
await logger.startSession();
logger.info('node.execution', 'Regenerating node', {
nodeId,
nodeType: node.type,
});
set({ isRunning: true, currentNodeId: nodeId });
try {
if (node.type === "nanoBanana") {
// Get fresh node data from store

Loading…
Cancel
Save