Files
tech-log-frontend/tests/unit/abortable-operation.test.ts
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

372 lines
12 KiB
TypeScript

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<string>((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<never>(() => {}));
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<string>((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<typeof setTimeout>);
},
};
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<never>(() => {})),
).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<never>(() => {})),
).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<never>((_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<never>((_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<never>((_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();
});
});