250 lines
7.3 KiB
JavaScript
250 lines
7.3 KiB
JavaScript
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, []);
|
|
});
|