Files
next-tjwater-frontend/src/shared/ai-elements/persona-resource.test.ts
T
jiang 2d8506fea0
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s
feat: add project-aware agent workspace
2026-10-22 18:52:18 +08:00

122 lines
5.6 KiB
TypeScript

import { webcrypto } from "node:crypto";
import { readFileSync } from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import asset from "./persona-resource-manifest.json";
const bytes = Uint8Array.from(readFileSync(`public${asset.path}`)).buffer;
const source = `${asset.path}?v=${asset.sha256}`;
const absolute = (path: string) => new URL(path, window.location.href).href;
function mockStorage() {
const entries = new Map<string, Response>();
const cache = {
match: vi.fn(async (key: string) => entries.get(absolute(key))?.clone()),
put: vi.fn(async (key: string, response: Response) => { entries.set(absolute(key), response.clone()); }),
keys: vi.fn(async () => Array.from(entries.keys(), (key) => new Request(key))),
delete: vi.fn(async (request: Request) => entries.delete(request.url))
};
vi.stubGlobal("caches", { open: vi.fn().mockResolvedValue(cache) });
return { entries, cache };
}
beforeEach(() => {
vi.stubGlobal("crypto", webcrypto);
vi.stubGlobal("caches", undefined);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.resetModules();
});
describe("Obsidian resource", () => {
it("shares one download across simultaneous and later renderer mounts", async () => {
const fetch = vi.fn().mockResolvedValue(new Response(bytes));
vi.stubGlobal("fetch", fetch);
const { loadPersonaResource } = await import("./persona-resource");
const [first, second] = await Promise.all([loadPersonaResource(), loadPersonaResource()]);
expect(new Uint8Array(first)).toEqual(new Uint8Array(bytes));
expect(second).toBe(first);
expect(await loadPersonaResource()).toBe(first);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
source,
expect.objectContaining({ signal: expect.any(AbortSignal) })
);
});
it("allows a new mount to retry a failed download", async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce(new Response(null, { status: 503 }))
.mockResolvedValueOnce(new Response(bytes));
vi.stubGlobal("fetch", fetch);
const { loadPersonaResource } = await import("./persona-resource");
await expect(loadPersonaResource()).rejects.toThrow("503");
expect(new Uint8Array(await loadPersonaResource())).toEqual(new Uint8Array(bytes));
expect(fetch).toHaveBeenCalledTimes(2);
});
it("loads from persistent storage after module memory is gone and the network is unavailable", async () => {
const { entries } = mockStorage();
const fetch = vi.fn().mockResolvedValue(new Response(bytes));
vi.stubGlobal("fetch", fetch);
await (await import("./persona-resource")).loadPersonaResource();
expect(entries.has(absolute(source))).toBe(true);
vi.resetModules();
fetch.mockRejectedValue(new TypeError("offline"));
const restored = await (await import("./persona-resource")).loadPersonaResource();
expect(new Uint8Array(restored)).toEqual(new Uint8Array(bytes));
expect(fetch).toHaveBeenCalledTimes(1);
});
it("fetches a new content version and removes only old entries for this animation", async () => {
const { entries } = mockStorage();
entries.set(absolute(`${asset.path}?v=old`), new Response("old animation"));
entries.set(absolute("/unrelated.riv"), new Response("keep"));
const fetch = vi.fn().mockResolvedValue(new Response(bytes));
vi.stubGlobal("fetch", fetch);
await (await import("./persona-resource")).loadPersonaResource();
expect(fetch).toHaveBeenCalledTimes(1);
expect(Array.from(entries.keys()).sort()).toEqual([absolute(source), absolute("/unrelated.riv")].sort());
});
it("replaces corrupted cached bytes even when their size and RIVE header still match", async () => {
const { entries } = mockStorage();
const corrupted = new Uint8Array(bytes.slice(0));
corrupted[100] ^= 1;
entries.set(absolute(source), new Response(corrupted));
const fetch = vi.fn().mockResolvedValue(new Response(bytes));
vi.stubGlobal("fetch", fetch);
await (await import("./persona-resource")).loadPersonaResource();
expect(fetch).toHaveBeenCalledTimes(1);
expect(new Uint8Array(await entries.get(absolute(source))!.arrayBuffer())).toEqual(new Uint8Array(bytes));
});
it.each(["unavailable", "denied", "full"])("keeps playing when storage is %s", async (failure) => {
if (failure === "denied") {
vi.stubGlobal("caches", { open: vi.fn().mockRejectedValue(new Error("Storage denied")) });
} else if (failure === "full") {
mockStorage().cache.put.mockRejectedValue(new Error("Quota exceeded"));
}
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(bytes)));
expect(new Uint8Array(await (await import("./persona-resource")).loadPersonaResource())).toEqual(new Uint8Array(bytes));
});
it("does not persist an HTML fallback or the wrong resource version and can retry", async () => {
const { cache } = mockStorage();
const wrongVersion = new Uint8Array(bytes.slice(0));
wrongVersion[100] ^= 1;
vi.stubGlobal("fetch", vi.fn()
.mockResolvedValueOnce(new Response("<!doctype html>"))
.mockResolvedValueOnce(new Response(wrongVersion))
.mockResolvedValueOnce(new Response(bytes)));
const { loadPersonaResource } = await import("./persona-resource");
await expect(loadPersonaResource()).rejects.toThrow("invalid animation data");
await expect(loadPersonaResource()).rejects.toThrow("version mismatch");
expect(cache.put).not.toHaveBeenCalled();
expect(new Uint8Array(await loadPersonaResource())).toEqual(new Uint8Array(bytes));
expect(cache.put).toHaveBeenCalledTimes(1);
});
});