Advance M7 workflows and release operations
This commit is contained in:
97
web/tests/unit/engine-manifest-v2.test.mjs
Normal file
97
web/tests/unit/engine-manifest-v2.test.mjs
Normal file
@@ -0,0 +1,97 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user