diff --git a/packages/engine/src/utils/urlDownloader.test.ts b/packages/engine/src/utils/urlDownloader.test.ts index c2c1324eee..93e0bab26e 100644 --- a/packages/engine/src/utils/urlDownloader.test.ts +++ b/packages/engine/src/utils/urlDownloader.test.ts @@ -112,14 +112,40 @@ function isoBmffMediaBytes(marker: string): Buffer { return Buffer.concat([ftyp, Buffer.from(marker)]); } -// Fake only Date: downloadToTemp's deadlines read Date.now() around sync work, so a slow -// runner can trip a false timeout with no timer-driven delay. Real timers keep firing, so -// setTimeout-based races are unaffected. +// Fake Date, and hold the 1_000 ms attempt deadline every test passes: on a slow runner one +// attempt (hash, fsync, refetch) can outlast it and throw "Download timeout". A test that +// needs the deadline calls fireAttemptDeadline(). Other timers (cache-lock poll, test +// delays, the 20 ms stalled-body test) stay real. +const ATTEMPT_DEADLINE_MS = 1_000; +const heldDeadlines = new Map, () => void>(); +let realSetTimeout: typeof setTimeout; + +function fireAttemptDeadline(): void { + for (const [handle, fire] of [...heldDeadlines]) { + heldDeadlines.delete(handle); + clearTimeout(handle); + fire(); + } +} + beforeEach(() => { vi.useFakeTimers({ toFake: ["Date"] }); + realSetTimeout = globalThis.setTimeout; + vi.spyOn(globalThis, "setTimeout").mockImplementation((( + handler: () => void, + ms?: number, + ...args: unknown[] + ) => { + if (ms !== ATTEMPT_DEADLINE_MS) return realSetTimeout(handler, ms, ...args); + const handle = realSetTimeout(() => undefined, 2 ** 31 - 1); + heldDeadlines.set(handle, handler); + return handle; + }) as typeof setTimeout); }); afterEach(() => { + vi.mocked(globalThis.setTimeout).mockRestore(); + heldDeadlines.clear(); vi.useRealTimers(); vi.unstubAllGlobals(); fsRaceControls.deleteBeforeLstatPath = undefined; @@ -307,6 +333,23 @@ describe("fetchPublicHttpsText", () => { }); describe("downloadToTemp atomic publication and bounded retry", () => { + it("does not race a real wall-clock deadline on a slow attempt", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockImplementation( + (_url: string, init: RequestInit) => + new Promise((resolve, reject) => { + init.signal?.addEventListener("abort", () => reject(new Error("aborted")), { + once: true, + }); + setTimeout(() => resolve(new Response("complete")), 1_200); + }), + ), + ); + const path = await downloadToTemp("https://cdn.example/slow.mp4", makeTempDir(), 1_000); + expect(readFileSync(path, "utf8")).toBe("complete"); + }); + it("follows a bounded redirect only after validating the next public HTTPS hop", async () => { const fetchMock = vi .fn() @@ -1485,12 +1528,15 @@ describe("downloadToTemp atomic publication and bounded retry", () => { 1_000, secondController.signal, ); - firstController.abort(); - - await expect(first).rejects.toMatchObject({ + const firstRejected = expect(first).rejects.toMatchObject({ kind: "cancelled", retryable: false, } satisfies Partial); + firstController.abort(); + await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); + fireAttemptDeadline(); + + await firstRejected; const path = await second; expect(readFileSync(path, "utf8")).toBe("complete"); expect(fetchMock).toHaveBeenCalledTimes(2);