feat: add project-aware agent workspace
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user