Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 3 months ago
parent
commit
bf82f9d767
  1. 76
      src/store/execution/__tests__/runWithFallback.test.ts
  2. 21
      src/store/execution/runWithFallback.ts

76
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<string, unknown>).__usedFallback === true
// No status:complete or status:error stamp after abort
const completeStamp = updateNodeData.mock.calls.find(
(c) => (c[1] as Record<string, unknown>).status === "complete"
);
expect(stampCall).toBeUndefined();
const errorStamp = updateNodeData.mock.calls.find(
(c) => (c[1] as Record<string, unknown>).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);
});
});
});

21
src/store/execution/runWithFallback.ts

@ -19,8 +19,11 @@ export interface RunWithFallbackOptions {
nodeId: string;
primary: SelectedModel;
fallback?: SelectedModel;
fallbackParameters?: Record<string, unknown>;
updateNodeData: (id: string, data: Partial<WorkflowNodeData>) => void;
runOnce: (model: SelectedModel) => Promise<void>;
runOnce: (model: SelectedModel, parametersOverride?: Record<string, unknown>) => Promise<void>;
/** Data to merge when transitioning to fallback (e.g. { outputImage: null }) */
clearOutput?: Partial<WorkflowNodeData>;
}
function isAbortError(err: unknown): boolean {
@ -40,7 +43,7 @@ function isSameModel(a: SelectedModel, b: SelectedModel): boolean {
export async function runWithFallback(
options: RunWithFallbackOptions
): Promise<void> {
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",

Loading…
Cancel
Save