import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import test from "node:test"; import ts from "typescript"; const repoRoot = path.resolve(import.meta.dirname, "../../.."); const protocolPath = path.join(repoRoot, "web/protocol/manifest.ts"); const protocolSource = fs.readFileSync(protocolPath, "utf8"); const transpiled = ts.transpileModule(protocolSource, { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: protocolPath, reportDiagnostics: true, }); assert.deepEqual(transpiled.diagnostics, []); const protocol = await import(`data:text/javascript;base64,${Buffer.from(transpiled.outputText).toString("base64")}`); const { validateWebEngineManifestV2 } = protocol; const goldenPath = path.join(repoRoot, "tests/golden/M6-04A/engine-manifest-v2.json"); const golden = JSON.parse(fs.readFileSync(goldenPath, "utf8")); const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/package.json"), "utf8")); function deepFreeze(value) { if (value && typeof value === "object") { Object.freeze(value); for (const item of Object.values(value)) deepFreeze(item); } return value; } function invalidCase(name, mutate, code, errorPath) { return { name, mutate, code, errorPath }; } test("M6-04A accepts an immutable single/pthread manifest without mutating it", () => { const input = deepFreeze(structuredClone(golden)); const parsed = validateWebEngineManifestV2(input); assert.deepEqual(parsed, golden); assert.notEqual(parsed, input); assert.notEqual(parsed.variants[0], input.variants[0]); assert.notEqual(parsed.variants[0].resources.js, input.variants[0].resources.js); assert.equal(parsed.variants[1].resources.pthreadWorker.url, parsed.variants[1].resources.js.url); assert.equal(protocol.WEB_ENGINE_WASM_PAGE_BYTES, 65_536); assert.deepEqual(protocol.WEB_ENGINE_MEMORY_LIMITS, { minimumInitialPages: 256, maximumPages: 32_768, }); const reversed = validateWebEngineManifestV2({ ...golden, variants: [...golden.variants].reverse() }); assert.deepEqual(reversed.variants.map((variant) => variant.id), ["single", "pthread"]); }); test("M6-04A rejects malformed variants, resources and memory declarations", async (t) => { const cases = [ invalidCase("old schema", (value) => { value.schemaVersion = 1; }, "PROTOCOL_MISMATCH", "schemaVersion"), invalidCase("unsupported protocol", (value) => { value.protocolVersion = 2; }, "PROTOCOL_MISMATCH", "protocolVersion"), invalidCase("missing release ID", (value) => { delete value.releaseId; }, "ENGINE_MANIFEST_INVALID", "releaseId"), invalidCase("invalid release ID", (value) => { value.releaseId = "release/id"; }, "ENGINE_MANIFEST_INVALID", "releaseId"), invalidCase("missing pthread variant", (value) => { value.variants.pop(); }, "ENGINE_MANIFEST_INVALID", "variants"), invalidCase("duplicate single variant", (value) => { value.variants[1].id = "single"; delete value.variants[1].resources.pthreadWorker; value.variants[1].memory.shared = false; }, "ENGINE_MANIFEST_INVALID", "variants"), invalidCase("single shared memory", (value) => { value.variants[0].memory.shared = true; }, "ENGINE_MANIFEST_INVALID", "variants[0].memory.shared"), invalidCase("single pthread worker", (value) => { value.variants[0].resources.pthreadWorker = structuredClone(value.variants[1].resources.pthreadWorker); }, "ENGINE_MANIFEST_INVALID", "variants[0].resources.pthreadWorker"), invalidCase("pthread unshared memory", (value) => { value.variants[1].memory.shared = false; }, "ENGINE_MANIFEST_INVALID", "variants[1].memory.shared"), invalidCase("missing pthread worker", (value) => { delete value.variants[1].resources.pthreadWorker; }, "ENGINE_MANIFEST_INVALID", "variants[1].resources.pthreadWorker"), invalidCase("invalid SHA-256", (value) => { value.variants[1].resources.wasm.sha256 = "ABC"; }, "ENGINE_MANIFEST_INVALID", "variants[1].resources.wasm.sha256"), invalidCase("remote resource URL", (value) => { value.variants[0].resources.js.url = "https://cdn.invalid/web_engine.single.js"; }, "ENGINE_MANIFEST_INVALID", "variants[0].resources.js.url"), invalidCase("file name mismatch", (value) => { value.variants[0].resources.wasm.url = "/vendor/blender/wrong.wasm"; }, "ENGINE_MANIFEST_INVALID", "variants[0].resources.wasm.url"), invalidCase("initial memory below floor", (value) => { value.variants[0].memory.initialPages = 255; }, "ENGINE_MANIFEST_INVALID", "variants[0].memory.initialPages"), invalidCase("maximum memory below initial", (value) => { value.variants[1].memory.maximumPages = 255; }, "ENGINE_MANIFEST_INVALID", "variants[1].memory.maximumPages"), invalidCase("maximum memory above ceiling", (value) => { value.variants[1].memory.maximumPages = 32_769; }, "ENGINE_MANIFEST_INVALID", "variants[1].memory.maximumPages"), invalidCase("aliased variant WASM", (value) => { value.variants[1].resources.wasm = structuredClone(value.variants[0].resources.wasm); }, "ENGINE_MANIFEST_INVALID", "variants.wasm"), invalidCase("worker aliases single JS", (value) => { value.variants[1].resources.pthreadWorker = structuredClone(value.variants[0].resources.js); }, "ENGINE_MANIFEST_INVALID", "variants.pthreadWorker"), invalidCase("same worker URL with another hash", (value) => { value.variants[1].resources.pthreadWorker.sha256 = "5".repeat(64); }, "ENGINE_MANIFEST_INVALID", "variants.pthreadWorker.sha256"), invalidCase("unknown manifest field", (value) => { value.defaultVariant = "pthread"; }, "ENGINE_MANIFEST_INVALID", "manifest.defaultVariant"), ]; for (const item of cases) { await t.test(item.name, () => { const input = structuredClone(golden); item.mutate(input); assert.throws( () => validateWebEngineManifestV2(input), (error) => error?.name === "WebEngineManifestValidationError" && error.code === item.code && error.path === item.errorPath, ); }); } }); test("M6-04B installs the production manifest with explicit variant paths", () => { const production = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/app/public/engine-manifest.json"), "utf8")); const parsed = validateWebEngineManifestV2(production); assert.equal(parsed.releaseId, `blender-wasm-${packageJson.version}`); assert.equal(parsed.variants[0].resources.wasm.url, "/vendor/blender/single/web_engine.wasm"); assert.equal(parsed.variants[1].resources.wasm.url, "/vendor/blender/pthread/web_engine.wasm"); assert.notEqual(parsed.variants[0].resources.wasm.sha256, parsed.variants[1].resources.wasm.sha256); });