mirror of https://github.com/garrytan/gstack.git
fix(design/variants): surface real 240s timeout in AbortError message
`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.
This commit is contained in:
parent
19770ea8b4
commit
5e5037b621
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Response> => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue