diff --git a/PLAYTEST_NOTES.md b/PLAYTEST_NOTES.md index 0a51a10c..8fcd50b7 100644 --- a/PLAYTEST_NOTES.md +++ b/PLAYTEST_NOTES.md @@ -1992,3 +1992,26 @@ Vite's existing large-chunk advisory remains informational. eight-heading pixel comparisons, profiles, before/after timing JSON, and screenshots. Measurements use the same desktop Edge/RX 9070 XT setup as the preceding audit. These CPU improvements do not certify Thor performance. + +# 2026-09-09 — bundled arena assets blocked by the browser content worker + +- Reproduced the reported collision-matched fallback in the production build + with the content worker active. Warnorken's `visual.glb?arena-runtime=1` + returned 404 even though the file was bundled and directly served with 200. + The worker omitted `/assets/game/arenas/` from its bundled asset prefixes and + incorrectly rejected these files as uninstalled optional content. +- Added the arena prefix to the worker. Installed updates still take priority; + missing optional dungeon content still returns 404. Behavioral worker tests + failed for all eight arenas before the fix and now cover visual, collision, + and navigation requests with the runtime query parameter. +- The production build passed. Reloading the same browser session activated + the fixed worker without clearing storage or downloading content. Warnorken + rendered textured stone geometry and Nagrand rendered its authored arena + geometry; neither showed the loading/fallback banner after loading, and no + new console errors were recorded. Checks used offline practice in the Codex + browser at `http://127.0.0.1:4174/`, backed by a local test server on port 4173. +- All 22 content tests and the eight-arena asset validator passed. Screenshots, + before/after error logs, asset validation, and build output are saved under + `playtest-artifacts/2026-09-09-arena-loading/`. The existing large-bundle + advisory remains. This verifies the local browser build, not a deployment + or an Android package. diff --git a/package-lock.json b/package-lock.json index a7133fa0..9c9abb21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "healer-man", - "version": "0.1.26", + "version": "0.1.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "healer-man", - "version": "0.1.26", + "version": "0.1.27", "dependencies": { "@capacitor/android": "8.4.1", "@capacitor/core": "8.4.1", diff --git a/package.json b/package.json index 664fd217..0c6d20a6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "healer-man", "private": true, - "version": "0.1.26", + "version": "0.1.27", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/public/content-cache-sw.js b/public/content-cache-sw.js index dada0253..d589f852 100644 --- a/public/content-cache-sw.js +++ b/public/content-cache-sw.js @@ -12,6 +12,7 @@ const BUNDLED_ASSET_PREFIXES = [ "/assets/characters/wow335a-v1/manifest.json", "/assets/characters/wow335a-v1/human/", "/assets/creatures/wailing-caverns/", + "/assets/game/arenas/", "/assets/game/dungeons/wailing-caverns/", ]; const BUNDLED_ASSET_PATHS = new Set([ diff --git a/release-version.json b/release-version.json index 76128d0f..d0b3d983 100644 --- a/release-version.json +++ b/release-version.json @@ -1,4 +1,4 @@ { - "versionName": "0.1.26", - "versionCode": 1026 + "versionName": "0.1.27", + "versionCode": 1027 } diff --git a/src/content/contentCacheWorker.test.ts b/src/content/contentCacheWorker.test.ts index f394fdb7..f6324286 100644 --- a/src/content/contentCacheWorker.test.ts +++ b/src/content/contentCacheWorker.test.ts @@ -1,5 +1,7 @@ import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; +import { runInNewContext } from "node:vm"; +import { describe, expect, it, vi } from "vitest"; +import { ARENA_STAGES } from "../game/arenaStages"; import { BROWSER_CONTENT_CACHE_NAME } from "./contentManager"; const workerSource = readFileSync( @@ -7,7 +9,61 @@ const workerSource = readFileSync( "utf8", ); +function contentWorker(cached?: Response) { + const handlers = new Map void>(); + const fetch = vi.fn(async () => new Response("bundled arena", { status: 200 })); + const match = vi.fn(async () => cached); + runInNewContext(workerSource, { + self: { + location: { origin: "https://healer.test" }, + addEventListener: (type: string, handler: (event: unknown) => void) => handlers.set(type, handler), + }, + caches: { open: async () => ({ match }) }, + fetch, + URL, + Response, + }); + return { + fetch, + match, + request: (path: string) => { + let response: Promise | undefined; + handlers.get("fetch")!({ + request: new Request(new URL(path, "https://healer.test")), + respondWith: (value: Promise) => { response = value; }, + }); + return response; + }, + }; +} + describe("browser content cache worker", () => { + it.each(ARENA_STAGES)("loads bundled $name assets without an optional content install", async (stage) => { + const worker = contentWorker(); + for (const asset of stage.assets) { + const response = await worker.request(`${asset.url}?arena-runtime=1`); + expect(response?.status).toBe(200); + expect(await response?.text()).toBe("bundled arena"); + } + expect(worker.fetch).toHaveBeenCalledTimes(stage.assets.length); + }); + + it("prefers installed arena updates over bundled files, ignoring the runtime query", async () => { + const worker = contentWorker(new Response("updated arena")); + const response = await worker.request(`${ARENA_STAGES[0].assets[0].url}?arena-runtime=1`); + expect(await response?.text()).toBe("updated arena"); + expect(worker.match).toHaveBeenCalledWith(expect.any(Request), { ignoreSearch: true }); + expect(worker.fetch).not.toHaveBeenCalled(); + }); + + it("still blocks optional dungeon assets that have not been installed", async () => { + const worker = contentWorker(); + const response = await worker.request("/assets/game/manastorm/optional/visual.glb"); + expect(response?.status).toBe(404); + expect(await response?.text()).toBe("Optional content is not installed."); + expect(worker.fetch).not.toHaveBeenCalled(); + }); + it("serves only cached same-origin asset requests from the active content cache", () => { expect(workerSource).toContain(`const CONTENT_CACHE_NAME = "${BROWSER_CONTENT_CACHE_NAME}";`); expect(workerSource).toContain('request.method !== "GET"');