From 5e5037b62124127a7382e3f372c6b580325211c3 Mon Sep 17 00:00:00 2001 From: Pablosinyores Date: Thu, 28 May 2026 19:11:51 +0530 Subject: [PATCH] fix(design/variants): surface real 240s timeout in AbortError message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `generateVariant` in `design/src/variants.ts` arms its abort timer at 240_000 ms (`setTimeout(() => controller.abort(), 240_000)` at line 61), but the AbortError branch at line 128 returns `"Timeout (120s)"` — half the actual bound. The mismatch is purely a stale string literal, but it's the only signal a caller gets when the API stalls. A user staring at `Timeout (120s)` has no way to know whether to bump the orchestrator timeout, retry, or drop the call — the surfaced number is off by 2x from the timer that fired. Surface the real bound: "Timeout (240s)". Adds a regression test that stubs fetch to a permanently-pending promise, fast-forwards only the 240_000 ms abort timer (leaves the leading exponential retry delays on their real values), and asserts the surfaced error string matches the configured bound. --- design/src/variants.ts | 2 +- design/test/variants-retry-after.test.ts | 52 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/design/src/variants.ts b/design/src/variants.ts index 257079dea..c5cafce64 100644 --- a/design/src/variants.ts +++ b/design/src/variants.ts @@ -125,7 +125,7 @@ export async function generateVariant( } catch (err: any) { clearTimeout(timeout); if (err.name === "AbortError") { - return { path: outputPath, success: false, error: "Timeout (120s)" }; + return { path: outputPath, success: false, error: "Timeout (240s)" }; } lastError = err.message; } diff --git a/design/test/variants-retry-after.test.ts b/design/test/variants-retry-after.test.ts index 2060791d5..9a7e05948 100644 --- a/design/test/variants-retry-after.test.ts +++ b/design/test/variants-retry-after.test.ts @@ -130,4 +130,56 @@ describe("generateVariant Retry-After handling", () => { const gap = calls[1].ts - calls[0].ts; expect(gap).toBeLessThan(500); }); + + test("AbortError surfaces the actual configured 240s timeout in the error message", async () => { + // Regression: `generateVariant`'s `setTimeout` aborts at 240_000 ms + // (240s) but the AbortError branch returned `"Timeout (120s)"`. A + // user staring at the failure has no way to know whether to bump + // the orchestrator timeout, retry, or drop the call — the message + // is off by 2x. Force the abort path and assert the surfaced + // string matches the real bound. + const fetchFn = (async (_input: any, init?: any): Promise => { + const signal = init?.signal as AbortSignal | undefined; + return await new Promise((_resolve, reject) => { + if (signal?.aborted) { + const err = new Error("aborted"); + err.name = "AbortError"; + reject(err); + return; + } + signal?.addEventListener("abort", () => { + const err = new Error("aborted"); + err.name = "AbortError"; + reject(err); + }); + }); + }) as typeof globalThis.fetch; + + const originalSetTimeout = globalThis.setTimeout; + // Force the 240_000 ms timer to fire on the next event-loop tick + // so the test runs in milliseconds instead of 4 minutes. Only the + // 240_000 ms timer maps to fast; the leading exponential delays + // (2_000+ ms on retry) keep their real value via this branch + // because attempt 0 never sleeps. + const fastSetTimeout = ((handler: any, timeout?: number, ...rest: any[]): any => { + if (timeout === 240_000) { + return originalSetTimeout(handler, 0, ...rest); + } + return originalSetTimeout(handler, timeout as number, ...rest); + }) as typeof globalThis.setTimeout; + (globalThis as any).setTimeout = fastSetTimeout; + + try { + const result = await generateVariant( + "fake-key", "prompt", outputPath, "1024x1024", "high", fetchFn, + ); + + expect(result.success).toBe(false); + // Critical: the message MUST report 240s (the real bound), not + // 120s (the pre-fix mismatched literal). + expect(result.error).toBe("Timeout (240s)"); + } finally { + (globalThis as any).setTimeout = originalSetTimeout; + } + }); });