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>
372 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|