import { createHash } from "node:crypto"; import { Capacitor } from "@capacitor/core"; import { afterEach, describe, expect, it, vi } from "vitest"; import { BROWSER_CONTENT_CACHE_NAME, checkForContentUpdates, contentPackHasUpdate, contentPackIsBundled, contentPackIsInstalled, contentPackIsUsable, getContentManagerSnapshot, installContentPack, removeContentPack, } from "./contentManager"; import type { ContentManifest } from "./contentManifest"; class MemoryCache { readonly responses = new Map(); async match(request: RequestInfo | URL): Promise { return this.responses.get(this.key(request))?.clone(); } async put(request: RequestInfo | URL, response: Response): Promise { this.responses.set(this.key(request), response.clone()); } async delete(request: RequestInfo | URL): Promise { return this.responses.delete(this.key(request)); } private key(request: RequestInfo | URL): string { if (request instanceof Request) return request.url; return new URL(String(request), "http://localhost").toString(); } } class MemoryCacheStorage { readonly stores = new Map(); async open(name: string): Promise { const cache = this.stores.get(name) ?? new MemoryCache(); this.stores.set(name, cache); return cache as unknown as Cache; } async delete(name: string): Promise { return this.stores.delete(name); } } function sha256(bytes: Uint8Array): string { return createHash("sha256").update(bytes).digest("hex"); } function contentManifest(version: string, objectName: string, bytes: Uint8Array): ContentManifest { return { schemaVersion: 1, version, createdAt: "2026-09-04T00:00:00.000Z", minimumAppVersion: "0.1.0", packs: [{ id: "characters:test", label: "Test characters", bytes: bytes.byteLength, fileCount: 1, bundledOnAndroid: false, dependencies: [], }], files: { "/assets/characters/test.glb": { packId: "characters:test", url: `/content/objects/${objectName}`, sha256: sha256(bytes), bytes: bytes.byteLength, }, }, }; } afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe("browser content packs", () => { it("downloads, updates, and removes verified optional content", async () => { const firstBytes = new TextEncoder().encode("first-model"); const updatedBytes = new TextEncoder().encode("updated-model"); let manifest = contentManifest("content-one", "first.glb", firstBytes); let corruptUpdate = false; const storage = new MemoryCacheStorage(); vi.spyOn(Capacitor, "isNativePlatform").mockReturnValue(false); vi.stubGlobal("caches", storage as unknown as CacheStorage); vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url.endsWith("/content/manifest.json")) return Response.json(manifest); if (url.endsWith("/first.glb")) return new Response(firstBytes); if (url.endsWith("/updated.glb")) { return new Response(corruptUpdate ? new Uint8Array(updatedBytes.byteLength).fill(7) : updatedBytes); } return new Response(null, { status: 404 }); })); await checkForContentUpdates(); expect(contentPackIsInstalled("characters:test")).toBe(false); expect(contentPackIsUsable("characters:test")).toBe(false); expect(contentPackHasUpdate("characters:test")).toBe(false); await installContentPack("characters:test"); expect(contentPackIsInstalled("characters:test")).toBe(true); expect(contentPackIsUsable("characters:test")).toBe(true); expect(contentPackHasUpdate("characters:test")).toBe(false); expect(getContentManagerSnapshot()).toMatchObject({ installingPackId: null, downloadedBytes: firstBytes.byteLength, totalBytes: firstBytes.byteLength, error: "", }); const active = storage.stores.get(BROWSER_CONTENT_CACHE_NAME); expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("first-model"); manifest = contentManifest("content-two", "updated.glb", updatedBytes); await checkForContentUpdates(); expect(contentPackHasUpdate("characters:test")).toBe(true); corruptUpdate = true; await expect(installContentPack("characters:test")).rejects.toThrow(/verification/i); expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("first-model"); corruptUpdate = false; await installContentPack("characters:test"); expect(contentPackHasUpdate("characters:test")).toBe(false); expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("updated-model"); await removeContentPack("characters:test"); expect(contentPackIsInstalled("characters:test")).toBe(false); expect(contentPackIsUsable("characters:test")).toBe(false); expect(await active?.match("http://localhost/assets/characters/test.glb")).toBeUndefined(); }); it("keeps default browser content installed and non-removable", async () => { vi.spyOn(Capacitor, "isNativePlatform").mockReturnValue(false); expect(contentPackIsBundled("core:ui")).toBe(true); expect(contentPackIsUsable("core:ui")).toBe(true); await expect(removeContentPack("core:ui")).resolves.toBeUndefined(); expect(contentPackIsUsable("core:ui")).toBe(true); }); });