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>
947 lines
26 KiB
TypeScript
947 lines
26 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type {
|
|
BrowserManagedDownloadCapabilityReceipt,
|
|
BrowserManagedDownloadCapabilityResolver,
|
|
DownloadDeliveryPort,
|
|
DownloadSource,
|
|
DownloadStrategy,
|
|
FileByteSource,
|
|
} from "../../src/application/ports/browser-file-storage/file.ts";
|
|
import {
|
|
DEFAULT_OBJECT_URL_RELEASE_GRACE_MS,
|
|
createDownloadDeliveryAdapter,
|
|
type SaveFileHandle,
|
|
} from "../../src/adapters/browser-files/download-delivery-adapter.ts";
|
|
import { ObjectUrlLeaseRegistry } from "../../src/adapters/browser-files/object-url-lease.ts";
|
|
import {
|
|
BrowserFilePolicyRegistry,
|
|
browserFilePolicyReference,
|
|
} from "../../src/adapters/browser-files/browser-file-policy-registry.ts";
|
|
|
|
const HARD_LIMITS = Object.freeze({
|
|
hardMaxObjectUrlBytes: 64,
|
|
hardMaxTransferBytes: 128,
|
|
});
|
|
const capabilityReceipt =
|
|
"capability:download-1" as BrowserManagedDownloadCapabilityReceipt;
|
|
const policyByStrategy = Object.freeze({
|
|
BROWSER_MANAGED: browserFilePolicyReference(
|
|
"download",
|
|
"browser-managed-pdf",
|
|
),
|
|
PROMPT_AND_STREAM: browserFilePolicyReference(
|
|
"download",
|
|
"prompt-and-stream-pdf",
|
|
),
|
|
BOUNDED_OBJECT_URL: browserFilePolicyReference(
|
|
"download",
|
|
"bounded-object-url-pdf",
|
|
),
|
|
});
|
|
const policies = new BrowserFilePolicyRegistry({
|
|
profiles: (
|
|
Object.entries(policyByStrategy) as Array<
|
|
[DownloadStrategy, (typeof policyByStrategy)[DownloadStrategy]]
|
|
>
|
|
).map(([strategy, reference]) => ({
|
|
reference,
|
|
download: {
|
|
strategy,
|
|
mediaType: "application/pdf",
|
|
safeExtension: ".pdf",
|
|
maxTransferBytes: 32,
|
|
maxBufferedBytes: 16,
|
|
integrity: "OPTIONAL" as const,
|
|
},
|
|
})),
|
|
hardLimits: {
|
|
maxInspectionBytes: 64,
|
|
maxRetainedFileBytes: 128,
|
|
maxPreviewBytes: 64,
|
|
maxObjectUrlBytes: 64,
|
|
maxTransferBytes: 128,
|
|
},
|
|
});
|
|
|
|
function capabilityResolver(
|
|
href: (resourceId: string) => string = () => "/unused",
|
|
overrides: Readonly<
|
|
Partial<{
|
|
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
|
|
resourceId: string;
|
|
mediaType: string;
|
|
safeExtension: string;
|
|
maxBytes: number;
|
|
expectedSha256: string;
|
|
expiresAtEpochMs: number;
|
|
}>
|
|
> = {},
|
|
): BrowserManagedDownloadCapabilityResolver {
|
|
return {
|
|
resolve(input) {
|
|
return {
|
|
ok: true,
|
|
value: {
|
|
capabilityReceipt:
|
|
overrides.capabilityReceipt ??
|
|
input.capabilityReceipt,
|
|
href: href(input.resourceId),
|
|
resourceId:
|
|
overrides.resourceId ?? input.resourceId,
|
|
mediaType:
|
|
overrides.mediaType ?? "application/pdf",
|
|
safeExtension: overrides.safeExtension ?? ".pdf",
|
|
maxBytes: overrides.maxBytes ?? 32,
|
|
...(overrides.expectedSha256
|
|
? { expectedSha256: overrides.expectedSha256 }
|
|
: {}),
|
|
expiresAtEpochMs:
|
|
overrides.expiresAtEpochMs ??
|
|
Number.MAX_SAFE_INTEGER,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
function successfulChunk(bytes: Uint8Array) {
|
|
return Object.freeze({ ok: true as const, value: bytes });
|
|
}
|
|
|
|
function byteSource(
|
|
chunks: readonly Uint8Array[],
|
|
byteLength: number | null = chunks.reduce(
|
|
(total, chunk) => total + chunk.byteLength,
|
|
0,
|
|
),
|
|
afterChunk?: (index: number) => void,
|
|
): FileByteSource {
|
|
return Object.freeze({
|
|
byteLength,
|
|
async *stream(
|
|
signal: AbortSignal,
|
|
): AsyncIterable<ReturnType<typeof successfulChunk>> {
|
|
for (const [index, chunk] of chunks.entries()) {
|
|
if (signal.aborted) {
|
|
throw new DOMException("aborted", "AbortError");
|
|
}
|
|
yield successfulChunk(chunk);
|
|
afterChunk?.(index);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function deliveryInput(
|
|
source: DownloadSource,
|
|
strategy: DownloadStrategy,
|
|
signal = new AbortController().signal,
|
|
) {
|
|
return {
|
|
policy: policyByStrategy[strategy],
|
|
source,
|
|
suggestedFileName: "../../invoice.exe.pdf",
|
|
maxTransferBytes: 32,
|
|
maxBufferedBytes: 16,
|
|
signal,
|
|
onProgress: vi.fn(),
|
|
} satisfies Parameters<DownloadDeliveryPort["deliver"]>[0];
|
|
}
|
|
|
|
function writableHandle(events: string[]): SaveFileHandle {
|
|
return Object.freeze({
|
|
async createWritable() {
|
|
return new WritableStream<Uint8Array>({
|
|
write(chunk) {
|
|
events.push(`write:${chunk.byteLength}`);
|
|
},
|
|
close() {
|
|
events.push("close");
|
|
},
|
|
abort() {
|
|
events.push("abort");
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("browser download delivery", () => {
|
|
it("captures composition dependencies instead of re-reading mutable option objects", async () => {
|
|
const originalHandoff = vi.fn();
|
|
const replacedHandoff = vi.fn();
|
|
const originalResolve = vi.fn(
|
|
capabilityResolver(
|
|
() => "/downloads/composition-bound",
|
|
).resolve,
|
|
);
|
|
const replacedResolve = vi.fn(
|
|
capabilityResolver(
|
|
() => "https://evil.example/replaced",
|
|
).resolve,
|
|
);
|
|
const host = { handoff: originalHandoff };
|
|
const browserManagedCapabilities = {
|
|
resolve: originalResolve,
|
|
};
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host,
|
|
browserManagedCapabilities,
|
|
baseOrigin: "https://app.example",
|
|
createTransferId: () => "transfer:composition",
|
|
userActivation: { isActive: true },
|
|
});
|
|
|
|
host.handoff = replacedHandoff;
|
|
browserManagedCapabilities.resolve = replacedResolve;
|
|
|
|
expect(
|
|
await adapter.deliver(
|
|
deliveryInput(
|
|
{
|
|
kind: "BROWSER_MANAGED_RESOURCE",
|
|
resourceId: "artifact-1",
|
|
capabilityReceipt,
|
|
},
|
|
"BROWSER_MANAGED",
|
|
),
|
|
),
|
|
).toMatchObject({
|
|
ok: true,
|
|
value: { kind: "BROWSER_HANDOFF" },
|
|
});
|
|
expect(originalResolve).toHaveBeenCalledOnce();
|
|
expect(replacedResolve).not.toHaveBeenCalled();
|
|
expect(originalHandoff).toHaveBeenCalledOnce();
|
|
expect(replacedHandoff).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("sanitizes browser-managed handoff and reports only handoff truth", async () => {
|
|
const handoff = vi.fn();
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff },
|
|
browserManagedCapabilities: capabilityResolver(
|
|
(resourceId) => `/downloads/${resourceId}`,
|
|
),
|
|
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",
|
|
),
|
|
),
|
|
).toEqual({
|
|
ok: true,
|
|
value: { kind: "BROWSER_HANDOFF", transferId: "transfer:1" },
|
|
});
|
|
// STO-02. Parse once, then execute exactly what was validated.
|
|
expect(handoff).toHaveBeenCalledWith(
|
|
"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({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff },
|
|
browserManagedCapabilities: capabilityResolver(
|
|
() => "https://evil.example/a?token=secret",
|
|
),
|
|
baseOrigin: "https://app.example",
|
|
userActivation: { isActive: true },
|
|
});
|
|
|
|
expect(
|
|
await adapter.deliver(
|
|
deliveryInput(
|
|
{
|
|
kind: "BROWSER_MANAGED_RESOURCE",
|
|
resourceId: "artifact-1",
|
|
capabilityReceipt,
|
|
},
|
|
"BROWSER_MANAGED",
|
|
),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
expect(handoff).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requires an exact, unexpired, server-bound synchronous capability receipt", async () => {
|
|
const source = {
|
|
kind: "BROWSER_MANAGED_RESOURCE" as const,
|
|
resourceId: "artifact-1",
|
|
capabilityReceipt,
|
|
};
|
|
const cases = [
|
|
{
|
|
resolver: capabilityResolver(() => "/unused", {
|
|
capabilityReceipt:
|
|
"capability:other" as BrowserManagedDownloadCapabilityReceipt,
|
|
}),
|
|
expectedCode: "POLICY_REJECTED",
|
|
},
|
|
{
|
|
resolver: capabilityResolver(undefined, {
|
|
resourceId: "artifact-2",
|
|
}),
|
|
expectedCode: "POLICY_REJECTED",
|
|
},
|
|
{
|
|
resolver: capabilityResolver(undefined, {
|
|
mediaType: "text/plain",
|
|
}),
|
|
expectedCode: "POLICY_REJECTED",
|
|
},
|
|
{
|
|
resolver: capabilityResolver(undefined, {
|
|
expiresAtEpochMs: 99,
|
|
}),
|
|
expectedCode: "EXPIRED_RESOURCE",
|
|
},
|
|
] as const;
|
|
|
|
for (const testCase of cases) {
|
|
const handoff = vi.fn();
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff },
|
|
browserManagedCapabilities: testCase.resolver,
|
|
now: () => 100,
|
|
baseOrigin: "https://app.example",
|
|
userActivation: { isActive: true },
|
|
});
|
|
expect(
|
|
await adapter.deliver(
|
|
deliveryInput(source, "BROWSER_MANAGED"),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: testCase.expectedCode },
|
|
});
|
|
expect(handoff).not.toHaveBeenCalled();
|
|
}
|
|
|
|
const missingReceipt = {
|
|
kind: "BROWSER_MANAGED_RESOURCE",
|
|
resourceId: "artifact-1",
|
|
} as unknown as DownloadSource;
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
userActivation: { isActive: true },
|
|
});
|
|
expect(
|
|
await adapter.deliver(
|
|
deliveryInput(missingReceipt, "BROWSER_MANAGED"),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
});
|
|
|
|
it("streams with backpressure and succeeds only after close", async () => {
|
|
const events: string[] = [];
|
|
const controller = new AbortController();
|
|
const handle = writableHandle(events);
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker: async () => handle,
|
|
createTransferId: () => "transfer:stream",
|
|
progressMinIntervalMs: 0,
|
|
userActivation: { isActive: true },
|
|
});
|
|
const input = deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: byteSource([
|
|
new Uint8Array([1, 2]),
|
|
new Uint8Array([3]),
|
|
]),
|
|
},
|
|
"PROMPT_AND_STREAM",
|
|
controller.signal,
|
|
);
|
|
|
|
expect(await adapter.deliver(input)).toEqual({
|
|
ok: true,
|
|
value: {
|
|
kind: "SAVED",
|
|
transferId: "transfer:stream",
|
|
bytesWritten: 3,
|
|
integrity: "NOT_PROVIDED",
|
|
},
|
|
});
|
|
expect(events).toEqual(["write:2", "write:1", "close"]);
|
|
expect(input.onProgress.mock.calls.map(([progress]) => progress.phase)).toEqual(
|
|
["PREPARING", "TRANSFERRING", "TRANSFERRING", "VERIFYING", "FINALIZING"],
|
|
);
|
|
});
|
|
|
|
it("snapshots the generated source before awaiting the save picker", async () => {
|
|
const events: string[] = [];
|
|
let resolvePicker:
|
|
| ((handle: SaveFileHandle) => void)
|
|
| undefined;
|
|
const originalStream = vi.fn(
|
|
async function* () {
|
|
yield successfulChunk(new Uint8Array([1, 2]));
|
|
},
|
|
);
|
|
const replacedStream = vi.fn(
|
|
async function* () {
|
|
yield successfulChunk(new Uint8Array([9]));
|
|
},
|
|
);
|
|
const bytes = {
|
|
byteLength: 2,
|
|
stream: originalStream,
|
|
};
|
|
const source = {
|
|
kind: "GENERATED" as const,
|
|
bytes,
|
|
};
|
|
const request = deliveryInput(
|
|
source,
|
|
"PROMPT_AND_STREAM",
|
|
);
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker: () =>
|
|
new Promise((resolve) => {
|
|
resolvePicker = resolve;
|
|
}),
|
|
createTransferId: () => "transfer:snapshot",
|
|
userActivation: { isActive: true },
|
|
});
|
|
|
|
const pending = adapter.deliver(request);
|
|
bytes.byteLength = 1;
|
|
bytes.stream = replacedStream;
|
|
(
|
|
request as { source: DownloadSource }
|
|
).source = {
|
|
kind: "BROWSER_MANAGED_RESOURCE",
|
|
resourceId: "replaced",
|
|
capabilityReceipt,
|
|
};
|
|
resolvePicker?.(writableHandle(events));
|
|
|
|
expect(await pending).toMatchObject({
|
|
ok: true,
|
|
value: { kind: "SAVED", bytesWritten: 2 },
|
|
});
|
|
expect(originalStream).toHaveBeenCalledOnce();
|
|
expect(replacedStream).not.toHaveBeenCalled();
|
|
expect(events).toEqual(["write:2", "close"]);
|
|
});
|
|
|
|
it("does not turn a completed close into a late abort failure", async () => {
|
|
const controller = new AbortController();
|
|
const handle: SaveFileHandle = {
|
|
async createWritable() {
|
|
return new WritableStream<Uint8Array>({
|
|
close() {
|
|
controller.abort();
|
|
},
|
|
});
|
|
},
|
|
};
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker: async () => handle,
|
|
createTransferId: () => "transfer:committed",
|
|
userActivation: { isActive: true },
|
|
});
|
|
|
|
expect(
|
|
await adapter.deliver(
|
|
deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: byteSource([new Uint8Array([1])]),
|
|
},
|
|
"PROMPT_AND_STREAM",
|
|
controller.signal,
|
|
),
|
|
),
|
|
).toMatchObject({
|
|
ok: true,
|
|
value: { kind: "SAVED", transferId: "transfer:committed" },
|
|
});
|
|
});
|
|
|
|
it("aborts the writable before close on integrity failure", async () => {
|
|
const events: string[] = [];
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker: async () => writableHandle(events),
|
|
createIntegrityVerifier: () => ({
|
|
update: () => {
|
|
events.push("hash");
|
|
},
|
|
verify: () => false,
|
|
}),
|
|
userActivation: { isActive: true },
|
|
});
|
|
const input = {
|
|
...deliveryInput(
|
|
{
|
|
kind: "GENERATED" as const,
|
|
bytes: byteSource([new Uint8Array([1, 2])]),
|
|
expectedSha256: "a".repeat(64),
|
|
},
|
|
"PROMPT_AND_STREAM",
|
|
),
|
|
};
|
|
|
|
expect(await adapter.deliver(input)).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INTEGRITY_FAILED" },
|
|
});
|
|
expect(events).toEqual(["hash", "write:2", "abort"]);
|
|
});
|
|
|
|
it("distinguishes save-picker dismissal from active cancellation", async () => {
|
|
const dismissed = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker: async () => {
|
|
throw new DOMException("dismissed", "AbortError");
|
|
},
|
|
userActivation: { isActive: true },
|
|
});
|
|
expect(
|
|
await dismissed.deliver(
|
|
deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: byteSource([new Uint8Array([1])]),
|
|
},
|
|
"PROMPT_AND_STREAM",
|
|
),
|
|
),
|
|
).toEqual({
|
|
ok: true,
|
|
value: { kind: "DISMISSED" },
|
|
});
|
|
|
|
const controller = new AbortController();
|
|
let rejectPicker:
|
|
| ((reason: DOMException) => void)
|
|
| undefined;
|
|
const aborted = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker: () =>
|
|
new Promise((_, reject: (reason: DOMException) => void) => {
|
|
rejectPicker = reject;
|
|
}),
|
|
userActivation: { isActive: true },
|
|
});
|
|
const pending = aborted.deliver(
|
|
deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: byteSource([new Uint8Array([1])]),
|
|
},
|
|
"PROMPT_AND_STREAM",
|
|
controller.signal,
|
|
),
|
|
);
|
|
controller.abort();
|
|
rejectPicker?.(new DOMException("closed", "AbortError"));
|
|
expect(await pending).toMatchObject({
|
|
ok: false,
|
|
error: { code: "ABORTED" },
|
|
});
|
|
});
|
|
|
|
it("bounds Blob buffering and revokes the lease after handoff", async () => {
|
|
const revoked: string[] = [];
|
|
const leases = new ObjectUrlLeaseRegistry({
|
|
createObjectURL: () => "blob:download-1",
|
|
revokeObjectURL: (url) => revoked.push(url),
|
|
});
|
|
let scheduled: (() => void) | undefined;
|
|
let scheduledDelay: number | undefined;
|
|
const handoff = vi.fn();
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
objectUrls: leases,
|
|
scheduler: {
|
|
setTimeout(callback, delayMs) {
|
|
scheduled = callback;
|
|
scheduledDelay = delayMs;
|
|
return 1;
|
|
},
|
|
},
|
|
createTransferId: () => "transfer:blob",
|
|
userActivation: { isActive: true },
|
|
});
|
|
|
|
expect(
|
|
await adapter.deliver(
|
|
deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: byteSource([
|
|
new Uint8Array([1, 2]),
|
|
new Uint8Array([3]),
|
|
]),
|
|
},
|
|
"BOUNDED_OBJECT_URL",
|
|
),
|
|
),
|
|
).toEqual({
|
|
ok: true,
|
|
value: {
|
|
kind: "BROWSER_HANDOFF",
|
|
transferId: "transfer:blob",
|
|
},
|
|
});
|
|
expect(handoff).toHaveBeenCalledWith(
|
|
"blob:download-1",
|
|
"invoice_exe.pdf",
|
|
);
|
|
expect(leases.activeLeaseCount).toBe(1);
|
|
expect(scheduledDelay).toBe(
|
|
DEFAULT_OBJECT_URL_RELEASE_GRACE_MS,
|
|
);
|
|
scheduled?.();
|
|
expect(leases.activeLeaseCount).toBe(0);
|
|
expect(revoked).toEqual(["blob:download-1"]);
|
|
});
|
|
|
|
it("captures object URL methods at registry construction", () => {
|
|
const originalCreate = vi.fn(() => "blob:captured");
|
|
const originalRevoke = vi.fn();
|
|
const replacedCreate = vi.fn(() => "blob:replaced");
|
|
const replacedRevoke = vi.fn();
|
|
const urlApi = {
|
|
createObjectURL: originalCreate,
|
|
revokeObjectURL: originalRevoke,
|
|
};
|
|
const leases = new ObjectUrlLeaseRegistry(urlApi);
|
|
urlApi.createObjectURL = replacedCreate;
|
|
urlApi.revokeObjectURL = replacedRevoke;
|
|
|
|
const lease = leases.create(new Blob(["safe"]));
|
|
lease.release();
|
|
|
|
expect(lease.url).toBe("blob:captured");
|
|
expect(originalCreate).toHaveBeenCalledOnce();
|
|
expect(originalRevoke).toHaveBeenCalledWith(
|
|
"blob:captured",
|
|
);
|
|
expect(replacedCreate).not.toHaveBeenCalled();
|
|
expect(replacedRevoke).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects a stream that crosses the bounded Blob cap", async () => {
|
|
const handoff = vi.fn();
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
userActivation: { isActive: true },
|
|
});
|
|
const input = {
|
|
...deliveryInput(
|
|
{
|
|
kind: "GENERATED" as const,
|
|
bytes: byteSource(
|
|
[new Uint8Array(10), new Uint8Array(10)],
|
|
null,
|
|
),
|
|
},
|
|
"BOUNDED_OBJECT_URL",
|
|
),
|
|
maxBufferedBytes: 16,
|
|
};
|
|
|
|
expect(await adapter.deliver(input)).toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(handoff).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects caller limits above runtime hard caps before side effects", async () => {
|
|
const handoff = vi.fn();
|
|
const showSaveFilePicker = vi.fn(async () => writableHandle([]));
|
|
const stream = vi.fn(
|
|
async function* (): AsyncIterable<
|
|
ReturnType<typeof successfulChunk>
|
|
> {
|
|
yield successfulChunk(new Uint8Array([1]));
|
|
},
|
|
);
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
hardMaxObjectUrlBytes: 8,
|
|
hardMaxTransferBytes: 12,
|
|
policies,
|
|
host: { handoff },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker,
|
|
userActivation: { isActive: true },
|
|
});
|
|
const source = {
|
|
kind: "GENERATED" as const,
|
|
bytes: { byteLength: null, stream },
|
|
};
|
|
|
|
expect(
|
|
await adapter.deliver({
|
|
...deliveryInput(source, "BOUNDED_OBJECT_URL"),
|
|
maxBufferedBytes: 9,
|
|
maxTransferBytes: 12,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(
|
|
await adapter.deliver({
|
|
...deliveryInput(source, "PROMPT_AND_STREAM"),
|
|
maxBufferedBytes: 8,
|
|
maxTransferBytes: 13,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(
|
|
await adapter.deliver({
|
|
...deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: { byteLength: 13, stream },
|
|
},
|
|
"PROMPT_AND_STREAM",
|
|
),
|
|
maxBufferedBytes: 8,
|
|
maxTransferBytes: 12,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(stream).not.toHaveBeenCalled();
|
|
expect(showSaveFilePicker).not.toHaveBeenCalled();
|
|
expect(handoff).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("aborts streaming when actual bytes cross the transfer hard cap", async () => {
|
|
const events: string[] = [];
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
hardMaxObjectUrlBytes: 8,
|
|
hardMaxTransferBytes: 8,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
showSaveFilePicker: async () => writableHandle(events),
|
|
userActivation: { isActive: true },
|
|
});
|
|
|
|
expect(
|
|
await adapter.deliver({
|
|
...deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: byteSource(
|
|
[new Uint8Array(5), new Uint8Array(4)],
|
|
null,
|
|
),
|
|
},
|
|
"PROMPT_AND_STREAM",
|
|
),
|
|
maxBufferedBytes: 8,
|
|
maxTransferBytes: 8,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(events).toEqual(["write:5", "abort"]);
|
|
});
|
|
|
|
it("treats a declared-length mismatch as integrity failure", async () => {
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
userActivation: { isActive: true },
|
|
});
|
|
expect(
|
|
await adapter.deliver(
|
|
deliveryInput(
|
|
{
|
|
kind: "GENERATED",
|
|
bytes: byteSource([new Uint8Array([1])], 2),
|
|
},
|
|
"BOUNDED_OBJECT_URL",
|
|
),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INTEGRITY_FAILED" },
|
|
});
|
|
});
|
|
|
|
it("maps typed and defensive raw stream failures to closed download errors", async () => {
|
|
const adapter = createDownloadDeliveryAdapter({
|
|
...HARD_LIMITS,
|
|
policies,
|
|
host: { handoff: vi.fn() },
|
|
browserManagedCapabilities: capabilityResolver(),
|
|
userActivation: { isActive: true },
|
|
});
|
|
const common = {
|
|
...deliveryInput(
|
|
{
|
|
kind: "GENERATED" as const,
|
|
bytes: {
|
|
byteLength: null,
|
|
async *stream() {
|
|
yield {
|
|
ok: false as const,
|
|
error: {
|
|
code: "NOT_READABLE" as const,
|
|
operation: "FILE_READ" as const,
|
|
retryable: true,
|
|
recovery: "REOPEN" as const,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
},
|
|
"BOUNDED_OBJECT_URL",
|
|
),
|
|
};
|
|
expect(await adapter.deliver(common)).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "NOT_READABLE",
|
|
operation: "DOWNLOAD",
|
|
},
|
|
});
|
|
|
|
expect(
|
|
await adapter.deliver({
|
|
...common,
|
|
source: {
|
|
kind: "GENERATED",
|
|
bytes: {
|
|
byteLength: null,
|
|
async *stream(signal: AbortSignal) {
|
|
if (signal.aborted) {
|
|
yield {
|
|
ok: false as const,
|
|
error: {
|
|
code: "ABORTED" as const,
|
|
operation: "FILE_READ" as const,
|
|
retryable: false,
|
|
recovery: "NONE" as const,
|
|
},
|
|
};
|
|
return;
|
|
}
|
|
throw new DOMException(
|
|
"native detail",
|
|
"NotReadableError",
|
|
);
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "NOT_READABLE",
|
|
operation: "DOWNLOAD",
|
|
},
|
|
});
|
|
});
|
|
});
|