import assert from "node:assert/strict"; import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { BUNDLED_ANDROID_PACKS, PROJECT_ROOT, RANDOMIZED_BITE_SIZED_PACK_DEFINITION, assertCampaignDungeonManifest, contentPackForPath, createContentManifest, loadCampaignDungeonPackDefinitions, publishContent, scanContentSources, } from "./publish-content.mjs"; test("gives randomized bite-sized its base location dependency closure", () => { assert.deepEqual(RANDOMIZED_BITE_SIZED_PACK_DEFINITION, { id: "activity:randomized-bite-sized", label: "Randomized Bite-Sized Base", bundledOnAndroid: false, dependencies: ["dungeon:stormwind-stockade"], }); }); test("assigns stable player-facing packs from logical asset paths", () => { assert.equal(contentPackForPath("assets/game/dungeons/wailing-caverns/visual.glb"), "dungeon:wailing-caverns"); assert.equal(contentPackForPath("assets/creatures/wailing-caverns/mutanus.glb"), "dungeon:wailing-caverns"); assert.equal(contentPackForPath("assets/characters/wow335a-v1/manifest.json"), "core:character-basic"); assert.equal(contentPackForPath("assets/characters/wow335a-v1/human/male/rig.glb"), "core:character-basic"); assert.equal(contentPackForPath("assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_00.webp"), "core:character-basic"); assert.equal(contentPackForPath("assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_01.webp"), "characters:wow335a-v1"); assert.equal(contentPackForPath("assets/characters/wow335a-v1/orc/male/rig.glb"), "characters:wow335a-v1"); assert.equal(contentPackForPath("assets/game/manastorm/stage-1/visual.glb"), "manastorm:stage-1"); assert.equal(contentPackForPath("assets/game/arenas/nagrand-arena/visual.glb"), "core:arenas"); assert.equal(BUNDLED_ANDROID_PACKS.includes("core:arenas"), false); assert.equal(contentPackForPath("assets/game/combat-effects/audio/smite.wav"), "core:combat-media"); assert.equal( contentPackForPath("assets/game/combat-effects/audio/manastorm/Manastorm_BossKill_GenericPortalsReady.wav"), "core:combat-media", ); assert.equal(BUNDLED_ANDROID_PACKS.includes("core:combat-media"), false); assert.equal(contentPackForPath("basis/basis_transcoder.wasm"), "core:runtime"); }); test("gives every WoW campaign dungeon an exact install dependency closure", async () => { const campaign = JSON.parse(await readFile(path.join( PROJECT_ROOT, "src", "game", "generated", "dungeonCampaignCatalog.json", ), "utf8")); const packDefinitions = await loadCampaignDungeonPackDefinitions(); const dungeonDefinitions = packDefinitions.filter((pack) => pack.id.startsWith("dungeon:")); const wailingCreatureManifest = JSON.parse(await readFile(path.join( PROJECT_ROOT, "public", "assets", "creatures", "wailing-caverns", "animated-manifest.json", ), "utf8")); assert.equal(campaign.definitions.length, 54); assert.equal(dungeonDefinitions.length, 54); assert.equal(wailingCreatureManifest.creatures.length, 16); const referencedSourceFiles = [ ...campaign.definitions.flatMap((dungeon) => [ ...[ ...dungeon.assets.visual, ...dungeon.assets.collision, dungeon.assets.navigation, ].map((asset) => ({ dungeonId: dungeon.id, url: asset.url })), ...Object.values(dungeon.entities).flatMap((entity) => ( entity.visual?.model?.url ? [{ dungeonId: dungeon.id, url: entity.visual.model.url }] : [] )), ]), ...wailingCreatureManifest.creatures.map((creature) => ({ dungeonId: "wailing-caverns", url: creature.url, })), ]; await Promise.all(referencedSourceFiles.map(async ({ dungeonId, url }) => { try { await access(path.join(PROJECT_ROOT, "public", ...url.slice(1).split("/"))); } catch { assert.fail(`${dungeonId} is missing ${url}`); } })); await Promise.all([ "wailing-caverns-visual.glb", "wailing-caverns-collision.glb", "wailing-caverns-navigation.glb", ].map((fileName) => access(path.join( PROJECT_ROOT, "src", "assets", "game", "dungeons", "wailing-caverns", fileName, )))); const logicalFiles = new Map(); const addFile = (logicalPath) => logicalFiles.set(logicalPath, { logicalPath, sourcePath: logicalPath, sha256: "a".repeat(64), bytes: 1, extension: path.extname(logicalPath), packId: contentPackForPath(logicalPath), }); addFile("assets/game/dungeons/wailing-caverns/visual.glb"); for (const dungeon of campaign.definitions) { for (const asset of [ ...dungeon.assets.visual, ...dungeon.assets.collision, dungeon.assets.navigation, ]) addFile(asset.url.slice(1)); for (const entity of Object.values(dungeon.entities)) { if (entity.visual?.model?.url) addFile(entity.visual.model.url.slice(1)); } } const manifest = createContentManifest([...logicalFiles.values()], { packDefinitions, version: "campaign-contract", }); assert.doesNotThrow(() => assertCampaignDungeonManifest(manifest, campaign.definitions)); const packs = new Map(manifest.packs.map((pack) => [pack.id, pack])); for (const dungeon of campaign.definitions) { const pack = packs.get(`dungeon:${dungeon.id}`); assert.ok(pack, dungeon.id); if (dungeon.id === "wailing-caverns") { assert.equal(pack.bundledOnAndroid, true); assert.deepEqual(pack.dependencies, []); continue; } const environmentPack = contentPackForPath(dungeon.assets.visual[0].url.slice(1)); const needsSharedCreatures = Object.values(dungeon.entities).some((entity) => ( entity.visual?.model?.url?.startsWith("/assets/game/creatures/shared/") )); assert.deepEqual( pack.dependencies, [environmentPack, ...(needsSharedCreatures ? ["shared:creatures"] : [])], dungeon.id, ); assert.deepEqual(packs.get(environmentPack)?.dependencies, [], dungeon.id); } for (const [dungeonId, environmentPack] of [ ["tinkertech-showdown", "manastorm:821-tinketechshowdown"], ["frozen-reach", "manastorm:908-alvaencounter"], ["forgotten-mine", "manastorm:1781-forgottenmine"], ]) { const pack = packs.get(`dungeon:${dungeonId}`); assert.equal(pack.fileCount, 0); assert.equal(pack.bytes, 0); assert.deepEqual(pack.dependencies, [environmentPack]); } }); test("default release publishing fails before writing an incomplete campaign manifest", async () => { const directory = await mkdtemp(path.join(os.tmpdir(), "healer-man-incomplete-content-test-")); const sourceDirectory = path.join(directory, "source"); const contentDirectory = path.join(directory, "content"); await mkdir(sourceDirectory, { recursive: true }); await writeFile(path.join(sourceDirectory, "placeholder.glb"), Buffer.from("incomplete")); try { await assert.rejects(publishContent({ contentDirectory, sources: [{ directory: sourceDirectory, logicalPrefix: "assets/game/dungeons/placeholder" }], }), /Release content manifest is missing/); await assert.rejects(access(contentDirectory), { code: "ENOENT" }); } finally { await rm(directory, { recursive: true, force: true }); } }); test("content versions change when only logical pack dependencies change", () => { const files = [{ logicalPath: "assets/game/dungeons/test/creature.glb", sourcePath: "creature.glb", sha256: "b".repeat(64), bytes: 1, extension: ".glb", packId: "dungeon:test", }]; const first = createContentManifest(files, { packDefinitions: [{ id: "dungeon:test", dependencies: ["manastorm:first"] }], }); const second = createContentManifest(files, { packDefinitions: [{ id: "dungeon:test", dependencies: ["manastorm:second"] }], }); assert.notEqual(first.version, second.version); }); test("publishes objects before a deterministic latest manifest", async () => { const directory = await mkdtemp(path.join(os.tmpdir(), "healer-man-content-test-")); const sourceDirectory = path.join(directory, "source"); const contentDirectory = path.join(directory, "content"); await mkdir(path.join(sourceDirectory, "wailing-caverns"), { recursive: true }); await writeFile(path.join(sourceDirectory, "wailing-caverns", "visual.glb"), Buffer.from("glb-one")); const sources = [{ directory: sourceDirectory, logicalPrefix: "assets/game/dungeons" }]; try { const scanned = await scanContentSources(sources); const firstManifest = createContentManifest(scanned, { createdAt: "2026-08-14T00:00:00.000Z" }); assert.equal(Object.keys(firstManifest.files).length, 1); assert.equal(firstManifest.packs[0].id, "dungeon:wailing-caverns"); const first = await publishContent({ contentDirectory, sources, includeCampaignPacks: false, createdAt: "2026-08-14T00:00:00.000Z", }); assert.equal(first.copiedObjects, 1); const latest = JSON.parse(await readFile(path.join(contentDirectory, "manifest.json"), "utf8")); assert.equal(latest.version, first.manifest.version); const entry = latest.files["/assets/game/dungeons/wailing-caverns/visual.glb"]; assert.match(entry.url, /^\/content\/objects\/[a-f0-9]{64}\.glb$/); assert.equal(await readFile(path.join(contentDirectory, "objects", path.basename(entry.url)), "utf8"), "glb-one"); const second = await publishContent({ contentDirectory, sources, includeCampaignPacks: false, createdAt: "2026-08-14T00:00:00.000Z", }); assert.equal(second.copiedObjects, 0); assert.equal(second.manifest.version, first.manifest.version); } finally { await rm(directory, { recursive: true, force: true }); } });