chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
|
||||
import {
|
||||
MemoryDurableObjectStore,
|
||||
MemoryBrowserFilePolicyRegistry,
|
||||
MemoryExampleQuarantinedUploadAdapter,
|
||||
MemoryFileSelectionAdapter,
|
||||
MemoryPublicResponseCache,
|
||||
MemoryStorageDurabilityAdapter,
|
||||
MemoryStructuredOfflineStore,
|
||||
MemoryTransientPreviewAdapter,
|
||||
RecordingDownloadDeliveryAdapter,
|
||||
asByteCount,
|
||||
asBrowserManagedDownloadCapabilityReceipt,
|
||||
asDurableObjectId,
|
||||
asLocalFileRef,
|
||||
asObjectGeneration,
|
||||
browserFilePolicyReference,
|
||||
failure,
|
||||
sanitizeDownloadFileName,
|
||||
} from "../../recipes/frontend-capabilities/index.ts";
|
||||
import type {
|
||||
BrowserFileComposition,
|
||||
BrowserPersistencePolicy,
|
||||
DurableObjectStorageComposition,
|
||||
ExampleBackendUploadComposition,
|
||||
PublicResponseCacheComposition,
|
||||
StorageDurabilityComposition,
|
||||
StructuredOfflineStorageComposition,
|
||||
} from "../../recipes/frontend-capabilities/index.ts";
|
||||
|
||||
async function* byteChunks(
|
||||
...values: ReadonlyArray<string>
|
||||
): AsyncIterable<Readonly<{ ok: true; value: Uint8Array }>> {
|
||||
for (const value of values) {
|
||||
yield {
|
||||
ok: true,
|
||||
value: new TextEncoder().encode(value),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function* failedByteChunks() {
|
||||
yield failure(
|
||||
"NOT_READABLE",
|
||||
true,
|
||||
"The fixture source became unreadable.",
|
||||
);
|
||||
}
|
||||
|
||||
describe("production browser file and storage recipe contracts", () => {
|
||||
it("separates picker dismissal, metadata policy and bounded content reads", async () => {
|
||||
const ref = asLocalFileRef("opaque-local-1");
|
||||
const selectionPolicy = {
|
||||
policyId: "support-attachment-v1",
|
||||
purpose: "support-attachment",
|
||||
classification: "PERSONAL" as const,
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxFileBytes: asByteCount(10),
|
||||
maxTotalBytes: asByteCount(10),
|
||||
allowEmpty: false,
|
||||
accept: [{ mediaType: "text/plain", extensions: [".txt"] }],
|
||||
};
|
||||
const filePolicy = browserFilePolicyReference(
|
||||
"support-attachment",
|
||||
"select-inspect-preview-text",
|
||||
);
|
||||
const policies = new MemoryBrowserFilePolicyRegistry([
|
||||
{
|
||||
reference: filePolicy,
|
||||
selection: selectionPolicy,
|
||||
inspection: {
|
||||
policyId: selectionPolicy.policyId,
|
||||
maxInspectionBytes: asByteCount(1_445),
|
||||
acceptedSignatures: [
|
||||
{
|
||||
mediaType: "text/plain",
|
||||
extensions: [".txt"],
|
||||
patterns: [
|
||||
{
|
||||
offset: asByteCount(0),
|
||||
bytes: [104],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
preview: {
|
||||
allowedMediaTypes: ["text/plain"],
|
||||
maxPreviewBytes: asByteCount(10),
|
||||
},
|
||||
},
|
||||
]);
|
||||
const adapter = new MemoryFileSelectionAdapter(
|
||||
[
|
||||
{
|
||||
ref,
|
||||
displayName: "notes.txt",
|
||||
bytes: new TextEncoder().encode("hello"),
|
||||
reportedMediaType: "text/plain",
|
||||
detectedMediaType: "text/plain",
|
||||
signature: "MATCHED",
|
||||
},
|
||||
],
|
||||
policies,
|
||||
);
|
||||
|
||||
adapter.setNextOutcome("DISMISSED");
|
||||
expect(await adapter.select({ policy: filePolicy })).toEqual({
|
||||
ok: true,
|
||||
value: { kind: "DISMISSED" },
|
||||
});
|
||||
|
||||
const selected = await adapter.select({ policy: filePolicy });
|
||||
expect(selected.ok && selected.value.kind).toBe("SELECTED");
|
||||
expect(
|
||||
await adapter.select({
|
||||
policy: browserFilePolicyReference(
|
||||
"support-attachment",
|
||||
"select-inspect-preview-text",
|
||||
),
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "POLICY_REJECTED" },
|
||||
});
|
||||
const inspected = await adapter.inspect({
|
||||
ref,
|
||||
policy: filePolicy,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(inspected).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
detectedMediaType: "text/plain",
|
||||
signature: "MATCHED",
|
||||
},
|
||||
});
|
||||
if (!inspected.ok || !inspected.value.verificationReceipt) return;
|
||||
expect(
|
||||
await adapter.readRange({
|
||||
ref,
|
||||
offset: asByteCount(1),
|
||||
length: asByteCount(3),
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
value: new TextEncoder().encode("ell"),
|
||||
});
|
||||
const previews = new MemoryTransientPreviewAdapter(
|
||||
adapter,
|
||||
policies,
|
||||
);
|
||||
const preview = await previews.create({
|
||||
ref,
|
||||
verificationReceipt: inspected.value.verificationReceipt,
|
||||
policy: filePolicy,
|
||||
maxPreviewBytes: asByteCount(10),
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(preview.ok).toBe(true);
|
||||
expect(previews.activeLeaseCount).toBe(1);
|
||||
if (preview.ok) {
|
||||
preview.value.release();
|
||||
preview.value.release();
|
||||
}
|
||||
expect(previews.activeLeaseCount).toBe(0);
|
||||
previews.dispose();
|
||||
expect(
|
||||
await previews.create({
|
||||
ref,
|
||||
verificationReceipt: inspected.value.verificationReceipt,
|
||||
policy: filePolicy,
|
||||
maxPreviewBytes: asByteCount(10),
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "PROVIDER_UNAVAILABLE" },
|
||||
});
|
||||
adapter.release(ref);
|
||||
expect(
|
||||
(
|
||||
await adapter.readRange({
|
||||
ref,
|
||||
offset: asByteCount(0),
|
||||
length: asByteCount(1),
|
||||
signal: new AbortController().signal,
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the optional backend upload example resumable and quarantined", async () => {
|
||||
const adapter = new MemoryExampleQuarantinedUploadAdapter(
|
||||
asByteCount(16),
|
||||
asByteCount(4),
|
||||
() => Date.parse("2026-07-27T00:00:00.000Z"),
|
||||
);
|
||||
const signal = new AbortController().signal;
|
||||
const created = await adapter.create({
|
||||
purpose: "avatar",
|
||||
byteLength: asByteCount(4),
|
||||
detectedMediaType: "image/png",
|
||||
signal,
|
||||
});
|
||||
expect(created.ok).toBe(true);
|
||||
if (!created.ok) return;
|
||||
|
||||
const part = await adapter.uploadPart({
|
||||
sessionId: created.value.sessionId,
|
||||
partNumber: 1,
|
||||
offset: asByteCount(0),
|
||||
bytes: new Uint8Array([1, 2, 3, 4]),
|
||||
checksumSha256: "part-checksum",
|
||||
idempotencyKey: "part-one-attempt",
|
||||
signal,
|
||||
});
|
||||
expect(part.ok).toBe(true);
|
||||
const replay = await adapter.uploadPart({
|
||||
sessionId: created.value.sessionId,
|
||||
partNumber: 1,
|
||||
offset: asByteCount(0),
|
||||
bytes: new Uint8Array([1, 2, 3, 4]),
|
||||
checksumSha256: "part-checksum",
|
||||
idempotencyKey: "part-one-attempt",
|
||||
signal,
|
||||
});
|
||||
expect(replay).toEqual(part);
|
||||
expect(
|
||||
await adapter.uploadPart({
|
||||
sessionId: created.value.sessionId,
|
||||
partNumber: 1,
|
||||
offset: asByteCount(0),
|
||||
bytes: new Uint8Array([9, 9, 9, 9]),
|
||||
checksumSha256: "different-request-body",
|
||||
idempotencyKey: "part-one-attempt",
|
||||
signal,
|
||||
}),
|
||||
).toMatchObject({ ok: false, failure: { code: "CONFLICT" } });
|
||||
if (!part.ok) return;
|
||||
expect(
|
||||
await adapter.complete({
|
||||
sessionId: created.value.sessionId,
|
||||
parts: [part.value],
|
||||
signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: { state: "QUARANTINED" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps each browser technology independently composable", () => {
|
||||
expectTypeOf<keyof BrowserFileComposition>().toEqualTypeOf<
|
||||
"picker" | "content" | "previews" | "downloads"
|
||||
>();
|
||||
expectTypeOf<
|
||||
keyof StructuredOfflineStorageComposition<unknown>
|
||||
>().toEqualTypeOf<"store" | "maintenance">();
|
||||
expectTypeOf<
|
||||
keyof StorageDurabilityComposition
|
||||
>().toEqualTypeOf<"durability">();
|
||||
expectTypeOf<
|
||||
keyof DurableObjectStorageComposition
|
||||
>().toEqualTypeOf<"store" | "maintenance">();
|
||||
expectTypeOf<
|
||||
keyof PublicResponseCacheComposition
|
||||
>().toEqualTypeOf<"cache">();
|
||||
expectTypeOf<
|
||||
keyof ExampleBackendUploadComposition
|
||||
>().toEqualTypeOf<"upload">();
|
||||
expectTypeOf<
|
||||
BrowserPersistencePolicy["logoutAction"]
|
||||
>().toEqualTypeOf<
|
||||
"KEEP_ORIGIN_SHARED" | "PURGE_PARTITION" | "EXPORT_THEN_PURGE"
|
||||
>();
|
||||
expectTypeOf<
|
||||
BrowserPersistencePolicy["accountDeletionAction"]
|
||||
>().toEqualTypeOf<"KEEP_ORIGIN_SHARED" | "PURGE_PARTITION">();
|
||||
expectTypeOf<
|
||||
BrowserPersistencePolicy["pressureAction"]
|
||||
>().toEqualTypeOf<"EVICT_RECONSTRUCTABLE" | "RETAIN">();
|
||||
});
|
||||
|
||||
it("distinguishes browser handoff from a verified streamed save", async () => {
|
||||
const browserPolicy = browserFilePolicyReference(
|
||||
"reports",
|
||||
"browser-managed-pdf",
|
||||
);
|
||||
const streamPolicy = browserFilePolicyReference(
|
||||
"exports",
|
||||
"stream-text",
|
||||
);
|
||||
const policies = new MemoryBrowserFilePolicyRegistry([
|
||||
{
|
||||
reference: browserPolicy,
|
||||
download: {
|
||||
strategy: "BROWSER_MANAGED",
|
||||
mediaType: "application/pdf",
|
||||
safeExtension: ".pdf",
|
||||
maxTransferBytes: asByteCount(10),
|
||||
maxBufferedBytes: asByteCount(10),
|
||||
integrity: "OPTIONAL",
|
||||
},
|
||||
},
|
||||
{
|
||||
reference: streamPolicy,
|
||||
download: {
|
||||
strategy: "PROMPT_AND_STREAM",
|
||||
mediaType: "text/plain",
|
||||
safeExtension: ".txt",
|
||||
maxTransferBytes: asByteCount(10),
|
||||
maxBufferedBytes: asByteCount(10),
|
||||
integrity: "REQUIRED",
|
||||
},
|
||||
},
|
||||
]);
|
||||
const capabilityReceipt =
|
||||
asBrowserManagedDownloadCapabilityReceipt(
|
||||
"capability:report",
|
||||
);
|
||||
const adapter = new RecordingDownloadDeliveryAdapter(
|
||||
policies,
|
||||
{
|
||||
resolve(input) {
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
capabilityReceipt,
|
||||
href: "/downloads/report",
|
||||
resourceId: input.resourceId,
|
||||
mediaType: "application/pdf",
|
||||
safeExtension: ".pdf",
|
||||
maxBytes: asByteCount(10),
|
||||
expiresAtEpochMs: 2_000,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
() => 1_000,
|
||||
);
|
||||
const progress: number[] = [];
|
||||
expect(sanitizeDownloadFileName("../CON\u202e.exe")).toBe("download.bin");
|
||||
|
||||
const handedOff = await adapter.deliver({
|
||||
policy: browserPolicy,
|
||||
source: {
|
||||
kind: "BROWSER_MANAGED_RESOURCE",
|
||||
resourceId: "opaque-resource",
|
||||
capabilityReceipt,
|
||||
},
|
||||
suggestedFileName: "report.pdf",
|
||||
maxTransferBytes: asByteCount(10),
|
||||
maxBufferedBytes: asByteCount(10),
|
||||
signal: new AbortController().signal,
|
||||
onProgress() {},
|
||||
});
|
||||
expect(handedOff).toMatchObject({
|
||||
ok: true,
|
||||
value: { kind: "BROWSER_HANDOFF" },
|
||||
});
|
||||
expect(
|
||||
await adapter.deliver({
|
||||
policy: browserPolicy,
|
||||
source: {
|
||||
kind: "BROWSER_MANAGED_RESOURCE",
|
||||
resourceId: "opaque-resource",
|
||||
capabilityReceipt:
|
||||
asBrowserManagedDownloadCapabilityReceipt(
|
||||
"capability:forged",
|
||||
),
|
||||
},
|
||||
suggestedFileName: "report.pdf",
|
||||
signal: new AbortController().signal,
|
||||
onProgress() {},
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "POLICY_REJECTED" },
|
||||
});
|
||||
|
||||
const saved = await adapter.deliver({
|
||||
policy: streamPolicy,
|
||||
source: {
|
||||
kind: "GENERATED",
|
||||
bytes: {
|
||||
byteLength: asByteCount(4),
|
||||
stream: () => byteChunks("ab", "cd"),
|
||||
},
|
||||
expectedSha256:
|
||||
"88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589",
|
||||
},
|
||||
suggestedFileName: "safe.txt",
|
||||
maxTransferBytes: asByteCount(10),
|
||||
maxBufferedBytes: asByteCount(10),
|
||||
signal: new AbortController().signal,
|
||||
onProgress(value) {
|
||||
progress.push(value.transferredBytes);
|
||||
},
|
||||
});
|
||||
expect(saved).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
kind: "SAVED",
|
||||
bytesWritten: 4,
|
||||
integrity: "VERIFIED",
|
||||
},
|
||||
});
|
||||
expect(progress).toEqual([2, 4, 4]);
|
||||
});
|
||||
|
||||
it("commits IndexedDB-like mutations atomically with CAS and idempotency", async () => {
|
||||
const store = new MemoryStructuredOfflineStore<{ title: string }>({
|
||||
maxBytes: 10_000,
|
||||
now: () => 1_000,
|
||||
});
|
||||
expect(
|
||||
(
|
||||
await store.open({
|
||||
partitionKey: "opaque-account-partition",
|
||||
onLifecycle() {},
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const createMutation = {
|
||||
kind: "PUT",
|
||||
id: "one",
|
||||
value: { title: "first" },
|
||||
payloadVersion: 1,
|
||||
expiresAtEpochMs: null,
|
||||
revision: { kind: "MUST_NOT_EXIST" },
|
||||
} as const;
|
||||
const committed = await store.commit({
|
||||
idempotencyKey: "create-one",
|
||||
mutations: [createMutation],
|
||||
});
|
||||
expect(committed).toMatchObject({
|
||||
ok: true,
|
||||
value: { replayed: false, revisions: { one: 1 } },
|
||||
});
|
||||
expect(await store.commit({
|
||||
idempotencyKey: "create-one",
|
||||
mutations: [createMutation],
|
||||
})).toMatchObject({ ok: true, value: { replayed: true } });
|
||||
expect(
|
||||
await store.commit({
|
||||
idempotencyKey: "create-one",
|
||||
mutations: [
|
||||
{
|
||||
kind: "DELETE",
|
||||
id: "one",
|
||||
revision: { kind: "ANY" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toMatchObject({ ok: false, failure: { code: "CONFLICT" } });
|
||||
|
||||
const rejectedBatch = await store.commit({
|
||||
idempotencyKey: "atomic-conflict",
|
||||
mutations: [
|
||||
{
|
||||
kind: "PUT",
|
||||
id: "two",
|
||||
value: { title: "must-not-commit" },
|
||||
payloadVersion: 1,
|
||||
expiresAtEpochMs: null,
|
||||
revision: { kind: "MUST_NOT_EXIST" },
|
||||
},
|
||||
{
|
||||
kind: "PUT",
|
||||
id: "one",
|
||||
value: { title: "stale" },
|
||||
payloadVersion: 1,
|
||||
expiresAtEpochMs: null,
|
||||
revision: { kind: "MATCH", revision: 999 },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(rejectedBatch).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "CONFLICT" },
|
||||
});
|
||||
expect(await store.read("two")).toEqual({ ok: true, value: null });
|
||||
});
|
||||
|
||||
it("makes quota and persistence downgrade explicit", async () => {
|
||||
const store = new MemoryStructuredOfflineStore<{ value: string }>({
|
||||
maxBytes: 16,
|
||||
});
|
||||
await store.open({ partitionKey: "partition", onLifecycle() {} });
|
||||
expect(
|
||||
await store.commit({
|
||||
idempotencyKey: "too-large",
|
||||
mutations: [
|
||||
{
|
||||
kind: "PUT",
|
||||
id: "one",
|
||||
value: { value: "far larger than the approved storage budget" },
|
||||
payloadVersion: 1,
|
||||
expiresAtEpochMs: null,
|
||||
revision: { kind: "MUST_NOT_EXIST" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "QUOTA_EXCEEDED" },
|
||||
});
|
||||
|
||||
const durability = new MemoryStorageDurabilityAdapter(
|
||||
70,
|
||||
100,
|
||||
"DENIED",
|
||||
);
|
||||
expect(await durability.inspect()).toMatchObject({
|
||||
ok: true,
|
||||
value: { pressure: "PRESSURE" },
|
||||
});
|
||||
expect(
|
||||
await durability.requestPersistence({
|
||||
reason: "PROTECT_UNSYNCED_USER_DATA",
|
||||
userInitiated: true,
|
||||
}),
|
||||
).toEqual({ ok: true, value: "DENIED" });
|
||||
});
|
||||
|
||||
it("keeps partially written OPFS objects invisible until logical commit", async () => {
|
||||
const store = new MemoryDurableObjectStore(asByteCount(32));
|
||||
const id = asDurableObjectId("opaque-object");
|
||||
store.failNextPutAfter("FILES_READY");
|
||||
const failed = await store.put({
|
||||
id,
|
||||
expectedGeneration: null,
|
||||
source: byteChunks("ab", "cd"),
|
||||
declaredByteLength: asByteCount(4),
|
||||
mediaType: "application/octet-stream",
|
||||
dataClass: "RECONSTRUCTABLE",
|
||||
retention: { kind: "EXPIRES", expiresAt: "2026-08-01T00:00:00.000Z" },
|
||||
signal: new AbortController().signal,
|
||||
onProgress() {},
|
||||
});
|
||||
expect(failed).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "PROVIDER_UNAVAILABLE" },
|
||||
});
|
||||
expect(await store.open(id)).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "NOT_FOUND" },
|
||||
});
|
||||
expect(
|
||||
await store.reconcile({ timeBudgetMs: 100, maxEntries: 10 }),
|
||||
).toMatchObject({ ok: true, value: { purgedCount: 1 } });
|
||||
|
||||
expect(
|
||||
await store.put({
|
||||
id,
|
||||
expectedGeneration: null,
|
||||
source: failedByteChunks(),
|
||||
declaredByteLength: asByteCount(4),
|
||||
mediaType: null,
|
||||
dataClass: "RECONSTRUCTABLE",
|
||||
retention: { kind: "EXPLICIT_DELETE" },
|
||||
signal: new AbortController().signal,
|
||||
onProgress() {},
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "NOT_READABLE" },
|
||||
});
|
||||
expect(
|
||||
await store.reconcile({ timeBudgetMs: 100, maxEntries: 10 }),
|
||||
).toMatchObject({ ok: true, value: { purgedCount: 1 } });
|
||||
|
||||
const committed = await store.put({
|
||||
id,
|
||||
expectedGeneration: null,
|
||||
source: byteChunks("abcd"),
|
||||
declaredByteLength: asByteCount(4),
|
||||
mediaType: null,
|
||||
dataClass: "RECONSTRUCTABLE",
|
||||
retention: { kind: "EXPLICIT_DELETE" },
|
||||
signal: new AbortController().signal,
|
||||
onProgress() {},
|
||||
});
|
||||
expect(committed).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
generation: 1,
|
||||
integrity: { algorithm: "SHA-256-TREE-V1" },
|
||||
},
|
||||
});
|
||||
const opened = await store.open(id);
|
||||
expect(opened.ok).toBe(true);
|
||||
if (opened.ok) {
|
||||
const chunks: number[] = [];
|
||||
for await (const chunkResult of opened.value.chunks) {
|
||||
expect(chunkResult.ok).toBe(true);
|
||||
if (chunkResult.ok) chunks.push(...chunkResult.value);
|
||||
}
|
||||
expect(new TextDecoder().decode(new Uint8Array(chunks))).toBe(
|
||||
"abcd",
|
||||
);
|
||||
}
|
||||
expect(
|
||||
await store.remove({
|
||||
id,
|
||||
expectedGeneration: asObjectGeneration(2),
|
||||
}),
|
||||
).toMatchObject({ ok: false, failure: { code: "CONFLICT" } });
|
||||
});
|
||||
|
||||
it("stages and activates only exact public Cache Storage representations", async () => {
|
||||
const cache = new MemoryPublicResponseCache({
|
||||
origin: "https://app.example.test",
|
||||
maxEntryBytes: 1_000,
|
||||
allowedMediaTypes: new Set(["text/javascript"]),
|
||||
});
|
||||
const entry = {
|
||||
url: "https://app.example.test/assets/app.js?v=1#ignored",
|
||||
byteLength: asByteCount(100),
|
||||
mediaType: "text/javascript",
|
||||
integritySha256: "sha256-entry",
|
||||
requestCredentials: "OMIT" as const,
|
||||
dataClass: "PUBLIC" as const,
|
||||
};
|
||||
expect(
|
||||
await cache.stageRelease({
|
||||
releaseId: "release-1",
|
||||
manifestDigest: "manifest-1",
|
||||
entries: [entry],
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toEqual({ ok: true, value: undefined });
|
||||
expect(
|
||||
await cache.activateRelease({
|
||||
releaseId: "release-1",
|
||||
manifestDigest: "manifest-1",
|
||||
expectedPreviousReleaseId: null,
|
||||
}),
|
||||
).toEqual({ ok: true, value: undefined });
|
||||
expect(
|
||||
await cache.lookup({
|
||||
url: "https://app.example.test/assets/app.js?v=1",
|
||||
requestCredentials: "OMIT",
|
||||
hasAuthorization: false,
|
||||
}),
|
||||
).toMatchObject({ ok: true, value: { kind: "HIT" } });
|
||||
expect(
|
||||
await cache.lookup({
|
||||
url: "https://app.example.test/assets/app.js?v=2",
|
||||
requestCredentials: "OMIT",
|
||||
hasAuthorization: false,
|
||||
}),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
value: { kind: "MISS", reason: "NOT_FOUND" },
|
||||
});
|
||||
expect(
|
||||
await cache.stageRelease({
|
||||
releaseId: "poisoned",
|
||||
manifestDigest: "bad",
|
||||
entries: [{ ...entry, url: "https://app.example.test/config.json" }],
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "POLICY_REJECTED" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
FakeBrowserPermissionAdapter,
|
||||
FakeClientWorkflowAdapter,
|
||||
FakeFeatureFlagAdapter,
|
||||
FakeFileTransferAdapter,
|
||||
FakeGeneratedApiAdapter,
|
||||
FakeLargeDataUiAdapter,
|
||||
FakeMultiTabAdapter,
|
||||
FakeRealtimeAdapter,
|
||||
FakeServiceWorkerUpdateAdapter,
|
||||
FakeWorkerTaskAdapter,
|
||||
MemoryOfflineRepository,
|
||||
RecordingAnalyticsAdapter,
|
||||
createUnavailableAdapters,
|
||||
} from "../../recipes/frontend-capabilities/index.ts";
|
||||
|
||||
describe("optional capability recipes", () => {
|
||||
it("requires realtime cleanup and rejects duplicate or out-of-order events", async () => {
|
||||
const adapter = new FakeRealtimeAdapter<{ value: string }>();
|
||||
const received: string[] = [];
|
||||
const subscription = await adapter.subscribe({
|
||||
channel: "orders",
|
||||
onEvent(result) {
|
||||
received.push(result.ok ? result.value.payload.value : result.failure.code);
|
||||
},
|
||||
});
|
||||
expect(subscription.ok).toBe(true);
|
||||
adapter.emit("orders", {
|
||||
id: "event-2",
|
||||
sequence: 2,
|
||||
occurredAt: "2026-07-26T00:00:00.000Z",
|
||||
payload: { value: "new" },
|
||||
});
|
||||
adapter.emit("orders", {
|
||||
id: "event-1",
|
||||
sequence: 1,
|
||||
occurredAt: "2026-07-26T00:00:00.000Z",
|
||||
payload: { value: "old" },
|
||||
});
|
||||
expect(received).toEqual(["new", "STALE_RESULT"]);
|
||||
if (subscription.ok) subscription.value.unsubscribe();
|
||||
expect(adapter.activeSubscriptionCount).toBe(0);
|
||||
});
|
||||
|
||||
it("models offline schema migration and closed-repository fallback", async () => {
|
||||
const repository = new MemoryOfflineRepository<{ id: string; name: string }>();
|
||||
expect(await repository.open({ schemaVersion: 1 })).toEqual({
|
||||
ok: true,
|
||||
value: undefined,
|
||||
});
|
||||
await repository.put({ id: "one", name: "offline" });
|
||||
expect(await repository.migrate({ from: 1, to: 2 })).toEqual({
|
||||
ok: true,
|
||||
value: undefined,
|
||||
});
|
||||
expect(await repository.get("one")).toEqual({
|
||||
ok: true,
|
||||
value: { id: "one", name: "offline" },
|
||||
});
|
||||
repository.close();
|
||||
expect((await repository.get("one")).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("covers service-worker stale update and explicit unregister", async () => {
|
||||
const adapter = new FakeServiceWorkerUpdateAdapter("v1", "v2");
|
||||
expect((await adapter.inspect()).ok).toBe(true);
|
||||
expect((await adapter.activate("stale")).ok).toBe(false);
|
||||
expect(await adapter.activate("v2")).toEqual({ ok: true, value: undefined });
|
||||
expect(await adapter.unregister()).toEqual({ ok: true, value: undefined });
|
||||
});
|
||||
|
||||
it("covers file validation, progress and AbortSignal cancellation", async () => {
|
||||
const adapter = new FakeFileTransferAdapter(10, new Set(["text/plain"]));
|
||||
const progress: number[] = [];
|
||||
const controller = new AbortController();
|
||||
const uploaded = await adapter.upload({
|
||||
file: {
|
||||
name: "safe.txt",
|
||||
size: 4,
|
||||
type: "text/plain",
|
||||
content: {
|
||||
byteLength: 4,
|
||||
chunks: (async function* () {
|
||||
yield new TextEncoder().encode("safe");
|
||||
})(),
|
||||
},
|
||||
},
|
||||
signal: controller.signal,
|
||||
onProgress(value) {
|
||||
progress.push(value.transferredBytes);
|
||||
},
|
||||
});
|
||||
expect(uploaded.ok).toBe(true);
|
||||
expect(progress).toEqual([4]);
|
||||
controller.abort();
|
||||
const cancelled = await adapter.download({
|
||||
resourceId: "one",
|
||||
signal: controller.signal,
|
||||
onProgress() {},
|
||||
});
|
||||
expect(cancelled).toMatchObject({
|
||||
ok: false,
|
||||
failure: { code: "ABORTED" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps generated API and feature flag vendors behind typed facades", async () => {
|
||||
const api = new FakeGeneratedApiAdapter("2026-07", {
|
||||
list: () => [{ id: "one" }],
|
||||
});
|
||||
expect(
|
||||
await api.execute<Array<{ id: string }>>({
|
||||
operationId: "list",
|
||||
contractVersion: "2026-07",
|
||||
}),
|
||||
).toEqual({ ok: true, value: [{ id: "one" }] });
|
||||
expect(
|
||||
(
|
||||
await api.execute({
|
||||
operationId: "list",
|
||||
contractVersion: "old",
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
|
||||
const flags = new FakeFeatureFlagAdapter<{ checkout: boolean }>({
|
||||
checkout: true,
|
||||
});
|
||||
expect(
|
||||
await flags.evaluate({
|
||||
key: "checkout",
|
||||
fallback: false,
|
||||
maxAgeMs: 1_000,
|
||||
}),
|
||||
).toEqual({ ok: true, value: true });
|
||||
});
|
||||
|
||||
it("discards cancelled worker results and de-duplicates multi-tab events", async () => {
|
||||
const worker = new FakeWorkerTaskAdapter<number, number>((value) => value * 2);
|
||||
const controller = new AbortController();
|
||||
worker.cancel("cancelled");
|
||||
expect(
|
||||
(
|
||||
await worker.run({
|
||||
taskId: "cancelled",
|
||||
generation: 1,
|
||||
payload: 2,
|
||||
signal: controller.signal,
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
|
||||
const tabs = new FakeMultiTabAdapter<{ refreshed: boolean }>();
|
||||
const observed: string[] = [];
|
||||
const cleanup = tabs.subscribe({
|
||||
sourceId: "tab-b",
|
||||
onEvent(result) {
|
||||
if (result.ok) observed.push(result.value.eventId);
|
||||
},
|
||||
});
|
||||
expect(
|
||||
tabs.publish({
|
||||
eventId: "one",
|
||||
sourceId: "tab-a",
|
||||
version: 1,
|
||||
payload: { refreshed: true },
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
expect(
|
||||
tabs.publish({
|
||||
eventId: "one",
|
||||
sourceId: "tab-a",
|
||||
version: 1,
|
||||
payload: { refreshed: true },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
expect(observed).toEqual(["one"]);
|
||||
cleanup();
|
||||
tabs.close();
|
||||
});
|
||||
|
||||
it("separates browser permission, local workflow and large-data behavior", async () => {
|
||||
const permissions = new FakeBrowserPermissionAdapter({
|
||||
notification: "denied",
|
||||
});
|
||||
expect(
|
||||
await permissions.request({ capability: "notification" }),
|
||||
).toEqual({ ok: true, value: "denied" });
|
||||
expect(
|
||||
(
|
||||
await permissions.request({
|
||||
capability: "clipboard-read",
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
|
||||
const workflow = new FakeClientWorkflowAdapter(
|
||||
{ step: 0 },
|
||||
(state, event: "next") => ({
|
||||
step: event === "next" ? state.step + 1 : state.step,
|
||||
}),
|
||||
);
|
||||
workflow.dispatch("next");
|
||||
expect(workflow.snapshot()).toEqual({ step: 1 });
|
||||
workflow.reset();
|
||||
expect(workflow.snapshot()).toEqual({ step: 0 });
|
||||
|
||||
const data = new FakeLargeDataUiAdapter<{ id: string; name: string }>();
|
||||
data.replace([{ id: "one", name: "row" }], 2);
|
||||
expect(data.window({ offset: 0, limit: 1, generation: 1 }).ok).toBe(false);
|
||||
expect(data.window({ offset: 0, limit: 1, generation: 2 })).toEqual({
|
||||
ok: true,
|
||||
value: [{ id: "one", name: "row" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("enforces analytics consent, redaction and bounded queues", () => {
|
||||
const adapter = new RecordingAnalyticsAdapter(1);
|
||||
expect(
|
||||
adapter.record({
|
||||
kind: "analytics",
|
||||
eventId: "page-viewed",
|
||||
consent: "denied",
|
||||
attributes: {},
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
expect(
|
||||
adapter.record({
|
||||
kind: "error",
|
||||
eventId: "render-failed",
|
||||
consent: "not-required",
|
||||
attributes: { routeId: "HOME", authToken: "must-not-survive" },
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
expect(adapter.records[0]?.attributes).toEqual({ routeId: "HOME" });
|
||||
expect(
|
||||
adapter.record({
|
||||
kind: "error",
|
||||
eventId: "second",
|
||||
consent: "not-required",
|
||||
attributes: {},
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("provides fail-closed unavailable adapters for every capability", async () => {
|
||||
const adapters = createUnavailableAdapters();
|
||||
const controller = new AbortController();
|
||||
const results = await Promise.all([
|
||||
adapters.realtime.heartbeat(),
|
||||
adapters.offline.open({ schemaVersion: 1 }),
|
||||
adapters.serviceWorker.unregister(),
|
||||
adapters.fileTransfer.download({
|
||||
resourceId: "one",
|
||||
signal: controller.signal,
|
||||
onProgress() {},
|
||||
}),
|
||||
adapters.generatedApi.execute({
|
||||
operationId: "one",
|
||||
contractVersion: "one",
|
||||
}),
|
||||
adapters.featureFlag.evaluate({
|
||||
key: "one",
|
||||
fallback: false,
|
||||
maxAgeMs: 0,
|
||||
}),
|
||||
adapters.worker.run({
|
||||
taskId: "one",
|
||||
generation: 1,
|
||||
payload: null,
|
||||
signal: controller.signal,
|
||||
}),
|
||||
adapters.browserPermission.request({ capability: "notification" }),
|
||||
adapters.analytics.flush(),
|
||||
]);
|
||||
expect(results.every((result) => !result.ok)).toBe(true);
|
||||
expect(adapters.multiTab.publish({
|
||||
eventId: "one",
|
||||
sourceId: "one",
|
||||
version: 1,
|
||||
payload: null,
|
||||
}).ok).toBe(false);
|
||||
expect(adapters.clientWorkflow.dispatch(null).ok).toBe(false);
|
||||
expect(
|
||||
adapters.largeDataUi.window({ offset: 0, limit: 1, generation: 1 }).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user