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>
This commit is contained in:
DongHyeonka
2026-08-15 12:04:58 +09:00
co-authored by Claude Opus 5
parent 002ba3624e
commit 4bff9ca151
142 changed files with 23010 additions and 1544 deletions
+371
View File
@@ -0,0 +1,371 @@
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();
});
});
@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from "vitest";
import { readBoundedJson } from "../../src/adapters/http/bounded-json.ts";
/**
* N-08. The legacy V2 reader delegates to the common bounded reader. These
* cases pin the preserved failure codes and prove that a throwing cancel or
* releaseLock cannot escape the closed result.
*/
function responseWith(
body: ReadableStream<Uint8Array> | string | null,
init: ResponseInit = {},
): Response {
return new Response(body, init);
}
describe("legacy bounded JSON compatibility", () => {
it("preserves RESPONSE_BODY_LIMIT for a declared oversize body", async () => {
const response = responseWith("{}", {
headers: { "content-length": "9999" },
});
await expect(readBoundedJson(response, 8)).resolves.toEqual({
ok: false,
code: "RESPONSE_BODY_LIMIT",
});
});
it("preserves RESPONSE_BODY_LIMIT for a headerless oversize body", async () => {
const response = responseWith("[1,2,3,4,5,6,7,8,9,10]");
await expect(readBoundedJson(response, 4)).resolves.toEqual({
ok: false,
code: "RESPONSE_BODY_LIMIT",
});
});
it.each([
["invalid UTF-8", new Uint8Array([0xff, 0xfe, 0xfd])],
["malformed JSON", new TextEncoder().encode("{")],
["an empty body", new Uint8Array(0)],
])("maps %s to MALFORMED_JSON", async (_label, bytes) => {
const response = responseWith(
new ReadableStream<Uint8Array>({
start(controller) {
if (bytes.byteLength > 0) controller.enqueue(bytes);
controller.close();
},
}),
);
await expect(readBoundedJson(response, 1_024)).resolves.toEqual({
ok: false,
code: "MALFORMED_JSON",
});
});
it("keeps a closed result when reader cancel or releaseLock throws", async () => {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode("[1,2,3,4,5,6,7,8]"));
},
});
const response = responseWith(stream);
const body = response.body!;
const nativeGetReader = body.getReader.bind(body);
vi.spyOn(body, "getReader").mockImplementation((() => {
const actual = nativeGetReader();
return {
...actual,
read: actual.read.bind(actual),
cancel: async () => {
throw new TypeError("cancel exploded");
},
releaseLock: () => {
throw new TypeError("releaseLock exploded");
},
};
}) as never);
await expect(readBoundedJson(response, 4)).resolves.toEqual({
ok: false,
code: "RESPONSE_BODY_LIMIT",
});
});
it("reads a bounded JSON value successfully", async () => {
const response = responseWith('{"a":1}');
await expect(readBoundedJson(response, 1_024)).resolves.toEqual({
ok: true,
value: { a: 1 },
});
});
});
+38 -1
View File
@@ -248,12 +248,49 @@ describe("browser download delivery", () => {
ok: true,
value: { kind: "BROWSER_HANDOFF", transferId: "transfer:1" },
});
// STO-02. Parse once, then execute exactly what was validated.
expect(handoff).toHaveBeenCalledWith(
"/downloads/artifact-1",
"https://app.example/downloads/artifact-1",
"invoice_exe.pdf",
);
});
it("executes the canonical target instead of a document-base-relative href", async () => {
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff },
// A relative href the host would otherwise resolve against a hostile
// document <base href="https://evil.example/">.
browserManagedCapabilities: capabilityResolver(() => "downloads/a"),
baseOrigin: "https://app.example",
createTransferId: () => "transfer:1",
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(
{
kind: "BROWSER_MANAGED_RESOURCE",
resourceId: "artifact-1",
capabilityReceipt,
},
"BROWSER_MANAGED",
),
),
).toMatchObject({ ok: true });
expect(handoff).toHaveBeenCalledWith(
"https://app.example/downloads/a",
"invoice_exe.pdf",
);
for (const [href] of handoff.mock.calls) {
expect(String(href).startsWith("https://app.example/")).toBe(true);
expect(String(href)).not.toContain("evil.example");
}
});
it("rejects cross-origin or query-bearing browser-managed targets", async () => {
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
@@ -6,6 +6,7 @@ import {
composeBrowserRpcRequestEncoderRegistry,
defineBrowserRpcOperation,
defineBrowserRpcProviderProfile,
installBrowserRpcContractBindings,
validateBrowserRpcContractBindings,
type BrowserRpcProviderProfile,
} from "../../../src/contracts/browser-rpc.ts";
@@ -23,6 +24,91 @@ import {
} from "./fixture.ts";
describe("Browser RPC contract registry", () => {
it("snapshots installed bindings before later source mutation", () => {
const operations: Record<string, ReturnType<typeof unaryOperation>> = {
GET_RPC_RESOURCE: unaryOperation(),
};
const installed = installBrowserRpcContractBindings({
operations,
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
});
const before = installed.operations.get("GET_RPC_RESOURCE");
expect(before?.totalDeadlineMs).toBeDefined();
// R-04. A post-validation mutation of the source registry must not reach
// the installed snapshot.
operations.GET_RPC_RESOURCE = {
...operations.GET_RPC_RESOURCE!,
totalDeadlineMs: 999_999,
};
expect(installed.operations.get("GET_RPC_RESOURCE")).toBe(before);
expect(
installed.operations.get("GET_RPC_RESOURCE")?.totalDeadlineMs,
).not.toBe(999_999);
});
it("rejects extra accessor and symbol keys without invoking getters", () => {
let getterCalls = 0;
const accessorOperation = Object.defineProperty(
{ ...unaryOperation() },
"totalDeadlineMs",
{
enumerable: true,
configurable: true,
get() {
getterCalls += 1;
return 1_000;
},
},
);
expect(() =>
installBrowserRpcContractBindings({
operations: { GET_RPC_RESOURCE: accessorOperation },
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
}),
).toThrow(TypeError);
expect(getterCalls).toBe(0);
const extraKeyOperation = {
...unaryOperation(),
unexpectedKey: "smuggled",
};
expect(() =>
installBrowserRpcContractBindings({
operations: {
GET_RPC_RESOURCE: extraKeyOperation as never,
},
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
}),
).toThrow(/unexpected key/u);
const symbolRegistry: Record<string, unknown> = {
GET_RPC_RESOURCE: unaryOperation(),
};
Object.defineProperty(symbolRegistry, Symbol("hidden"), {
enumerable: true,
value: unaryOperation(),
});
expect(() =>
installBrowserRpcContractBindings({
operations: symbolRegistry as never,
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
}),
).toThrow(/symbol keys/u);
});
it("closes exact operation, provider, schema, mapper and encoder bindings", () => {
const operation = unaryOperation();
const profile = unaryProfile();
@@ -0,0 +1,790 @@
import { describe, expect, it } from "vitest";
import {
createBrowserRpcRuntime,
defineBrowserRpcTransport,
type BrowserRpcServerStreamLease,
type BrowserRpcStreamFrame,
type BrowserRpcTransport,
} from "../../../src/adapters/browser-rpc/index.ts";
import { decodeServerStreamLease } from "../../../src/adapters/browser-rpc/transport.ts";
import { installBrowserRpcContractBindings } from "../../../src/contracts/browser-rpc.ts";
import {
MAPPERS,
SCHEMA_CODECS,
STREAM_ENCODER,
UNARY_ENCODER,
isResourceView,
streamOperation,
streamProfile,
unaryOperation,
unaryProfile,
} from "./fixture.ts";
function unaryRuntime(
transport: BrowserRpcTransport,
extra: Record<string, unknown> = {},
) {
return createBrowserRpcRuntime({
operations: { GET_RPC_RESOURCE: unaryOperation() },
profiles: { CONNECT_REFERENCE_UNARY: unaryProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
transports: { CONNECT_REFERENCE_UNARY: transport },
...extra,
});
}
function streamingRuntime(
transport: BrowserRpcTransport,
extra: Record<string, unknown> = {},
) {
return createBrowserRpcRuntime({
operations: { WATCH_RPC_RESOURCES: streamOperation() },
profiles: { CONNECT_REFERENCE_STREAM: streamProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER },
transports: { CONNECT_REFERENCE_STREAM: transport },
...extra,
});
}
function unaryTransport(
invokeUnary: BrowserRpcTransport["invokeUnary"],
): BrowserRpcTransport {
return defineBrowserRpcTransport({
runtimeProfileId: "CONNECT_REFERENCE_UNARY",
providerId: "REFERENCE_RPC",
protocol: "CONNECT_HTTP",
rpcKind: "UNARY",
invokeUnary,
});
}
type LeaseProbe = Readonly<{
transport: BrowserRpcTransport;
cancels: string[];
readonly opened: number;
close(): void;
}>;
/**
* RPC-RR-01. A transport whose physical stream does not close on its own. The
* runtime must cancel it exactly once and must not admit a second stream for
* the same operation until `waitClosed()` settles.
*/
function nonCooperativeStreamTransport(): LeaseProbe {
const cancels: string[] = [];
const counter = { opened: 0 };
let release: (() => void) | undefined;
const closed = new Promise<void>((resolve) => {
release = resolve;
});
const openServerStream = (): BrowserRpcServerStreamLease => {
counter.opened += 1;
return {
streamId: `physical-${counter.opened}`,
frames: {
[Symbol.asyncIterator]: () =>
({
// Never yields and never settles: the runtime's own deadline is the
// only thing that can end the call.
next: () => new Promise<never>(() => {}),
}) as AsyncIterator<BrowserRpcStreamFrame>,
},
cancel(reason: string) {
cancels.push(reason);
},
waitClosed: () => closed,
};
};
const transport = defineBrowserRpcTransport({
runtimeProfileId: "CONNECT_REFERENCE_STREAM",
providerId: "REFERENCE_RPC",
protocol: "CONNECT_HTTP",
rpcKind: "SERVER_STREAM",
openServerStream,
});
return Object.freeze({
transport,
cancels,
get opened() {
return counter.opened;
},
close: () => release?.(),
}) as LeaseProbe;
}
function streamTransport(
frames: readonly BrowserRpcStreamFrame[],
): BrowserRpcTransport {
return defineBrowserRpcTransport({
runtimeProfileId: "CONNECT_REFERENCE_STREAM",
providerId: "REFERENCE_RPC",
protocol: "CONNECT_HTTP",
rpcKind: "SERVER_STREAM",
openServerStream: () => ({
streamId: "remediation-stream",
frames: {
async *[Symbol.asyncIterator]() {
for (const frame of frames) yield frame;
},
},
cancel() {},
async waitClosed() {},
}),
});
}
async function collect(
source: AsyncIterable<unknown>,
): Promise<readonly unknown[]> {
const seen: unknown[] = [];
for await (const value of source) seen.push(value);
return seen;
}
/**
* RPC-RR-02. A collaborator that throws synchronously must not escape the
* Result contract. A generation fence captured outside the protected boundary
* and a `clock.sleep` invoked outside a promise boundary both did exactly that,
* and the second also skipped the listener and timer release.
*/
describe("RPC-RR-02 synchronous collaborator throws stay inside Result", () => {
const throwingFence = {
capture: () => {
throw new TypeError("fence exploded");
},
isCurrent: () => true,
};
it("closes a unary call whose fence throws", async () => {
const runtime = unaryRuntime(
unaryTransport(async () => ({
ok: true,
message: { id: "a", name: "A" },
encodedBytes: 8,
})),
{ generationFence: throwingFence },
);
const result = await runtime
.bindUnary("GET_RPC_RESOURCE", isResourceView)
.execute({ resourceId: "resource-1" });
expect(result.ok).toBe(false);
});
it("closes a stream whose fence throws", async () => {
const runtime = streamingRuntime(
streamTransport([{ kind: "TERMINAL", ok: true }]),
{ generationFence: throwingFence },
);
const results = await collect(
runtime
.bindServerStream("WATCH_RPC_RESOURCES", isResourceView)
.open({ resourceId: "scope-1" }),
);
expect(results.length).toBeGreaterThan(0);
expect(results.every((value) => (value as { ok: boolean }).ok === false)).toBe(
true,
);
});
it("closes a unary call whose clock throws synchronously", async () => {
const runtime = unaryRuntime(
unaryTransport(() => new Promise(() => {})),
{
clock: {
now: () => 0,
sleep: () => {
throw new TypeError("clock exploded");
},
},
},
);
const result = await runtime
.bindUnary("GET_RPC_RESOURCE", isResourceView)
.execute({ resourceId: "resource-1" });
expect(result.ok).toBe(false);
});
});
/**
* RPC-RR-03. The runtime reads a validated snapshot, never the caller's
* objects: an accessor is refused without being invoked, and no installed
* registry exposes a mutator.
*/
describe("RPC-RR-03 transport and binding registries are snapshots", () => {
it("never invokes a transport accessor", () => {
let getterCalls = 0;
const hostile = {} as Record<string, unknown>;
Object.defineProperties(hostile, {
runtimeProfileId: {
enumerable: true,
get: () => {
getterCalls += 1;
return "CONNECT_REFERENCE_UNARY";
},
},
providerId: { enumerable: true, value: "REFERENCE_RPC" },
protocol: { enumerable: true, value: "CONNECT_HTTP" },
rpcKind: { enumerable: true, value: "UNARY" },
invokeUnary: {
enumerable: true,
value: async () => ({ ok: true, message: {}, encodedBytes: 1 }),
},
});
expect(() =>
unaryRuntime(hostile as unknown as BrowserRpcTransport),
).toThrow(TypeError);
expect(getterCalls).toBe(0);
});
it("rejects an unexpected own field on a transport row", () => {
const transport = unaryTransport(async () => ({
ok: true,
message: { id: "a", name: "A" },
encodedBytes: 8,
}));
const widened = { ...transport, injected: true };
expect(() =>
unaryRuntime(widened as unknown as BrowserRpcTransport),
).toThrow(TypeError);
});
it("exposes no mutation API on any installed binding registry", () => {
const installed = installBrowserRpcContractBindings({
operations: { GET_RPC_RESOURCE: unaryOperation() },
profiles: { CONNECT_REFERENCE_UNARY: unaryProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
});
for (const registry of Object.values(installed)) {
const record = registry as unknown as Record<string, unknown>;
for (const mutator of ["set", "delete", "clear"]) {
expect(record[mutator]).toBeUndefined();
}
expect(() =>
Map.prototype.clear.call(registry as never),
).toThrow();
}
expect(installed.operations.get("GET_RPC_RESOURCE")).toBeDefined();
});
});
/**
* RPC-RR-04. Transport values are decoded, not adopted. `in` and a direct
* property read run accessors and admit inherited or extra fields, and keeping
* the caller's object lets it change after validation.
*/
describe("RPC-RR-04 transport results and frames are exactly decoded", () => {
const hostileResults: readonly (readonly [string, () => unknown])[] = [
["extra own field", () => ({ ok: true, message: {}, encodedBytes: 1, injected: 1 })],
[
"inherited fields",
() =>
Object.create({ ok: true, message: {}, encodedBytes: 1 }) as object,
],
[
"throwing getter",
() => {
const value: Record<string, unknown> = { message: {}, encodedBytes: 1 };
Object.defineProperty(value, "ok", {
enumerable: true,
get: () => {
throw new TypeError("hostile getter");
},
});
return value;
},
],
[
"symbol key",
() => ({
ok: true,
message: {},
encodedBytes: 1,
[Symbol("injected")]: 1,
}),
],
];
for (const [label, build] of hostileResults) {
it(`refuses a unary result with ${label}`, async () => {
const runtime = unaryRuntime(
unaryTransport(async () => build() as never),
);
const result = await runtime
.bindUnary("GET_RPC_RESOURCE", isResourceView)
.execute({ resourceId: "resource-1" });
expect(result.ok).toBe(false);
});
}
it("refuses an unknown transport failure code", async () => {
const runtime = unaryRuntime(
unaryTransport(async () => ({
ok: false,
failure: { code: "MADE_UP_CODE" },
}) as never),
);
const result = await runtime
.bindUnary("GET_RPC_RESOURCE", isResourceView)
.execute({ resourceId: "resource-1" });
expect(result.ok).toBe(false);
});
it("refuses a retryAfterMs outside the hard ceiling", async () => {
const runtime = unaryRuntime(
unaryTransport(async () => ({
ok: false,
failure: { code: "NETWORK_UNREACHABLE", retryAfterMs: -1 },
}) as never),
);
const result = await runtime
.bindUnary("GET_RPC_RESOURCE", isResourceView)
.execute({ resourceId: "resource-1" });
expect(result.ok).toBe(false);
});
it("refuses a stream frame with an extra own field", async () => {
const runtime = streamingRuntime(
streamTransport([
{
kind: "MESSAGE",
message: { id: "a", name: "A" },
encodedBytes: 4,
injected: 1,
} as never,
]),
);
const results = await collect(
runtime
.bindServerStream("WATCH_RPC_RESOURCES", isResourceView)
.open({ resourceId: "scope-1" }),
);
expect(results.every((value) => (value as { ok: boolean }).ok === false)).toBe(
true,
);
});
});
/**
* RPC-RR-01. A bare `AsyncIterable` gave the runtime no way to cancel the
* physical stream or to learn when it actually closed, so a timed-out call left
* the first stream running against the server while a second was admitted.
*/
describe("RPC-RR-01 server stream leases and the DRAINING fence", () => {
const shortDeadline = () =>
streamOperation({ totalDeadlineMs: 25, idleDeadlineMs: 25 });
it("cancels the physical stream exactly once after a timeout", async () => {
const probe = nonCooperativeStreamTransport();
const runtime = createBrowserRpcRuntime({
operations: { WATCH_RPC_RESOURCES: shortDeadline() },
profiles: { CONNECT_REFERENCE_STREAM: streamProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER },
transports: { CONNECT_REFERENCE_STREAM: probe.transport },
});
const results = await collect(
runtime
.bindServerStream("WATCH_RPC_RESOURCES", isResourceView)
.open({ resourceId: "scope-1" }),
);
expect(results.every((value) => (value as { ok: boolean }).ok === false)).toBe(
true,
);
expect(probe.cancels.length).toBe(1);
});
it("refuses a second stream while the first has not confirmed closure", async () => {
const probe = nonCooperativeStreamTransport();
const runtime = createBrowserRpcRuntime({
operations: { WATCH_RPC_RESOURCES: shortDeadline() },
profiles: { CONNECT_REFERENCE_STREAM: streamProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER },
transports: { CONNECT_REFERENCE_STREAM: probe.transport },
});
const stream = runtime.bindServerStream(
"WATCH_RPC_RESOURCES",
isResourceView,
);
await collect(stream.open({ resourceId: "scope-1" }));
expect(probe.opened).toBe(1);
const second = await collect(stream.open({ resourceId: "scope-1" }));
expect(second).toHaveLength(1);
expect(second[0]).toMatchObject({
ok: false,
error: { code: "RPC_STREAM_DRAINING" },
});
// The refused call never reached the transport.
expect(probe.opened).toBe(1);
// Once the transport confirms closure the operation admits work again.
probe.close();
await new Promise((resolve) => setTimeout(resolve, 5));
await collect(stream.open({ resourceId: "scope-1" }));
expect(probe.opened).toBe(2);
});
it("refuses a malformed lease as a protocol failure", async () => {
for (const malformed of [
{ frames: { [Symbol.asyncIterator]: () => ({ next: async () => ({ done: true, value: undefined }) }) }, cancel() {}, async waitClosed() {} },
{ streamId: "", frames: { [Symbol.asyncIterator]: () => ({ next: async () => ({ done: true, value: undefined }) }) }, cancel() {}, async waitClosed() {} },
{ streamId: "s1", frames: {}, cancel() {}, async waitClosed() {} },
{ streamId: "s1", frames: { [Symbol.asyncIterator]: () => ({ next: async () => ({ done: true, value: undefined }) }) }, cancel: 1, async waitClosed() {} },
]) {
const runtime = createBrowserRpcRuntime({
operations: { WATCH_RPC_RESOURCES: streamOperation() },
profiles: { CONNECT_REFERENCE_STREAM: streamProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER },
transports: {
CONNECT_REFERENCE_STREAM: defineBrowserRpcTransport({
runtimeProfileId: "CONNECT_REFERENCE_STREAM",
providerId: "REFERENCE_RPC",
protocol: "CONNECT_HTTP",
rpcKind: "SERVER_STREAM",
openServerStream: () => malformed as never,
}),
},
});
const results = await collect(
runtime
.bindServerStream("WATCH_RPC_RESOURCES", isResourceView)
.open({ resourceId: "scope-1" }),
);
expect(
results.every((value) => (value as { ok: boolean }).ok === false),
).toBe(true);
}
});
});
/**
* RPC-01. A `waitClosed()` that rejects, throws, or does not even return a
* promise is not evidence that the physical stream closed. Absorbing all three
* into a fulfilled `undefined` let the registry prune a live stream and admit a
* second one for the same operation against the same server.
*/
describe("RPC-01 only a positive receipt confirms physical closure", () => {
const shortDeadline = () =>
streamOperation({ totalDeadlineMs: 25, idleDeadlineMs: 25 });
function leaseProbeWith(
waitClosed: () => unknown,
): Readonly<{
transport: BrowserRpcTransport;
cancels: string[];
opened(): number;
}> {
const cancels: string[] = [];
const counter = { opened: 0 };
const transport = defineBrowserRpcTransport({
runtimeProfileId: "CONNECT_REFERENCE_STREAM",
providerId: "REFERENCE_RPC",
protocol: "CONNECT_HTTP",
rpcKind: "SERVER_STREAM",
openServerStream: () => {
counter.opened += 1;
return {
streamId: `physical-${counter.opened}`,
frames: {
[Symbol.asyncIterator]: () =>
({
next: () => new Promise<never>(() => {}),
}) as AsyncIterator<BrowserRpcStreamFrame>,
},
cancel(reason: string) {
cancels.push(reason);
},
waitClosed: waitClosed as () => Promise<void>,
};
},
});
return Object.freeze({
transport,
cancels,
opened: () => counter.opened,
});
}
const negativeReceipts = [
{
label: "rejects",
waitClosed: () => Promise.reject(new Error("never closed")),
},
{
label: "throws synchronously",
waitClosed: () => {
throw new TypeError("waitClosed exploded");
},
},
{ label: "returns a non-promise", waitClosed: () => "closed" },
{ label: "never settles", waitClosed: () => new Promise<void>(() => {}) },
];
for (const { label, waitClosed } of negativeReceipts) {
it(`keeps the operation DRAINING when waitClosed ${label}`, async () => {
const probe = leaseProbeWith(waitClosed);
const runtime = createBrowserRpcRuntime({
operations: { WATCH_RPC_RESOURCES: shortDeadline() },
profiles: { CONNECT_REFERENCE_STREAM: streamProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER },
transports: { CONNECT_REFERENCE_STREAM: probe.transport },
});
const stream = runtime.bindServerStream(
"WATCH_RPC_RESOURCES",
isResourceView,
);
await collect(stream.open({ resourceId: "scope-1" }));
expect(probe.opened()).toBe(1);
expect(probe.cancels).toHaveLength(1);
const second = await collect(stream.open({ resourceId: "scope-1" }));
expect(second).toHaveLength(1);
expect(second[0]).toMatchObject({
ok: false,
error: { code: "RPC_STREAM_DRAINING" },
});
// No second physical stream was ever opened.
expect(probe.opened()).toBe(1);
});
}
it("admits a second stream once waitClosed fulfils", async () => {
let release: (() => void) | undefined;
const closed = new Promise<void>((resolve) => {
release = resolve;
});
const probe = leaseProbeWith(() => closed);
const runtime = createBrowserRpcRuntime({
operations: { WATCH_RPC_RESOURCES: shortDeadline() },
profiles: { CONNECT_REFERENCE_STREAM: streamProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER },
transports: { CONNECT_REFERENCE_STREAM: probe.transport },
});
const stream = runtime.bindServerStream(
"WATCH_RPC_RESOURCES",
isResourceView,
);
await collect(stream.open({ resourceId: "scope-1" }));
release?.();
await new Promise((resolve) => setTimeout(resolve, 5));
await collect(stream.open({ resourceId: "scope-1" }));
expect(probe.opened()).toBe(2);
});
/**
* RPC-02. Cleanup reads foreign state, so it must stay inside the result
* boundary: a throwing `return` accessor replaced the already selected
* outcome with a native rejection and skipped the rest of the teardown.
*/
it("keeps the selected outcome when the iterator return accessor throws", async () => {
const cancels: string[] = [];
const transport = defineBrowserRpcTransport({
runtimeProfileId: "CONNECT_REFERENCE_STREAM",
providerId: "REFERENCE_RPC",
protocol: "CONNECT_HTTP",
rpcKind: "SERVER_STREAM",
openServerStream: () => ({
streamId: "physical-hostile",
frames: {
[Symbol.asyncIterator]: () =>
Object.defineProperty(
{ next: () => new Promise<never>(() => {}) },
"return",
{
enumerable: true,
get() {
throw new TypeError("return getter escaped");
},
},
) as AsyncIterator<BrowserRpcStreamFrame>,
},
cancel(reason: string) {
cancels.push(reason);
},
waitClosed: async () => {},
}),
});
const runtime = createBrowserRpcRuntime({
operations: { WATCH_RPC_RESOURCES: shortDeadline() },
profiles: { CONNECT_REFERENCE_STREAM: streamProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER },
transports: { CONNECT_REFERENCE_STREAM: transport },
});
const results = await collect(
runtime
.bindServerStream("WATCH_RPC_RESOURCES", isResourceView)
.open({ resourceId: "scope-1" }),
);
expect(results).toHaveLength(1);
expect(results[0]).toMatchObject({
ok: false,
error: { code: "RPC_TOTAL_DEADLINE_EXCEEDED" },
});
expect(cancels).toHaveLength(1);
});
it("decodes a hostile frames iterator to null instead of throwing", async () => {
const hostile = {
streamId: "physical-hostile",
frames: Object.defineProperty({}, Symbol.asyncIterator, {
enumerable: true,
get() {
throw new TypeError("async iterator getter escaped");
},
}),
cancel() {},
async waitClosed() {},
};
expect(decodeServerStreamLease(hostile)).toBeNull();
});
});
/**
* RPC-04. An exact union is exactly these own data keys on a plain object. A
* custom prototype could otherwise carry metadata past the boundary while the
* own shape still looked valid, and a missing wire field could reach a
* permissive schema as `undefined`.
*/
describe("RPC-04 transport results are an exact union", () => {
const hostileResults = [
{
label: "valid own fields with an inherited extra",
result: () =>
Object.assign(Object.create({ injected: "prototype" }), {
ok: true,
message: { id: "a", name: "A" },
encodedBytes: 8,
}),
},
{
label: "a missing message",
result: () => ({ ok: true, encodedBytes: 8 }),
},
{
label: "a non-enumerable own extra",
result: () =>
Object.defineProperty(
{ ok: true, message: { id: "a", name: "A" }, encodedBytes: 8 },
"injected",
{ enumerable: false, value: true },
),
},
{
label: "a failure with an inherited code",
result: () => ({
ok: false,
failure: Object.create({ code: "SERVER_FAILURE" }) as object,
}),
},
];
for (const { label, result } of hostileResults) {
it(`rejects ${label} as a protocol mismatch`, async () => {
const runtime = unaryRuntime(
unaryTransport(async () => result() as never),
);
await expect(
runtime
.bindUnary("GET_RPC_RESOURCE", isResourceView)
.execute({ resourceId: "scope-1" }),
).resolves.toMatchObject({
ok: false,
error: { code: "RPC_PROTOCOL_MISMATCH" },
});
});
}
});
/**
* RPC-03. Every registry is snapshotted before anything validates it, so a
* hostile accessor never runs, and a row that hides fields behind a prototype
* or a non-enumerable key is refused rather than installed.
*/
describe("RPC-03 registries are snapshotted before they are validated", () => {
const build = (operations: Record<string, unknown>) =>
createBrowserRpcRuntime({
operations: operations as never,
profiles: { CONNECT_REFERENCE_UNARY: unaryProfile() },
schemaCodecs: SCHEMA_CODECS,
mappers: MAPPERS,
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
transports: {
CONNECT_REFERENCE_UNARY: unaryTransport(async () => ({
ok: true,
message: { id: "a", name: "A" },
encodedBytes: 8,
})),
},
});
it("never invokes a registry accessor", () => {
let reads = 0;
const operations = Object.defineProperty({}, "GET_RPC_RESOURCE", {
enumerable: true,
get() {
reads += 1;
return unaryOperation();
},
});
expect(() => build(operations)).toThrow(TypeError);
expect(reads).toBe(0);
});
const hostileRows = [
{
label: "an inherited extra field",
row: () =>
Object.assign(Object.create({ injected: true }), unaryOperation()),
},
{
label: "a non-enumerable own extra field",
row: () =>
Object.defineProperty({ ...unaryOperation() }, "injected", {
enumerable: false,
value: true,
}),
},
{
label: "a symbol field",
row: () => ({
...unaryOperation(),
[Symbol.for("injected")]: true,
}),
},
];
for (const { label, row } of hostileRows) {
it(`refuses to install a row with ${label}`, () => {
expect(() => build({ GET_RPC_RESOURCE: row() })).toThrow(TypeError);
});
}
});
@@ -5,6 +5,7 @@ import {
createUnavailableBrowserRpcTransport,
defineBrowserRpcTransport,
type BrowserRpcObservation,
type BrowserRpcServerStreamLease,
type BrowserRpcStreamFrame,
type BrowserRpcTransport,
} from "../../../src/adapters/browser-rpc/index.ts";
@@ -257,8 +258,12 @@ describe("Browser RPC provider-neutral runtime", () => {
failure: { code: "UNAVAILABLE" },
};
},
async *openServerStream() {
yield Object.freeze({ kind: "TERMINAL", ok: true });
openServerStream() {
return serverStreamLease(
(async function* () {
yield Object.freeze({ kind: "TERMINAL" as const, ok: true as const });
})(),
);
},
}),
).toThrow("transport is invalid");
@@ -425,11 +430,41 @@ function streamTransport(
protocol: "CONNECT_HTTP",
rpcKind: "SERVER_STREAM",
openServerStream(call) {
return source(call.signal);
return serverStreamLease(source(call.signal));
},
});
}
/**
* RPC-RR-01. Wraps a plain frame sequence in the lease the transport contract
* requires. `cancel` resolves `waitClosed`, which is what a cooperative
* transport does.
*/
let leaseSequence = 0;
function serverStreamLease(
frames: AsyncIterable<BrowserRpcStreamFrame>,
options: Readonly<{
onCancel?: (reason: string) => void;
closeOnCancel?: boolean;
}> = {},
): BrowserRpcServerStreamLease {
leaseSequence += 1;
let release: (() => void) | undefined;
const closed = new Promise<void>((resolve) => {
release = resolve;
});
return Object.freeze({
streamId: `test-stream-${leaseSequence}`,
frames,
cancel(reason: string) {
options.onCancel?.(reason);
if (options.closeOnCancel !== false) release?.();
},
waitClosed: () => closed,
});
}
function message(
id: string,
name: string,
+2 -1
View File
@@ -19,6 +19,7 @@ import {
validateCiArtifact,
} from "../../scripts/lib/ci-artifact-validator.ts";
import { writeCiGateLogAtomic } from "../../scripts/lib/ci-gate-log.ts";
import { linkFixtureNodeModules } from "../../scripts/lib/fixture-node-modules.ts";
import { captureCiCandidateArchive, verifyCiCandidateArchive } from "../../scripts/lib/ci-candidate-archive.ts";
import {
CANDIDATE_ARCHIVE_USAGE,
@@ -2131,7 +2132,7 @@ async function ensureProviderBaseFixture(): Promise<string> {
recursive: true,
});
await rm(path.join(root, "artifacts/release"), { recursive: true, force: true });
await symlink(path.join(sourceRoot, "node_modules"), path.join(root, "node_modules"), "dir");
await linkFixtureNodeModules(root, sourceRoot);
const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], {
cwd: sourceRoot,
encoding: "utf8",
@@ -44,6 +44,31 @@ describe("conditional validator CAS sidecar", () => {
expect(store.acceptNotModified(binding, 7, true)).toBe(false);
});
it("keeps colon-bearing validator tuples injective", () => {
const selectedScope = scope();
// N-05. Both bindings are individually valid and, under a delimiter join,
// encode to the same key.
const first = {
definitionId: "resource:detail",
identityToken: "identity-token-00000001",
representationVersion: 1,
scope: selectedScope.snapshot,
};
const second = {
definitionId: "resource",
identityToken: "detail:identity-token-00000001",
representationVersion: 1,
scope: selectedScope.snapshot,
};
const store = createConditionalValidatorStore();
expect(store.install(first, '"etag-a"', 7)).toBe(true);
expect(store.install(second, '"etag-b"', 7)).toBe(true);
expect(store.prepare(first, 7)).toBe('"etag-a"');
expect(store.prepare(second, 7)).toBe('"etag-b"');
});
it("rejects malformed validators and bounded-capacity overflow", () => {
const firstScope = scope();
const store = createConditionalValidatorStore(1);
@@ -0,0 +1,337 @@
import { describe, expect, it } from "vitest";
import {
composeContractContributions,
type ComposedContractContributions,
} from "../../src/contracts/external-contract-runtime.ts";
import {
installRestAuthProfileRegistry,
INSTALLED_REST_AUTH_PROFILES,
REST_AUTH_PROFILES,
} from "../../src/contracts/rest-profiles.ts";
import { TEST_CONTRACT_CONTRIBUTION } from "../helpers/external-contract-fixture.ts";
/**
* LIVE-02 / LIVE-03. `Object.freeze(new Map(...))` freezes the wrapper object,
* not the backing store: `set`, `delete` and `clear` keep working. Every
* registry the executor consults after composition must therefore be a read
* facade over a private store, and the rows it hands back must be exact
* own-data snapshots that a later mutation of the source cannot reach.
*/
const MUTATORS = ["set", "delete", "clear"] as const;
function borrowedMapMutation(
facade: unknown,
mutator: (typeof MUTATORS)[number],
): "THREW" | "MUTATED" {
try {
switch (mutator) {
case "set":
Map.prototype.set.call(facade as never, "INJECTED", {} as never);
break;
case "delete":
Map.prototype.delete.call(facade as never, "ANONYMOUS");
break;
case "clear":
Map.prototype.clear.call(facade as never);
break;
}
return "MUTATED";
} catch {
return "THREW";
}
}
describe("LIVE-02 installed REST auth profile registry", () => {
it("exposes no mutation API", () => {
const registry = INSTALLED_REST_AUTH_PROFILES as unknown as Record<
string,
unknown
>;
for (const mutator of MUTATORS) {
expect(registry[mutator]).toBeUndefined();
}
});
it("survives a cast mutation and a borrowed Map.prototype mutator", () => {
const registry = installRestAuthProfileRegistry(REST_AUTH_PROFILES);
const before = registry.size;
const identity = registry.get("ANONYMOUS");
expect(identity).toBeDefined();
for (const mutator of MUTATORS) {
expect(borrowedMapMutation(registry, mutator)).toBe("THREW");
}
expect(registry.size).toBe(before);
expect(registry.get("ANONYMOUS")).toBe(identity);
expect(registry.get("REFERENCE_EXTERNAL_BEARER")?.credentials).toBe("omit");
});
it("does not observe a post-installation mutation of the source record", () => {
const source: Record<string, (typeof REST_AUTH_PROFILES)["ANONYMOUS"]> = {
ANONYMOUS: {
authProfileId: "ANONYMOUS",
transport: "ANONYMOUS",
credentials: "omit",
allowedCredentialHeaders: [],
requiredCredentialHeaders: [],
},
};
const registry = installRestAuthProfileRegistry(source);
delete source.ANONYMOUS;
expect(registry.get("ANONYMOUS")?.transport).toBe("ANONYMOUS");
});
it("keeps read APIs the executor depends on", () => {
const registry = INSTALLED_REST_AUTH_PROFILES;
expect(registry.has("ANONYMOUS")).toBe(true);
expect(registry.has("NO_SUCH_PROFILE")).toBe(false);
expect([...registry.keys()].sort()).toEqual([
"ANONYMOUS",
"REFERENCE_EXTERNAL_BEARER",
]);
expect([...registry.entries()].length).toBe(registry.size);
expect([...registry.values()].length).toBe(registry.size);
});
});
describe("LIVE-03 composed contract registry", () => {
function compose(): ComposedContractContributions {
return composeContractContributions([TEST_CONTRACT_CONTRIBUTION] as never);
}
it("exposes no mutation API on either lookup", () => {
const composed = compose();
for (const facade of [composed.httpByOperationId, composed.eventByType]) {
const record = facade as unknown as Record<string, unknown>;
for (const mutator of MUTATORS) {
expect(record[mutator]).toBeUndefined();
}
for (const mutator of MUTATORS) {
expect(borrowedMapMutation(facade, mutator)).toBe("THREW");
}
}
expect(composed.httpByOperationId.size).toBeGreaterThan(0);
});
it("snapshots the frontend policy so a later source mutation cannot reach it", () => {
const mutablePolicy = {
...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend,
};
const contribution = {
...TEST_CONTRACT_CONTRIBUTION,
http: [
{
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
frontend: mutablePolicy,
},
],
};
const composed = composeContractContributions([contribution] as never);
const operationId = [...composed.httpByOperationId.keys()][0]!;
const installedBefore =
composed.httpByOperationId.get(operationId)!.frontend.totalDeadlineMs;
mutablePolicy.totalDeadlineMs = 999_999;
expect(
composed.httpByOperationId.get(operationId)!.frontend.totalDeadlineMs,
).toBe(installedBefore);
expect(installedBefore).not.toBe(999_999);
});
it("rejects an accessor or inherited policy field", () => {
const inherited = Object.create({ diagnosticsOperation: "INHERITED" }) as
Record<string, unknown>;
for (const [key, value] of Object.entries(
TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend,
)) {
if (key === "diagnosticsOperation") continue;
inherited[key] = value;
}
const accessor = { ...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend };
Object.defineProperty(accessor, "totalDeadlineMs", {
configurable: true,
enumerable: true,
get: () => 10_000,
});
for (const frontend of [inherited, accessor]) {
expect(() =>
composeContractContributions([
{
...TEST_CONTRACT_CONTRIBUTION,
http: [{ ...TEST_CONTRACT_CONTRIBUTION.http[0]!, frontend }],
},
] as never),
).toThrow();
}
});
/**
* NS-02. Composition validated the caller's object and then read it again to
* copy it. Between those two reads a stateful answer could swap a deadline or
* a retry budget, so the value that passed the ceiling check and the value the
* executor ran with were two different things.
*/
describe("validate the snapshot, never the source", () => {
/** Answers a safe value to a plain read and a hostile one to a copy. */
const statefulFrontend = () =>
new Proxy(
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend },
{
get(target, key, receiver) {
if (key === "totalDeadlineMs") return 10_000;
return Reflect.get(target, key, receiver);
},
getOwnPropertyDescriptor(target, key) {
if (key === "totalDeadlineMs") {
return {
configurable: true,
enumerable: true,
value: 999_999,
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
},
);
const contributionWith = (row: unknown) => [
{ ...TEST_CONTRACT_CONTRIBUTION, http: [row] },
];
it("validates the deadline it will install, not the one it was shown", () => {
// The copied value exceeds the hard ceiling, so composition must stop
// rather than install a deadline no check ever saw.
expect(() =>
composeContractContributions(
contributionWith({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
frontend: statefulFrontend(),
}) as never,
),
).toThrow();
});
it("keys the registry by the operation id it copied, not the one it was shown", () => {
const base = { ...TEST_CONTRACT_CONTRIBUTION.http[0]!.contract };
const contract = new Proxy(base, {
get(target, key, receiver) {
if (key === "operationId") return base.operationId;
return Reflect.get(target, key, receiver);
},
getOwnPropertyDescriptor(target, key) {
if (key === "operationId") {
return {
configurable: true,
enumerable: true,
value: "SwappedOperation",
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
});
const composed = composeContractContributions(
contributionWith({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
contract,
}) as never,
);
expect([...composed.httpByOperationId.keys()]).toEqual([
"SwappedOperation",
]);
expect(
composed.httpByOperationId.get("SwappedOperation")!.contract
.operationId,
).toBe("SwappedOperation");
});
const hostileRows = [
{
label: "an installed row with an extra own field",
row: () => ({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
injected: true,
}),
},
{
label: "an installed row with a symbol field",
row: () => ({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
[Symbol.for("injected")]: true,
}),
},
{
label: "an installed row with a custom prototype",
row: () =>
Object.assign(Object.create({ injected: true }), {
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
}),
},
{
label: "an installed row hiding a non-enumerable own field",
row: () =>
Object.defineProperty(
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]! },
"injected",
{ enumerable: false, value: true },
),
},
{
label: "an installed row behind a throwing ownKeys trap",
row: () =>
new Proxy(
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]! },
{
ownKeys() {
throw new TypeError("hostile ownKeys trap");
},
},
),
},
];
for (const { label, row } of hostileRows) {
it(`refuses to compose ${label}`, () => {
expect(() =>
composeContractContributions(contributionWith(row()) as never),
).toThrow();
});
}
it("refuses a contribution whose own shape is not exact", () => {
for (const contribution of [
{ ...TEST_CONTRACT_CONTRIBUTION, injected: true },
Object.assign(Object.create({ injected: true }), {
...TEST_CONTRACT_CONTRIBUTION,
}),
{
...TEST_CONTRACT_CONTRIBUTION,
[Symbol.for("injected")]: true,
},
]) {
expect(() =>
composeContractContributions([contribution] as never),
).toThrow();
}
});
it("refuses an event contract whose own shape is not exact", () => {
const events = TEST_CONTRACT_CONTRIBUTION.events;
if (events.length === 0) return;
for (const event of [
{ ...events[0]!, injected: true },
Object.assign(Object.create({ injected: true }), { ...events[0]! }),
]) {
expect(() =>
composeContractContributions([
{ ...TEST_CONTRACT_CONTRIBUTION, events: [event] },
] as never),
).toThrow();
}
});
});
});
+63 -3
View File
@@ -112,9 +112,14 @@ class FakeStorageEventTarget implements StorageEventTargetFacade {
this.listeners.delete(listener);
}
emit(key: string | null, newValue: string | null): void {
emit(
key: string | null,
newValue: string | null,
storageArea: "EXPECTED_LOCAL_STORAGE" | "OTHER_OR_UNKNOWN" =
"EXPECTED_LOCAL_STORAGE",
): void {
for (const listener of [...this.listeners]) {
listener({ key, newValue });
listener({ key, newValue, storageArea });
}
}
}
@@ -135,6 +140,9 @@ class FakeStorageBus {
});
}
/** Simulates the event arriving from a different Storage area. */
emitAsForeignArea = false;
set(
owner: FakeStorageEventTarget,
key: string,
@@ -142,7 +150,15 @@ class FakeStorageBus {
): void {
this.values.set(key, value);
for (const target of this.targets) {
if (target !== owner) target.emit(key, value);
if (target !== owner) {
target.emit(
key,
value,
this.emitAsForeignArea
? "OTHER_OR_UNKNOWN"
: "EXPECTED_LOCAL_STORAGE",
);
}
}
}
@@ -487,6 +503,50 @@ describe("browser cross-context invalidation transport", () => {
second.close();
});
it("rejects storage pulses from another or unknown storage area", () => {
const storageBus = new FakeStorageBus();
const publisherStorage = storageBus.createEndpoint();
const receiverStorage = storageBus.createEndpoint();
const storageOnly = {
createBroadcastChannel: () => {
throw new DOMException("Denied", "SecurityError");
},
};
const publisher = createBrowserCrossContextInvalidation(
dependencies("tab-a", {
...storageOnly,
storage: publisherStorage.storage,
storageEvents: publisherStorage.target,
}),
);
const receiver = createBrowserCrossContextInvalidation(
dependencies("tab-b", {
...storageOnly,
storage: receiverStorage.storage,
storageEvents: receiverStorage.target,
}),
);
const received = vi.fn();
receiver.subscribe(received);
// N-09. sessionStorage and every other Storage area raise the same event,
// so an identical key and value from a foreign area must be ignored.
storageBus.emitAsForeignArea = true;
expect(
publisher.publish({ topic: TOPIC, topicVersion: 1 }),
).toMatchObject({ ok: true });
expect(received).not.toHaveBeenCalled();
storageBus.emitAsForeignArea = false;
expect(
publisher.publish({ topic: TOPIC, topicVersion: 1 }),
).toMatchObject({ ok: true });
expect(received).toHaveBeenCalledOnce();
publisher.close();
receiver.close();
});
it("enters explicit local-only degradation when every transport fails", () => {
const storageBus = new FakeStorageBus();
const endpoint = storageBus.createEndpoint();
@@ -12,6 +12,45 @@ const profile = {
} as const;
describe("bounded cursor pagination runtime", () => {
it("returns PAGINATION_ABORTED when a non-cooperative page resolves after abort", async () => {
const controller = new AbortController();
let releasePage: ((page: unknown) => void) | undefined;
const loadPage = vi.fn(
() =>
new Promise((resolve) => {
releasePage = resolve as (page: unknown) => void;
}),
);
const runtime = createCursorPaginationRuntime({
definitionId: "bounded",
profile,
loadPage: loadPage as never,
});
const loading = runtime.loadAll({ signal: controller.signal });
await Promise.resolve();
controller.abort();
const result = await loading;
expect(result).toMatchObject({
ok: false,
error: { kind: "REQUEST_ABORTED", code: "PAGINATION_ABORTED" },
});
// The late page completion must be ignored, not accumulated.
releasePage?.({
ok: true,
value: {
items: ["late"],
nextCursor: null,
hasMore: false,
snapshotToken: "snapshot-1",
},
});
await Promise.resolve();
expect(loadPage).toHaveBeenCalledOnce();
});
it("loads a stable finite chain without exposing cursors in its value", async () => {
const loadPage = vi
.fn()
@@ -119,3 +158,121 @@ describe("bounded cursor pagination runtime", () => {
});
});
});
/**
* NS-07. The profile was validated once and then re-read on every page, so
* raising `maxPages` after construction widened a cap that had already been
* checked — the runtime issued more requests and returned more items than the
* validated profile allowed.
*/
describe("NS-07 the caps are the ones that were validated", () => {
it("keeps the page cap captured at construction", async () => {
const mutable: {
profileId: string;
maxPages: number;
maxTotalItems: number;
maxEstimatedBytes: number;
maxCursorBytes: number;
allowSparsePage: boolean;
} = { ...profile, maxPages: 1 };
const loadPage = vi.fn(async () => ({
ok: true as const,
value: {
items: [1],
nextCursor: `cursor-${loadPage.mock.calls.length}`,
hasMore: true,
snapshotToken: "snapshot-a",
},
}));
const runtime = createCursorPaginationRuntime({
definitionId: "LIST_ALL",
profile: mutable,
loadPage,
});
mutable.maxPages = 3;
mutable.maxTotalItems = 99;
await expect(runtime.loadAll({})).resolves.toMatchObject({
ok: false,
error: { code: "PAGINATION_PAGE_LIMIT" },
});
expect(loadPage).toHaveBeenCalledTimes(1);
});
it("keeps the loader captured at construction", async () => {
const original = vi.fn(async () => ({
ok: true as const,
value: {
items: [1],
nextCursor: null,
hasMore: false,
snapshotToken: null,
},
}));
const replacement = vi.fn();
const dependencies = {
definitionId: "LIST_ALL",
profile,
loadPage: original,
};
const runtime = createCursorPaginationRuntime(dependencies);
dependencies.loadPage = replacement as never;
await expect(runtime.loadAll({})).resolves.toMatchObject({ ok: true });
expect(original).toHaveBeenCalledTimes(1);
expect(replacement).not.toHaveBeenCalled();
});
const hostileProfiles: readonly (readonly [string, () => unknown])[] = [
[
"an accessor cap",
() =>
Object.defineProperty({ ...profile }, "maxPages", {
enumerable: true,
get: () => 3,
}),
],
[
"an inherited cap",
() => Object.create({ ...profile }) as unknown,
],
["an extra own field", () => ({ ...profile, injected: true })],
[
"a symbol field",
() => ({ ...profile, [Symbol.for("injected")]: true }),
],
[
"a non-enumerable own field",
() =>
Object.defineProperty({ ...profile }, "injected", {
enumerable: false,
value: true,
}),
],
[
"a throwing ownKeys trap",
() =>
new Proxy(
{ ...profile },
{
ownKeys() {
throw new TypeError("hostile ownKeys trap");
},
},
),
],
];
for (const [label, build] of hostileProfiles) {
it(`refuses to build a runtime from ${label}`, () => {
expect(() =>
createCursorPaginationRuntime({
definitionId: "LIST_ALL",
profile: build() as never,
loadPage: vi.fn(),
}),
).toThrow(TypeError);
});
}
});
+89
View File
@@ -0,0 +1,89 @@
import { mkdtemp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { linkFixtureNodeModules } from "../../scripts/lib/fixture-node-modules.ts";
/**
* A removal or provider fixture runs `pnpm` inside a throwaway copy of the
* repository. pnpm does not recognise the modules directory it finds there and
* purges it, and with `CI=true` it does so without a prompt. When
* `<fixture>/node_modules` was a single directory symlink, that purge followed
* the link and deleted the repository's own installed dependencies mid-run —
* a test suite uninstalling the workspace it was running in.
*/
describe("fixture node_modules linking", () => {
async function sourceTree() {
const source = await mkdtemp(path.join(tmpdir(), "fixture-source-"));
const modules = path.join(source, "node_modules");
await mkdir(path.join(modules, "left", "lib"), { recursive: true });
await mkdir(path.join(modules, "@scope", "right"), { recursive: true });
await mkdir(path.join(modules, ".bin"), { recursive: true });
await writeFile(path.join(modules, "left", "lib", "index.js"), "export default 1;\n");
await writeFile(path.join(modules, "@scope", "right", "package.json"), "{}\n");
await writeFile(path.join(modules, ".modules.yaml"), "{}\n");
return source;
}
it("survives a recursive delete of the fixture's node_modules", async () => {
const source = await sourceTree();
const fixture = await mkdtemp(path.join(tmpdir(), "fixture-root-"));
try {
await linkFixtureNodeModules(fixture, source);
// The fixture resolves the same packages...
expect(
await readFile(
path.join(fixture, "node_modules/left/lib/index.js"),
"utf8",
),
).toContain("export default 1;");
expect(
(await readdir(path.join(fixture, "node_modules"))).sort(),
).toEqual([".bin", ".modules.yaml", "@scope", "left"]);
// ...and this is exactly what pnpm's purge does.
await rm(path.join(fixture, "node_modules"), {
recursive: true,
force: true,
});
// The repository's real dependencies are untouched.
expect(
await readFile(
path.join(source, "node_modules/left/lib/index.js"),
"utf8",
),
).toContain("export default 1;");
expect(
await stat(path.join(source, "node_modules/@scope/right/package.json")),
).toBeDefined();
expect(
(await readdir(path.join(source, "node_modules"))).sort(),
).toEqual([".bin", ".modules.yaml", "@scope", "left"]);
} finally {
await rm(source, { recursive: true, force: true });
await rm(fixture, { recursive: true, force: true });
}
});
it("creates node_modules as a real directory, never a symlink", async () => {
const source = await sourceTree();
const fixture = await mkdtemp(path.join(tmpdir(), "fixture-root-"));
try {
await linkFixtureNodeModules(fixture, source);
const { lstat } = await import("node:fs/promises");
const entry = await lstat(path.join(fixture, "node_modules"));
expect(entry.isSymbolicLink()).toBe(false);
expect(entry.isDirectory()).toBe(true);
expect(
(await lstat(path.join(fixture, "node_modules/left"))).isSymbolicLink(),
).toBe(true);
} finally {
await rm(source, { recursive: true, force: true });
await rm(fixture, { recursive: true, force: true });
}
});
});
+85 -29
View File
@@ -10,6 +10,8 @@ import {
const installed: InstalledHttpContract<unknown, unknown, unknown> =
TEST_LIST_HTTP_CONTRACT;
const ROUTE_ID = "TEST_ROUTE";
const scope = Object.freeze({
generation: 1,
fingerprint: "scope-1",
@@ -72,7 +74,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher: async () =>
Response.json(
@@ -82,7 +83,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
});
await expect(
executor.execute(installed, { limit: 20 }, { scope }),
executor.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, scope }),
).resolves.toMatchObject({
kind: "RATE_LIMITED",
effect: "NOT_APPLICABLE",
@@ -104,7 +105,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: {},
credentials: "omit" as const,
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
@@ -119,7 +119,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
executor.execute(
createInstalled,
{ name: "created" },
{ scope, ...(intent === undefined ? {} : { intent }) },
{ routeId: ROUTE_ID, scope, ...(intent === undefined ? {} : { intent }) },
),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
@@ -143,7 +143,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: {},
credentials: "omit" as const,
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
@@ -154,7 +153,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
});
await expect(
executor.execute(installed, { limit: 20 }, { scope, intent }),
executor.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, scope, intent }),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: {
@@ -173,14 +172,14 @@ describe("descriptor-driven HTTP execution lifetime", () => {
label: "query with the canonical reserved header",
operation: installed,
input: { limit: 20 },
context: { scope },
context: { routeId: ROUTE_ID, scope },
headerName: "Idempotency-Key",
},
{
label: "valid KEYED command with a case-variant reserved header",
operation: createInstalled,
input: { name: "created" },
context: { scope, intent: mutationIntent() },
context: { routeId: ROUTE_ID, scope, intent: mutationIntent() },
headerName: "iDeMpOtEnCy-KeY",
},
])(
@@ -189,7 +188,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: { [headerName]: "credential-owned-key" },
credentials: "omit" as const,
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
@@ -234,7 +232,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher,
sleep: async () => {},
@@ -252,7 +249,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
executor.execute(
retryingCreate,
{ name: "created" },
{ scope, intent: mutationIntent({ idempotencyKey: "logical-key" }) },
{ routeId: ROUTE_ID, scope, intent: mutationIntent({ idempotencyKey: "logical-key" }) },
),
).resolves.toMatchObject({ kind: "SUCCESS" });
expect(fetcher).toHaveBeenCalledTimes(2);
@@ -275,7 +272,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher,
});
@@ -284,7 +280,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
executor.execute(
installed,
{ limit: 20 },
{ scope },
{ routeId: ROUTE_ID, scope },
),
).resolves.toMatchObject({ kind: "SUCCESS" });
expect(fetcher).toHaveBeenCalledOnce();
@@ -300,7 +296,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher: vi.fn(async (input) => {
urls.push(String(input));
@@ -317,6 +312,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
createInstalled,
{ name: "created" },
{
routeId: ROUTE_ID,
scope,
intent: Object.freeze({
intentId: "private-intent-id",
@@ -348,12 +344,11 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher,
});
await expect(executor.execute(installed, input, { scope })).resolves.toMatchObject({
await expect(executor.execute(installed, input, { routeId: ROUTE_ID, scope })).resolves.toMatchObject({
kind: "SUCCESS",
});
expect(String((fetcher.mock.calls as unknown[][])[0]?.[0])).toContain(
@@ -368,7 +363,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher: vi.fn(),
});
@@ -389,11 +383,11 @@ describe("descriptor-driven HTTP execution lifetime", () => {
},
} as unknown as typeof installed;
await expect(executor.execute(throwing, { limit: 20 }, { scope })).resolves.toMatchObject({
await expect(executor.execute(throwing, { limit: 20 }, { routeId: ROUTE_ID, scope })).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
effect: "NOT_APPLICABLE",
});
await expect(executor.execute(malformed, { limit: 20 }, { scope })).resolves.toMatchObject({
await expect(executor.execute(malformed, { limit: 20 }, { routeId: ROUTE_ID, scope })).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
effect: "NOT_APPLICABLE",
});
@@ -406,7 +400,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher: vi.fn(async () => Response.json({ malformed: true }, { status: 201 })),
});
@@ -416,6 +409,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
createInstalled,
{ name: "created" },
{
routeId: ROUTE_ID,
scope,
intent: mutationIntent(),
},
@@ -441,7 +435,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher: vi.fn(async () => {
current = false;
@@ -458,6 +451,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
createInstalled,
{ name: "created" },
{
routeId: ROUTE_ID,
scope: fencedScope,
intent: mutationIntent({ intentId: "intent-2", idempotencyKey: "key-2" }),
},
@@ -469,6 +463,64 @@ describe("descriptor-driven HTTP execution lifetime", () => {
});
});
it("keeps a dispatched command MAYBE_APPLIED when a retry-time fence lands between scope checks", async () => {
// Attempt 1 dispatches an idempotent command and receives 429. The retry
// sleep resolves, the loop-entry scope check is still current, and only the
// pre-dispatch final invariant observes the fence.
let armed = false;
let checksAfterArming = 0;
const idempotentCommand: InstalledHttpContract<unknown, unknown, unknown> = {
...createInstalled,
contract: {
...createInstalled.contract,
retrySemantics: "IDEMPOTENT" as const,
},
frontend: { ...createInstalled.frontend, retryBudget: 1 as const },
};
const racingScope = Object.freeze({
...scope,
isCurrent: () => {
if (!armed) return true;
checksAfterArming += 1;
// The retry loop entry still observes a current scope; the pre-dispatch
// final invariant is the first observation of the fence.
return checksAfterArming <= 1;
},
});
const fetcher = vi.fn(async () =>
Response.json({ type: "about:blank", title: "slow down", status: 429 }, {
status: 429,
}),
);
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 1,
attachCredentials: () => ({ kind: "READY", headers: {} }),
fetcher,
sleep: async () => {
armed = true;
},
random: () => 0,
});
const outcome = await executor.execute(
idempotentCommand,
{ name: "created" },
{
routeId: ROUTE_ID,
scope: racingScope,
intent: mutationIntent({ idempotencyKey: null }),
},
);
expect(fetcher).toHaveBeenCalledTimes(1);
expect(outcome).toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: { kind: "SCOPE_FENCED" },
effect: "MAYBE_APPLIED",
});
});
it("settles a credential hang at the total operation deadline", async () => {
vi.useFakeTimers();
let settled = false;
@@ -479,7 +531,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
});
const result = executor
.execute(operation({ deadlineMs: 5 }), { limit: 20 }, { scope })
.execute(operation({ deadlineMs: 5 }), { limit: 20 }, { routeId: ROUTE_ID, scope })
.then((outcome) => {
settled = true;
return outcome;
@@ -519,7 +571,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher,
monotonicNow: () => 0,
@@ -532,7 +583,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
const result = executor.execute(
operation({ deadlineMs: 5 }),
{ limit: 20 },
{ scope },
{ routeId: ROUTE_ID, scope },
);
await vi.advanceTimersByTimeAsync(5);
await flushMicrotasks();
@@ -548,7 +599,14 @@ describe("descriptor-driven HTTP execution lifetime", () => {
expect(observe).toHaveBeenCalledTimes(iterationCount);
for (const [observation] of observe.mock.calls) {
expect(observation).toEqual(
expect.objectContaining({ attempts: 1, certainty: "TIMEOUT" }),
expect.objectContaining({
attemptCount: 1,
errorKind: "TIMEOUT",
terminalReason: "TIMEOUT",
cancellationOwner: "DEADLINE",
routeId: ROUTE_ID,
operationId: "TEST_LIST_ENTITIES",
}),
);
}
} finally {
@@ -566,7 +624,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher: vi.fn(async () =>
Response.json(
@@ -582,7 +639,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
});
const result = executor
.execute(installed, { limit: 20 }, { scope, signal: caller.signal })
.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, scope, signal: caller.signal })
.then((outcome) => {
settled = true;
return outcome;
@@ -608,7 +665,6 @@ describe("descriptor-driven HTTP execution lifetime", () => {
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
}),
fetcher: vi.fn(async () => new Response(body, { status: 200 })),
});
@@ -617,7 +673,7 @@ describe("descriptor-driven HTTP execution lifetime", () => {
executor.execute(
operation({ responseBody: "NONE" }),
{ limit: 20 },
{ scope },
{ routeId: ROUTE_ID, scope },
),
).resolves.toMatchObject({
kind: "TRANSPORT_FAILURE",
+214 -5
View File
@@ -203,6 +203,7 @@ describe("production image CDN runtime", () => {
const resolved = await runtime.presentation.resolve({
asset: accepted.value,
preset,
signal: new AbortController().signal,
});
expect(resolved.ok).toBe(true);
if (!resolved.ok) return;
@@ -317,10 +318,11 @@ describe("production image CDN runtime", () => {
runtime.presentation.resolve({
asset: accepted.value,
preset,
signal: new AbortController().signal,
width: 9_999,
query: "format=svg",
src: "data:text/html,active",
} as Parameters<typeof runtime.presentation.resolve>[0]),
} as unknown as Parameters<typeof runtime.presentation.resolve>[0]),
).resolves.toMatchObject({
ok: false,
error: { code: "INVALID_INPUT" },
@@ -332,6 +334,7 @@ describe("production image CDN runtime", () => {
"card-landscape",
"render-public-product-image",
),
signal: new AbortController().signal,
}),
).resolves.toMatchObject({
ok: false,
@@ -341,6 +344,7 @@ describe("production image CDN runtime", () => {
runtime.presentation.resolve({
asset: {} as typeof accepted.value,
preset,
signal: new AbortController().signal,
}),
).resolves.toMatchObject({
ok: false,
@@ -385,6 +389,7 @@ describe("production image CDN runtime", () => {
const resolved = await runtime.presentation.resolve({
asset: accepted.value,
preset: presetReference,
signal: new AbortController().signal,
});
expect(resolved.ok).toBe(true);
if (!resolved.ok) return;
@@ -782,6 +787,7 @@ describe("production image CDN runtime", () => {
runtime.presentation.resolve({
asset: accepted.value,
preset,
signal: new AbortController().signal,
}),
).resolves.toMatchObject({
ok: false,
@@ -840,6 +846,7 @@ describe("production image CDN runtime", () => {
runtime.presentation.resolve({
asset: accepted.value,
preset: lazy,
signal: new AbortController().signal,
}),
).resolves.toMatchObject({
ok: false,
@@ -999,7 +1006,13 @@ describe("production image CDN runtime", () => {
expect(abortDeadline.clearTimeout).toHaveBeenCalledOnce();
});
it("bounds concurrent capability verification and releases the slot after abort", async () => {
/**
* TR-RR-07. The concurrency cap exists to bound *physical* verification work.
* Releasing the slot when the wrapper's abort resolved let an abandoned
* verifier keep running while a new one was admitted, so repeated aborts
* produced more concurrent work than the configured cap allows.
*/
it("holds the verification slot until the raw verifier settles", async () => {
const preset = imageCdnPresetReference(
"verification-concurrency",
"bound-image-verification-concurrency",
@@ -1011,10 +1024,13 @@ describe("production image CDN runtime", () => {
},
});
let verificationAttempt = 0;
let releaseFirst: ((value: boolean) => void) | undefined;
const verify = vi.fn(() => {
verificationAttempt += 1;
return verificationAttempt === 1
? new Promise<boolean>(() => undefined)
? new Promise<boolean>((resolve) => {
releaseFirst = resolve;
})
: Promise.resolve(true);
});
const runtime = createImageCdnRuntime({
@@ -1047,9 +1063,24 @@ describe("production image CDN runtime", () => {
ok: false,
error: { code: "ABORTED" },
});
// The caller's wait ended, but the raw verifier has not. Admitting a second
// one here would put two physical verifications under a cap of one.
await expect(
runtime.assets.acceptBackendIssued(issued),
).resolves.toMatchObject({ ok: true });
).resolves.toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(verify).toHaveBeenCalledOnce();
// Once the raw verifier settles the slot is free again.
releaseFirst?.(true);
await vi.waitFor(async () => {
await expect(
runtime.assets.acceptBackendIssued(issued),
).resolves.toMatchObject({ ok: true });
});
expect(verify).toHaveBeenCalledTimes(2);
});
@@ -1110,6 +1141,7 @@ describe("production image CDN runtime", () => {
runtime.presentation.resolve({
asset: accepted.value,
preset,
signal: new AbortController().signal,
}),
).resolves.toMatchObject({ ok: true });
const acceptedPrivate =
@@ -1125,6 +1157,7 @@ describe("production image CDN runtime", () => {
runtime.presentation.resolve({
asset: accepted.value,
preset,
signal: new AbortController().signal,
}),
).resolves.toMatchObject({
ok: false,
@@ -1479,6 +1512,10 @@ describe("browser image probe", () => {
close: vi.fn(),
}));
for (const cacheControl of [
// BT-IMG-02. Unmatched quotes must not be unwrapped into a bare number.
'public, max-age="31536000, immutable',
'public, max-age=31536000", immutable',
'public, max-age="31536000\\", immutable',
"public, public, max-age=31536000, immutable",
"public, max-age=31536000, s-maxage=60, immutable",
"public, max-age=31536000, immutable, must-revalidate",
@@ -1645,7 +1682,9 @@ describe("browser image probe", () => {
responseAt(imageUrl, png, {
status: 200,
headers: {
"cache-control": "private, no-store",
// TR-RR-09. A private response carries `no-store` and nothing else
// that describes cacheability.
"cache-control": "no-store",
"content-type": "image/png",
},
}),
@@ -1735,6 +1774,67 @@ describe("browser image probe", () => {
});
});
/**
* TR-RR-09. The recorded BT-IMG-02 contract for a private response is a
* fail-closed matrix. Accepting `no-store` next to a directive that describes
* cacheability lets a self-contradictory policy read as acceptable.
*/
it("applies the full private Cache-Control matrix", async () => {
const png = pngBytes(640, 360);
const probeWith = async (cacheControl: string) => {
const probe = createBrowserImageProbe({
fetcher: (async () =>
responseAt(imageUrl, png, {
status: 200,
headers: {
"cache-control": cacheControl,
"content-type": "image/png",
},
})) as typeof fetch,
createBitmap: async () => ({
width: 640,
height: 360,
close: vi.fn(),
}),
});
return await probe.probe(
request({
delivery: "PRIVATE_SIGNED",
minimumPublicMaxAgeSeconds: 0,
}),
);
};
// Only `no-store`, plus a syntactically valid unknown extension.
expect(await probeWith("no-store")).toMatchObject({ ok: true });
expect(await probeWith('no-store, x-vendor="a,b"')).toMatchObject({
ok: true,
});
for (const companion of [
"public",
"private",
"immutable",
"max-age=60",
"s-maxage=60",
"no-cache",
"must-revalidate",
"proxy-revalidate",
]) {
expect(await probeWith(`no-store, ${companion}`)).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
}
for (const withoutNoStore of ["private", "no-cache", "max-age=0"]) {
expect(await probeWith(withoutNoStore)).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
}
});
it("times out a stalled body, aborts the composed signal and cancels its reader", async () => {
const manual = manualImageProbeScheduler();
const cancel = vi.fn(async () => undefined);
@@ -1836,6 +1936,115 @@ describe("browser image probe", () => {
expect(close).toHaveBeenCalledOnce();
});
});
/**
* X-AUDIT-02. `probe()` promises a `BrowserDataResult`. A scheduler that
* cannot install the probe deadline must close the probe inside that contract
* rather than rejecting it, and must not leave the caller's listener behind.
*/
describe("scheduler boundary", () => {
const trackedSignal = () => {
const controller = new AbortController();
const added: string[] = [];
const removed: string[] = [];
const add = controller.signal.addEventListener.bind(controller.signal);
const remove = controller.signal.removeEventListener.bind(
controller.signal,
);
Object.defineProperty(controller.signal, "addEventListener", {
configurable: true,
value: (type: string, ...rest: readonly unknown[]) => {
added.push(type);
return (add as (...args: readonly unknown[]) => unknown)(
type,
...rest,
);
},
});
Object.defineProperty(controller.signal, "removeEventListener", {
configurable: true,
value: (type: string, ...rest: readonly unknown[]) => {
removed.push(type);
return (remove as (...args: readonly unknown[]) => unknown)(
type,
...rest,
);
},
});
return { controller, added, removed };
};
it("closes the probe when the scheduler cannot install the deadline", async () => {
const fetcher = vi.fn(async () => new Response(null, { status: 200 }));
const { controller, added, removed } = trackedSignal();
const probe = createBrowserImageProbe({
fetcher: fetcher as unknown as typeof fetch,
createBitmap: vi.fn(),
timeoutMs: 1_000,
scheduler: {
setTimeout: () => {
throw new TypeError("image scheduler install exploded");
},
clearTimeout: vi.fn(),
},
});
await expect(
probe.probe({ ...request(), signal: controller.signal }),
).resolves.toMatchObject({
ok: false,
error: { code: "UNAVAILABLE", retryable: true, recovery: "RETRY" },
});
expect(fetcher).not.toHaveBeenCalled();
expect(added.filter((type) => type === "abort")).toHaveLength(1);
expect(removed.filter((type) => type === "abort")).toHaveLength(1);
});
it("starts no timer and no fetch for an already aborted caller", async () => {
const fetcher = vi.fn(async () => new Response(null, { status: 200 }));
const setTimeout_ = vi.fn(() => 1);
const controller = new AbortController();
controller.abort();
const probe = createBrowserImageProbe({
fetcher: fetcher as unknown as typeof fetch,
createBitmap: vi.fn(),
timeoutMs: 1_000,
scheduler: { setTimeout: setTimeout_, clearTimeout: vi.fn() },
});
await expect(
probe.probe({ ...request(), signal: controller.signal }),
).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" } });
expect(fetcher).not.toHaveBeenCalled();
expect(setTimeout_).not.toHaveBeenCalled();
});
it("keeps the classified outcome when clearing the deadline throws", async () => {
const png = pngBytes(640, 360);
const probe = createBrowserImageProbe({
fetcher: (async () =>
responseAt(imageUrl, png, {
status: 200,
headers: publicImageHeaders("image/png", png.byteLength),
})) as typeof fetch,
createBitmap: vi.fn(async () => ({
width: 640,
height: 360,
close: vi.fn(),
})),
timeoutMs: 1_000,
scheduler: {
setTimeout: (callback: () => void, milliseconds: number) =>
setTimeout(callback, milliseconds),
clearTimeout: () => {
throw new TypeError("image scheduler clear exploded");
},
},
});
await expect(probe.probe(request())).resolves.toMatchObject({ ok: true });
});
});
});
describe("P-256 image capability verifier", () => {
+31
View File
@@ -596,6 +596,37 @@ describe("IndexedDB bounded codec maintenance", () => {
});
});
it("stops codec migration commit at the cooperative deadline", async () => {
const memory = new MemoryIndexedDbFactory();
await prepareSchema(memory);
seedLegacy(memory, "row-a", "first");
seedLegacy(memory, "row-b", "second");
const policy = defaultPolicy();
// STO-06. The clock only advances past the deadline once the commit
// transaction is already open, so the stop must happen inside the commit
// chain rather than before transform.
let calls = 0;
const maintenance = createMaintenance(memory, policy, {
now: () => {
calls += 1;
// Scan, prepare and the first commit record stay inside the budget.
return calls <= 6 ? 0 : 5_000;
},
});
const result = await maintenance.migrateCodecBatch({
maxRows: 10,
maxDurationMs: 1_000,
});
expect(result.ok).toBe(true);
if (!result.ok) return;
// The batch is incomplete and says so; it never claims a full pass.
expect(result.value.state).toBe("MORE");
expect(result.value.budgetExhausted).toBe(true);
expect(result.value.checkpointedRows).toBeLessThan(2);
});
it("fails closed when a historical payload cannot be transformed", async () => {
const memory = new MemoryIndexedDbFactory();
await prepareSchema(memory);
@@ -0,0 +1,149 @@
import { describe, expect, it, vi } from "vitest";
import { createCursorPaginationRuntime } from "../../src/adapters/query-cache/cursor-pagination-runtime.ts";
import {
defineMutationIntent,
isValidIdempotencyKey,
} from "../../src/contracts/mutation-intent.ts";
import type {
CursorPage,
CursorPaginationProfile,
} from "../../src/contracts/cursor-pagination.ts";
import type { Result } from "../../src/application/result.ts";
const PROFILE: CursorPaginationProfile = Object.freeze({
profileId: "TEST_PAGINATION_V1",
maxPages: 3,
maxTotalItems: 30,
maxEstimatedBytes: 32_768,
maxCursorBytes: 512,
allowSparsePage: false,
});
function page(
items: readonly number[],
nextCursor: string | null,
): CursorPage<number> {
return Object.freeze({
items: Object.freeze([...items]),
nextCursor,
hasMore: nextCursor !== null,
snapshotToken: "snapshot-1",
});
}
/**
* OPT-NET-01. A loader rejection is evidence about the data source. Reporting
* it as `PAGINATION_ABORTED` because a signal merely *exists* erases a real
* network or contract failure and files it under a user decision nobody made.
*/
describe("OPT-NET-01 cursor pagination abort classification", () => {
it("preserves a loader rejection while the signal is still live", async () => {
const controller = new AbortController();
const runtime = createCursorPaginationRuntime<number>({
definitionId: "TEST_PAGINATION",
profile: PROFILE,
loadPage: async () => {
throw new TypeError("upstream exploded");
},
});
await expect(
runtime.loadAll({ signal: controller.signal }),
).rejects.toThrow("upstream exploded");
});
it("keeps the same rejection when no signal is supplied", async () => {
const runtime = createCursorPaginationRuntime<number>({
definitionId: "TEST_PAGINATION",
profile: PROFILE,
loadPage: async () => {
throw new TypeError("upstream exploded");
},
});
await expect(runtime.loadAll({})).rejects.toThrow("upstream exploded");
});
it("classifies a rejection during a real abort as PAGINATION_ABORTED", async () => {
const controller = new AbortController();
const runtime = createCursorPaginationRuntime<number>({
definitionId: "TEST_PAGINATION",
profile: PROFILE,
loadPage: async () => {
controller.abort();
throw new TypeError("cancelled upstream");
},
});
const result = await runtime.loadAll({ signal: controller.signal });
expect(result.ok).toBe(false);
expect(result.ok ? null : result.error.code).toBe("PAGINATION_ABORTED");
});
it("does not admit a page that resolves after the abort", async () => {
const controller = new AbortController();
const loadPage = vi.fn(
async (): Promise<Result<CursorPage<number>>> => {
controller.abort();
await new Promise((resolve) => setTimeout(resolve, 5));
return { ok: true, value: page([1, 2], null) };
},
);
const runtime = createCursorPaginationRuntime<number>({
definitionId: "TEST_PAGINATION",
profile: PROFILE,
loadPage,
});
const result = await runtime.loadAll({ signal: controller.signal });
expect(result.ok).toBe(false);
expect(result.ok ? null : result.error.code).toBe("PAGINATION_ABORTED");
});
});
/**
* OPT-NET-02. One idempotency-key authority. Two validators drift, and the
* looser one becomes the way a control character reaches a request header.
*/
describe("OPT-NET-02 shared idempotency key validation", () => {
const rejected = [
"",
" ",
"key\nwith-newline",
"key\u0000null",
"key\u007fdelete",
"key\u009fc1",
"a".repeat(257),
];
it("rejects the same values at intent definition and at admission", () => {
for (const value of rejected) {
expect(isValidIdempotencyKey(value)).toBe(false);
expect(() =>
defineMutationIntent({
intentId: "intent-1",
operationId: "OP",
canonicalInputIdentity: "identity",
idempotencyKey: value,
createdAtMonotonicMs: 1,
}),
).toThrow(TypeError);
}
});
it("accepts the bounded printable and Unicode values both sides allow", () => {
for (const value of ["key-1", "a".repeat(256), "키-값", "ключ"]) {
expect(isValidIdempotencyKey(value)).toBe(true);
expect(
defineMutationIntent({
intentId: "intent-1",
operationId: "OP",
canonicalInputIdentity: "identity",
idempotencyKey: value,
createdAtMonotonicMs: 1,
}).idempotencyKey,
).toBe(value);
}
});
});
+300 -3
View File
@@ -290,7 +290,10 @@ function createWorker(
return browserDataSuccess(undefined);
},
async cleanupTransaction() {
return browserDataSuccess(undefined);
return browserDataSuccess({ kind: "CLEANED" as const });
},
async abortPreparedPut() {
return browserDataSuccess({ kind: "CLEANED" as const });
},
async finalizePut() {
return browserDataSuccess(undefined);
@@ -349,6 +352,123 @@ describe("OPFS byte-store coordinator", () => {
expect(JSON.stringify(observations)).not.toContain("object_12345678");
});
/**
* NS-04. Awaiting `journal.complete` without checking it reported a settled
* transaction that nobody settled: the caller saw plain success and success
* telemetry while the durable row stayed `COMMITTED`, so the reconcile
* backlog grew invisibly.
*/
it("does not report a settled write when the journal cannot complete it", async () => {
const journal = createJournal();
journal.complete = async () =>
browserDataFailure("UNAVAILABLE", "INDEXEDDB_WRITE", {
retryable: true,
recovery: "RECONCILE",
});
const observations: { operation: string; outcome: string }[] = [];
const adapter = createOpfsByteStoreAdapter({
journal,
worker: createWorker(),
scope,
storagePolicy,
policy: {
...resolveOpfsRuntimePolicy(),
chunkSizeBytes: 64 * 1024,
maxObjectBytes: 64 * 1024,
maxChunkCount: 1,
},
createTransactionId: () => "transaction_12345678",
now: () => 100,
observer: (event) =>
observations.push(event as { operation: string; outcome: string }),
});
const result = await adapter.objects.put({
objectId: "object_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: sourceFrom(new Uint8Array([1, 2, 3])),
});
expect(result.ok).toBe(false);
if (!result.ok) {
// The payload is durable, so recovery is reconciliation, not a rewrite.
expect(result.error.recovery).toBe("RECONCILE");
expect(result.error.operation).toBe("OBJECT_WRITE");
}
expect(
observations.some(
(event) =>
event.operation === "OBJECT_WRITE" && event.outcome === "SUCCEEDED",
),
).toBe(false);
// The committed row stays the reconciler's authority.
expect(journal.transactions.get("transaction_12345678")?.phase).toBe(
"COMMITTED",
);
expect(journal.objects.get("object_12345678")).toBeDefined();
});
it("records maintenance debt when a committed delete cannot be completed", async () => {
const journal = createJournal();
const observations: { operation: string; outcome: string }[] = [];
const policy = {
...resolveOpfsRuntimePolicy(),
chunkSizeBytes: 64 * 1024,
maxObjectBytes: 64 * 1024,
maxChunkCount: 1,
};
const writer = createOpfsByteStoreAdapter({
journal,
worker: createWorker(),
scope,
storagePolicy,
policy,
createTransactionId: () => "transaction_12345678",
now: () => 100,
});
await writer.objects.put({
objectId: "object_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: sourceFrom(new Uint8Array([1, 2, 3])),
});
// The journal can commit the deletion but cannot settle the transaction.
const remover = createOpfsByteStoreAdapter({
journal: {
...journal,
complete: async () =>
browserDataFailure("UNAVAILABLE", "INDEXEDDB_WRITE", {
retryable: true,
recovery: "RECONCILE",
}),
},
worker: createWorker(),
scope,
storagePolicy,
policy,
createTransactionId: () => "transaction_87654321",
now: () => 200,
observer: (event) =>
observations.push(event as { operation: string; outcome: string }),
});
// Logical deletion is already committed, so the caller must not be asked to
// repeat a non-idempotent delete — but it is not a settled success either.
const result = await remover.objects.remove({
objectId: "object_12345678",
expectedGeneration: 1,
});
expect(result.ok).toBe(true);
const deleteOutcomes = observations
.filter((event) => event.operation === "OBJECT_DELETE")
.map((event) => event.outcome);
expect(deleteOutcomes.at(-1)).toBe("DEGRADED");
expect(deleteOutcomes).not.toContain("SUCCEEDED");
});
it("deep-snapshots scope and policy at composition against caller mutation", async () => {
const journal = createJournal();
const mutableScope = { ...scope };
@@ -501,7 +621,183 @@ describe("OPFS byte-store coordinator", () => {
expect(replacementWorkerMethod).not.toHaveBeenCalled();
});
it("keeps a committed journal row for reconciliation when cleanup fails", async () => {
it("keeps PREPARING journal when compensating cleanup is aborted or unavailable", async () => {
for (const failing of [
{
async abortPreparedPut() {
return browserDataFailure("ABORTED", "OBJECT_RECONCILE");
},
},
{
async abortPreparedPut() {
return browserDataSuccess({ kind: "EFFECT_UNKNOWN" as const });
},
},
]) {
const journal = createJournal();
const worker = createWorker({
async preparePut() {
return browserDataFailure("QUOTA_EXCEEDED", "OBJECT_WRITE");
},
...failing,
});
const adapter = createOpfsByteStoreAdapter({
journal,
worker,
scope,
storagePolicy,
createTransactionId: () => "transaction_12345678",
now: () => 100,
});
const result = await adapter.objects.put({
objectId: "object_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: sourceFrom(new Uint8Array([1])),
});
expect(result.ok).toBe(false);
// The journal row is the only durable evidence that staging bytes may
// still exist, so it survives an unconfirmed compensation.
expect(
journal.transactions.get("transaction_12345678")?.phase,
).toBe("PREPARING");
}
});
it("does not roll back journal after an unknown worker mutation effect", async () => {
const journal = createJournal();
const worker = createWorker({
async abortPreparedPut() {
return browserDataSuccess({ kind: "EFFECT_UNKNOWN" as const });
},
});
const rollback = vi.spyOn(journal, "rollback");
vi.spyOn(journal, "markFilesReady").mockResolvedValueOnce(
browserDataFailure("UNAVAILABLE", "OBJECT_WRITE"),
);
const adapter = createOpfsByteStoreAdapter({
journal,
worker,
scope,
storagePolicy,
createTransactionId: () => "transaction_12345678",
now: () => 100,
});
const result = await adapter.objects.put({
objectId: "object_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: sourceFrom(new Uint8Array([1])),
});
expect(result.ok).toBe(false);
expect(rollback).not.toHaveBeenCalled();
});
it("holds the OPFS mutation lease until exact physical cleanup completes", async () => {
const journal = createJournal();
const observed: Array<Readonly<Record<string, unknown>>> = [];
const worker = createWorker({
async preparePut() {
return browserDataFailure("QUOTA_EXCEEDED", "OBJECT_WRITE");
},
async abortPreparedPut(request) {
observed.push({ ...request });
return browserDataSuccess({ kind: "CLEANED" as const });
},
});
const compensation = new AbortController();
const caller = new AbortController();
caller.abort();
const adapter = createOpfsByteStoreAdapter({
journal,
worker,
scope,
storagePolicy,
createTransactionId: () => "transaction_12345678",
createPhysicalGenerationId: () => "f".repeat(32) as never,
compensationSignal: compensation.signal,
now: () => 100,
});
await adapter.objects.put({
objectId: "object_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: sourceFrom(new Uint8Array([1])),
});
expect(observed).toHaveLength(1);
expect(observed[0]).toMatchObject({
transactionId: "transaction_12345678",
physicalGenerationId: "f".repeat(32),
});
// Compensation never inherits the caller signal.
expect(observed[0]?.signal).toBe(compensation.signal);
expect(
journal.transactions.has("transaction_12345678"),
).toBe(false);
});
it("delayed stale cleanup cannot delete a reused logical generation", async () => {
const journal = createJournal();
const issued: string[] = [];
let nextToken = 0;
const worker = createWorker({
async abortPreparedPut(request) {
issued.push(request.physicalGenerationId);
return browserDataSuccess({ kind: "CLEANED" as const });
},
});
let transaction = 0;
// T1 abandons its prepared put at the same logical generation 1.
vi.spyOn(journal, "markFilesReady").mockResolvedValueOnce(
browserDataFailure("UNAVAILABLE", "OBJECT_WRITE"),
);
const adapter = createOpfsByteStoreAdapter({
journal,
worker,
scope,
storagePolicy,
createTransactionId: () => `transaction_1234567${(transaction += 1)}`,
createPhysicalGenerationId: () =>
String(nextToken += 1).padStart(32, "0") as never,
now: () => 100,
});
const first = await adapter.objects.put({
objectId: "object_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: sourceFrom(new Uint8Array([1])),
});
expect(first.ok).toBe(false);
// T2 legitimately reuses logical generation 1 with a different token.
const second = await adapter.objects.put({
objectId: "object_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: sourceFrom(new Uint8Array([1])),
});
expect(second.ok).toBe(true);
if (second.ok) expect(second.value.generation).toBe(1);
// The stale compensation targeted only T1's physical token.
expect(issued).toEqual([String(1).padStart(32, "0")]);
const stored = journal.objects.get("object_12345678");
expect(stored?.physicalSchemaVersion).toBe(1);
});
/**
* STO-RR-01. Finalization runs after the commit fence, so the payload is
* durable and the journal row must survive for reconciliation. What the
* caller must not be told is that the write settled: the previous generation
* and the staging directory are still there.
*/
it("reports a failed finalization instead of a plain success", async () => {
const journal = createJournal();
const worker = createWorker({
async finalizePut() {
@@ -533,7 +829,8 @@ describe("OPFS byte-store coordinator", () => {
source: sourceFrom(new Uint8Array([1])),
});
expect(result.ok).toBe(true);
expect(result.ok).toBe(false);
expect(result.ok ? null : result.error.operation).toBe("OBJECT_WRITE");
expect(
journal.transactions.get("transaction_12345678")?.phase,
).toBe("COMMITTED");
+626
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type {
OpfsPreparedObject,
OpfsPhysicalGenerationId,
OpfsStorageScope,
} from "../../src/application/ports/browser-file-storage/opfs-ports.ts";
import type {
@@ -14,13 +15,16 @@ import {
createOpfsWorkerGateway,
type OpfsWorkerLike,
} from "../../src/adapters/storage/opfs/opfs-worker-client.ts";
import { OPFS_WORKER_PROTOCOL_VERSION } from "../../src/adapters/storage/opfs/opfs-worker-protocol.ts";
import type {
OpfsWorkerRequest,
OpfsWorkerResponse,
} from "../../src/adapters/storage/opfs/opfs-worker-protocol.ts";
import {
createOpfsWorkerRuntime,
startBrowserOpfsDedicatedWorker,
type OpfsMutationLeaseManager,
type OpfsWorkerMessageHost,
} from "../../src/adapters/storage/opfs/opfs-worker-runtime.ts";
const scopeA: OpfsStorageScope = Object.freeze({
@@ -170,18 +174,23 @@ function notFound(): DOMException {
return new DOMException("Entry was not found.", "NotFoundError");
}
const PHYSICAL_GENERATION_A = "a".repeat(32) as OpfsPhysicalGenerationId;
function beginRequest(
requestId: string,
transactionId: string,
scope: OpfsStorageScope,
physicalGenerationId: OpfsPhysicalGenerationId = PHYSICAL_GENERATION_A,
): OpfsWorkerRequest {
return {
requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "BEGIN_PUT",
transactionId,
scope,
objectId: "object_12345678",
generation: 1,
physicalGenerationId,
declaredByteLength: 1,
mediaType: "application/octet-stream",
createdAtEpochMs: 100,
@@ -263,6 +272,7 @@ describe("OPFS dedicated worker runtime", () => {
await Promise.resolve();
const aborted = await runtime.handleRequest({
requestId: "request_abort_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "ABORT_PUT",
scope: scopeA,
transactionId: "transaction_12345678",
@@ -314,6 +324,7 @@ describe("OPFS dedicated worker runtime", () => {
const appending = runtime.handleRequest({
requestId: "request_append_5678",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "APPEND_CHUNK",
scope: scopeA,
transactionId: "transaction_56785678",
@@ -323,6 +334,7 @@ describe("OPFS dedicated worker runtime", () => {
await Promise.resolve();
const aborting = runtime.handleRequest({
requestId: "request_abort_5678",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "ABORT_PUT",
scope: scopeA,
transactionId: "transaction_56785678",
@@ -369,6 +381,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: `request_append_iso_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "APPEND_CHUNK",
scope: targetScope,
transactionId,
@@ -381,6 +394,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_abort_iso_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "ABORT_PUT",
scope: scopeA,
transactionId,
@@ -388,6 +402,7 @@ describe("OPFS dedicated worker runtime", () => {
).toMatchObject({ ok: true });
const finished = await runtime.handleRequest({
requestId: "request_finish_iso_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINISH_PUT",
scope: scopeB,
transactionId,
@@ -397,11 +412,14 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_verify_iso_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "VERIFY_OBJECT",
preparedObject,
}),
).toEqual({
requestId: "request_verify_iso_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: true,
});
@@ -430,6 +448,7 @@ describe("OPFS dedicated worker runtime", () => {
);
await runtime.handleRequest({
requestId: `request_gc_append_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "APPEND_CHUNK",
scope: targetScope,
transactionId,
@@ -439,6 +458,7 @@ describe("OPFS dedicated worker runtime", () => {
const object = preparedValue(
await runtime.handleRequest({
requestId: `request_gc_finish_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINISH_PUT",
scope: targetScope,
transactionId,
@@ -447,6 +467,7 @@ describe("OPFS dedicated worker runtime", () => {
preparedByScope.set(targetScope.authorityToken, object);
await runtime.handleRequest({
requestId: `request_gc_finalize_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINALIZE_PUT",
transactionId,
preparedObject: object,
@@ -459,6 +480,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_gc_list_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "LIST_ORPHAN_CANDIDATES",
scope: scopeA,
olderThanEpochMs: cutoff,
@@ -468,6 +490,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_gc_delete_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "DELETE_ORPHAN_CHUNK",
scope: scopeA,
digestHex,
@@ -478,22 +501,28 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_gc_verify_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "VERIFY_OBJECT",
preparedObject: preparedByScope.get(scopeA.authorityToken)!,
}),
).toEqual({
requestId: "request_gc_verify_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: false,
});
expect(
await runtime.handleRequest({
requestId: "request_gc_verify_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "VERIFY_OBJECT",
preparedObject: preparedByScope.get(scopeB.authorityToken)!,
}),
).toEqual({
requestId: "request_gc_verify_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: true,
});
@@ -511,10 +540,13 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_caps_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "CAPABILITIES",
}),
).toEqual({
requestId: "request_caps_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: {
available: false,
@@ -564,6 +596,8 @@ describe("OPFS worker client lifecycle", () => {
listener?.({
data: {
requestId: "request_collision_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "CAPABILITIES",
ok: true,
value: {
available: true,
@@ -628,11 +662,15 @@ describe("OPFS worker client lifecycle", () => {
message.kind === "VERIFY_OBJECT"
? {
requestId: message.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: message.kind,
ok: true,
value: true,
}
: {
requestId: message.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: message.kind,
ok: true,
value: new Uint8Array([4, 2]).buffer,
};
@@ -728,3 +766,591 @@ describe("OPFS worker client lifecycle", () => {
});
});
});
/**
* STO-RR-01. A Web Lock is not reentrant. Any path that re-acquires the origin
* mutation lease while already holding it stops making progress forever, and a
* lock the runtime waits on cannot be observed by a fake that hands out an
* unlimited number of leases.
*/
function strictNonReentrantLeases(
counters: { acquires: number; releases: number },
): OpfsMutationLeaseManager {
let held = false;
return {
async acquire() {
if (held) {
// A second holder waits for the first to release. Nothing here ever
// does, which is exactly what a deadlock looks like.
return await new Promise<never>(() => {});
}
held = true;
counters.acquires += 1;
let released = false;
return {
release() {
if (released) return;
released = true;
held = false;
counters.releases += 1;
},
};
},
};
}
function withTimeout<Value>(
operation: Promise<Value>,
label: string,
ms = 200,
): Promise<Value> {
return Promise.race([
operation,
new Promise<never>((_resolve, reject) => {
setTimeout(() => reject(new Error(`${label} did not settle`)), ms);
}),
]);
}
describe("STO-RR-01 OPFS finalization under a non-reentrant lock", () => {
async function completedPut(
runtime: ReturnType<typeof createOpfsWorkerRuntime>,
transactionId: string,
): Promise<OpfsPreparedObject> {
expect(
await runtime.handleRequest(
beginRequest(`request_begin_${transactionId}`, transactionId, scopeA),
),
).toMatchObject({ ok: true });
expect(
await runtime.handleRequest({
requestId: `request_append_${transactionId}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "APPEND_CHUNK",
scope: scopeA,
transactionId,
sequence: 0,
bytes: new Uint8Array([9]).buffer,
}),
).toMatchObject({ ok: true });
const finished = await runtime.handleRequest({
requestId: `request_finish_${transactionId}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINISH_PUT",
scope: scopeA,
transactionId,
});
expect(finished).toMatchObject({ ok: true });
return preparedValue(finished);
}
it("finalizes a normal PUT with exactly one lock acquisition", async () => {
const root = new MemoryDirectory();
const counters = { acquires: 0, releases: 0 };
const runtime = createOpfsWorkerRuntime({
root: root as unknown as FileSystemDirectoryHandle,
crypto: globalThis.crypto,
policy: runtimePolicy,
leaseManager: strictNonReentrantLeases(counters),
dedicatedWorker: true,
supportsSynchronousAccessHandles: false,
});
const transactionId = "transaction_final_0001";
const prepared = await completedPut(runtime, transactionId);
const before = counters.acquires;
const finalized = await withTimeout(
runtime.handleRequest({
requestId: "request_finalize_0001",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINALIZE_PUT",
transactionId,
preparedObject: prepared,
}),
"FINALIZE_PUT",
);
expect(finalized).toMatchObject({ ok: true });
expect(counters.acquires - before).toBe(1);
expect(counters.acquires).toBe(counters.releases);
expect(
root.has([
"authorities",
scopeA.authorityToken,
scopeA.namespaceToken,
scopeA.partitionToken,
"staging",
transactionId,
]),
).toBe(false);
});
it("leaves the lock free for the next mutation after a finalized PUT", async () => {
const root = new MemoryDirectory();
const counters = { acquires: 0, releases: 0 };
const runtime = createOpfsWorkerRuntime({
root: root as unknown as FileSystemDirectoryHandle,
crypto: globalThis.crypto,
policy: runtimePolicy,
leaseManager: strictNonReentrantLeases(counters),
dedicatedWorker: true,
supportsSynchronousAccessHandles: false,
});
const first = "transaction_final_0002";
const prepared = await completedPut(runtime, first);
await withTimeout(
runtime.handleRequest({
requestId: "request_finalize_0002",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINALIZE_PUT",
transactionId: first,
preparedObject: prepared,
}),
"first FINALIZE_PUT",
);
const second = "transaction_final_0003";
await expect(
withTimeout(completedPut(runtime, second), "second PUT"),
).resolves.toMatchObject({ descriptor: { objectId: "object_12345678" } });
expect(counters.acquires).toBe(counters.releases);
});
});
/**
* STO-RR-02. A failure raised while serving a validated request must answer
* that request. Defaulting the response kind to `CAPABILITIES` made the client's
* own expected-kind check reject it as a protocol breach, so a quota or
* integrity failure reached the caller as `UNSUPPORTED`.
*/
describe("STO-RR-02 worker failure responses echo the request kind", () => {
const failingRoot = {
async getDirectoryHandle(): Promise<FileSystemDirectoryHandle> {
throw new DOMException("Out of room", "QuotaExceededError");
},
async getFileHandle(): Promise<FileSystemFileHandle> {
throw new DOMException("Out of room", "QuotaExceededError");
},
async removeEntry(): Promise<void> {
throw new DOMException("Out of room", "QuotaExceededError");
},
async *entries(): AsyncIterableIterator<never> {},
} as unknown as FileSystemDirectoryHandle;
it("keeps the validated kind on every failure path", async () => {
const counters = { acquires: 0, releases: 0 };
const runtime = createOpfsWorkerRuntime({
root: failingRoot,
crypto: globalThis.crypto,
policy: runtimePolicy,
leaseManager: strictNonReentrantLeases(counters),
dedicatedWorker: true,
supportsSynchronousAccessHandles: false,
});
const requests: readonly OpfsWorkerRequest[] = [
beginRequest("request_kind_begin", "transaction_kind_0001", scopeA),
{
requestId: "request_kind_remove",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "REMOVE_OBJECT",
scope: scopeA,
objectId: "object_12345678",
generation: 1,
},
{
requestId: "request_kind_cleanup",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "CLEANUP_TRANSACTION",
scope: scopeA,
transactionId: "transaction_kind_0001",
},
{
requestId: "request_kind_orphans",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "LIST_ORPHAN_CANDIDATES",
scope: scopeA,
olderThanEpochMs: 1,
maxEntries: 1,
},
];
for (const request of requests) {
const response = await runtime.handleRequest(request);
expect(response).toMatchObject({
ok: false,
kind: request.kind,
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
});
}
});
it("still reports a protocol-level failure for an unreadable envelope", async () => {
const counters = { acquires: 0, releases: 0 };
const runtime = createOpfsWorkerRuntime({
root: failingRoot,
crypto: globalThis.crypto,
policy: runtimePolicy,
leaseManager: strictNonReentrantLeases(counters),
dedicatedWorker: true,
supportsSynchronousAccessHandles: false,
});
expect(
await runtime.handleRequest({
requestId: "request_kind_broken",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "NOT_A_KIND",
}),
).toMatchObject({ ok: false, kind: "CAPABILITIES" });
});
});
/**
* STO-RR-03. The client decoder is the trust boundary for anything a worker
* says. A `code` that is merely a string lets an arbitrary value escape the
* closed `BrowserDataFailure` taxonomy into application code.
*/
describe("STO-RR-03 worker responses are decoded against closed sets", () => {
function respondingWorker(
reply: (request: OpfsWorkerRequest) => unknown,
): OpfsWorkerLike {
const listeners = new Set<(event: MessageEvent<unknown>) => void>();
return {
postMessage(message: unknown) {
const response = reply(message as OpfsWorkerRequest);
queueMicrotask(() => {
for (const listener of listeners) {
listener({ data: response } as MessageEvent<unknown>);
}
});
},
addEventListener(_type: "message", listener: (event: MessageEvent<unknown>) => void) {
listeners.add(listener);
},
removeEventListener(_type: "message", listener: (event: MessageEvent<unknown>) => void) {
listeners.delete(listener);
},
} as unknown as OpfsWorkerLike;
}
const hostileReplies: readonly (readonly [string, (request: OpfsWorkerRequest) => unknown])[] = [
[
"unknown failure code",
(request) => ({
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: false,
failure: { code: "EVIL", retryable: false },
}),
],
[
"unknown request kind",
(request) => ({
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "NOT_A_KIND",
ok: false,
failure: { code: "UNAVAILABLE", retryable: false },
}),
],
[
"non-boolean retryable",
(request) => ({
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: false,
failure: { code: "UNAVAILABLE", retryable: "yes" },
}),
],
[
"inherited failure fields",
(request) => ({
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: false,
failure: Object.create({ code: "UNAVAILABLE", retryable: false }) as object,
}),
],
[
"throwing getter",
(request) => {
const response: Record<string, unknown> = {
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
ok: false,
failure: { code: "UNAVAILABLE", retryable: false },
};
Object.defineProperty(response, "kind", {
enumerable: true,
get: () => {
throw new TypeError("hostile getter");
},
});
return response;
},
],
[
"extra own field",
(request) => ({
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: false,
failure: { code: "UNAVAILABLE", retryable: false, injected: 1 },
}),
],
];
for (const [label, reply] of hostileReplies) {
it(`closes a ${label} as UNSUPPORTED without rejecting`, async () => {
const gateway = createOpfsWorkerGateway({
worker: respondingWorker(reply),
policy: runtimePolicy,
createRequestId: () => `request_hostile_${label.replace(/\W/gu, "")}`,
});
const result = await withTimeout(gateway.capabilities(), label);
expect(result.ok).toBe(false);
expect(result.ok ? null : result.error.code).toBe("UNSUPPORTED");
});
}
/**
* NS-06. Correlation was read separately and the pending row and its timer
* were removed before the reply was decoded. A trap that threw inside the
* decoder therefore left the public promise pending forever, and a throwing
* `requestId` getter produced a timeout instead of a prompt protocol failure.
*/
const uncorrelatableReplies: readonly (readonly [
string,
(request: OpfsWorkerRequest) => unknown,
])[] = [
[
"throwing requestId getter",
(request) =>
Object.defineProperty(
{
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: true,
value: {},
},
"requestId",
{
enumerable: true,
get: () => {
throw new TypeError("hostile requestId getter");
},
},
),
],
[
"throwing ownKeys trap",
(request) =>
new Proxy(
{
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: true,
value: {},
},
{
ownKeys() {
throw new TypeError("hostile ownKeys trap");
},
},
),
],
[
"descriptor trap that throws after correlation",
(request) => {
let reads = 0;
return new Proxy(
{
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: true,
value: {},
},
{
getOwnPropertyDescriptor(target, key) {
reads += 1;
if (reads > 1) {
throw new TypeError("stateful descriptor trap");
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
},
);
},
],
[
"symbol-keyed field",
(request) => ({
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: true,
value: {},
[Symbol.for("injected")]: true,
}),
],
[
"non-enumerable own field",
(request) =>
Object.defineProperty(
{
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: true,
value: {},
},
"injected",
{ enumerable: false, value: true },
),
],
];
for (const [label, reply] of uncorrelatableReplies) {
it(`closes a ${label} promptly as UNSUPPORTED`, async () => {
const gateway = createOpfsWorkerGateway({
worker: respondingWorker(reply),
policy: { ...runtimePolicy, rpcTimeoutMs: 60_000 },
createRequestId: () => `request_uncorr_${label.replace(/\W/gu, "")}`,
});
// The RPC timeout is far beyond the test budget, so a pass here means the
// reply itself closed the request rather than the timer.
const result = await withTimeout(gateway.capabilities(), label);
expect(result.ok).toBe(false);
expect(result.ok ? null : result.error.code).toBe("UNSUPPORTED");
});
}
it("keeps serving requests after a malformed reply", async () => {
let replies = 0;
const gateway = createOpfsWorkerGateway({
worker: respondingWorker((request) => {
replies += 1;
if (replies === 1) return { requestId: request.requestId };
return {
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: false,
failure: { code: "QUOTA_EXCEEDED", retryable: true },
};
}),
policy: { ...runtimePolicy, rpcTimeoutMs: 60_000 },
createRequestId: () => `request_sequence_${replies}`,
});
const first = await withTimeout(gateway.capabilities(), "first");
expect(first.ok ? null : first.error.code).toBe("UNSUPPORTED");
const second = await withTimeout(gateway.capabilities(), "second");
expect(second.ok ? null : second.error.code).toBe("QUOTA_EXCEEDED");
});
it("still admits a well-formed closed failure", async () => {
const gateway = createOpfsWorkerGateway({
worker: respondingWorker((request) => ({
requestId: request.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: request.kind,
ok: false,
failure: { code: "QUOTA_EXCEEDED", retryable: true },
})),
policy: runtimePolicy,
createRequestId: () => "request_wellformed_1234",
});
const result = await withTimeout(gateway.capabilities(), "well-formed");
expect(result.ok).toBe(false);
expect(result.ok ? null : result.error.code).toBe("QUOTA_EXCEEDED");
});
});
/**
* NS-05. When the runtime never came up, the message host answered with a
* default `CAPABILITIES` kind. The gateway saw that as an expected-kind
* mismatch and replaced the real cause — `BLOCKED`, `QUOTA_EXCEEDED` — with a
* generic `UNSUPPORTED` protocol breach, so the outage was misreported.
*/
describe("NS-05 a bootstrap failure answers the request it belongs to", () => {
function hostFor(): Readonly<{
host: OpfsWorkerMessageHost;
posted: OpfsWorkerResponse[];
deliver(message: unknown): void;
}> {
const listeners: ((event: MessageEvent<unknown>) => void)[] = [];
const posted: OpfsWorkerResponse[] = [];
return {
host: {
addEventListener(_type, listener) {
listeners.push(listener);
},
postMessage(message) {
posted.push(message);
},
},
posted,
deliver(message: unknown) {
for (const listener of listeners) {
listener({ data: message } as MessageEvent<unknown>);
}
},
};
}
const requestKinds = [
"CAPABILITIES",
"BEGIN_PUT",
"APPEND_CHUNK",
"FINISH_PUT",
"ABORT_PUT",
"VERIFY_OBJECT",
"READ_CHUNK",
"REMOVE_OBJECT",
"CLEANUP_TRANSACTION",
"FINALIZE_PUT",
"LIST_ORPHAN_CANDIDATES",
"DELETE_ORPHAN_CHUNK",
] as const;
for (const kind of requestKinds) {
it(`preserves the ${kind} correlation when bootstrap fails`, async () => {
const { host, posted, deliver } = hostFor();
const start = startBrowserOpfsDedicatedWorker(host, {
storageManager: {
getDirectory: () =>
Promise.reject(
new DOMException("blocked", "SecurityError"),
),
} as unknown as StorageManager,
crypto: globalThis.crypto,
});
// The bootstrap rejection must not escape the worker entry point either.
await expect(start).rejects.toBeInstanceOf(Error);
deliver({
requestId: "request_bootstrap_failure",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind,
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(posted).toHaveLength(1);
expect(posted[0]).toMatchObject({
requestId: "request_bootstrap_failure",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind,
ok: false,
});
});
}
});
+868 -21
View File
@@ -1,5 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { PRESIGNED_TRANSFER_PROTOCOL } from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
import type {
PresignedDownloadCapability,
PresignedDownloadByteSource,
@@ -43,6 +45,8 @@ function downloadCapabilityPayload(
) {
const digest = sha256Hex(bytes);
return {
// BT-PRE-02. Every capability envelope carries the top-level protocol.
protocol: PRESIGNED_TRANSFER_PROTOCOL,
capabilityReceipt: "capability-download-1",
method: "GET",
binding: {
@@ -81,6 +85,7 @@ function uploadCapabilityPayload(input: Readonly<{
checksum: string;
}>, overrides: Readonly<Record<string, unknown>> = {}) {
return {
protocol: PRESIGNED_TRANSFER_PROTOCOL,
capabilityReceipt: "capability-upload-1",
method: "PUT",
binding: {
@@ -602,13 +607,15 @@ describe("presigned transfer", () => {
});
expect(issued.ok).toBe(true);
if (!issued.ok) return;
expect(
await harness.executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal: new AbortController().signal,
}),
).toMatchObject({
await expect(
firstStreamResult(
await harness.executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal: new AbortController().signal,
}),
),
).resolves.toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
@@ -621,13 +628,15 @@ describe("presigned transfer", () => {
});
expect(issued.ok).toBe(true);
if (!issued.ok) return;
expect(
await harness.executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal: new AbortController().signal,
}),
).toMatchObject({
await expect(
firstStreamResult(
await harness.executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal: new AbortController().signal,
}),
),
).resolves.toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
@@ -640,18 +649,532 @@ describe("presigned transfer", () => {
});
expect(issued.ok).toBe(true);
if (!issued.ok) return;
expect(
await harness.executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal: new AbortController().signal,
}),
).toMatchObject({
await expect(
firstStreamResult(
await harness.executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal: new AbortController().signal,
}),
),
).resolves.toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
});
it.each([
{ label: "missing", protocol: undefined },
{ label: "V0", protocol: "PRESIGNED_TRANSFER_V0" },
{ label: "V2", protocol: "PRESIGNED_TRANSFER_V2" },
])(
"requires PRESIGNED_TRANSFER_V1 in request and response ($label)",
async ({ protocol }) => {
const bytes = new Uint8Array([1, 2, 3]);
const payload = downloadCapabilityPayload(bytes);
const body =
protocol === undefined
? (({ protocol: _dropped, ...rest }) => rest)(
payload as Record<string, unknown>,
)
: { ...payload, protocol };
const requests: unknown[] = [];
const fetcher = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
if (String(input) === CONTROL_ENDPOINT) {
requests.push(JSON.parse(String(init?.body)));
return jsonResponse(body as never);
}
return downloadResponse(bytes.slice().buffer, payload);
}) as unknown as typeof fetch;
const { provider, vault } = createHarness({ fetcher });
const issued = await provider.issueDownload({
resourceId: "resource-1",
signal: new AbortController().signal,
});
// BT-PRE-02. The request always declares V1, and a response that does not
// is closed before the vault ever registers it.
expect(requests[0]).toMatchObject({
protocol: PRESIGNED_TRANSFER_PROTOCOL,
});
expect(issued).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
// The rejected envelope never reached vault registration.
expect(vault.resolve).toBeTypeOf("function");
},
);
it.each([
{ label: "encoded slash", path: "/files/a%2Fb" },
{ label: "encoded backslash", path: "/files/a%5Cb" },
{ label: "double-encoded dot segment", path: "/files/%252e%252e" },
{ label: "lowercase percent-hex", path: "/files/a%c3%a9" },
{ label: "encoded NUL", path: "/files/a%00b" },
])("rejects a provider path that can decode again ($label)", async ({ path }) => {
const bytes = new Uint8Array([1, 2, 3]);
const payload = downloadCapabilityPayload(bytes, {
path,
href: `${DATA_ORIGIN}${path}`,
});
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
String(input) === CONTROL_ENDPOINT
? jsonResponse(payload)
: downloadResponse(bytes.slice().buffer, payload),
) as unknown as typeof fetch;
const { provider } = createHarness({ fetcher });
await expect(
provider.issueDownload({
resourceId: "resource-1",
signal: new AbortController().signal,
}),
).resolves.toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
});
it("accepts a valid opaque UTF-8 path segment", async () => {
const bytes = new Uint8Array([1, 2, 3]);
// BT-PRE-05. Canonical uppercase percent-hex for a real UTF-8 segment.
const path = `/files/${encodeURIComponent("caf\u00e9")}`;
const payload = downloadCapabilityPayload(bytes, {
path,
href: `${DATA_ORIGIN}${path}?sig=do-not-log-this`,
});
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
String(input) === CONTROL_ENDPOINT
? jsonResponse(payload)
: downloadResponse(bytes.slice().buffer, payload),
) as unknown as typeof fetch;
const { provider } = createHarness({ fetcher });
await expect(
provider.issueDownload({
resourceId: "resource-1",
signal: new AbortController().signal,
}),
).resolves.toMatchObject({ ok: true });
});
it("bounds a fetch that ignores its abort signal and cancels the late body", async () => {
const bytes = new Uint8Array([1, 2, 3]);
const payload = downloadCapabilityPayload(bytes);
const cancel = vi.fn(async () => {});
let releaseControl: ((response: Response) => void) | undefined;
const timers: Array<() => void> = [];
const fetcher = vi.fn(
async () =>
await new Promise<Response>((resolve) => {
releaseControl = resolve;
}),
) as unknown as typeof fetch;
const { provider } = createHarness({
fetcher,
scheduler: {
setTimeout: (callback: () => void) => {
timers.push(callback);
return timers.length;
},
clearTimeout: () => {},
},
});
const issuing = provider.issueDownload({
resourceId: "resource-1",
signal: new AbortController().signal,
});
await Promise.resolve();
// BT-PRE-03. The timeout fires while the fetch is still pending and never
// settles on its own.
timers.forEach((fire) => fire());
await expect(issuing).resolves.toMatchObject({ ok: false });
releaseControl?.({ body: { cancel } } as unknown as Response);
await Promise.resolve();
await Promise.resolve();
expect(cancel).toHaveBeenCalledOnce();
void payload;
});
/**
* TR-RR-05. A scheduler that cannot install the deadline leaves the operation
* unbounded. Releasing the caller listener and continuing anyway meant a
* later abort was invisible, so an install failure is itself terminal: the
* request fails closed with a typed Result and its resources are released.
*/
it("fails closed when the scheduler cannot install the deadline", async () => {
const bytes = new Uint8Array([1, 2, 3]);
const payload = downloadCapabilityPayload(bytes);
const caller = new AbortController();
const remove = vi.spyOn(caller.signal, "removeEventListener");
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
String(input) === CONTROL_ENDPOINT
? jsonResponse(payload)
: downloadResponse(bytes.slice().buffer, payload),
) as unknown as typeof fetch;
const { provider } = createHarness({
fetcher,
scheduler: {
setTimeout: () => {
throw new TypeError("scheduler exploded");
},
clearTimeout: () => {},
},
});
// The public result stays a typed Result, not a rejection.
await expect(
provider.issueDownload({
resourceId: "resource-1",
signal: caller.signal,
}),
).resolves.toMatchObject({ ok: false });
expect(remove).toHaveBeenCalled();
});
it.each([
{ label: "href/origin mismatch", patch: { origin: "https://evil.example" } },
{ label: "href/path mismatch", patch: { path: "/files/other" } },
{ label: "credentials in href", patch: { href: `https://u:p@objects.example${DOWNLOAD_PATH}` } },
{ label: "maxBytes below byteLength", patch: { maxBytes: 0 } },
{ label: "malformed digest", patch: { expectedSha256: "not-a-digest" } },
{ label: "non-positive expiry", patch: { expiresAtEpochMs: 0 } },
// TR-RR-03. The registration is a versioned exact union: plaintext, an
// ambient credential header, an unknown protocol version and any extra
// own field are all refused at the issuer seam.
{ label: "unknown protocol version", patch: { protocol: "PRESIGNED_TRANSFER_V0" } },
{ label: "missing protocol version", patch: { protocol: undefined } },
{
label: "plaintext target",
patch: {
href: `http://objects.example${DOWNLOAD_PATH}`,
origin: "http://objects.example",
},
},
{
label: "ambient credential header",
patch: {
requestHeaders: [{ name: "authorization", value: "Bearer leak" }],
},
},
{
label: "cookie response header",
patch: {
requiredResponseHeaders: [{ name: "set-cookie", value: "a=b" }],
},
},
{ label: "status outside 2xx", patch: { expectedStatus: 302 } },
{ label: "extra own field", patch: { injected: true } },
])(
"rejects a malformed registration at the vault issuer seam ($label)",
({ patch }) => {
// BT-PRE-04. The vault owns these invariants itself, so a second issuer
// cannot register a weaker capability of the same type.
const vault = createPresignedCapabilityVault({
now: () => NOW,
maxActiveCapabilities: 4,
});
const base = {
protocol: PRESIGNED_TRANSFER_PROTOCOL,
capabilityReceipt: "capability-direct-1",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
href: `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
origin: DATA_ORIGIN,
path: DOWNLOAD_PATH,
allowedQueryParameters: [],
requestHeaders: [],
requiredResponseHeaders: [],
digestRequestHeader: null,
digestResponseHeader: null,
receiptResponseHeader: null,
expectedStatus: 200,
expectedResponseByteLength: 3,
mediaType: "application/octet-stream",
byteLength: 3,
maxBytes: 3,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
};
expect(
vault.register({ ...base, ...patch } as never),
).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
// The same registration without the defect is accepted.
expect(vault.register(base as never)).toMatchObject({ ok: true });
vault.dispose();
},
);
/**
* TR-01. The vault validated the issuer's own object and then read it again
* to copy it. Between those reads a stateful issuer could show an allowed
* header set to the forbidden-header check and hand `Authorization` to the
* stored binding, so the executor sent a credential no rule had approved.
*/
describe("TR-01 the stored capability is the one that was validated", () => {
const baseRegistration = () => ({
protocol: PRESIGNED_TRANSFER_PROTOCOL,
capabilityReceipt: "capability-snapshot-1",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
href: `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
origin: DATA_ORIGIN,
path: DOWNLOAD_PATH,
allowedQueryParameters: [],
requestHeaders: [{ name: "x-safe", value: "1" }],
requiredResponseHeaders: [],
digestRequestHeader: null,
digestResponseHeader: null,
receiptResponseHeader: null,
expectedStatus: 200,
expectedResponseByteLength: 3,
mediaType: "application/octet-stream",
byteLength: 3,
maxBytes: 3,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
});
const freshVault = () =>
createPresignedCapabilityVault({
now: () => NOW,
maxActiveCapabilities: 4,
});
it("refuses a header row that answers differently on a second read", () => {
const vault = freshVault();
let nameReads = 0;
const header = new Proxy(
{ name: "x-safe", value: "1" },
{
getOwnPropertyDescriptor(target, key) {
if (key === "name") {
nameReads += 1;
return {
configurable: true,
enumerable: true,
value: nameReads > 1 ? "authorization" : "x-safe",
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
},
);
const registered = vault.register({
...baseRegistration(),
requestHeaders: [header],
} as never);
if (registered.ok) {
// A single read means the value that was checked is the value stored.
const resolved = vault.resolve(registered.value);
expect(resolved.ok).toBe(true);
if (resolved.ok) {
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
"x-safe",
]);
}
}
vault.dispose();
});
const hostileRegistrations: readonly (readonly [string, () => unknown])[] = [
[
"an accessor field",
() =>
Object.defineProperty(baseRegistration(), "href", {
enumerable: true,
get: () => `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
}),
],
[
"an inherited field",
() => Object.assign(Object.create({ injected: true }), baseRegistration()),
],
[
"a symbol field",
() => ({ ...baseRegistration(), [Symbol.for("injected")]: true }),
],
[
"a non-enumerable own field",
() =>
Object.defineProperty(baseRegistration(), "injected", {
enumerable: false,
value: true,
}),
],
[
"a throwing ownKeys trap",
() =>
new Proxy(baseRegistration(), {
ownKeys() {
throw new TypeError("hostile ownKeys trap");
},
}),
],
[
"a null header array",
() => ({ ...baseRegistration(), requestHeaders: null }),
],
[
"a non-iterable header array",
() => ({ ...baseRegistration(), requestHeaders: { length: 1 } }),
],
[
"a header row with an extra field",
() => ({
...baseRegistration(),
requestHeaders: [{ name: "x-safe", value: "1", injected: true }],
}),
],
[
"an accessor header name",
() => ({
...baseRegistration(),
requestHeaders: [
Object.defineProperty({ value: "1" }, "name", {
enumerable: true,
get: () => "x-safe",
}),
],
}),
],
[
"a binding with an extra field",
() => ({
...baseRegistration(),
binding: { kind: "DOWNLOAD", resourceId: "r", injected: true },
}),
],
[
"a null binding",
() => ({ ...baseRegistration(), binding: null }),
],
];
for (const [label, build] of hostileRegistrations) {
it(`rejects ${label} as POLICY_REJECTED`, () => {
const vault = freshVault();
expect(vault.register(build() as never)).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
vault.dispose();
});
}
it("does not observe a mutation of the issuer's object after registration", () => {
const vault = freshVault();
const registration = baseRegistration();
const registered = vault.register(registration as never);
expect(registered.ok).toBe(true);
if (!registered.ok) return;
registration.requestHeaders[0]!.name = "authorization";
registration.expiresAtEpochMs = NOW + 999_999;
const resolved = vault.resolve(registered.value);
expect(resolved.ok).toBe(true);
if (!resolved.ok) return;
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
"x-safe",
]);
expect(resolved.value.expiresAtEpochMs).toBe(NOW + 60_000);
vault.dispose();
});
});
it("does not fetch a presigned download until stream consumption", async () => {
const bytes = new Uint8Array([1, 2, 3]);
const responsePayload = downloadCapabilityPayload(bytes);
let downloadFetches = 0;
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
if (String(input) === CONTROL_ENDPOINT) {
return jsonResponse(responsePayload);
}
downloadFetches += 1;
return downloadResponse(bytes.slice().buffer, responsePayload);
}) as unknown as typeof fetch;
const { provider, executor } = createHarness({ fetcher });
const signal = new AbortController().signal;
const issued = await provider.issueDownload({
resourceId: "resource-1",
signal,
});
expect(issued.ok).toBe(true);
if (!issued.ok) return;
const opened = await executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal,
});
expect(opened.ok).toBe(true);
if (!opened.ok) return;
// BT-PRE-01. open() performs no network I/O.
expect(downloadFetches).toBe(0);
for await (const chunk of opened.value.stream(signal)) {
expect(chunk.ok).toBe(true);
}
expect(downloadFetches).toBe(1);
opened.value.close();
});
it("closes an unused download source without network I/O", async () => {
const bytes = new Uint8Array([1, 2, 3]);
const responsePayload = downloadCapabilityPayload(bytes);
let downloadFetches = 0;
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
if (String(input) === CONTROL_ENDPOINT) {
return jsonResponse(responsePayload);
}
downloadFetches += 1;
return downloadResponse(bytes.slice().buffer, responsePayload);
}) as unknown as typeof fetch;
const { provider, executor } = createHarness({ fetcher });
const signal = new AbortController().signal;
const issued = await provider.issueDownload({
resourceId: "resource-1",
signal,
});
expect(issued.ok).toBe(true);
if (!issued.ok) return;
const opened = await executor.downloadSources.open({
resourceId: "resource-1",
capability: issued.value,
signal,
});
expect(opened.ok).toBe(true);
if (!opened.ok) return;
opened.value.close();
// close() is idempotent and never starts the transfer.
opened.value.close();
expect(downloadFetches).toBe(0);
// A stream after close is one terminal conflict, still without fetching.
const results = [];
for await (const chunk of opened.value.stream(signal)) {
results.push(chunk);
}
expect(results).toMatchObject([
{ ok: false, error: { code: "CONFLICT" } },
]);
expect(downloadFetches).toBe(0);
});
it.each([
{
name: "truncation",
@@ -1632,4 +2155,328 @@ describe("presigned transfer", () => {
});
expect(written).toEqual([...bytes]);
});
/**
* TR-RR-04. A presigned byte source owns a fetch reader and a capability
* lease, and its port requires `close()`. The delivery consumer never called
* it, so every outcome — success, validation failure, writer failure and
* abort — leaked both.
*/
it.each([
{ label: "success", mode: "SUCCESS" as const },
{ label: "writer failure", mode: "WRITER_FAILURE" as const },
{ label: "abort", mode: "ABORT" as const },
])("closes the presigned source exactly once on $label", async ({ mode }) => {
const bytes = new Uint8Array([1, 2, 3]);
let closes = 0;
const controller = new AbortController();
const source = {
byteLength: bytes.byteLength,
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
capability: undefined as never,
close() {
closes += 1;
},
async *stream() {
if (mode === "ABORT") controller.abort();
yield { ok: true as const, value: bytes };
},
};
const capability = Object.freeze({
capabilityReceipt: "capability-close-1",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
mediaType: "application/octet-stream",
byteLength: bytes.byteLength,
maxBytes: bytes.byteLength,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
});
source.capability = capability as never;
const closePolicy = browserFilePolicyReference(
"download",
"presigned-close",
);
const policies = new BrowserFilePolicyRegistry({
profiles: [
{
reference: closePolicy,
download: {
strategy: "PROMPT_AND_STREAM",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 64,
maxBufferedBytes: 8,
integrity: "REQUIRED",
},
},
],
hardLimits: {
maxInspectionBytes: 64,
maxRetainedFileBytes: 64,
maxPreviewBytes: 64,
maxObjectUrlBytes: 64,
maxTransferBytes: 64,
},
});
const handle: SaveFileHandle = {
async createWritable() {
return new WritableStream<Uint8Array>({
write() {
if (mode === "WRITER_FAILURE") {
throw new TypeError("writer exploded");
}
},
});
},
};
const downloads = createDownloadDeliveryAdapter({
host: { handoff() {} },
policies,
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 64,
browserManagedCapabilities: {
resolve() {
throw new TypeError("not used");
},
},
openAuthorizedSource: async () =>
({ ok: true, value: source }) as never,
showSaveFilePicker: async () => handle,
userActivation: { isActive: true },
now: () => NOW,
});
const deliveryResult = await downloads.deliver({
policy: closePolicy,
source: {
kind: "AUTHORIZED_STREAM_RESOURCE",
resourceId: "resource-1",
capability: capability as never,
},
suggestedFileName: "artifact.bin",
signal: controller.signal,
onProgress() {},
});
void deliveryResult;
expect(closes).toBe(1);
});
/**
* TR-02. A lease that resolved after the abort already ended the delivery
* never reached the holder, so nothing closed it: the fetch reader and the
* capability lease outlived the terminal result.
*/
it("closes a source lease that arrives after the delivery was aborted", async () => {
const bytes = new Uint8Array([1, 2, 3]);
let closes = 0;
const controller = new AbortController();
let releaseOpen:
| ((value: { ok: true; value: unknown }) => void)
| undefined;
const source = {
byteLength: bytes.byteLength,
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
capability: undefined as never,
close() {
closes += 1;
},
async *stream() {
yield { ok: true as const, value: bytes };
},
};
const capability = Object.freeze({
capabilityReceipt: "capability-late-1",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
mediaType: "application/octet-stream",
byteLength: bytes.byteLength,
maxBytes: bytes.byteLength,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
});
source.capability = capability as never;
const latePolicy = browserFilePolicyReference("download", "presigned-late");
const policies = new BrowserFilePolicyRegistry({
profiles: [
{
reference: latePolicy,
download: {
strategy: "PROMPT_AND_STREAM",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 64,
maxBufferedBytes: 8,
integrity: "REQUIRED",
},
},
],
hardLimits: {
maxInspectionBytes: 64,
maxRetainedFileBytes: 64,
maxPreviewBytes: 64,
maxObjectUrlBytes: 64,
maxTransferBytes: 64,
},
});
const downloads = createDownloadDeliveryAdapter({
host: { handoff() {} },
policies,
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 64,
browserManagedCapabilities: {
resolve() {
throw new TypeError("not used");
},
},
// Ignores the signal entirely and resolves only when the test says so.
openAuthorizedSource: () =>
new Promise((resolve) => {
releaseOpen = resolve as never;
}) as never,
showSaveFilePicker: async () => ({
async createWritable() {
return new WritableStream<Uint8Array>({ write() {} });
},
}),
userActivation: { isActive: true },
now: () => NOW,
});
const delivering = downloads.deliver({
policy: latePolicy,
source: {
kind: "AUTHORIZED_STREAM_RESOURCE",
resourceId: "resource-1",
capability: capability as never,
},
suggestedFileName: "artifact.bin",
signal: controller.signal,
onProgress() {},
});
await new Promise((resolve) => setTimeout(resolve, 0));
controller.abort();
const delivered = await delivering;
expect(delivered.ok).toBe(false);
// The lease arrives only now, long after the terminal result.
releaseOpen?.({ ok: true, value: source });
await new Promise((resolve) => setTimeout(resolve, 0));
expect(closes).toBe(1);
});
it("does not leave a late rejection unhandled after an abort", async () => {
const unhandled: unknown[] = [];
const onUnhandled = (reason: unknown) => unhandled.push(reason);
process.on("unhandledRejection", onUnhandled);
try {
const controller = new AbortController();
let rejectOpen: ((reason: unknown) => void) | undefined;
const rejectPolicy = browserFilePolicyReference(
"download",
"presigned-late-reject",
);
const policies = new BrowserFilePolicyRegistry({
profiles: [
{
reference: rejectPolicy,
download: {
strategy: "PROMPT_AND_STREAM",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 64,
maxBufferedBytes: 8,
integrity: "REQUIRED",
},
},
],
hardLimits: {
maxInspectionBytes: 64,
maxRetainedFileBytes: 64,
maxPreviewBytes: 64,
maxObjectUrlBytes: 64,
maxTransferBytes: 64,
},
});
const downloads = createDownloadDeliveryAdapter({
host: { handoff() {} },
policies,
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 64,
browserManagedCapabilities: {
resolve() {
throw new TypeError("not used");
},
},
openAuthorizedSource: () =>
new Promise((_resolve, reject) => {
rejectOpen = reject;
}) as never,
showSaveFilePicker: async () => ({
async createWritable() {
return new WritableStream<Uint8Array>({ write() {} });
},
}),
userActivation: { isActive: true },
now: () => NOW,
});
const delivering = downloads.deliver({
policy: rejectPolicy,
source: {
kind: "AUTHORIZED_STREAM_RESOURCE",
resourceId: "resource-1",
capability: Object.freeze({
capabilityReceipt: "capability-late-2",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
mediaType: "application/octet-stream",
byteLength: 3,
maxBytes: 3,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
}) as never,
},
suggestedFileName: "artifact.bin",
signal: controller.signal,
onProgress() {},
});
await new Promise((resolve) => setTimeout(resolve, 0));
controller.abort();
await delivering;
rejectOpen?.(new Error("late open failure"));
await new Promise((resolve) => setTimeout(resolve, 10));
expect(unhandled).toEqual([]);
} finally {
process.off("unhandledRejection", onUnhandled);
}
});
});
/**
* BT-PRE-01. The download lease is lazy, so a response-shape rejection is
* observed on first consumption rather than at `open()`.
*/
async function firstStreamResult(
opened: Awaited<
ReturnType<
ReturnType<typeof createHarness>["executor"]["downloadSources"]["open"]
>
>,
): Promise<unknown> {
if (!opened.ok) return opened;
try {
for await (const chunk of opened.value.stream(
new AbortController().signal,
)) {
if (!chunk.ok) return chunk;
}
return { ok: true };
} finally {
opened.value.close();
}
}
+494
View File
@@ -6,6 +6,7 @@ import type {
} from "../../src/application/ports/browser-file-storage/cache-storage-ports.ts";
import {
createDefaultPublicCachePolicy,
resolvePublicCachePolicy,
type PublicCacheRuntimePolicy,
} from "../../src/adapters/cache-storage/public-cache-policy.ts";
import {
@@ -140,6 +141,159 @@ async function manifestFor(
}
describe("public response Cache Storage adapter", () => {
it("rejects a policy that enables variants but strips Vary", () => {
const base = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
// STO-03. Enabling variants while dropping Vary from the response
// allowlist makes every stored variant collide on the same key.
expect(() =>
resolvePublicCachePolicy({
...base,
allowedRequestHeaderNames: ["accept", "accept-language"],
allowedVaryHeaderNames: ["accept-language"],
allowedResponseHeaderNames: base.allowedResponseHeaderNames.filter(
(name) => name !== "vary",
),
}),
).toThrow(TypeError);
// The same policy with Vary preserved is accepted.
expect(() =>
resolvePublicCachePolicy({
...base,
allowedRequestHeaderNames: ["accept", "accept-language"],
allowedVaryHeaderNames: ["accept-language"],
}),
).not.toThrow();
});
it("restages an evicted entry even when the release marker remains", async () => {
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([9, 9, 9, 9]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/evicted.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const cacheStorage = new MemoryCacheStorage();
let fetches = 0;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () => {
fetches += 1;
return new Response(bytes, {
headers: {
"cache-control": "public",
"content-type": "application/javascript",
},
});
},
});
const manifest = await manifestFor("evicted-release", [asset], policy);
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: true,
});
expect(fetches).toBe(1);
// The browser evicts the payload but leaves the marker behind.
const cacheName = [...cacheStorage.caches.keys()].find((name) =>
name.includes("evicted-release"),
);
if (!cacheName) throw new Error("staged cache missing");
const cache = cacheStorage.caches.get(cacheName)!;
const payloadIndex = cache.responses.findIndex(
(entry) => entry.request.url === asset.absoluteUrl,
);
expect(payloadIndex).toBeGreaterThanOrEqual(0);
cache.responses.splice(payloadIndex, 1);
// A marker is a claim, not evidence: restaging must repair.
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: true,
});
expect(fetches).toBe(2);
expect(
await adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).toMatchObject({ ok: true });
});
it("activates a verified prestaged release without a fetcher", async () => {
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([4, 4, 4, 4]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/offline.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const cacheStorage = new MemoryCacheStorage();
const online = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public",
"content-type": "application/javascript",
},
}),
});
const manifest = await manifestFor("offline-release", [asset], policy);
expect(await online.admin.stageRelease(manifest)).toMatchObject({
ok: true,
});
// STO-05. Activation and cleanup perform no network I/O, so a missing
// fetcher must not make an offline rollback UNSUPPORTED.
const offline = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
});
expect(
await offline.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).toMatchObject({ ok: true });
});
it("cleans exact owned caches without a fetcher", async () => {
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const cacheStorage = new MemoryCacheStorage();
const offline = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
});
expect(await offline.admin.cleanupOwned()).toMatchObject({
ok: true,
});
});
it("stages, re-verifies and atomically activates an exact release", async () => {
const cacheStorage = new MemoryCacheStorage();
const bytes = new Uint8Array([1, 2, 3, 4]);
@@ -374,6 +528,150 @@ describe("public response Cache Storage adapter", () => {
await expect(cacheStorage.keys()).resolves.toEqual([]);
});
/**
* NS-08. Handing the signal to each `Request` only asked a cooperative fetch
* to stop. A stream that ignored it held the mutation lock forever, and work
* that finished after the abort still wrote its asset and its marker.
*/
it("does not wait for a non-cooperative fetch after the caller aborts", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([1, 2, 3, 4]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/never-settles.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes) },
};
const manifest = await manifestFor("never-settles", [asset], policy);
const fetchStarted = deferred<void>();
let locksHeld = 0;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: {
async run(signal, operation) {
locksHeld += 1;
try {
return await operation();
} finally {
locksHeld -= 1;
}
},
},
policy,
fetcher: () => {
fetchStarted.resolve(undefined);
// Ignores the signal entirely.
return new Promise<Response>(() => {});
},
});
const controller = new AbortController();
const staging = adapter.admin.stageRelease(manifest, {
signal: controller.signal,
});
await fetchStarted.promise;
controller.abort();
await expect(staging).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
expect(locksHeld).toBe(0);
await expect(cacheStorage.keys()).resolves.toEqual([]);
});
it("writes neither asset nor marker when a digest completes after the abort", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([9, 9, 9, 9]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/late-digest.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes) },
};
const manifest = await manifestFor("late-digest", [asset], policy);
const digestStarted = deferred<void>();
const releaseDigest = deferred<void>();
let delayFirstDigest = true;
const realCrypto = globalThis.crypto;
const delayedCrypto = {
subtle: {
async digest(
algorithm: AlgorithmIdentifier,
data: BufferSource,
): Promise<ArrayBuffer> {
if (delayFirstDigest) {
delayFirstDigest = false;
digestStarted.resolve(undefined);
await releaseDigest.promise;
}
return await realCrypto.subtle.digest(algorithm, data);
},
},
} as unknown as Crypto;
let puts = 0;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: new Proxy(cacheStorage, {
get(target, key, receiver) {
if (key === "open") {
return async (name: string) => {
const cache = await target.open(name);
return new Proxy(cache, {
get(cacheTarget, cacheKey, cacheReceiver) {
if (cacheKey === "put") {
return async (...args: readonly unknown[]) => {
puts += 1;
return await (
cacheTarget.put as (
...values: readonly unknown[]
) => Promise<void>
)(...args);
};
}
return Reflect.get(cacheTarget, cacheKey, cacheReceiver);
},
});
};
}
return Reflect.get(target, key, receiver);
},
}) as unknown as CacheStorage,
crypto: delayedCrypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
const controller = new AbortController();
const staging = adapter.admin.stageRelease(manifest, {
signal: controller.signal,
});
await digestStarted.promise;
controller.abort();
releaseDigest.resolve(undefined);
await expect(staging).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
// Neither the asset nor the activation marker may be written by work the
// abort already disowned.
expect(puts).toBe(0);
});
it("keeps the original activation signal while waiting for the mutation lock", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
@@ -1215,3 +1513,199 @@ describe("public response Cache Storage adapter", () => {
expect(await cacheStorage.keys()).toEqual([]);
});
});
/**
* STO-RR-04 / STO-RR-05. A release cache that is currently serving traffic is
* the last thing a repair may destroy. A transient marker read failure is not
* evidence of damage, and a repair that has not yet fetched anything has not
* yet earned the right to delete what still works.
*/
describe("public response cache repair is failure-atomic", () => {
async function stagedRelease(releaseRegistryId: string) {
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const firstBytes = new Uint8Array([1, 1, 1, 1]);
const secondBytes = new Uint8Array([2, 2, 2, 2]);
const assets: readonly PublicCacheAsset[] = [
{
absoluteUrl: "https://assets.example.test/first.js",
expectedByteLength: firstBytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(firstBytes),
},
},
{
absoluteUrl: "https://assets.example.test/second.js",
expectedByteLength: secondBytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(secondBytes),
},
},
];
const bodies = new Map<string, Uint8Array>([
[assets[0]!.absoluteUrl, firstBytes],
[assets[1]!.absoluteUrl, secondBytes],
]);
const cacheStorage = new MemoryCacheStorage();
const fetchLog: string[] = [];
let failFrom: string | null = null;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async (request: Request) => {
fetchLog.push(request.url);
if (failFrom !== null && request.url === failFrom) {
throw new TypeError("network is down");
}
const body = bodies.get(request.url);
if (!body) throw new TypeError(`unknown asset ${request.url}`);
return new Response(Uint8Array.from(body), {
headers: {
"cache-control": "public",
"content-type": "application/javascript",
},
});
},
});
const manifest = await manifestFor(releaseRegistryId, assets, policy);
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: true,
});
expect(
await adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).toMatchObject({ ok: true });
const cacheName = [...cacheStorage.caches.keys()].find((name) =>
name.includes(releaseRegistryId),
);
if (!cacheName) throw new Error("staged cache missing");
return {
adapter,
assets,
cacheName,
cacheStorage,
fetchLog,
manifest,
setFailure(url: string | null) {
failFrom = url;
},
};
}
it("does not delete an active candidate when the marker read fails transiently", async () => {
const release = await stagedRelease("transient-marker");
const cache = release.cacheStorage.caches.get(release.cacheName)!;
const realMatch = cache.match.bind(cache);
let markerReads = 0;
const assetUrls = new Set(release.assets.map((asset) => asset.absoluteUrl));
cache.match = async (request: RequestInfo | URL) => {
const url =
request instanceof Request ? request.url : String(request);
if (!assetUrls.has(url)) {
markerReads += 1;
throw new DOMException("Storage is busy", "InvalidStateError");
}
return await realMatch(request);
};
const restaged = await release.adapter.admin.stageRelease(release.manifest);
expect(markerReads).toBeGreaterThan(0);
expect(restaged.ok).toBe(false);
expect(release.cacheStorage.caches.has(release.cacheName)).toBe(true);
cache.match = realMatch;
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[0]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
});
it("keeps every healthy asset when one repair fetch fails", async () => {
const release = await stagedRelease("partial-repair");
const cache = release.cacheStorage.caches.get(release.cacheName)!;
// Corrupt only the first asset's stored bytes.
const corrupted = cache.responses.findIndex(
(entry) => entry.request.url === release.assets[0]!.absoluteUrl,
);
expect(corrupted).toBeGreaterThanOrEqual(0);
cache.responses.splice(corrupted, 1);
release.setFailure(release.assets[0]!.absoluteUrl);
const restaged = await release.adapter.admin.stageRelease(release.manifest);
expect(restaged.ok).toBe(false);
// The cache still exists and the healthy asset is still served.
expect(release.cacheStorage.caches.has(release.cacheName)).toBe(true);
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[1]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
});
it("still removes a candidate this call created when staging fails", async () => {
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([7, 7, 7, 7]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/fresh.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const cacheStorage = new MemoryCacheStorage();
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () => {
throw new TypeError("network is down");
},
});
const manifest = await manifestFor("fresh-release", [asset], policy);
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: false,
});
expect(await cacheStorage.keys()).toEqual([]);
});
it("repairs an evicted asset in place and keeps the release usable", async () => {
const release = await stagedRelease("in-place-repair");
const cache = release.cacheStorage.caches.get(release.cacheName)!;
const evicted = cache.responses.findIndex(
(entry) => entry.request.url === release.assets[1]!.absoluteUrl,
);
cache.responses.splice(evicted, 1);
expect(
await release.adapter.admin.stageRelease(release.manifest),
).toMatchObject({ ok: true });
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[1]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[0]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
});
});
@@ -214,6 +214,47 @@ describe("live/poll authoritative writer handoff", () => {
void second;
});
it("tracks a retired active writer after handoff queue overflow", async () => {
let release: ((result: RealtimeResult<void>) => void) | undefined;
const harness = createHarness({
limits: {
...limits,
maxActiveQueueCount: 2,
maxActiveQueueBytes: 10,
},
apply: vi.fn(async ({ value }) => {
if (value === "first") {
return await new Promise<RealtimeResult<void>>((resolve) => {
release = resolve;
});
}
return realtimeSuccess(undefined);
}),
});
const writer = harness.coordinator.currentWriter()!;
const first = writer.write("first", 5);
const second = writer.write("second", 5);
await flush();
await expect(writer.write("overflow", 1)).resolves.toMatchObject({
ok: false,
error: { kind: "QUEUE_OVERFLOW" },
});
// R-03. The fail-close dropped the active reference, but the writer is
// still running, so close() must not claim quiescence.
const closing = harness.coordinator.close();
await flush();
harness.clock.advance(100);
await expect(closing).resolves.toMatchObject({
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
});
release?.(realtimeSuccess(undefined));
void first;
void second;
});
it("fences and aborts live, waits for quiescence, then recovers before activating poll", async () => {
const liveEffect = deferred<RealtimeResult<void>>();
const checkpoint = deferred<RealtimeResult<void>>();
@@ -486,12 +527,87 @@ describe("live/poll authoritative writer handoff", () => {
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
});
await expect(harness.coordinator.close()).resolves.toMatchObject({
// RT-RR-03. A second close re-runs rather than replaying a cached verdict.
// The writer is still hung, so it still reports a timeout.
const second = harness.coordinator.close();
await flush();
harness.clock.advance(100);
await expect(second).resolves.toMatchObject({
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
});
});
/**
* RT-RR-03. Caching the first timeout forever meant a writer that later
* settled could never be proved quiescent: every subsequent close replayed
* the stale failure and the retained registry could never be pruned.
*/
it("converges to success once a retired writer finally settles", async () => {
let release: ((value: RealtimeResult<void>) => void) | undefined;
const harness = createHarness({
apply: vi.fn(
async () =>
await new Promise<RealtimeResult<void>>((resolve) => {
release = resolve;
}),
),
});
const live = harness.coordinator.currentWriter()!;
void live.write("late-settle", 8);
await flush();
const first = harness.coordinator.close();
await flush();
harness.clock.advance(100);
await expect(first).resolves.toMatchObject({
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
});
// The writer finishes after the first close gave up.
release?.(realtimeSuccess(undefined));
await flush();
await flush();
const second = harness.coordinator.close();
await flush();
harness.clock.advance(100);
await expect(second).resolves.toMatchObject({ ok: true });
});
/**
* RT-RR-04. Checkpoint work is an external authority call like a writer
* tail. Racing it against a timeout bounded the public wait but left it out
* of the retained registry, so `close()` could report quiescence while the
* checkpoint was still running.
*/
it("does not report quiescence while a checkpoint is still running", async () => {
let checkpointSignal: AbortSignal | undefined;
const harness = createHarness({
recover: vi.fn(async ({ signal }) => {
checkpointSignal = signal;
return await new Promise<RealtimeResult<void>>(() => {});
}),
});
const transition = harness.coordinator.switchToPoll();
await flush();
expect(checkpointSignal).toBeDefined();
const closing = harness.coordinator.close();
await flush();
harness.clock.advance(100);
await expect(closing).resolves.toMatchObject({
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
});
harness.clock.advance(100);
await flush();
await transition;
});
it("rejects invalid initial authority and resource ceilings", () => {
expect(() =>
createLivePollHandoffCoordinator({
@@ -29,6 +29,91 @@ import {
} from "./fixture.ts";
describe("transport-independent realtime stream coordinator", () => {
it("keeps the stream DRAINING until a non-cooperative effect settles", async () => {
const wedged = deferred<RealtimeResult<void>>();
const harness = createHarness({
apply: async () => wedged.promise,
taskLimits: { effectTimeoutMs: 5, drainTimeoutMs: 5 },
});
await harness.initialize();
const applied = await harness.coordinator.accept(
harness.event({
eventId: "event-00000001",
sequence: "1",
resumeCursor: "cursor-00000001",
}),
);
// Bounded for the caller, and explicitly non-retryable.
expect(applied).toMatchObject({
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "APPLY", retryable: false },
});
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
// DRAINING refuses new admission rather than queueing behind the wedge.
await expect(
harness.coordinator.accept(
harness.event({
eventId: "event-00000002",
sequence: "2",
resumeCursor: "cursor-00000002",
}),
),
).resolves.toMatchObject({ ok: false, error: { kind: "CLOSED" } });
// close() cannot claim quiescence while the task is still running.
await expect(harness.coordinator.close()).resolves.toMatchObject({
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
});
// Only actual settlement ends DRAINING.
wedged.resolve(realtimeSuccess(undefined));
await expect(harness.coordinator.close()).resolves.toMatchObject({
ok: true,
});
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("CLOSED");
});
it("bounds non-cooperative recovery and rejects its late checkpoint", async () => {
const wedged = deferred<RealtimeResult<RealtimeRecoveryCommit>>();
let recoveries = 0;
const harness = createHarness({
recover: async () => {
recoveries += 1;
return recoveries === 1
? realtimeSuccess(snapshotCommit("0"))
: wedged.promise;
},
taskLimits: { recoveryTimeoutMs: 5, drainTimeoutMs: 5 },
});
await harness.initialize();
const recovered = await harness.coordinator.recover(
STREAM_ID,
"SEQUENCE_GAP",
);
expect(recovered).toMatchObject({
ok: false,
error: { kind: "IDLE_TIMEOUT", operation: "RECOVER", retryable: false },
});
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
const beforeLateCommit = harness.coordinator.getResumeState(STREAM_ID);
// A late checkpoint from the abandoned attempt cannot commit.
wedged.resolve(realtimeSuccess(snapshotCommit("9")));
for (let flush = 0; flush < 10; flush += 1) await Promise.resolve();
expect(harness.coordinator.getResumeState(STREAM_ID)).toEqual(
beforeLateCommit,
);
// Settlement returns the stream to OPEN, marked STALE for an authoritative
// recovery rather than silently trusting the abandoned attempt.
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("OPEN");
expect(harness.coordinator.inspect(STREAM_ID).freshness).toBe("STALE");
});
it("applies one stream sequentially and commits each cursor after its effect", async () => {
const first = deferred<RealtimeResult<void>>();
const applied: string[] = [];
@@ -858,6 +943,118 @@ describe("transport-independent realtime stream coordinator", () => {
});
});
/**
* RT-RR-01 / RT-RR-02. A physical effect exists from the moment the coordinator
* calls the authority, not from the moment its public wait expires. Registering
* only on timeout let a `close()` that arrived first see an empty retained set
* and report quiescence while the raw task was still running against the
* authority.
*/
describe("realtime physical task ownership", () => {
it("does not report quiescence while an apply is still running", async () => {
const applyGate = deferred<RealtimeResult<void>>();
const harness = createHarness({
apply: async () => await applyGate.promise,
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
});
await harness.initialize();
const applying = harness.coordinator.accept(harness.event());
await Promise.resolve();
await Promise.resolve();
// close() arrives long before the effect deadline.
const closed = await harness.coordinator.close();
expect(closed.ok).toBe(false);
expect(closed.ok ? null : closed.error.kind).toBe("IDLE_TIMEOUT");
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
applyGate.resolve(realtimeSuccess(undefined));
await applying;
});
it("reports quiescence once the raw task settles", async () => {
const applyGate = deferred<RealtimeResult<void>>();
const harness = createHarness({
apply: async () => await applyGate.promise,
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 200 },
});
await harness.initialize();
const applying = harness.coordinator.accept(harness.event());
await Promise.resolve();
const closing = harness.coordinator.close();
applyGate.resolve(realtimeSuccess(undefined));
await applying;
expect(await closing).toMatchObject({ ok: true });
});
it("does not start a queued event once the stream is DRAINING", async () => {
const firstApply = deferred<RealtimeResult<void>>();
let applyCalls = 0;
const harness = createHarness({
apply: async () => {
applyCalls += 1;
if (applyCalls === 1) return await firstApply.promise;
return realtimeSuccess(undefined);
},
taskLimits: { effectTimeoutMs: 15, drainTimeoutMs: 50 },
});
await harness.initialize();
const first = harness.coordinator.accept(
harness.event({ eventId: "event-0001", sequence: "1" }),
);
// Queued behind the first while it is still inside its deadline.
const second = harness.coordinator.accept(
harness.event({ eventId: "event-0002", sequence: "2" }),
);
await first;
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
// The queued event is dropped at execution time, not applied and not
// recovered: admission happened before DRAINING, execution happens inside
// it, and the second decision is the one that counts.
const secondResult = await second;
expect(secondResult).toMatchObject({
ok: true,
value: { outcome: "DROPPED", reason: "CLOSED" },
});
expect(applyCalls).toBe(1);
firstApply.resolve(realtimeSuccess(undefined));
});
it("discards the resume token when an effect is abandoned", async () => {
const firstApply = deferred<RealtimeResult<void>>();
let applyCalls = 0;
const harness = createHarness({
apply: async () => {
applyCalls += 1;
if (applyCalls === 1) return await firstApply.promise;
return realtimeSuccess(undefined);
},
taskLimits: { effectTimeoutMs: 15, drainTimeoutMs: 50 },
});
await harness.initialize();
expect(harness.coordinator.inspect(STREAM_ID).hasResumeState).toBe(true);
await harness.coordinator.accept(
harness.event({ eventId: "event-0001", sequence: "1" }),
);
// The abandoned effect may have applied part of its change, so the token it
// was based on is no longer authoritative evidence.
const inspection = harness.coordinator.inspect(STREAM_ID);
expect(inspection.hasResumeState).toBe(false);
expect(inspection.freshness).toBe("UNKNOWN");
firstApply.resolve(realtimeSuccess(undefined));
});
});
type HarnessOptions = Readonly<{
registry?: RealtimePolicyRegistry;
mappers?: typeof TEST_MAPPERS | Readonly<Record<string, typeof TEST_MAPPER>>;
@@ -866,6 +1063,13 @@ type HarnessOptions = Readonly<{
request: RealtimeRecoveryRequest,
) => Promise<RealtimeResult<RealtimeRecoveryCommit>>;
observe?: (observation: RealtimeObservation) => void;
taskLimits?: Readonly<{
effectTimeoutMs?: number;
recoveryTimeoutMs?: number;
drainTimeoutMs?: number;
}>;
scheduleTimeout?: (callback: () => void, delayMs: number) => unknown;
clearScheduledTimeout?: (handle: unknown) => void;
}>;
function createHarness(options: HarnessOptions = {}) {
@@ -911,6 +1115,13 @@ function createHarness(options: HarnessOptions = {}) {
},
now: () => 10_000,
observe: options.observe,
...(options.taskLimits ? { taskLimits: options.taskLimits } : {}),
...(options.scheduleTimeout
? { scheduleTimeout: options.scheduleTimeout }
: {}),
...(options.clearScheduledTimeout
? { clearScheduledTimeout: options.clearScheduledTimeout }
: {}),
});
return {
@@ -973,3 +1184,168 @@ function deferred<Value>() {
});
return { promise, resolve };
}
/**
* RT-01. The registry entry has to exist before the collaborator is called.
* Registering after the invocation returned left a window in which an authority
* that re-entered `close()` from inside its own callback saw an empty set, so
* `close()` reported quiescence while its effect was still running.
*/
describe("RT-01 reentrant close cannot pass the registration window", () => {
it("refuses quiescence when apply re-enters close during its invocation", async () => {
const applyGate = deferred<RealtimeResult<void>>();
let closeResult: RealtimeResult<void> | undefined;
let harness: ReturnType<typeof createHarness> | undefined;
harness = createHarness({
apply: () => {
// Re-entered from inside the invocation itself.
void harness!.coordinator.close().then((result) => {
closeResult = result;
});
return applyGate.promise;
},
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
});
await harness.initialize();
const applying = harness.coordinator.accept(harness.event());
await new Promise((resolve) => setTimeout(resolve, 50));
expect(closeResult?.ok).toBe(false);
expect(closeResult?.ok ? null : closeResult?.error.kind).toBe(
"IDLE_TIMEOUT",
);
applyGate.resolve(realtimeSuccess(undefined));
await applying;
// Once the raw task settled, a second close does converge.
await expect(harness.coordinator.close()).resolves.toMatchObject({
ok: true,
});
});
it("refuses quiescence when recovery re-enters close during its invocation", async () => {
const recoveryGate = deferred<RealtimeResult<RealtimeRecoveryCommit>>();
let closeResult: RealtimeResult<void> | undefined;
let harness: ReturnType<typeof createHarness> | undefined;
let recoveries = 0;
harness = createHarness({
recover: () => {
recoveries += 1;
if (recoveries === 1) {
return Promise.resolve(realtimeSuccess(snapshotCommit("0")));
}
void harness!.coordinator.close().then((result) => {
closeResult = result;
});
return recoveryGate.promise;
},
taskLimits: { recoveryTimeoutMs: 10_000, drainTimeoutMs: 20 },
});
await harness.initialize();
const recovering = harness.coordinator.recover(STREAM_ID, "SEQUENCE_GAP");
await new Promise((resolve) => setTimeout(resolve, 50));
expect(closeResult?.ok).toBe(false);
expect(closeResult?.ok ? null : closeResult?.error.kind).toBe(
"IDLE_TIMEOUT",
);
recoveryGate.resolve(realtimeSuccess(snapshotCommit("0")));
await recovering;
});
});
/**
* RT-02. A scheduler that cannot install a deadline leaves the wait unbounded.
* Letting the exception escape turned a typed realtime result into a native
* rejection and, through the caller's own catch, started a recovery that
* overlapped the effect still running.
*/
describe("RT-02 a throwing scheduler stays inside the result contract", () => {
/** Installs deadlines normally until the stream is initialized. */
function breakableScheduler() {
const state = { broken: false };
return {
state,
scheduleTimeout: (callback: () => void, delayMs: number) => {
if (state.broken) throw new TypeError("scheduleTimeout exploded");
return setTimeout(callback, delayMs);
},
clearScheduledTimeout: (handle: unknown) => {
clearTimeout(handle as ReturnType<typeof setTimeout>);
},
};
}
it("does not start a recovery that overlaps a pending apply", async () => {
const applyGate = deferred<RealtimeResult<void>>();
const scheduler = breakableScheduler();
let recoveriesAfterApply = 0;
const harness = createHarness({
apply: () => applyGate.promise,
recover: async () => {
recoveriesAfterApply += 1;
return realtimeSuccess(snapshotCommit("0"));
},
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
scheduleTimeout: scheduler.scheduleTimeout,
clearScheduledTimeout: scheduler.clearScheduledTimeout,
});
await harness.initialize();
recoveriesAfterApply = 0;
scheduler.state.broken = true;
const accepted = await harness.coordinator.accept(harness.event());
// The apply is still pending, so no recovery may have run beside it.
expect(recoveriesAfterApply).toBe(0);
expect(accepted.ok).toBe(false);
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
applyGate.resolve(realtimeSuccess(undefined));
});
it("returns a typed close failure instead of rejecting natively", async () => {
const applyGate = deferred<RealtimeResult<void>>();
const scheduler = breakableScheduler();
const harness = createHarness({
apply: () => applyGate.promise,
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
scheduleTimeout: scheduler.scheduleTimeout,
clearScheduledTimeout: scheduler.clearScheduledTimeout,
});
await harness.initialize();
scheduler.state.broken = true;
const applying = harness.coordinator.accept(harness.event());
// Let the apply reach the authority before the fence arrives.
await Promise.resolve();
await Promise.resolve();
const closed = await harness.coordinator.close();
// A drain that could not be bounded is not proof of quiescence.
expect(closed.ok).toBe(false);
expect(closed.ok ? null : closed.error.kind).toBe("IDLE_TIMEOUT");
applyGate.resolve(realtimeSuccess(undefined));
await applying;
});
it("keeps the classified outcome when clearing a timer throws", async () => {
const harness = createHarness({
clearScheduledTimeout: () => {
throw new TypeError("clearScheduledTimeout exploded");
},
});
await harness.initialize();
await expect(
harness.coordinator.accept(harness.event()),
).resolves.toMatchObject({ ok: true });
await expect(harness.coordinator.close()).resolves.toMatchObject({
ok: true,
});
});
});
+38 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
REALTIME_WEBSOCKET_PROTOCOL,
@@ -25,6 +25,43 @@ function encode(value: unknown): string {
}
describe("realtime WebSocket protocol", () => {
it("rejects oversized text before allocating a full UTF-8 copy", () => {
const encoderSpy = vi.spyOn(TextEncoder.prototype, "encode");
try {
const oversize = "a".repeat(64);
expect(decodeWebSocketServerFrame(oversize, 8)).toMatchObject({
ok: false,
error: { code: "FRAME_TOO_LARGE" },
});
expect(encoderSpy).not.toHaveBeenCalled();
} finally {
encoderSpy.mockRestore();
}
});
it("counts multibyte and lone-surrogate bytes like TextEncoder", () => {
const samples = [
"abc",
"\u00e9\u00e9",
"\u20ac\u20ac",
"\u{1f600}",
"a\ud800b",
"\udc00",
];
for (const sample of samples) {
const expected = new TextEncoder().encode(sample).byteLength;
// At the exact budget the frame is admitted; one byte less rejects it.
expect(
decodeWebSocketServerFrame(sample, expected).ok ||
decodeWebSocketServerFrame(sample, expected),
).toBeTruthy();
expect(decodeWebSocketServerFrame(sample, expected - 1)).toMatchObject({
ok: false,
error: { code: "FRAME_TOO_LARGE" },
});
}
});
it("decodes and freezes an exact WELCOME frame", () => {
const result = decodeWebSocketServerFrame(
encode({
+37 -10
View File
@@ -183,7 +183,7 @@ describe("IndexedDB resumable upload checkpoint", () => {
).toMatchObject({ ok: true });
expect(await runtime.admin.deletePartition()).toEqual({
ok: true,
value: { state: "DELETED" },
value: { state: "DELETED", effect: "APPLIED" },
});
expect(await runtime.store.read("upload_key_01")).toMatchObject({
ok: false,
@@ -191,21 +191,48 @@ describe("IndexedDB resumable upload checkpoint", () => {
});
});
it("bounds a partition deletion blocked by another browser context", async () => {
it("returns PENDING UNKNOWN when deleteDatabase is still blocked", async () => {
const memory = new MemoryIndexedDbFactory();
const factory = deletingFactory(memory, "BLOCKED");
const runtime = createIndexedDbResumableUploadCheckpointRuntime({
scope,
factory: deletingFactory(memory, "BLOCKED"),
factory,
blockedTimeoutMs: 1,
});
// BT-UP-03. The native request is still live, so the deadline is not
// evidence that nothing happened.
expect(await runtime.admin.deletePartition()).toEqual({
ok: true,
value: {
state: "PENDING",
effect: "UNKNOWN",
reason: "BLOCKED_DEADLINE",
},
});
});
it("keeps the checkpoint store closed until a pending delete is resolved externally", async () => {
const memory = new MemoryIndexedDbFactory();
const factory = deletingFactory(memory, "BLOCKED");
const runtime = createIndexedDbResumableUploadCheckpointRuntime({
scope,
factory,
blockedTimeoutMs: 1,
});
expect(await runtime.admin.deletePartition()).toMatchObject({
ok: false,
error: {
code: "BLOCKED",
retryable: true,
recovery: "RELOAD_OTHER_CONTEXTS",
},
ok: true,
value: { state: "PENDING" },
});
// A second runtime over the same realm and database would race an unknown
// native effect.
expect(() =>
createIndexedDbResumableUploadCheckpointRuntime({
scope,
factory,
blockedTimeoutMs: 1,
}),
).toThrow(TypeError);
});
it("never reports a false abort after irreversible deleteDatabase dispatch", async () => {
@@ -219,7 +246,7 @@ describe("IndexedDB resumable upload checkpoint", () => {
controller.abort();
expect(await deletion).toEqual({
ok: true,
value: { state: "DELETED" },
value: { state: "DELETED", effect: "APPLIED" },
});
const preAborted = new AbortController();
@@ -67,6 +67,56 @@ function createTransport(
}
describe("resumable upload fetch transport", () => {
it.each([
{ label: "delta-seconds", header: "2", now: 1_000, expected: 2_000 },
{ label: "HTTP-date ahead", header: "Thu, 01 Jan 1970 00:00:03 GMT", now: 1_000, expected: 2_000 },
{ label: "HTTP-date behind (clock rollback)", header: "Thu, 01 Jan 1970 00:00:01 GMT", now: 9_000, expected: 0 },
])(
"resolves Retry-After against the injected clock ($label)",
async ({ header, now, expected }) => {
// BT-UP-02. Both branches use the same captured `now`, so boundaries and
// clock rollback are deterministic.
const transport = createTransport(
(async () =>
responseAt(ENDPOINTS.GET_STATUS, null, {
status: 429,
headers: { "retry-after": header },
})) as unknown as typeof fetch,
{ nowEpochMs: () => now },
);
const result = await transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: new AbortController().signal,
});
expect(result).toMatchObject({
ok: false,
error: { code: "UNAVAILABLE", retryAfterMs: expected },
});
},
);
it("ignores an invalid Retry-After date instead of guessing", async () => {
const transport = createTransport(
(async () =>
responseAt(ENDPOINTS.GET_STATUS, null, {
status: 429,
headers: { "retry-after": "not-a-date" },
})) as unknown as typeof fetch,
{ nowEpochMs: () => 1_000 },
);
const result = await transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: new AbortController().signal,
});
expect(result).toMatchObject({ ok: false });
if (result.ok) return;
expect(result.error.retryAfterMs).toBeUndefined();
});
it("uses a closed operation map and fixed production fetch policy", async () => {
let receivedUrl = "";
let receivedInit: RequestInit | undefined;
@@ -444,4 +494,158 @@ describe("resumable upload Web Lock", () => {
},
]);
});
/**
* X-AUDIT-02. The public port promises a `UploadProviderResult`. A scheduler
* that cannot install the attempt deadline must close the attempt inside that
* contract instead of rejecting it, and must not leave the caller listener
* attached to the parent signal.
*/
describe("scheduler boundary", () => {
const trackedSignal = () => {
const controller = new AbortController();
const added: string[] = [];
const removed: string[] = [];
const add = controller.signal.addEventListener.bind(controller.signal);
const remove = controller.signal.removeEventListener.bind(
controller.signal,
);
Object.defineProperty(controller.signal, "addEventListener", {
configurable: true,
value: (type: string, ...rest: readonly unknown[]) => {
added.push(type);
return (add as (...args: readonly unknown[]) => unknown)(
type,
...rest,
);
},
});
Object.defineProperty(controller.signal, "removeEventListener", {
configurable: true,
value: (type: string, ...rest: readonly unknown[]) => {
removed.push(type);
return (remove as (...args: readonly unknown[]) => unknown)(
type,
...rest,
);
},
});
return { controller, added, removed };
};
it("closes the attempt when the scheduler cannot install the deadline", async () => {
const fetcher = vi.fn(async () => new Response(null, { status: 200 }));
const { controller, added, removed } = trackedSignal();
const transport = createTransport(fetcher as unknown as typeof fetch, {
scheduler: {
setTimeout: () => {
throw new TypeError("upload scheduler install exploded");
},
clearTimeout: () => {},
},
});
const result = await transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: controller.signal,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe("UNAVAILABLE");
expect(result.error.retryable).toBe(true);
}
expect(fetcher).not.toHaveBeenCalled();
expect(added.filter((type) => type === "abort")).toHaveLength(1);
expect(removed.filter((type) => type === "abort")).toHaveLength(1);
});
it("keeps the classified outcome when clearing the deadline throws", async () => {
const fetcher = vi.fn(
async () =>
jsonResponseAt(ENDPOINTS.GET_STATUS, { sessionId: "session_01" }),
);
const { controller, added, removed } = trackedSignal();
const transport = createTransport(fetcher as unknown as typeof fetch, {
scheduler: {
setTimeout: (callback: () => void, delayMs: number) =>
setTimeout(callback, delayMs),
clearTimeout: () => {
throw new TypeError("upload scheduler clear exploded");
},
},
});
const result = await transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: controller.signal,
});
expect(result.ok).toBe(true);
expect(added.filter((type) => type === "abort")).toHaveLength(1);
expect(removed.filter((type) => type === "abort")).toHaveLength(1);
});
it("uses the scheduler methods captured at construction", async () => {
const fetcher = vi.fn(
async () =>
jsonResponseAt(ENDPOINTS.GET_STATUS, { sessionId: "session_01" }),
);
const scheduler = {
setTimeout: (callback: () => void, delayMs: number) =>
setTimeout(callback, delayMs),
clearTimeout: (handle: unknown) => {
clearTimeout(handle as ReturnType<typeof setTimeout>);
},
};
const transport = createTransport(fetcher as unknown as typeof fetch, {
scheduler,
});
scheduler.setTimeout = () => {
throw new TypeError("mutated upload setTimeout");
};
scheduler.clearTimeout = () => {
throw new TypeError("mutated upload clearTimeout");
};
await expect(
transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: new AbortController().signal,
}),
).resolves.toMatchObject({ ok: true });
});
it("starts no timer and no fetch for an already aborted caller", async () => {
const fetcher = vi.fn(async () => new Response(null, { status: 200 }));
const setTimeout_ = vi.fn(
(callback: () => void, delayMs: number) =>
setTimeout(callback, delayMs) as unknown,
);
const controller = new AbortController();
controller.abort();
const transport = createTransport(fetcher as unknown as typeof fetch, {
scheduler: {
setTimeout: setTimeout_,
clearTimeout: (handle: unknown) => {
clearTimeout(handle as ReturnType<typeof setTimeout>);
},
},
});
const result = await transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: controller.signal,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error.code).toBe("ABORTED");
expect(fetcher).not.toHaveBeenCalled();
expect(setTimeout_).not.toHaveBeenCalled();
});
});
});
@@ -1,5 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { PRESIGNED_TRANSFER_PROTOCOL } from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
import type {
PresignedUploadPartCapabilityProvider,
} from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
@@ -156,6 +158,182 @@ function rangeSource(bytes: Uint8Array) {
}
describe("resumable upload HTTP control plane", () => {
/**
* TR-RR-08. `Object.keys` sees only enumerable own string keys, so a symbol
* or non-enumerable extra passed unseen and a later property read invoked
* whatever accessor the sender installed escaping the Result contract as a
* rejection of a public method.
*/
it("closes every hostile control-plane object as typed CORRUPT_DATA", async () => {
const fingerprint: UploadFileFingerprint = Object.freeze({
algorithm: "SHA-256-PARTS-V1",
digestHex: "a".repeat(64),
byteLength: 4,
partSizeBytes: 4,
partCount: 1,
});
const validSession = () => ({
protocol: RESUMABLE_UPLOAD_PROTOCOL,
sessionId: "session_01",
requestBindingSha256: "b".repeat(64),
fingerprint,
partSizeBytes: 4,
partCount: 1,
maxConcurrency: 1,
expiresAtEpochMs: NOW + 10_000,
});
const hostile: readonly (readonly [string, () => unknown])[] = [
[
"throwing getter",
() => {
const value = validSession() as Record<string, unknown>;
Object.defineProperty(value, "sessionId", {
configurable: true,
enumerable: true,
get: () => {
throw new TypeError("hostile getter");
},
});
return value;
},
],
[
"symbol key",
() => ({ ...validSession(), [Symbol("injected")]: "leak" }),
],
[
"non-enumerable extra",
() => {
const value = validSession() as Record<string, unknown>;
Object.defineProperty(value, "signedUrl", {
configurable: true,
enumerable: false,
value: "https://objects.example/secret?signature=leak",
});
return value;
},
],
[
"ownKeys trap",
() =>
new Proxy(validSession() as Record<string, unknown>, {
ownKeys() {
throw new TypeError("hostile ownKeys");
},
}),
],
[
"getOwnPropertyDescriptor trap",
() =>
new Proxy(validSession() as Record<string, unknown>, {
getOwnPropertyDescriptor() {
throw new TypeError("hostile descriptor");
},
}),
],
[
"a nested fingerprint with an extra field",
() => ({
...validSession(),
fingerprint: { ...fingerprint, injected: true },
}),
],
[
"a nested fingerprint behind an accessor",
() => {
const value = validSession() as Record<string, unknown>;
Object.defineProperty(value, "fingerprint", {
configurable: true,
enumerable: true,
get: () => fingerprint,
});
return value;
},
],
[
"a custom prototype",
() =>
Object.assign(Object.create({ injected: true }), validSession()),
],
];
for (const [label, build] of hostile) {
const control = createResumableUploadHttpControlPlane({
transport: {
async execute() {
return browserDataSuccess(build());
},
},
partCapabilities: { issueUploadPart: vi.fn() },
});
const result = await control.createSession({
protocol: RESUMABLE_UPLOAD_PROTOCOL,
uploadKey: "upload_key_strict",
purpose: "attachment",
mediaType: "application/octet-stream",
requestBindingSha256: "b".repeat(64),
fingerprint,
requestedPartSizeBytes: 4,
requestedMaxConcurrency: 1,
idempotencyKey: "upload-create-idempotency-01",
signal: activeSignal,
});
expect(result, label).toMatchObject({
ok: false,
error: { code: "CORRUPT_DATA", operation: "UPLOAD_SESSION" },
});
expect(JSON.stringify(result)).not.toContain("signature=leak");
}
/**
* TR-05. The decoder checked the sender's object and then read it again to
* build the result, so a stateful answer could show a safe `sessionId` to
* the regex and hand an unvalidated one to the receipt. Reading once means
* the value that was validated is the value that is returned.
*/
let sessionIdReads = 0;
const statefulControl = createResumableUploadHttpControlPlane({
transport: {
async execute() {
return browserDataSuccess(
new Proxy(validSession() as Record<string, unknown>, {
getOwnPropertyDescriptor(target, key) {
if (key === "sessionId") {
sessionIdReads += 1;
return {
configurable: true,
enumerable: true,
value: sessionIdReads > 1 ? "../../unsafe" : "session_01",
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
}),
);
},
},
partCapabilities: { issueUploadPart: vi.fn() },
});
const stateful = await statefulControl.createSession({
protocol: RESUMABLE_UPLOAD_PROTOCOL,
uploadKey: "upload_key_strict",
purpose: "attachment",
mediaType: "application/octet-stream",
requestBindingSha256: "b".repeat(64),
fingerprint,
requestedPartSizeBytes: 4,
requestedMaxConcurrency: 1,
idempotencyKey: "upload-create-idempotency-02",
signal: activeSignal,
});
expect(sessionIdReads).toBe(1);
expect(JSON.stringify(stateful)).not.toContain("../../unsafe");
if (stateful.ok) {
expect(stateful.value.sessionId).toBe("session_01");
}
});
it("rejects unknown response fields so URLs cannot cross the DTO boundary", async () => {
const fingerprint: UploadFileFingerprint = Object.freeze({
algorithm: "SHA-256-PARTS-V1",
@@ -272,6 +450,8 @@ describe("resumable upload HTTP control plane", () => {
}),
);
return jsonResponseAt(CAPABILITY_ENDPOINT, {
// BT-PRE-02. The capability envelope declares its wire protocol.
protocol: PRESIGNED_TRANSFER_PROTOCOL,
capabilityReceipt: `capability-upload-${capabilitySequence}`,
method: "PUT",
binding: request.binding,
+233
View File
@@ -347,6 +347,65 @@ function executorFor(
}
describe("production resumable upload runtime", () => {
it("disposes through one drain that proves quiescence", async () => {
// BT-UP-06. close() closes admission; dispose() awaits real settlement.
const checkpoints = new MemoryCheckpointStore();
const harness = createControlHarness();
let releaseUpload: (() => void) | undefined;
const runtime = createResumableUploadRuntime({
controlPlane: harness.control,
partExecutor: executorFor(harness, {
delay: async () => {
await new Promise<void>((resolve) => {
releaseUpload = resolve;
});
},
}),
checkpoints,
mutationLock: noContentionLock,
crypto,
policy: runtimePolicy(),
now: () => 1_000,
random: () => 0,
sleep: async () => {},
});
const uploading = runtime.upload({
uploadKey: "upload_key_01",
purpose: "attachment",
mediaType: "application/octet-stream",
source: byteStreamSource(new Uint8Array([1, 2, 3, 4, 5])),
signal: activeSignal,
});
await vi.waitFor(() => expect(releaseUpload).toBeDefined());
expect(runtime.lifecycle()).toBe("OPEN");
const first = runtime.dispose();
const second = runtime.dispose();
// Duplicate dispose is single-flight.
expect(first).toBe(second);
expect(runtime.lifecycle()).toBe("CLOSING");
// New admission is refused while draining.
await expect(
runtime.upload({
uploadKey: "upload_key_02",
purpose: "attachment",
mediaType: "application/octet-stream",
source: byteStreamSource(new Uint8Array([1])),
signal: activeSignal,
}),
).resolves.toMatchObject({ ok: false, error: { code: "UNAVAILABLE" } });
// The checkpoint store cannot close before the operation settles.
expect(checkpoints.closed).toBe(false);
releaseUpload?.();
await first;
await uploading;
expect(runtime.lifecycle()).toBe("CLOSED");
expect(checkpoints.closed).toBe(true);
});
it("has a valid production default policy", () => {
expect(() => resolveResumableUploadRuntimePolicy()).not.toThrow();
expect(() =>
@@ -1221,3 +1280,177 @@ describe("production resumable upload runtime", () => {
}
});
});
/**
* TR-RR-06. A non-cooperative mutation lock or provider must not make teardown
* unbounded: `dispose()` bounds its drain and reports honestly when the runtime
* is still CLOSING, and an abort is admitted physical work it cannot step over.
*/
describe("TR-RR-06 bounded resumable teardown", () => {
it("reports an unproved drain instead of waiting forever", async () => {
const checkpoints = new MemoryCheckpointStore();
const harness = createControlHarness();
const runtime = createResumableUploadRuntime({
controlPlane: harness.control,
partExecutor: executorFor(harness, { delay: async () => {} }),
checkpoints,
// A lock that never grants: dispose must still be bounded.
mutationLock: Object.freeze({
async run<Value>(): Promise<Value> {
return await new Promise<never>(() => {});
},
}),
crypto,
policy: runtimePolicy({ cleanupDeadlineMs: 20 }),
now: () => 1_000,
random: () => 0,
sleep: async () => {},
});
void runtime.upload({
uploadKey: "upload_key_hung",
purpose: "attachment",
mediaType: "application/octet-stream",
source: byteStreamSource(new Uint8Array([1, 2, 3, 4, 5])),
signal: activeSignal,
});
await Promise.resolve();
await Promise.resolve();
const disposed = await runtime.dispose();
expect(disposed.ok).toBe(false);
// Still CLOSING: physical work the caller must not treat as finished.
expect(runtime.lifecycle()).toBe("CLOSING");
expect(checkpoints.closed).toBe(false);
});
it("closes once every admitted operation settles", async () => {
const checkpoints = new MemoryCheckpointStore();
const harness = createControlHarness();
const runtime = createResumableUploadRuntime({
controlPlane: harness.control,
partExecutor: executorFor(harness, { delay: async () => {} }),
checkpoints,
mutationLock: noContentionLock,
crypto,
policy: runtimePolicy({ cleanupDeadlineMs: 200 }),
now: () => 1_000,
random: () => 0,
sleep: async () => {},
});
const disposed = await runtime.dispose();
expect(disposed).toMatchObject({ ok: true });
expect(runtime.lifecycle()).toBe("CLOSED");
expect(checkpoints.closed).toBe(true);
});
});
/**
* TR-04. `dispose()` proved quiescence from the wrapper registry only. A
* provider that ignored its attempt deadline let the wrapper settle first and
* leave the set empty, so teardown reported a drained runtime and closed the
* checkpoint store while the provider was still running.
*/
describe("TR-04 teardown waits for raw provider work, not only its wrapper", () => {
it("refuses to report a drained runtime while a provider is still running", async () => {
const harness = createControlHarness();
const checkpoints = new MemoryCheckpointStore();
const closeStore = vi.spyOn(checkpoints, "close");
let releaseProvider: (() => void) | undefined;
// Ignores the attempt signal entirely and outlives its own deadline.
harness.control.createSession = () =>
new Promise((resolve) => {
releaseProvider = () =>
resolve(
browserDataFailure("UNAVAILABLE", "UPLOAD_SESSION", {
retryable: true,
recovery: "RESUME",
}),
);
});
const runtime = createResumableUploadRuntime({
controlPlane: harness.control,
partExecutor: executorFor(harness),
checkpoints,
mutationLock: noContentionLock,
crypto,
policy: runtimePolicy({
providerAttemptTimeoutMs: 5,
cleanupDeadlineMs: 25,
maxRetries: 0,
}),
now: () => 1_000,
random: () => 0,
sleep: async () => {},
});
const uploading = runtime.upload({
uploadKey: "upload_key_raw",
purpose: "attachment",
mediaType: "application/octet-stream",
source: byteStreamSource(new Uint8Array([1, 2, 3, 4, 5])),
signal: activeSignal,
});
await vi.waitFor(() => expect(releaseProvider).toBeDefined());
// The wrapper has already given up on the attempt.
await uploading;
const disposed = await runtime.dispose();
expect(disposed.ok).toBe(false);
if (!disposed.ok) {
expect(disposed.error.code).toBe("UNAVAILABLE");
expect(disposed.error.recovery).toBe("RESUME");
}
// The store stays open while something could still write a checkpoint.
expect(closeStore).not.toHaveBeenCalled();
releaseProvider?.();
});
it("reports a drained runtime once the raw provider settles", async () => {
const harness = createControlHarness();
const checkpoints = new MemoryCheckpointStore();
let releaseProvider: (() => void) | undefined;
harness.control.createSession = () =>
new Promise((resolve) => {
releaseProvider = () =>
resolve(
browserDataFailure("UNAVAILABLE", "UPLOAD_SESSION", {
retryable: true,
recovery: "RESUME",
}),
);
});
const runtime = createResumableUploadRuntime({
controlPlane: harness.control,
partExecutor: executorFor(harness),
checkpoints,
mutationLock: noContentionLock,
crypto,
policy: runtimePolicy({
providerAttemptTimeoutMs: 5,
cleanupDeadlineMs: 1_000,
maxRetries: 0,
}),
now: () => 1_000,
random: () => 0,
sleep: async () => {},
});
const uploading = runtime.upload({
uploadKey: "upload_key_raw_2",
purpose: "attachment",
mediaType: "application/octet-stream",
source: byteStreamSource(new Uint8Array([1, 2, 3, 4, 5])),
signal: activeSignal,
});
await vi.waitFor(() => expect(releaseProvider).toBeDefined());
await uploading;
const disposing = runtime.dispose();
releaseProvider?.();
await expect(disposing).resolves.toMatchObject({ ok: true });
expect(runtime.lifecycle()).toBe("CLOSED");
});
});
+32
View File
@@ -138,6 +138,38 @@ describe("runtime adapter composition", () => {
adapters.infrastructure.dispose();
});
it("runtime infrastructure disposal disposes telemetry first", async () => {
const fetcher = vi.fn(async () => new Response(null, { status: 204 }));
const adapters = await createRuntimeAdapters({
runtime: {
...runtime,
config: {
...runtime.config,
TELEMETRY_ENABLED: true,
TELEMETRY_ENDPOINT: "https://telemetry.test/events",
},
} as Runtime,
release,
host: {},
fetcher: fetcher as unknown as typeof fetch,
});
adapters.outputPorts.telemetry.emit("api.request.failed", {
error_kind: "SERVER_FAILURE",
http_status_group: "5xx",
attempt_count_bucket: "1",
route_id: "TEST_ROUTE",
});
expect(adapters.outputPorts.telemetry.pendingCount()).toBe(1);
adapters.infrastructure.dispose();
expect(adapters.outputPorts.telemetry.pendingCount()).toBe(0);
await Promise.resolve();
await Promise.resolve();
expect(fetcher).not.toHaveBeenCalled();
});
it("replaces the QueryClient and coordinator for each session generation", async () => {
const adapters = await createRuntimeAdapters({ runtime, release, host: {} });
const previousClient = adapters.infrastructure.queryClient;
+110 -2
View File
@@ -1,9 +1,31 @@
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { resolveServiceWorkerBuildInput } from "../../scripts/lib/service-worker-build-input.ts";
import {
canonicalStaticManifestBytes,
type StaticAssetRow,
} from "../../src/contracts/service-worker-static-manifest.ts";
const digest = (character: string) => `sha256:${character.repeat(64)}`;
/** SW-05. The gate recomputes this from the shared canonical bytes. */
function setDigestFor(rows: readonly StaticAssetRow[]): string {
return `sha256:${createHash("sha256")
.update(canonicalStaticManifestBytes(rows))
.digest("hex")}`;
}
const ASSET_ROWS: readonly StaticAssetRow[] = Object.freeze([
Object.freeze({
url: "/assets/app.0123456789abcdef.js",
sha256: digest("c"),
bytes: 128,
contentType: "text/javascript",
}),
]);
describe("service worker build input", () => {
const selection = {
mode: "ACTIVE" as const,
@@ -14,8 +36,8 @@ describe("service worker build input", () => {
schemaVersion: 1 as const,
buildId: "build-1",
releaseId: "release-1",
setDigest: digest("a"),
assets: [],
setDigest: setDigestFor(ASSET_ROWS),
assets: [...ASSET_ROWS],
};
it("rejects a direct worker build when ACTIVE selection or generated inputs are absent", () => {
@@ -84,4 +106,90 @@ describe("service worker build input", () => {
releaseManifestUrl: "/release-manifest.json",
});
});
it("rejects asset row or canonical set-digest tampering at build input", () => {
const base = {
selection,
contractSet: { setDigest: digest("b") },
runtimeConfig: { RELEASE_MANIFEST_URL: "/release-manifest.json" },
buildId: "build-1",
releaseId: "release-1",
};
const tampered: readonly Readonly<{
label: string;
assets: unknown;
}>[] = [
{
label: "stale set digest",
assets: { ...assets, setDigest: digest("a") },
},
{
label: "tampered byte length",
assets: {
...assets,
assets: [{ ...ASSET_ROWS[0]!, bytes: 129 }],
},
},
{
label: "cross-origin url",
assets: {
...assets,
assets: [
{ ...ASSET_ROWS[0]!, url: "https://evil.example/a.js" },
],
},
},
{
label: "dot segment",
assets: {
...assets,
assets: [{ ...ASSET_ROWS[0]!, url: "/assets/../a.js" }],
},
},
{
label: "extension and content type mismatch",
assets: {
...assets,
assets: [{ ...ASSET_ROWS[0]!, contentType: "text/css" }],
},
},
{
label: "unknown row field",
assets: {
...assets,
assets: [{ ...ASSET_ROWS[0]!, extra: "smuggled" }],
},
},
{
label: "duplicate url",
assets: {
...assets,
assets: [ASSET_ROWS[0]!, ASSET_ROWS[0]!],
},
},
];
for (const entry of tampered) {
expect(
() =>
resolveServiceWorkerBuildInput({
...base,
assets: entry.assets as never,
}),
entry.label,
).toThrow(TypeError);
}
});
it("accepts generator-shaped output unchanged", () => {
expect(() =>
resolveServiceWorkerBuildInput({
selection,
assets,
contractSet: { setDigest: digest("b") },
runtimeConfig: { RELEASE_MANIFEST_URL: "/release-manifest.json" },
buildId: "build-1",
releaseId: "release-1",
}),
).not.toThrow();
});
});
+494 -5
View File
@@ -49,6 +49,8 @@ function pageContainer(options: { waiting?: boolean; controlled?: boolean } = {}
} as unknown as ServiceWorkerContainer;
return {
container,
waiting,
controlled,
registration,
waitingMessages,
controllerMessages,
@@ -118,6 +120,249 @@ function workerScope() {
return { scope, clients, deleted };
}
describe("service worker static cache authority", () => {
const setDigest = `sha256:${"c".repeat(64)}` as const;
const currentCacheName = `${STATIC_CACHE_PREFIX}${setDigest.slice(
"sha256:".length,
"sha256:".length + 16,
)}`;
function staticRuntime(
caches: Readonly<{
open: (name: string) => Promise<unknown>;
keys: () => Promise<readonly string[]>;
delete: (name: string) => Promise<boolean>;
}>,
) {
return createServiceWorkerRuntime(
{
caches,
clients: { matchAll: vi.fn(async () => []) },
registrationScope: `${ORIGIN}/app/`,
skipWaiting: vi.fn(async () => {}),
fetcher: vi.fn(),
digest: vi.fn(),
} as never,
{
identity: { ...identity, staticAssetSetDigest: setDigest },
handlers: ["PWA_STATIC_ASSETS"],
manifest: {
schemaVersion: 1,
buildId: identity.buildId,
releaseId: identity.releaseId,
setDigest,
// SW-URL-01. Generator output is root-relative.
assets: [
{
url: "/assets/app.0123456789abcdef.js",
sha256: `sha256:${"d".repeat(64)}`,
bytes: 10,
contentType: "text/javascript",
},
],
},
runtimeConfigUrl: "/runtime-config.json",
releaseManifestUrl: "/release-manifest.json",
},
);
}
it("classifies a generated root-relative asset against an absolute Request URL", async () => {
const currentResponse = new Response("current", { status: 200 });
const opened: string[] = [];
const runtime = staticRuntime({
open: vi.fn(async (name: string) => {
opened.push(name);
return {
match: async () => currentResponse.clone(),
delete: async () => true,
};
}),
keys: vi.fn(async () => [currentCacheName]),
delete: vi.fn(async () => true),
});
const served = await runtime.onFetch({
method: "GET",
url: `${ORIGIN}/assets/app.0123456789abcdef.js`,
});
expect(served).not.toBeNull();
expect(opened).toEqual([currentCacheName]);
});
it("matches static responses only in the current release cache", async () => {
const previousCacheName = `${STATIC_CACHE_PREFIX}${"e".repeat(16)}`;
const previousMatch = vi.fn(
async () => new Response("previous", { status: 200 }),
);
const deleteEntry = async (): Promise<boolean> => true;
const emptyMatch = async (): Promise<Response | undefined> => undefined;
const previousCache = { match: previousMatch, delete: deleteEntry };
const emptyCache = { match: emptyMatch, delete: deleteEntry };
const runtime = staticRuntime({
open: vi.fn(async (name: string) =>
name === previousCacheName ? previousCache : emptyCache,
),
keys: vi.fn(async () => [currentCacheName, previousCacheName]),
delete: vi.fn(async () => true),
});
// Only the previous cache holds the entry, so the request falls through to
// the network rather than serving a stale release.
await expect(
runtime.onFetch({
method: "GET",
url: `${ORIGIN}/assets/app.0123456789abcdef.js`,
}),
).resolves.toBeNull();
expect(previousMatch).not.toHaveBeenCalled();
});
it("deletes an invalid hit only from the current release cache", async () => {
const deletes: string[] = [];
const runtime = staticRuntime({
open: vi.fn(async (name: string) => {
const recordDelete = async (url: string): Promise<boolean> => {
deletes.push(`${name}:${url}`);
return true;
};
return {
match: async () => new Response("bad", { status: 500 }),
delete: recordDelete,
};
}),
keys: vi.fn(async () => [currentCacheName]),
delete: vi.fn(async () => true),
});
await expect(
runtime.onFetch({
method: "GET",
url: `${ORIGIN}/assets/app.0123456789abcdef.js`,
}),
).resolves.toBeNull();
expect(deletes).toEqual([
`${currentCacheName}:${ORIGIN}/assets/app.0123456789abcdef.js`,
]);
});
});
describe("service worker exact ownership and truthful removal", () => {
it("deletes only exact owned static cache names", async () => {
const fixture = workerScope();
fixture.scope.caches.keys = vi.fn(async () => [
`${STATIC_CACHE_PREFIX}${"a".repeat(16)}`,
`${STATIC_CACHE_PREFIX}${"b".repeat(16)}`,
// SW-02. Same prefix, not owned.
`${STATIC_CACHE_PREFIX}not-owned`,
`${STATIC_CACHE_PREFIX}${"c".repeat(17)}`,
`${STATIC_CACHE_PREFIX}${"A".repeat(16)}`,
"foreign-cache",
]);
const runtime = createServiceWorkerRuntime(fixture.scope as never, {
identity,
handlers: [],
manifest: null,
runtimeConfigUrl: "/runtime-config.json",
releaseManifestUrl: "/release-manifest.json",
});
const request = createServiceWorkerMessage({
kind: "CACHE_RESET_REQUEST",
sourceBuildId: "page-build",
targetBuildId: identity.buildId,
nonce: "nonce-reset-0001",
});
await runtime.onCacheResetRequest(request, fixture.clients[0] as never);
expect(fixture.deleted).toEqual([
`${STATIC_CACHE_PREFIX}${"a".repeat(16)}`,
`${STATIC_CACHE_PREFIX}${"b".repeat(16)}`,
]);
});
it.each([
{
label: "unregister false",
unregister: async () => false,
expected: { kind: "FAILED" },
},
{
label: "unregister rejects",
unregister: async () => {
throw new TypeError("unregister exploded");
},
expected: { kind: "FAILED" },
},
{
label: "unregister true",
unregister: async () => true,
expected: { kind: "DISABLED" },
},
])(
"does not hide $label behind DISABLED",
async ({ unregister, expected }) => {
const registration = {
scope: `${ORIGIN}/`,
installing: null,
waiting: null,
active: { scriptURL: SCRIPT_URL },
unregister: vi.fn(unregister),
update: vi.fn(async () => {}),
} as unknown as ServiceWorkerRegistration;
const controller = createServiceWorkerPageController({
container: {
controller: null,
register: vi.fn(),
getRegistration: vi.fn(async () => registration),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
} as never,
routerBasePath: "/",
origin: ORIGIN,
selection: {
mode: "REMOVE_REGISTRATION",
scriptPath: "service-worker.js",
handlers: [],
},
} as never);
await expect(controller.start()).resolves.toMatchObject(expected);
},
);
it("reports an ownership mismatch as INCOMPATIBLE rather than DISABLED", async () => {
const foreign = {
scope: `${ORIGIN}/`,
installing: null,
waiting: null,
active: { scriptURL: `${ORIGIN}/someone-else.js` },
unregister: vi.fn(async () => true),
update: vi.fn(async () => {}),
} as unknown as ServiceWorkerRegistration;
const controller = createServiceWorkerPageController({
container: {
controller: null,
register: vi.fn(),
getRegistration: vi.fn(async () => foreign),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
} as never,
routerBasePath: "/",
origin: ORIGIN,
selection: {
mode: "REMOVE_REGISTRATION",
scriptPath: "service-worker.js",
handlers: [],
},
} as never);
await expect(controller.start()).resolves.toMatchObject({
kind: "INCOMPATIBLE",
});
expect(foreign.unregister).not.toHaveBeenCalled();
});
});
describe("service worker page protocol", () => {
it("does not attach late listeners when stopped during registration", async () => {
const browser = pageContainer();
@@ -141,7 +386,12 @@ describe("service worker page protocol", () => {
const browser = pageContainer({ waiting: true });
const controller = pageController(browser.container);
await controller.start();
const source = { postMessage: vi.fn() };
// SW-06. Replies are correlated by source identity, so the fake request
// comes from the registration's waiting worker.
const source = browser.waiting as unknown as {
postMessage(message: unknown): void;
};
const sourceMessages = vi.spyOn(source, "postMessage");
browser.dispatch(
createServiceWorkerMessage({
@@ -153,7 +403,7 @@ describe("service worker page protocol", () => {
source,
);
expect(source.postMessage).toHaveBeenCalledWith(
expect(sourceMessages).toHaveBeenCalledWith(
expect.objectContaining({
kind: "CLIENT_DRAINED",
sourceBuildId: "page-build",
@@ -201,7 +451,7 @@ describe("service worker page protocol", () => {
nonce: request.nonce,
}),
cachesDeleted: 2,
});
}, browser.controlled ?? undefined);
await expect(result).resolves.toEqual({ kind: "RESET", cachesDeleted: 2 });
await controller.stop();
@@ -457,7 +707,7 @@ describe("service worker static asset install", () => {
it("aborts and rolls back a candidate cache at the overall install deadline", async () => {
vi.useFakeTimers();
const deleteCache = vi.fn(async () => true);
const deleteCache = vi.fn(async (_name: string) => true);
const fetcher = vi.fn(
(_input: RequestInfo | URL, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
@@ -482,7 +732,47 @@ describe("service worker static asset install", () => {
code: "INSTALL_DEADLINE_EXCEEDED",
});
expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
expect(deleteCache).toHaveBeenCalledTimes(1);
// SW-09. The public result closes at the deadline with one exact delete,
// and a second exact delete is registered once the abandoned install work
// actually settles. Both target the same owned candidate cache.
await vi.advanceTimersByTimeAsync(0);
expect(deleteCache).toHaveBeenCalledTimes(2);
expect(new Set(deleteCache.mock.calls.map((call) => call[0])).size).toBe(1);
vi.useRealTimers();
});
it("observes and cleans non-cooperative late install work", async () => {
vi.useFakeTimers();
const deleteCache = vi.fn(async (_name: string) => true);
const cancel = vi.fn(async () => {});
let releaseFetch: ((response: Response) => void) | undefined;
// A fetch that ignores the abort signal entirely.
const fetcher = vi.fn(
() =>
new Promise<Response>((resolve) => {
releaseFetch = resolve;
}),
);
const result = installStaticAssets(manifest, {
caches: {
open: vi.fn(async () => ({ put: vi.fn() }) as unknown as Cache),
delete: deleteCache,
},
fetcher: fetcher as typeof fetch,
digest: vi.fn(),
});
await vi.advanceTimersByTimeAsync(SERVICE_WORKER_BOUNDS.installDeadlineMs);
await expect(result).resolves.toEqual({
kind: "REJECTED",
code: "INSTALL_DEADLINE_EXCEEDED",
});
// The late response arrives after the public bound; its body is cancelled
// and no unhandled rejection escapes.
releaseFetch?.({ body: { cancel } } as unknown as Response);
await vi.advanceTimersByTimeAsync(0);
expect(cancel).toHaveBeenCalledOnce();
vi.useRealTimers();
});
@@ -564,4 +854,203 @@ describe("service worker static asset install", () => {
vi.useRealTimers();
}
});
it("accepts replies only from the captured waiting or controller source", async () => {
const browser = pageContainer({ waiting: true });
const controller = pageController(browser.container);
await controller.start();
const foreign = { postMessage: vi.fn() };
// SW-06. A same-origin but unrecognised source must not close admission.
browser.dispatch(
createServiceWorkerMessage({
kind: "CLIENT_DRAIN_REQUEST",
sourceBuildId: "worker-build",
targetBuildId: "page-build",
nonce: "drain-foreign",
}),
foreign,
);
expect(foreign.postMessage).not.toHaveBeenCalled();
await controller.stop();
});
it("coalesces concurrent activation and reset commands", async () => {
const browser = pageContainer({ waiting: true });
const controller = pageController(browser.container);
await controller.start();
// SW-06. Ten concurrent callers issue exactly one request.
const activations = Array.from({ length: 10 }, () =>
controller.requestActivation(),
);
await Promise.resolve();
expect(browser.waitingMessages).toHaveLength(1);
expect(new Set(activations).size).toBe(1);
await controller.stop();
await Promise.allSettled(activations);
});
it("ends an activation whose reply source was swapped", async () => {
const browser = pageContainer({ waiting: true });
const controller = pageController(browser.container);
await controller.start();
const activation = controller.requestActivation();
await Promise.resolve();
const request = browser.waitingMessages.at(-1) as { nonce?: string };
// A different worker answers: terminate immediately rather than waiting for
// the drain timeout.
browser.dispatch(
createServiceWorkerMessage({
kind: "ACTIVATED_RELOAD_REQUIRED",
sourceBuildId: "worker-build",
targetBuildId: "page-build",
nonce: request.nonce,
}),
{ postMessage: vi.fn() },
);
await expect(activation).resolves.toEqual({ kind: "PROTOCOL_MISMATCH" });
await controller.stop();
});
});
/**
* SW-01. The marker read must be bounded in bytes, not only in logic. Adding a
* whole chunk and *then* comparing the running total meant a corrupt body could
* hand activation a 1 MiB chunk against a 257-byte ceiling, and the reader lock
* was never released.
*/
describe("SW-01 the activation marker read is bounded in bytes", () => {
function scopeWithMarkerBody(
body: ReadableStream<Uint8Array> | null,
headers: Readonly<Record<string, string>> = {},
) {
const markerUrl = "__service-worker-activation-v1__";
const response = body
? new Response(body, { status: 200, headers })
: new Response("", { status: 200, headers });
const cache = {
match: vi.fn(async (request: RequestInfo | URL) =>
String(request).includes(markerUrl) ? response : undefined,
),
put: vi.fn(async () => {}),
delete: vi.fn(async () => true),
} as unknown as Cache;
return {
response,
scope: {
caches: {
open: vi.fn(async () => cache),
// The current static cache must exist for its marker to be read.
keys: vi.fn(async () => [
staticCacheName(identity.staticAssetSetDigest),
]),
delete: vi.fn(async () => true),
match: vi.fn(),
},
clients: { matchAll: vi.fn(async () => []) },
skipWaiting: vi.fn(async () => {}),
fetcher: vi.fn(),
digest: vi.fn(),
},
};
}
const runtimeFor = (scope: unknown) =>
createServiceWorkerRuntime(scope as never, {
identity,
handlers: ["PWA_STATIC_ASSETS"],
manifest: {
schemaVersion: 1,
buildId: identity.buildId,
releaseId: identity.releaseId,
setDigest: identity.staticAssetSetDigest as `sha256:${string}`,
assets: [],
},
runtimeConfigUrl: "/runtime-config.json",
releaseManifestUrl: "/release-manifest.json",
});
it("never retains a single chunk larger than the marker ceiling", async () => {
let delivered = 0;
let cancels = 0;
const oversized = new ReadableStream<Uint8Array>({
pull(controller) {
delivered += 1;
controller.enqueue(new Uint8Array(1_048_576));
},
cancel() {
cancels += 1;
},
});
const fixture = scopeWithMarkerBody(oversized);
// Activation still completes; the marker is simply not admitted.
await expect(runtimeFor(fixture.scope).onActivate()).resolves.toBeTypeOf(
"number",
);
// Every oversized chunk that arrived was refused before being retained,
// and the reader that saw it was cancelled. The marker is probed once per
// candidate cache, so the counts track each other rather than a constant.
expect(cancels).toBeGreaterThanOrEqual(1);
expect(delivered).toBeLessThanOrEqual(2);
expect(fixture.response.body?.locked).toBe(false);
});
it("bounds a stream that never produces a chunk", async () => {
let cancels = 0;
const stalled = new ReadableStream<Uint8Array>({
pull() {
return new Promise<void>(() => {});
},
cancel() {
cancels += 1;
},
});
const fixture = scopeWithMarkerBody(stalled);
await expect(
runtimeFor(fixture.scope).onActivate(),
).resolves.toBeTypeOf("number");
expect(cancels).toBeGreaterThanOrEqual(1);
}, 10_000);
it("cancels the body it refuses for a declared oversize", async () => {
let cancels = 0;
const declared = new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(new Uint8Array(8));
},
cancel() {
cancels += 1;
},
});
const fixture = scopeWithMarkerBody(declared, {
"content-length": "1048576",
});
await expect(
runtimeFor(fixture.scope).onActivate(),
).resolves.toBeTypeOf("number");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(cancels).toBeGreaterThanOrEqual(1);
});
it("refuses a marker body that is not valid UTF-8", async () => {
const invalid = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array([0xff, 0xfe, 0xfd]));
controller.close();
},
});
const fixture = scopeWithMarkerBody(invalid);
await expect(
runtimeFor(fixture.scope).onActivate(),
).resolves.toBeTypeOf("number");
expect(fixture.response.body?.locked).toBe(false);
});
});
@@ -0,0 +1,176 @@
import { describe, expect, it, vi } from "vitest";
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import {
CACHEABLE_ASSET_CONTENT_TYPES,
decodeStaticAssetManifest,
isCanonicalStaticAssetUrl,
} from "../../src/contracts/service-worker-static-manifest.ts";
/**
* SW-RR-03. The build generator and the shared runtime decoder must agree on
* exactly which asset kinds exist. A generator that emits `.mjs` or `.png` while
* the decoder refuses them turns a correct build into a runtime contract
* failure, and the reverse admits a kind no build produces.
*/
describe("SW-RR-03 one authoritative cacheable asset table", () => {
it("covers every extension the generator emits", async () => {
const generator = await import(
"../../scripts/generate-service-worker-assets.ts"
);
expect(generator).toBeDefined();
for (const extension of [
".js",
".mjs",
".css",
".woff2",
".svg",
".png",
".webp",
]) {
expect(CACHEABLE_ASSET_CONTENT_TYPES[extension]).toBeDefined();
}
});
it("decodes a manifest row for every table entry", () => {
const assets = Object.entries(CACHEABLE_ASSET_CONTENT_TYPES).map(
([extension, contentType], index) => ({
url: `/assets/name-abcdefgh${index}${extension}`,
sha256: `sha256:${"a".repeat(64)}`,
bytes: 16,
contentType,
}),
);
const decoded = decodeStaticAssetManifest({
schemaVersion: 1,
buildId: "build-1",
releaseId: "release-1",
assets,
setDigest: `sha256:${"b".repeat(64)}`,
});
expect(decoded).toMatchObject({ ok: true });
});
it("refuses a row whose extension is not in the table", () => {
const decoded = decodeStaticAssetManifest({
schemaVersion: 1,
buildId: "build-1",
releaseId: "release-1",
setDigest: `sha256:${"b".repeat(64)}`,
assets: [
{
url: "/assets/control-abcdefgh.json",
sha256: `sha256:${"a".repeat(64)}`,
bytes: 16,
contentType: "application/json",
},
],
});
expect(decoded.ok).toBe(false);
});
});
/**
* SW-02. Sharing only the extension table left the generator and the decoder
* with different path grammars: the generator emitted a URL for a directory
* containing an `@` or a space, and the decoder then refused the manifest it
* had just produced, failing the release build at install time.
*/
describe("SW-02 the generator and the decoder share one path grammar", () => {
async function distWith(
files: Readonly<Record<string, string>>,
): Promise<string> {
const root = await mkdtemp(path.join(tmpdir(), "sw-assets-"));
for (const [relative, content] of Object.entries(files)) {
const absolute = path.join(root, relative);
await mkdir(path.dirname(absolute), { recursive: true });
await writeFile(absolute, content);
}
return root;
}
it("emits a manifest the runtime decoder accepts for every table entry", async () => {
const { collectStaticAssets } = await import(
"../../scripts/generate-service-worker-assets.ts"
);
const files: Record<string, string> = {};
for (const [index, extension] of Object.keys(
CACHEABLE_ASSET_CONTENT_TYPES,
).entries()) {
files[`assets/name-abcdefg${index}${extension}`] = `content-${index}`;
}
files["assets/nested/deep/name-abcdefgz.js"] = "nested";
const root = await distWith(files);
const manifest = await collectStaticAssets(root, "build-1", "release-1");
expect(manifest.assets.length).toBe(
Object.keys(CACHEABLE_ASSET_CONTENT_TYPES).length + 1,
);
expect(decodeStaticAssetManifest(manifest)).toMatchObject({ ok: true });
for (const asset of manifest.assets) {
expect(isCanonicalStaticAssetUrl(asset.url)).toBe(true);
}
});
it.each([
{ label: "an at sign", name: "bad@name-abcdefgh.js" },
{ label: "a space", name: "bad name-abcdefgh.js" },
{ label: "a percent escape", name: "bad%20name-abcdefgh.js" },
{ label: "a hash", name: "bad#name-abcdefgh.js" },
])(
"stops the build rather than emitting a path the decoder refuses ($label)",
async ({ name }) => {
const { collectStaticAssets } = await import(
"../../scripts/generate-service-worker-assets.ts"
);
const root = await distWith({
"assets/good-abcdefgh.js": "ok",
[`assets/${name}`]: "bad",
});
await expect(
collectStaticAssets(root, "build-1", "release-1"),
).rejects.toThrow(/not canonical/u);
},
);
it("agrees with the decoder on the whole path policy table", () => {
const cases: readonly (readonly [string, boolean])[] = [
["/assets/name-abcdefgh.js", true],
["/assets/nested/name-abcdefgh.js", true],
["/assets/bad@name-abcdefgh.js", false],
["/assets/bad name-abcdefgh.js", false],
["/assets/bad%20name-abcdefgh.js", false],
["/assets/bad#name-abcdefgh.js", false],
["/assets/../name-abcdefgh.js", false],
["/assets/./name-abcdefgh.js", false],
["assets/name-abcdefgh.js", false],
["/assets/\\name-abcdefgh.js", false],
["/assets/naïve-abcdefgh.js", false],
];
for (const [url, canonical] of cases) {
expect(isCanonicalStaticAssetUrl(url), url).toBe(canonical);
const decoded = decodeStaticAssetManifest({
schemaVersion: 1,
buildId: "build-1",
releaseId: "release-1",
setDigest: `sha256:${"a".repeat(64)}`,
assets: [
{
url,
sha256: `sha256:${"a".repeat(64)}`,
bytes: 4,
contentType: "text/javascript",
},
],
});
// The decoder still owns the digest check, so a canonical path may fail
// for other reasons; a non-canonical one must always fail.
if (!canonical) expect(decoded.ok, url).toBe(false);
}
});
});
+119
View File
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { createDiagnosticsAdapter } from "../../src/adapters/diagnostics/bounded-diagnostics.ts";
import {
createTelemetryAdapter,
safeTraceparent,
@@ -236,4 +237,122 @@ describe("best-effort telemetry adapter", () => {
expect(adapter.pendingCount()).toBe(0);
expect(remove).toHaveBeenCalledWith("pagehide", expect.any(Function));
});
it("drops queued events and invalidates scheduled callbacks on dispose", async () => {
const callbacks: Array<() => void> = [];
const fetcher = vi.fn(async () => new Response(null, { status: 204 }));
const adapter = createTelemetryAdapter({
enabled: true,
endpoint: "https://telemetry.test/events",
schedule: (callback) => callbacks.push(callback),
fetcher,
});
adapter.emit("api.request.failed", validAttributes);
expect(adapter.pendingCount()).toBe(1);
adapter.dispose();
expect(adapter.pendingCount()).toBe(0);
for (const callback of callbacks) callback();
await Promise.resolve();
expect(fetcher).not.toHaveBeenCalled();
// Disposal is a silent shutdown, not a recursive drop event.
expect(adapter.dropReasons()).toEqual({});
});
it("ignores emit after dispose", async () => {
const fetcher = vi.fn(async () => new Response(null, { status: 204 }));
const adapter = createTelemetryAdapter({
enabled: true,
endpoint: "https://telemetry.test/events",
schedule: () => {},
fetcher,
});
adapter.dispose();
adapter.emit("api.request.failed", validAttributes);
await adapter.flush();
expect(adapter.pendingCount()).toBe(0);
expect(fetcher).not.toHaveBeenCalled();
});
it("aborts an in-flight sink and prevents post-dispose rescheduling", async () => {
const callbacks: Array<() => void> = [];
let observedSignal: AbortSignal | undefined;
let releaseSink: (() => void) | undefined;
const fetcher = vi.fn(async (_input: unknown, init?: RequestInit) => {
observedSignal = init?.signal ?? undefined;
await new Promise<void>((resolve) => {
releaseSink = resolve;
});
return new Response(null, { status: 204 });
});
const adapter = createTelemetryAdapter({
enabled: true,
endpoint: "https://telemetry.test/events",
schedule: (callback) => callbacks.push(callback),
fetcher: fetcher as unknown as typeof fetch,
});
adapter.emit("api.request.failed", validAttributes);
callbacks.splice(0).forEach((callback) => callback());
await vi.waitFor(() => expect(fetcher).toHaveBeenCalledOnce());
adapter.dispose();
expect(observedSignal?.aborted).toBe(true);
// The sink ignored the abort and settles late.
releaseSink?.();
await Promise.resolve();
await Promise.resolve();
expect(callbacks).toHaveLength(0);
expect(fetcher).toHaveBeenCalledOnce();
});
it("joins an already active flush", async () => {
let releaseSink: (() => void) | undefined;
const fetcher = vi.fn(async () => {
await new Promise<void>((resolve) => {
releaseSink = resolve;
});
return new Response(null, { status: 204 });
});
const adapter = createTelemetryAdapter({
enabled: true,
endpoint: "https://telemetry.test/events",
schedule: () => {},
fetcher: fetcher as unknown as typeof fetch,
});
adapter.emit("api.request.failed", validAttributes);
const first = adapter.flush();
await vi.waitFor(() => expect(fetcher).toHaveBeenCalledOnce());
let secondSettled = false;
const second = adapter.flush().then(() => {
secondSettled = true;
});
await Promise.resolve();
expect(secondSettled).toBe(false);
releaseSink?.();
await first;
await second;
expect(secondSettled).toBe(true);
adapter.dispose();
});
it.each([Number.NaN, Number.POSITIVE_INFINITY, 0, -1, 1.5])(
"rejects invalid telemetry and diagnostics capacity %s",
(value) => {
expect(() =>
createTelemetryAdapter({
enabled: true,
endpoint: "https://telemetry.test/events",
maxQueue: value,
}),
).toThrow(TypeError);
expect(() => createDiagnosticsAdapter({ maxEntries: value })).toThrow(
TypeError,
);
},
);
});
+31
View File
@@ -42,6 +42,37 @@ function manualScheduler() {
}
describe("Web Push durable control fence", () => {
it.each([0, 2, 3, 9_999])(
"rejects a CAS receipt that is not the exact next revision (%i)",
async (revision) => {
// WP-01. Only the exact next revision is evidence that this command
// actually wrote the control it claims to have written.
const dependencies = createFakePushControlStoreDependencies();
const repository = dependencies.repository;
const compareAndSwap = repository.compareAndSwap.bind(repository);
repository.compareAndSwap = async (input) => {
const written = await compareAndSwap(input);
return written.ok
? {
ok: true as const,
value: { ...written.value, revision },
}
: written;
};
const store = createPushAssociationFenceStore(dependencies);
await expect(
store.prepare({
authority: firstAuthority,
updatedAt: "2026-07-28T00:00:00.000Z",
}),
).resolves.toMatchObject({
ok: false,
error: { code: "CONTROL_CORRUPT" },
});
},
);
it("CASes UNASSOCIATED to ACTIVE and prevents tombstone resurrection", async () => {
const store = createPushAssociationFenceStore(
createFakePushControlStoreDependencies(),
+213
View File
@@ -445,6 +445,8 @@ describe("Web Push worker runtime", () => {
event: "web_push_subscription_rotated",
outcome: "DEGRADED",
reason: "DEADLINE_EXCEEDED",
countBucket: expect.any(String),
truncated: expect.any(Boolean),
});
runtime.dispose();
});
@@ -489,6 +491,8 @@ describe("Web Push worker runtime", () => {
event: "web_push_subscription_rotated",
outcome: "DEGRADED",
reason: "ABORTED",
countBucket: expect.any(String),
truncated: expect.any(Boolean),
});
const throwingStore = await activeFence();
@@ -540,6 +544,8 @@ describe("Web Push worker runtime", () => {
event: "web_push_subscription_rotated",
outcome: "DEGRADED",
reason: "ABORTED",
countBucket: expect.any(String),
truncated: expect.any(Boolean),
});
throwingRuntime.dispose();
});
@@ -615,3 +621,210 @@ describe("Web Push worker runtime", () => {
expect(listeners.size).toBe(0);
});
});
/**
* WP-01. One click is one terminal record. The adapter emitted the terminal
* event from inside `process` *and* again from the `waitUntil` wrapper, so an
* ordinary click was counted twice. A late rejection also downgraded the
* certainty from `MAYBE_APPLIED` to `NOT_APPLIED`, telling operators the click
* had definitely not been applied when nobody knew that, and the late
* observation ran outside `waitUntil`, so a worker shutdown lost the evidence.
*/
describe("WP-01 the click handler has one observation authority", () => {
type Observation = Readonly<{
event: string;
outcome: string;
reason?: string;
nativeEffect?: string;
}>;
async function clickAdapterWith(
clients: Readonly<{
matchControlledWindowClients(): Promise<readonly unknown[]>;
openWindow(target: string): Promise<unknown>;
}>,
scheduler?: TimeoutScheduler,
) {
const store = await activeFence();
const observations: Observation[] = [];
const adapter = createNotificationClickAdapter({
fenceStore: store,
registry,
origin: "https://app.example.test",
now: () => now,
clients: clients as never,
observer: {
record(observation) {
observations.push(observation as Observation);
},
},
...(scheduler ? { scheduler } : {}),
});
return { adapter, observations };
}
const dispatched = (observations: readonly Observation[]) =>
observations.filter(
(observation) => observation.event === "web_push_click_dispatched",
);
it("records exactly one terminal event for an ordinary focus", async () => {
const focus = vi.fn(async () => {});
const { adapter, observations } = await clickAdapterWith({
async matchControlledWindowClients() {
return [
{
url: "https://app.example.test/current",
focus,
postMessage() {},
},
];
},
openWindow: vi.fn(async () => null),
});
let waited: Promise<void> | null = null;
const result = await adapter.handle({
notification: { data: clickData(), close() {} },
waitUntil(task) {
waited = task;
},
});
await waited;
expect(result).toEqual({ ok: true, value: undefined });
expect(dispatched(observations)).toEqual([
{
event: "web_push_click_dispatched",
outcome: "SUCCEEDED",
nativeEffect: "CONFIRMED",
},
]);
});
it("confirms NOT_APPLIED only for an explicit null window", async () => {
const { adapter, observations } = await clickAdapterWith({
async matchControlledWindowClients() {
return [];
},
openWindow: vi.fn(async () => null),
});
let waited: Promise<void> | null = null;
const result = await adapter.handle({
notification: { data: clickData(), close() {} },
waitUntil(task) {
waited = task;
},
});
await waited;
expect(result.ok).toBe(false);
expect(dispatched(observations)).toEqual([
expect.objectContaining({
outcome: "FAILED",
nativeEffect: "NOT_APPLIED",
}),
]);
});
it("keeps MAYBE_APPLIED when the native effect rejects after the deadline", async () => {
const clock = manualScheduler();
let rejectFocus: ((reason: unknown) => void) | undefined;
const { adapter, observations } = await clickAdapterWith(
{
async matchControlledWindowClients() {
return [
{
url: "https://app.example.test/current",
focus: () =>
new Promise<void>((_resolve, reject) => {
rejectFocus = reject;
}),
postMessage() {},
},
];
},
openWindow: vi.fn(async () => null),
},
clock.scheduler,
);
let waited: Promise<void> | null = null;
const handling = adapter.handle({
notification: { data: clickData(), close() {} },
waitUntil(task) {
waited = task;
},
});
await vi.waitFor(() => expect(rejectFocus).toBeDefined());
clock.expireAll();
const result = await handling;
expect(result.ok).toBe(false);
// The effect lands only now, after the terminal result.
rejectFocus?.(new Error("focus failed late"));
await waited;
const records = dispatched(observations);
expect(records).toHaveLength(2);
// The evidence record never claims the click was definitely not applied.
expect(records.at(-1)).toEqual({
event: "web_push_click_dispatched",
outcome: "DEGRADED",
reason: "ABORTED",
nativeEffect: "MAYBE_APPLIED",
});
});
it("waits for the late effect evidence inside waitUntil", async () => {
const clock = manualScheduler();
let resolveFocus: (() => void) | undefined;
const { adapter, observations } = await clickAdapterWith(
{
async matchControlledWindowClients() {
return [
{
url: "https://app.example.test/current",
focus: () =>
new Promise<void>((resolve) => {
resolveFocus = resolve;
}),
postMessage() {},
},
];
},
openWindow: vi.fn(async () => null),
},
clock.scheduler,
);
let waited: Promise<void> | null = null;
const handling = adapter.handle({
notification: { data: clickData(), close() {} },
waitUntil(task) {
waited = task;
},
});
await vi.waitFor(() => expect(resolveFocus).toBeDefined());
clock.expireAll();
await handling;
let waitUntilSettled = false;
const pendingWait = waited as Promise<void> | null;
void pendingWait?.then(() => {
waitUntilSettled = true;
});
await Promise.resolve();
await Promise.resolve();
// The handler's lifetime is still open because the effect has not landed.
expect(waitUntilSettled).toBe(false);
resolveFocus?.();
await waited;
expect(
dispatched(observations).at(-1),
).toEqual({
event: "web_push_click_dispatched",
outcome: "DEGRADED",
reason: "ABORTED",
nativeEffect: "CONFIRMED",
});
});
});