chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,909 @@
|
||||
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" },
|
||||
});
|
||||
expect(handoff).toHaveBeenCalledWith(
|
||||
"/downloads/artifact-1",
|
||||
"invoice_exe.pdf",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user