From bf82f9d767e42782a7be8e5b05e49c4053292413 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 11 Apr 2026 20:32:39 +1200 Subject: [PATCH] feat: runWithFallback passes fallbackParameters to runOnce Updates the runOnce signature to accept an optional parametersOverride. Primary call always passes undefined; fallback call passes the fallbackParameters from options. Includes tests for all three cases. Co-Authored-By: Claude Sonnet 4.5 --- .../__tests__/runWithFallback.test.ts | 76 +++++++++++++++++-- src/store/execution/runWithFallback.ts | 21 ++++- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/store/execution/__tests__/runWithFallback.test.ts b/src/store/execution/__tests__/runWithFallback.test.ts index 90f063a8..2b379277 100644 --- a/src/store/execution/__tests__/runWithFallback.test.ts +++ b/src/store/execution/__tests__/runWithFallback.test.ts @@ -53,7 +53,7 @@ describe("runWithFallback", () => { }); expect(runOnce).toHaveBeenCalledTimes(1); - expect(runOnce).toHaveBeenCalledWith(primary); + expect(runOnce).toHaveBeenCalledWith(primary, undefined); // Only the initial clear call — no metadata update after success. expect(updateNodeData).toHaveBeenCalledTimes(1); }); @@ -88,8 +88,8 @@ describe("runWithFallback", () => { }); expect(runOnce).toHaveBeenCalledTimes(2); - expect(runOnce).toHaveBeenNthCalledWith(1, primary); - expect(runOnce).toHaveBeenNthCalledWith(2, fallback); + expect(runOnce).toHaveBeenNthCalledWith(1, primary, undefined); + expect(runOnce).toHaveBeenNthCalledWith(2, fallback, undefined); // Last call should be the metadata stamp. const lastCall = updateNodeData.mock.calls.at(-1)!; @@ -149,7 +149,7 @@ describe("runWithFallback", () => { expect(runOnce).toHaveBeenCalledTimes(1); }); - it("fallback AbortError: rethrows and does not stamp metadata", async () => { + it("fallback AbortError: rethrows without status:complete or status:error stamp", async () => { const runOnce = vi .fn() .mockRejectedValueOnce(new Error("primary boom")) @@ -165,11 +165,15 @@ describe("runWithFallback", () => { }) ).rejects.toMatchObject({ name: "AbortError" }); - // No status:complete stamp, no status:error combined message - const stampCall = updateNodeData.mock.calls.find( - (c) => (c[1] as Record).__usedFallback === true + // No status:complete or status:error stamp after abort + const completeStamp = updateNodeData.mock.calls.find( + (c) => (c[1] as Record).status === "complete" ); - expect(stampCall).toBeUndefined(); + const errorStamp = updateNodeData.mock.calls.find( + (c) => (c[1] as Record).status === "error" + ); + expect(completeStamp).toBeUndefined(); + expect(errorStamp).toBeUndefined(); }); it("primary === fallback (same provider+modelId): skips fallback and rethrows", async () => { @@ -189,4 +193,60 @@ describe("runWithFallback", () => { expect(runOnce).toHaveBeenCalledTimes(1); }); + + describe("fallbackParameters", () => { + it("primary succeeds: runOnce called without parameters override", async () => { + const runOnce = vi.fn().mockResolvedValueOnce(undefined); + await runWithFallback({ + nodeId: "n1", + primary, + fallback, + fallbackParameters: { mode: "720p" }, + updateNodeData, + runOnce, + }); + + expect(runOnce).toHaveBeenCalledTimes(1); + expect(runOnce).toHaveBeenCalledWith(primary, undefined); + }); + + it("primary fails, fallback succeeds: runOnce called with fallbackParameters", async () => { + const fbParams = { mode: "720p" }; + const runOnce = vi + .fn() + .mockRejectedValueOnce(new Error("primary boom")) + .mockResolvedValueOnce(undefined); + + await runWithFallback({ + nodeId: "n1", + primary, + fallback, + fallbackParameters: fbParams, + updateNodeData, + runOnce, + }); + + expect(runOnce).toHaveBeenCalledTimes(2); + expect(runOnce).toHaveBeenNthCalledWith(1, primary, undefined); + expect(runOnce).toHaveBeenNthCalledWith(2, fallback, fbParams); + }); + + it("primary fails, fallback succeeds, no fallbackParameters: runOnce called with undefined", async () => { + const runOnce = vi + .fn() + .mockRejectedValueOnce(new Error("primary boom")) + .mockResolvedValueOnce(undefined); + + await runWithFallback({ + nodeId: "n1", + primary, + fallback, + updateNodeData, + runOnce, + }); + + expect(runOnce).toHaveBeenCalledTimes(2); + expect(runOnce).toHaveBeenNthCalledWith(2, fallback, undefined); + }); + }); }); diff --git a/src/store/execution/runWithFallback.ts b/src/store/execution/runWithFallback.ts index 3515a1df..e2739aaa 100644 --- a/src/store/execution/runWithFallback.ts +++ b/src/store/execution/runWithFallback.ts @@ -19,8 +19,11 @@ export interface RunWithFallbackOptions { nodeId: string; primary: SelectedModel; fallback?: SelectedModel; + fallbackParameters?: Record; updateNodeData: (id: string, data: Partial) => void; - runOnce: (model: SelectedModel) => Promise; + runOnce: (model: SelectedModel, parametersOverride?: Record) => Promise; + /** Data to merge when transitioning to fallback (e.g. { outputImage: null }) */ + clearOutput?: Partial; } function isAbortError(err: unknown): boolean { @@ -40,7 +43,7 @@ function isSameModel(a: SelectedModel, b: SelectedModel): boolean { export async function runWithFallback( options: RunWithFallbackOptions ): Promise { - const { nodeId, primary, fallback, updateNodeData, runOnce } = options; + const { nodeId, primary, fallback, fallbackParameters, updateNodeData, runOnce, clearOutput } = options; // Clear any prior fallback metadata before we start. updateNodeData(nodeId, { @@ -51,7 +54,7 @@ export async function runWithFallback( let primaryError: unknown; try { - await runOnce(primary); + await runOnce(primary, undefined); return; } catch (err) { if (isAbortError(err)) throw err; @@ -65,8 +68,18 @@ export async function runWithFallback( const primaryErrMsg = errorMessage(primaryError); + // Clear error state and stale output, show the fallback is now running. + updateNodeData(nodeId, { + ...clearOutput, + status: "running", + error: null, + __usedFallback: true, + __fallbackModelUsed: fallback.displayName, + __primaryError: primaryErrMsg, + }); + try { - await runOnce(fallback); + await runOnce(fallback, fallbackParameters); // Success on fallback: stamp metadata and ensure status reflects completion. updateNodeData(nodeId, { status: "complete",