Release Healer Man 0.1.14
This commit is contained in:
1 parent
67d7a8ebe7
commit
7b41b102fa
147 files changed
+169892
-1342
No files matched your search
@@ -0,0 +1,150 @@
|
||||
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<string, Response>();
|
||||
|
||||
async match(request: RequestInfo | URL): Promise<Response | undefined> {
|
||||
return this.responses.get(this.key(request))?.clone();
|
||||
}
|
||||
|
||||
async put(request: RequestInfo | URL, response: Response): Promise<void> {
|
||||
this.responses.set(this.key(request), response.clone());
|
||||
}
|
||||
|
||||
async delete(request: RequestInfo | URL): Promise<boolean> {
|
||||
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<string, MemoryCache>();
|
||||
|
||||
async open(name: string): Promise<Cache> {
|
||||
const cache = this.stores.get(name) ?? new MemoryCache();
|
||||
this.stores.set(name, cache);
|
||||
return cache as unknown as Cache;
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<boolean> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user