Advance M7 workflows and release operations
This commit is contained in:
249
web/tests/unit/engine-variant-fallback.test.mjs
Normal file
249
web/tests/unit/engine-variant-fallback.test.mjs
Normal file
@@ -0,0 +1,249 @@
|
||||
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, "../../..");
|
||||
|
||||
function transpileDataUrl(filePath, replacements = new Map()) {
|
||||
const result = ts.transpileModule(fs.readFileSync(filePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: filePath,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
assert.deepEqual(result.diagnostics, []);
|
||||
let source = result.outputText;
|
||||
for (const [specifier, replacement] of replacements) {
|
||||
source = source.replaceAll(`"${specifier}"`, JSON.stringify(replacement));
|
||||
}
|
||||
return `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
||||
}
|
||||
|
||||
const gatesUrl = transpileDataUrl(path.join(repoRoot, "web/protocol/capability-gates.ts"));
|
||||
const selectorUrl = transpileDataUrl(
|
||||
path.join(repoRoot, "web/protocol/engine-variant.ts"),
|
||||
new Map([["./capability-gates", gatesUrl]]),
|
||||
);
|
||||
const bootstrapUrl = transpileDataUrl(
|
||||
path.join(repoRoot, "web/app/src/engine-client/engine-variant-bootstrap.ts"),
|
||||
new Map([["../../../protocol/engine-variant", selectorUrl]]),
|
||||
);
|
||||
const {
|
||||
bootstrapWebEngineRelease,
|
||||
engineVariantStatusLabel,
|
||||
EngineVariantLoadError,
|
||||
} = await import(bootstrapUrl);
|
||||
const manifest = JSON.parse(fs.readFileSync(
|
||||
path.join(repoRoot, "tests/golden/M6-04A/engine-manifest-v2.json"),
|
||||
"utf8",
|
||||
));
|
||||
const ready = { crossOriginIsolated: true, sharedArrayBuffer: true, worker: true };
|
||||
|
||||
class FakeSession {
|
||||
constructor(variant) {
|
||||
this.variant = variant;
|
||||
this.opened = [];
|
||||
this.disposals = 0;
|
||||
this.state = {
|
||||
handles: 1,
|
||||
workers: variant.id === "pthread" ? 1 : 0,
|
||||
timers: 0,
|
||||
pendingRequests: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async openProject(project) {
|
||||
this.opened.push(project);
|
||||
}
|
||||
|
||||
resourceState() {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
testOnlySeedTrackedResources() {
|
||||
this.state.timers = 1;
|
||||
this.state.pendingRequests = 1;
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
this.disposals += 1;
|
||||
this.state = { handles: 0, workers: 0, timers: 0, pendingRequests: 0 };
|
||||
return this.resourceState();
|
||||
}
|
||||
}
|
||||
|
||||
function dependencies() {
|
||||
const sessions = [];
|
||||
return {
|
||||
sessions,
|
||||
initialize: async (variant) => {
|
||||
const session = new FakeSession(variant);
|
||||
sessions.push(session);
|
||||
return session;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("M6-05A/B/C injects one pthread failure, cleans it and falls back once", async () => {
|
||||
const deps = dependencies();
|
||||
const outcome = await bootstrapWebEngineRelease(
|
||||
manifest.releaseId,
|
||||
manifest,
|
||||
"AUTO",
|
||||
ready,
|
||||
deps,
|
||||
undefined,
|
||||
{ failPthreadAfterInitialize: true, seedTrackedPthreadResources: true },
|
||||
);
|
||||
|
||||
assert.deepEqual(outcome.result.attempted, ["pthread", "single"]);
|
||||
assert.equal(deps.sessions.length, 2);
|
||||
assert.equal(deps.sessions[0].disposals, 1);
|
||||
assert.deepEqual(outcome.result.failedAttemptCleanup, {
|
||||
handles: 0,
|
||||
workers: 0,
|
||||
timers: 0,
|
||||
pendingRequests: 0,
|
||||
});
|
||||
assert.equal(outcome.result.selected, "single");
|
||||
assert.deepEqual(outcome.result.fallbackReason, {
|
||||
code: "PTHREAD_INITIALIZATION_FAILED",
|
||||
message: "TEST_PTHREAD_INITIALIZATION_FAILURE",
|
||||
});
|
||||
assert.equal(outcome.result.openCount, 0);
|
||||
assert.equal(deps.sessions[0].opened.length, 0);
|
||||
assert.equal(deps.sessions[1].opened.length, 0);
|
||||
});
|
||||
|
||||
test("M6-05D opens a pending project once only after the fallback settles", async () => {
|
||||
const deps = dependencies();
|
||||
const project = { id: "pending-project" };
|
||||
const outcome = await bootstrapWebEngineRelease(
|
||||
manifest.releaseId,
|
||||
manifest,
|
||||
"AUTO",
|
||||
ready,
|
||||
deps,
|
||||
project,
|
||||
{ failPthreadAfterInitialize: true },
|
||||
);
|
||||
|
||||
assert.equal(outcome.result.openCount, 1);
|
||||
assert.deepEqual(deps.sessions[0].opened, []);
|
||||
assert.deepEqual(deps.sessions[1].opened, [project]);
|
||||
});
|
||||
|
||||
test("M6-05E exposes selected, attempted and fallbackReason without pthread READY UI", async () => {
|
||||
const deps = dependencies();
|
||||
const outcome = await bootstrapWebEngineRelease(
|
||||
manifest.releaseId,
|
||||
manifest,
|
||||
"AUTO",
|
||||
ready,
|
||||
deps,
|
||||
undefined,
|
||||
{ failPthreadAfterInitialize: true },
|
||||
);
|
||||
const label = engineVariantStatusLabel(outcome.result);
|
||||
|
||||
assert.deepEqual(
|
||||
{
|
||||
selected: outcome.result.selected,
|
||||
attempted: outcome.result.attempted,
|
||||
fallbackReason: outcome.result.fallbackReason?.code,
|
||||
},
|
||||
{
|
||||
selected: "single",
|
||||
attempted: ["pthread", "single"],
|
||||
fallbackReason: "PTHREAD_INITIALIZATION_FAILED",
|
||||
},
|
||||
);
|
||||
assert.equal(label, "Runtime: single (pthread fallback: PTHREAD_INITIALIZATION_FAILED)");
|
||||
assert.doesNotMatch(label, /pthread\s+ready/i);
|
||||
});
|
||||
|
||||
test("M6-08E treats a manifest hash mismatch as fatal without fallback or project open", async () => {
|
||||
const initialized = [];
|
||||
const project = { id: "must-stay-unopened" };
|
||||
await assert.rejects(
|
||||
bootstrapWebEngineRelease(
|
||||
manifest.releaseId,
|
||||
manifest,
|
||||
"AUTO",
|
||||
ready,
|
||||
{
|
||||
initialize: async (variant) => {
|
||||
initialized.push(variant.id);
|
||||
if (variant.id === "pthread") {
|
||||
throw new EngineVariantLoadError("ENGINE_VARIANT_RESOURCE_HASH_MISMATCH", variant.resources.wasm.url);
|
||||
}
|
||||
return new FakeSession(variant);
|
||||
},
|
||||
},
|
||||
project,
|
||||
),
|
||||
(error) => error?.code === "ENGINE_VARIANT_INTEGRITY_FAILED" &&
|
||||
error.cause?.code === "ENGINE_VARIANT_RESOURCE_HASH_MISMATCH" &&
|
||||
JSON.stringify(error.attempted) === JSON.stringify(["pthread"]),
|
||||
);
|
||||
assert.deepEqual(initialized, ["pthread"]);
|
||||
});
|
||||
|
||||
test("M6-08D returns refresh-required before initialization or project open", async () => {
|
||||
let initializationCount = 0;
|
||||
const outcome = await bootstrapWebEngineRelease(
|
||||
"blender-wasm-previous",
|
||||
manifest,
|
||||
"AUTO",
|
||||
ready,
|
||||
{
|
||||
initialize: async (variant) => {
|
||||
initializationCount += 1;
|
||||
return new FakeSession(variant);
|
||||
},
|
||||
},
|
||||
{ id: "must-stay-unopened" },
|
||||
);
|
||||
|
||||
assert.deepEqual(outcome, {
|
||||
result: {
|
||||
status: "REFRESH_REQUIRED",
|
||||
expectedReleaseId: "blender-wasm-previous",
|
||||
actualReleaseId: manifest.releaseId,
|
||||
manifest: null,
|
||||
},
|
||||
session: null,
|
||||
});
|
||||
assert.equal(initializationCount, 0);
|
||||
});
|
||||
|
||||
test("M6-08E normalizes a fallback variant hash mismatch and never opens the project", async () => {
|
||||
const initialized = [];
|
||||
const pthreadSession = new FakeSession(manifest.variants[1]);
|
||||
await assert.rejects(
|
||||
bootstrapWebEngineRelease(
|
||||
manifest.releaseId,
|
||||
manifest,
|
||||
"AUTO",
|
||||
ready,
|
||||
{
|
||||
initialize: async (variant) => {
|
||||
initialized.push(variant.id);
|
||||
if (variant.id === "single") {
|
||||
throw new EngineVariantLoadError("ENGINE_VARIANT_RESOURCE_HASH_MISMATCH", variant.resources.wasm.url);
|
||||
}
|
||||
return pthreadSession;
|
||||
},
|
||||
},
|
||||
{ id: "must-stay-unopened" },
|
||||
{ failPthreadAfterInitialize: true },
|
||||
),
|
||||
(error) => error?.code === "ENGINE_VARIANT_INTEGRITY_FAILED" &&
|
||||
error.cause?.code === "ENGINE_VARIANT_RESOURCE_HASH_MISMATCH" &&
|
||||
JSON.stringify(error.attempted) === JSON.stringify(["pthread", "single"]),
|
||||
);
|
||||
assert.deepEqual(initialized, ["pthread", "single"]);
|
||||
assert.equal(pthreadSession.disposals, 1);
|
||||
assert.deepEqual(pthreadSession.opened, []);
|
||||
});
|
||||
Reference in New Issue
Block a user