diff --git a/src/artifacts/ChangeTimeBaseline.test.ts b/src/artifacts/ChangeTimeBaseline.test.ts index 1ca5f24014..8c9aa7275a 100644 --- a/src/artifacts/ChangeTimeBaseline.test.ts +++ b/src/artifacts/ChangeTimeBaseline.test.ts @@ -271,3 +271,17 @@ it("does not consume Windows limits on other hosts and preserves cancellation du ), ).rejects.toBe(reason); }); + +it("preserves cancellation during a Windows read even when the timestamp is stable", async () => { + const controller = new AbortController(); + const reason = new Error("cancelled during read"); + await expect( + settleChangeTimeBaseline( + async () => { + controller.abort(reason); + return statsAt(1); + }, + { platform: "win32", now: () => 10000, signal: controller.signal }, + ), + ).rejects.toBe(reason); +}); diff --git a/src/filesystem/ChangeTimeBaseline.ts b/src/filesystem/ChangeTimeBaseline.ts index 18cddff603..3e85aec7cd 100644 --- a/src/filesystem/ChangeTimeBaseline.ts +++ b/src/filesystem/ChangeTimeBaseline.ts @@ -28,7 +28,8 @@ export interface ChangeTimeSettleSeams { readonly sleep?: ((delayMs: number) => Promise) | undefined; /** Launch environment. Omission reads `process.env`. */ readonly environment?: - Readonly> | undefined; + | Readonly> + | undefined; /** Already resolved budget. Omission parses `environment`. */ readonly limits?: WindowsChangeTimeSettleLimits | undefined; /** Cancels the wait; the last observation is not returned once aborted. */ @@ -111,10 +112,10 @@ export const settleChangeTimeBaseline = async ( const sleep = seams.sleep ?? defaultSleep; let observed = await read(); for (let poll = 0; poll < limits.maxPolls; poll += 1) { + throwIfAborted(seams); if (!changeTimeMayStillDrift(observed, settled)) return observed; // A settle window is bounded by driftMs, not by the poll budget, so a // cancelled scope must not keep an inventory waiting it out. - throwIfAborted(seams); await sleep(limits.pollMs); throwIfAborted(seams); observed = await read();