import { describe, expect, it, vi } from "vitest"; import { compensateLateHandle, createAbortableOperation, } from "../../src/adapters/platform/abortable-operation.ts"; /** * BT-X-01 golden tests. The utility owns abort mechanics only: it must never * import or imply a subsystem result taxonomy. */ describe("shared abortable operation mechanics", () => { it("records the first terminal owner and never overwrites it", async () => { const caller = new AbortController(); const timers: Array<() => void> = []; const operation = createAbortableOperation({ signal: caller.signal, timeoutMs: 10, setTimer: (callback) => { timers.push(callback); return timers.length; }, clearTimer: () => {}, }); expect(operation.terminal()).toBeNull(); caller.abort(); expect(operation.terminal()).toBe("CALLER_ABORT"); // A later deadline or close cannot rewrite the owner. timers.forEach((callback) => callback()); operation.close(); expect(operation.terminal()).toBe("CALLER_ABORT"); }); it("reports a deadline owner and aborts the composed signal", async () => { const timers: Array<() => void> = []; const operation = createAbortableOperation({ timeoutMs: 5, setTimer: (callback) => { timers.push(callback); return timers.length; }, clearTimer: () => {}, }); timers[0]?.(); expect(operation.terminal()).toBe("DEADLINE"); expect(operation.signal.aborted).toBe(true); }); it("race returns a terminal owner instead of a bare value", async () => { const caller = new AbortController(); const operation = createAbortableOperation({ signal: caller.signal }); let release: ((value: string) => void) | undefined; const pending = new Promise((resolve) => { release = resolve; }); const raced = operation.race(pending); caller.abort(); await expect(raced).resolves.toEqual({ kind: "TERMINAL", terminal: "CALLER_ABORT", }); // The late value is observed and discarded. release?.("late"); await expect(operation.race(pending)).resolves.toEqual({ kind: "TERMINAL", terminal: "CALLER_ABORT", }); }); it("race returns the value when nothing terminal happened", async () => { const operation = createAbortableOperation(); await expect(operation.race(Promise.resolve(7))).resolves.toEqual({ kind: "VALUE", value: 7, }); operation.close(); expect(operation.terminal()).toBe("CLOSED"); }); /** * TR-RR-05. A collaborator's own rejection is evidence about the work. * Reporting it as `TERMINAL/CLOSED` erased the reason the operation failed * and made `race()` disagree with `terminal()`, which still said no owner. */ it("keeps a rejection distinct from a terminal owner", async () => { const operation = createAbortableOperation(); const reason = new Error("upstream failed"); await expect( operation.race(Promise.reject(reason)), ).resolves.toEqual({ kind: "REJECTED", reason }); expect(operation.terminal()).toBeNull(); }); it("agrees with terminal() on the first owner", async () => { const caller = new AbortController(); const operation = createAbortableOperation({ signal: caller.signal }); const pending = operation.race(new Promise(() => {})); caller.abort(); // A later close cannot overwrite the first owner in either place. operation.close(); await expect(pending).resolves.toEqual({ kind: "TERMINAL", terminal: "CALLER_ABORT", }); expect(operation.terminal()).toBe("CALLER_ABORT"); }); it("compensates a late value exactly once instead of admitting it", async () => { const caller = new AbortController(); const operation = createAbortableOperation({ signal: caller.signal }); const compensated: string[] = []; let release: ((value: string) => void) | undefined; const pending = operation.race( new Promise((resolve) => { release = resolve; }), (value) => compensated.push(value), ); caller.abort(); expect(await pending).toEqual({ kind: "TERMINAL", terminal: "CALLER_ABORT", }); release?.("late-value"); await Promise.resolve(); await Promise.resolve(); expect(compensated).toEqual(["late-value"]); }); it("ignores a scheduler method replaced after construction", async () => { const scheduler = { setTimer: (callback: () => void, delayMs: number) => setTimeout(callback, delayMs), clearTimer: (handle: unknown) => { clearTimeout(handle as ReturnType); }, }; const operation = createAbortableOperation({ timeoutMs: 5, setTimer: scheduler.setTimer, clearTimer: scheduler.clearTimer, }); scheduler.setTimer = () => { throw new TypeError("replaced after construction"); }; await expect( operation.race(new Promise(() => {})), ).resolves.toEqual({ kind: "TERMINAL", terminal: "DEADLINE" }); }); it("close is idempotent and releases listeners and timers exactly once", () => { const caller = new AbortController(); const remove = vi.spyOn(caller.signal, "removeEventListener"); const clearTimer = vi.fn(); const operation = createAbortableOperation({ signal: caller.signal, timeoutMs: 10, setTimer: () => "handle", clearTimer, }); operation.close(); operation.close(); operation.close(); expect(remove).toHaveBeenCalledTimes(1); expect(clearTimer).toHaveBeenCalledTimes(1); }); /** * TR-RR-05. A scheduler that cannot install the deadline leaves the operation * unbounded. Removing the caller listener and leaving no terminal owner made * every later abort invisible, so installation failure is itself terminal and * closes atomically with the resources already created. */ it("closes atomically when the scheduler cannot install the deadline", async () => { const caller = new AbortController(); const remove = vi.spyOn(caller.signal, "removeEventListener"); const operation = createAbortableOperation({ signal: caller.signal, timeoutMs: 10, setTimer: () => { throw new TypeError("scheduler exploded"); }, clearTimer: () => {}, }); expect(operation.terminal()).toBe("CLOSED"); expect(remove).toHaveBeenCalledTimes(1); // The operation is bounded, so a caller abort afterwards cannot be lost. caller.abort(); await expect( operation.race(new Promise(() => {})), ).resolves.toEqual({ kind: "TERMINAL", terminal: "CLOSED" }); }); /** * X-AUDIT-01. The outcome must be decided by which callback was actually * observed first, not by how many microtasks the primitive happens to drain * before it gives up waiting. A terminal owner fixed synchronously owns the * result even when the task rejects later in the same call stack. */ describe("settle-once ownership across callback orderings", () => { const terminalOwners = [ { label: "caller abort", owner: "CALLER_ABORT" as const, trigger: (caller: AbortController, operation: { close(): void }, timers: Array<() => void>) => { void operation; void timers; caller.abort(); }, }, { label: "deadline", owner: "DEADLINE" as const, trigger: ( caller: AbortController, operation: { close(): void }, timers: Array<() => void>, ) => { void caller; void operation; timers.forEach((callback) => callback()); }, }, { label: "close", owner: "CLOSED" as const, trigger: ( caller: AbortController, operation: { close(): void }, timers: Array<() => void>, ) => { void caller; void timers; operation.close(); }, }, ]; const createHarness = () => { const caller = new AbortController(); const timers: Array<() => void> = []; const operation = createAbortableOperation({ signal: caller.signal, timeoutMs: 1_000, setTimer: (callback) => { timers.push(callback); return timers.length; }, clearTimer: () => {}, }); return { caller, timers, operation }; }; for (const { label, owner, trigger } of terminalOwners) { it(`keeps ${label} as the owner when the task rejects in the same call stack`, async () => { const { caller, timers, operation } = createHarness(); let reject: ((reason: unknown) => void) | undefined; const task = new Promise((_resolve, rejectTask) => { reject = rejectTask; }); const raced = operation.race(task); trigger(caller, operation, timers); expect(operation.terminal()).toBe(owner); reject?.(new Error("rejected after the terminal owner was fixed")); await expect(raced).resolves.toEqual({ kind: "TERMINAL", terminal: owner }); expect(operation.terminal()).toBe(owner); }); it(`keeps ${label} as the owner regardless of how many microtasks drain`, async () => { for (const drains of [0, 1, 2, 3, 5, 9]) { const { caller, timers, operation } = createHarness(); let reject: ((reason: unknown) => void) | undefined; const task = new Promise((_resolve, rejectTask) => { reject = rejectTask; }); const raced = operation.race(task); trigger(caller, operation, timers); for (let turn = 0; turn < drains; turn += 1) { await Promise.resolve(); } reject?.(new Error("late rejection")); await expect(raced).resolves.toEqual({ kind: "TERMINAL", terminal: owner, }); } }); } it("keeps a rejection that was observed before any terminal owner", async () => { const { caller, operation } = createHarness(); const reason = new Error("task failed first"); const raced = operation.race(Promise.reject(reason)); // Let the rejection callback actually run before the abort is requested. await Promise.resolve(); await Promise.resolve(); caller.abort(); await expect(raced).resolves.toEqual({ kind: "REJECTED", reason }); }); it("keeps a value that was observed before any terminal owner", async () => { const { caller, operation } = createHarness(); const compensated: string[] = []; const raced = operation.race(Promise.resolve("early"), (value) => compensated.push(value), ); await Promise.resolve(); await Promise.resolve(); caller.abort(); await expect(raced).resolves.toEqual({ kind: "VALUE", value: "early" }); expect(compensated).toEqual([]); }); it("never compensates or re-raises a rejection that lands after the owner", async () => { const unhandled: unknown[] = []; const onUnhandled = (reason: unknown) => unhandled.push(reason); process.on("unhandledRejection", onUnhandled); try { const { caller, operation } = createHarness(); const compensated: unknown[] = []; let reject: ((reason: unknown) => void) | undefined; const task = new Promise((_resolve, rejectTask) => { reject = rejectTask; }); const raced = operation.race(task, (value) => compensated.push(value)); caller.abort(); reject?.(new Error("late rejection")); await expect(raced).resolves.toEqual({ kind: "TERMINAL", terminal: "CALLER_ABORT", }); await Promise.resolve(); await Promise.resolve(); await new Promise((resolve) => setTimeout(resolve, 0)); expect(compensated).toEqual([]); expect(unhandled).toEqual([]); } finally { process.off("unhandledRejection", onUnhandled); } }); }); it("compensates a late native handle without changing the outcome", async () => { const cancel = vi.fn(async () => {}); compensateLateHandle(Promise.resolve({ body: { cancel } })); await Promise.resolve(); await Promise.resolve(); expect(cancel).toHaveBeenCalledOnce(); // A rejecting handle is swallowed. compensateLateHandle(Promise.reject(new Error("late"))); await Promise.resolve(); }); });