Checkpoint web parity through Chromium input tasks
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions

This commit is contained in:
mes123456
2026-08-19 10:39:03 -04:00
parent 5a11045ca5
commit 380cbed4ff
634 changed files with 41862 additions and 212 deletions

View File

@@ -0,0 +1,21 @@
import { expect, test } from "@playwright/test";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const root = path.resolve(import.meta.dirname, "../../..");
test("N-023 asset malicious ZIP/TAR fixtures remain in the archive security regression", async () => {
const output = execFileSync(process.execPath, [path.join(root, "tools/web/check-malicious-archive-fixtures.mjs")], {
cwd: root,
encoding: "utf8",
});
expect(output).toContain("malicious-archive-fixtures-ok cases=6 zip=3 tar=3 extraction=disabled");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/files/web/archive-security/manifest.json"), "utf8"));
expect(manifest.extractionAllowed).toBe(false);
expect(manifest.cases).toHaveLength(6);
expect(manifest.cases.map((fixture: { expectedCode: string }) => fixture.expectedCode)).toEqual(
Array.from({ length: 6 }, () => "IO_ARCHIVE_UNSAFE"),
);
});

View File

@@ -0,0 +1,42 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"), "utf8"));
const fixtureRoot = path.join(root, "tests/files/web/m12_glb_desktop_v1");
test("imports the M12-06A desktop GLB fixture group in a Chromium Worker", async ({ page }) => {
await page.goto("/");
const fixtures = report.fixtures.map((fixture: { id: string; file: string; sha256: string; semantic: unknown }) => ({
id: fixture.id,
bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.file))),
sourceSha256: fixture.sha256,
expected: fixture.semantic,
}));
const result = await page.evaluate(async (input) => new Promise<Record<string, unknown>>((resolve, reject) => {
const worker = new Worker("/src/workers/glb-desktop-import-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, unknown>>) => {
worker.terminate();
resolve(event.data);
};
worker.onerror = (event) => {
worker.terminate();
reject(new Error(event.message));
};
const transferred = input.map((fixture) => {
const bytes = Uint8Array.from(fixture.bytes);
return { ...fixture, bytes: bytes.buffer };
});
worker.postMessage({ fixtures: transferred }, transferred.map((fixture) => fixture.bytes));
}), fixtures);
expect(result.ok).toBe(true);
expect(result.results).toEqual([
{ id: "mesh", sourceSha256: report.fixtures[0].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["COLOR_0", "NORMAL", "POSITION"], materials: 1, nodes: 1, animations: 0 },
{ id: "pbr", sourceSha256: report.fixtures[1].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["NORMAL", "POSITION"], materials: 1, nodes: 1, animations: 0 },
{ id: "uv", sourceSha256: report.fixtures[2].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["NORMAL", "POSITION", "TEXCOORD_0"], materials: 1, nodes: 1, animations: 0 },
{ id: "skin", sourceSha256: report.fixtures[3].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["JOINTS_0", "NORMAL", "POSITION", "WEIGHTS_0"], materials: 1, nodes: 4, animations: 0 },
{ id: "animation", sourceSha256: report.fixtures[4].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["NORMAL", "POSITION"], materials: 1, nodes: 1, animations: 1 },
]);
});

View File

@@ -0,0 +1,106 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const mainReport = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06C/desktop-main-report.json"), "utf8"));
const reportPath = path.join(root, "tests/golden/M12-06D/web-loss-report.json");
const fixtureRoot = path.join(root, "tests/files/web/m12_glb_main_v1");
const sha256 = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex");
test("writes a machine GLB loss report for every persisted Main fixture", async ({ page }) => {
await page.goto("/");
const fixtures = mainReport.fixtures.map((fixture: { id: string; blend: { file: string; sha256: string } }) => ({
id: fixture.id,
bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.blend.file))),
sourceBlendSha256: fixture.blend.sha256,
}));
const generated = await page.evaluate(async (input) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const digest = async (bytes: ArrayBuffer): Promise<string> => {
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
return Array.from(hash, (value) => value.toString(16).padStart(2, "0")).join("");
};
const reports = [];
for (const fixture of input) {
const client = new WebEngineClient({ timeoutMs: 30_000 });
try {
const opened = await client.openBlend(Uint8Array.from(fixture.bytes).buffer);
const assets = [];
for (const image of opened.snapshot.images) {
const asset = await client.requestAsset(image.assetId);
if (asset.status === "packed" && asset.data && asset.mimeType) assets.push({ assetId: image.assetId, mimeType: asset.mimeType, data: asset.data });
}
const worker = new Worker("/src/workers/glb-loss-report-test.worker.ts", { type: "module" });
const result = await new Promise<{ report: any; output: ArrayBuffer | null }>((resolve, reject) => {
worker.onmessage = (event: MessageEvent<{ ok: boolean; report?: any; output?: ArrayBuffer | null; error?: string }>) => {
worker.terminate();
if (!event.data.ok || !event.data.report) reject(new Error(event.data.error ?? "GLB loss report worker failed"));
else resolve({ report: event.data.report, output: event.data.output ?? null });
};
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const transfer: Transferable[] = [];
for (const geometry of opened.geometryBuffers) for (const value of Object.values(geometry)) if (value instanceof ArrayBuffer) transfer.push(value);
for (const asset of assets) transfer.push(asset.data);
for (const chunk of opened.nonMeshGeometryBuffers ?? []) for (const value of Object.values(chunk)) if (value instanceof ArrayBuffer) transfer.push(value);
worker.postMessage({ snapshot: opened.snapshot, geometryBuffers: opened.geometryBuffers, assetBuffers: assets, nonMeshGeometryBuffers: opened.nonMeshGeometryBuffers ?? [] }, transfer);
});
reports.push({
fixtureId: fixture.id,
sourceBlendSha256: fixture.sourceBlendSha256,
lossReport: result.report,
output: result.output ? { byteLength: result.output.byteLength, sha256: await digest(result.output), bytes: Array.from(new Uint8Array(result.output)) } : null,
});
}
finally {
client.terminate();
}
}
return reports;
}, fixtures);
const report = {
schemaVersion: 1,
task: "M12-06D",
operation: "WEB_GLB_EXPORT_LOSS_REPORT",
fixtureCount: generated.length,
fixtures: generated,
nextTask: "M12-06E",
};
if (process.env.UPDATE_GLB_LOSS_REPORT === "1") {
const outputRoot = path.join(root, "tests/files/web/m12_glb_web_v1");
fs.mkdirSync(outputRoot, { recursive: true });
for (const fixture of generated) {
if (fixture.output?.bytes) fs.writeFileSync(path.join(outputRoot, `${fixture.fixtureId}.glb`), Buffer.from(fixture.output.bytes));
}
}
for (const fixture of report.fixtures) if (fixture.output?.bytes) delete fixture.output.bytes;
if (process.env.UPDATE_GLB_LOSS_REPORT === "1") {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
}
const expected = JSON.parse(fs.readFileSync(reportPath, "utf8"));
expect(report).toEqual(expected);
expect(report.fixtureCount).toBe(5);
for (const fixture of report.fixtures) {
expect(fixture.lossReport.schemaVersion).toBe(1);
expect(fixture.lossReport.operation).toBe("GLB_EXPORT_LOSS_REPORT");
if (fixture.fixtureId === "mesh") {
expect(fixture.lossReport.canExport).toBe(false);
expect(fixture.lossReport.errorCount).toBe(1);
expect(fixture.lossReport.losses.map((loss: { code: string }) => loss.code)).toEqual(["LINKED_MATERIAL_INPUT_UNEVALUATED", "SHADER_GRAPH_UNMAPPABLE"]);
expect(fixture.output).toBeNull();
}
else {
expect(fixture.lossReport.canExport).toBe(true);
expect(fixture.lossReport.errorCount).toBe(0);
expect(fixture.output?.byteLength).toBeGreaterThan(128);
expect(fixture.output?.sha256).toMatch(/^[0-9a-f]{64}$/);
}
expect(fixture.lossReport.losses).toEqual([...fixture.lossReport.losses].sort((left, right) =>
left.code.localeCompare(right.code) || left.severity.localeCompare(right.severity) ||
(left.id ?? "").localeCompare(right.id ?? "") || left.message.localeCompare(right.message)));
}
});

View File

@@ -0,0 +1,98 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06C/desktop-main-report.json"), "utf8"));
const fixtureRoot = path.join(root, "tests/files/web/m12_glb_main_v1");
function stableSnapshot(snapshot: Record<string, any>) {
return {
objects: snapshot.nodes.filter((node: any) => node.id?.startsWith("object:")).map((node: any) => node.id).sort(),
meshes: snapshot.meshes.map((mesh: any) => mesh.id).sort(),
materials: snapshot.materials.map((material: any) => material.id).sort(),
images: snapshot.images.map((image: any) => image.id).sort(),
armatures: (snapshot.armatures ?? []).map((armature: any) => armature.id).sort(),
actions: snapshot.animations.map((animation: any) => animation.id).sort(),
};
}
async function sha256(bytes: ArrayBuffer): Promise<string> {
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join("");
}
test("persists desktop-imported GLB Main data through WebEngine save and reopen", async ({ page }) => {
await page.goto("/");
const fixtures = report.fixtures.map((fixture: { id: string; blend: { file: string; sha256: string }; graph: { stableIds: Record<string, string[]> } }) => ({
id: fixture.id,
bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.blend.file))),
sourceSha256: fixture.blend.sha256,
expected: fixture.graph.stableIds,
}));
const result = await page.evaluate(async (input) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const output = [];
const stableSnapshot = (snapshot: Record<string, any>) => ({
objects: snapshot.nodes.filter((node: any) => node.id?.startsWith("object:")).map((node: any) => node.id).sort(),
meshes: snapshot.meshes.map((mesh: any) => mesh.id).sort(),
materials: snapshot.materials.map((material: any) => material.id).sort(),
images: snapshot.images.map((image: any) => image.id).sort(),
armatures: (snapshot.armatures ?? []).map((armature: any) => armature.id).sort(),
actions: snapshot.animations.map((animation: any) => animation.id).sort(),
});
const digestSha256 = async (bytes: ArrayBuffer): Promise<string> => {
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join("");
};
for (const fixture of input) {
const client = new WebEngineClient({ timeoutMs: 30_000 });
try {
const opened = await client.openBlend(Uint8Array.from(fixture.bytes).buffer);
const before = opened.snapshot;
const beforeIds = stableSnapshot(before);
const objectId = before.activeObjectId ?? before.nodes.find((node: any) => node.id?.startsWith("object:"))?.id;
if (!objectId) throw new Error(`${fixture.id} did not produce an active Main object`);
const edited = await client.applyCommand({ type: "setObjectVisibility", objectId, visible: false });
const saved = await client.saveBlend();
const savedSha256 = await digestSha256(saved);
if (savedSha256 === fixture.sourceSha256) throw new Error(`${fixture.id} save did not serialize the Main visibility edit`);
const reopened = await client.openBlend(saved);
const after = reopened.snapshot;
const afterIds = stableSnapshot(after);
const afterObject = after.nodes.find((node: any) => node.id === objectId);
const resources = await client.openResourceStatus();
output.push({
id: fixture.id,
before: beforeIds,
after: afterIds,
expected: fixture.expected,
revision: [before.revision, edited.snapshot.revision, after.revision],
savedSha256,
sourceSha256: fixture.sourceSha256,
visibilityAfterReopen: afterObject?.visible,
resources,
});
}
finally {
client.terminate();
}
}
return output;
}, fixtures);
for (const item of result) {
expect(item.before).toEqual(item.expected);
expect(item.after).toEqual(item.before);
expect(item.revision[1]).toBeGreaterThan(item.revision[0]);
expect(item.revision[2]).toBeGreaterThan(0);
expect(item.savedSha256).not.toEqual(item.sourceSha256);
expect(item.visibilityAfterReopen).toBe(false);
expect(item.resources).toMatchObject({ activeRequests: 0, liveInputBytes: 0, liveStagingFiles: 0 });
expect(item.before.objects.every((id: string) => id.startsWith("object:"))).toBe(true);
expect(item.before.meshes.every((id: string) => id.startsWith("mesh:"))).toBe(true);
expect(item.before.materials.every((id: string) => id.startsWith("material:"))).toBe(true);
expect(item.before.images.every((id: string) => id.startsWith("image:"))).toBe(true);
}
expect(result).toHaveLength(5);
});

View File

@@ -0,0 +1,46 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const source = fs.readFileSync(path.join(root, "tests/files/web/m12_glb_desktop_v1/mesh.glb"));
const maxBytes = 512 * 1024;
function rewriteJson(mutator: (document: any) => void): Buffer {
const jsonLength = source.readUInt32LE(12);
const document = JSON.parse(source.subarray(20, 20 + jsonLength).toString("utf8").trim());
mutator(document);
const json = Buffer.from(JSON.stringify(document));
const paddedLength = (json.length + 3) & ~3;
const output = Buffer.alloc(12 + 8 + paddedLength + (source.length - (20 + jsonLength)));
output.writeUInt32LE(0x46546c67, 0); output.writeUInt32LE(2, 4); output.writeUInt32LE(output.length, 8);
output.writeUInt32LE(paddedLength, 12); output.writeUInt32LE(0x4e4f534a, 16); json.copy(output, 20);
output.fill(0x20, 20 + json.length, 20 + paddedLength);
output.writeUInt32LE(source.readUInt32LE(20 + jsonLength), 20 + paddedLength);
output.writeUInt32LE(source.readUInt32LE(24 + jsonLength), 24 + paddedLength);
source.subarray(28 + jsonLength).copy(output, 28 + paddedLength);
return output;
}
test("rejects GLB sparse, extension, external URI and over-budget cases in a Chromium Worker", async ({ page }) => {
await page.goto("/");
const cases = [
{ id: "sparse", bytes: rewriteJson((document) => { document.accessors[0].sparse = { count: 1 }; }) },
{ id: "extension", bytes: rewriteJson((document) => { document.extensionsUsed = ["KHR_draco_mesh_compression"]; }) },
{ id: "external-uri", bytes: rewriteJson((document) => { document.buffers[0].uri = "external.bin"; }) },
{ id: "over-budget", bytes: Buffer.concat([source, Buffer.alloc(maxBytes + 1 - source.length)]) },
].map((candidate) => ({ id: candidate.id, bytes: Array.from(candidate.bytes) }));
const result = await page.evaluate((input) => new Promise<{ ok: boolean; results?: Array<{ id: string; code: string }>; error?: string }>((resolve, reject) => {
const worker = new Worker("/src/workers/glb-negative-cases-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<{ ok: boolean; results?: Array<{ id: string; code: string }>; error?: string }>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const cases = input.map((candidate) => ({ id: candidate.id, bytes: Uint8Array.from(candidate.bytes).buffer }));
worker.postMessage({ cases }, cases.map((candidate) => candidate.bytes));
}), cases);
expect(result).toEqual({ ok: true, results: [
{ id: "sparse", code: "GLB_SPARSE_ACCESSOR_UNSUPPORTED" },
{ id: "extension", code: "GLB_EXTENSION_UNSUPPORTED" },
{ id: "external-uri", code: "GLB_EXTERNAL_URI_BLOCKED" },
{ id: "over-budget", code: "GLB_IMPORT_BUDGET_EXCEEDED" },
] });
});

View File

@@ -0,0 +1,127 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const glbBytes = Array.from(fs.readFileSync(path.join(root, "tests/files/web/m12_glb_desktop_v1/pbr.glb")));
const blendBytes = Array.from(fs.readFileSync(path.join(root, "tests/files/web/m12_glb_main_v1/pbr.blend")));
test("M12-06G cancels GLB import/export without publishing temporary output", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (fixtures) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const client = new WebEngineClient({ timeoutMs: 30_000 });
const opened = await client.openBlend(Uint8Array.from(fixtures.blend).buffer);
const assets = [];
for (const image of opened.snapshot.images) {
const asset = await client.requestAsset(image.assetId);
if (asset.status === "packed" && asset.data && asset.mimeType) assets.push({ assetId: image.assetId, data: asset.data, mimeType: asset.mimeType });
}
const cancel = async (operation: "IMPORT" | "EXPORT") => new Promise<Record<string, unknown>>((resolve, reject) => {
const worker = new Worker("/src/workers/glb-recovery-test.worker.ts", { type: "module" });
const requestId = `glb-${operation.toLowerCase()}-cancel-1`;
worker.onmessage = (event: MessageEvent<Record<string, unknown>>) => {
worker.terminate();
if (!event.data.ok) reject(new Error(String(event.data.error)));
else resolve(event.data);
};
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const bytes = Uint8Array.from(fixtures.glb).buffer;
worker.postMessage({
type: "run",
requestId,
operation,
bytes,
snapshot: operation === "EXPORT" ? opened.snapshot : undefined,
geometryBuffers: operation === "EXPORT" ? opened.geometryBuffers : undefined,
assetBuffers: operation === "EXPORT" ? assets : undefined,
nonMeshGeometryBuffers: operation === "EXPORT" ? opened.nonMeshGeometryBuffers ?? [] : undefined,
baseRevision: opened.snapshot.revision,
workerGeneration: 1,
});
setTimeout(() => worker.postMessage({ type: "cancel", targetRequestId: requestId }), 3);
});
const imported = await cancel("IMPORT");
const exported = await cancel("EXPORT");
client.terminate();
return { imported, exported };
}, { glb: glbBytes, blend: blendBytes });
for (const operation of [result.imported, result.exported]) {
expect(operation.ok).toBe(true);
expect((operation.receipt as { status: string }).status).toBe("CANCELLED");
expect((operation.receipt as { errorCode: string }).errorCode).toBe("GLB_OPERATION_CANCELLED");
expect((operation.receipt as { temporaryBytes: number; liveRequests: number; committed: boolean })).toMatchObject({ temporaryBytes: 0, liveRequests: 0, committed: false });
}
});
test("M12-06G recovers GLB import after Worker restart and retains old OPFS asset after quota", async ({ page }) => {
await page.goto("/");
const cdp = await page.context().newCDPSession(page);
await cdp.send("Storage.overrideQuotaForOrigin", { origin: new URL(page.url()).origin, quotaSize: 64 * 1024 });
const result = await page.evaluate(async (fixture) => {
const { StorageClient } = await import("/src/storage/StorageClient.ts");
const { beginGLBRecoveryOperation, blockGLBRecoveryForQuota, recoverGLBRecoveryOperation } = await import("/src/testing/glb-recovery.ts");
const runWorker = (generation: number) => new Promise<{ receipt: any; result: { outputSha256: string } }>((resolve, reject) => {
const worker = new Worker("/src/workers/glb-recovery-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<{ ok: boolean; receipt?: any; result?: { outputSha256: string }; error?: string }>) => {
worker.terminate();
if (!event.data.ok || !event.data.receipt || !event.data.result) reject(new Error(event.data.error ?? "GLB worker failed"));
else resolve({ receipt: event.data.receipt, result: event.data.result });
};
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ type: "run", requestId: `glb-import-generation-${generation}`, operation: "IMPORT", bytes: Uint8Array.from(fixture).buffer, baseRevision: 4, workerGeneration: generation });
});
const firstRun = await runWorker(1);
const restartedRun = await runWorker(2);
const recovered = recoverGLBRecoveryOperation(firstRun.receipt, 2);
const projectId = `glb-recovery-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const storage = new StorageClient();
await storage.ensureProject(projectId);
const asset = await storage.putAsset(projectId, Uint8Array.from(fixture).buffer, "model/gltf-binary", "imports/model.glb");
let quotaError = "";
let quotaReceipt;
const quotaPayload = Uint8Array.from({ length: 128 * 1024 }, (_, index) => (index * 7) & 0xff).buffer;
const quotaRunning = beginGLBRecoveryOperation({ operationId: "export-quota-1", operation: "EXPORT", workerGeneration: 2, baseRevision: 4, inputBytes: quotaPayload.byteLength, inputSha256: await crypto.subtle.digest("SHA-256", quotaPayload).then((digest) => Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")) });
try { await storage.putAsset(projectId, quotaPayload, "model/gltf-binary", "imports/rejected.glb"); }
catch (error) { quotaError = error instanceof Error ? error.message : String(error); quotaReceipt = blockGLBRecoveryForQuota(quotaRunning); }
storage.terminate();
const restartedStorage = new StorageClient();
const restored = await restartedStorage.readAsset(projectId, asset.sha256);
const assets = await restartedStorage.listAssets(projectId);
restartedStorage.terminate();
return {
firstHash: firstRun.result.outputSha256,
restartedHash: restartedRun.result.outputSha256,
recoveredStatus: recovered.status,
recoveredGeneration: recovered.workerGeneration,
quotaError,
quotaCode: quotaReceipt?.errorCode,
assetSha256: asset.sha256,
restoredSha256: restored.asset.sha256,
restoredBytes: restored.data.byteLength,
assetCount: assets.assets.length,
projectId,
backendPath: asset.path,
};
}, glbBytes);
expect(result.firstHash).toMatch(/^[a-f0-9]{64}$/);
expect(result.restartedHash).toBe(result.firstHash);
expect(result.recoveredStatus).toBe("RECOVERED");
expect(result.recoveredGeneration).toBe(2);
expect(result.quotaError).toMatch(/QuotaExceededError|storage quota|exceed its storage quota/i);
expect(result.quotaCode).toBe("GLB_OPFS_QUOTA");
expect(result.restoredSha256).toBe(result.assetSha256);
expect(result.restoredBytes).toBe(glbBytes.length);
expect(result.assetCount).toBe(1);
expect(result.backendPath).toMatch(/^projects\//);
await cdp.send("Storage.overrideQuotaForOrigin", { origin: new URL(page.url()).origin, quotaSize: 1024 * 1024 * 1024 });
const recoveredStorage = await page.evaluate(async (projectId) => {
const { StorageClient } = await import("/src/storage/StorageClient.ts");
const storage = new StorageClient();
const small = await storage.putAsset(projectId, Uint8Array.from([5, 6, 7]).buffer, "model/gltf-binary", "imports/recovery.glb");
const assets = await storage.listAssets(projectId);
storage.terminate();
return { persisted: small.persisted, assetCount: assets.assets.length };
}, result.projectId);
expect(recoveredStorage).toEqual({ persisted: true, assetCount: 2 });
});

View File

@@ -0,0 +1,68 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixtures = {
OBJ: fs.readFileSync(path.join(root, "tests/files/web/m12_obj_multi_v1/multi-object.obj")),
STL: fs.readFileSync(path.join(root, "tests/files/web/m12_stl_capability_v1/capability-binary.stl")),
PLY: fs.readFileSync(path.join(root, "tests/files/web/m12_ply_mapping_v1/mapping-ascii.ply")),
};
const reportPath = path.join(root, "tests/golden/M12-07J/io-format-recovery-report.json");
const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex");
test("M12-07J recovers OBJ/STL/PLY after cancellation, OOM budget faults and Worker restart", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (input) => {
const recovery = await import("/src/testing/io-format-recovery.ts");
const run = (format: "OBJ" | "STL" | "PLY", bytes: number[], generation: number, phase: string) => new Promise<any>((resolve, reject) => {
const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" });
const requestId = `${format.toLowerCase()}-${phase}-${generation}`;
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const payload = Uint8Array.from(bytes).buffer;
worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: generation, baseRevision: 4 }, [payload]);
});
const cancel = (format: "OBJ" | "STL" | "PLY", bytes: number[]) => new Promise<any>((resolve, reject) => {
const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" });
const requestId = `${format.toLowerCase()}-cancel-1`;
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const payload = Uint8Array.from(bytes).buffer;
worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: 1, baseRevision: 4 }, [payload]);
setTimeout(() => worker.postMessage({ type: "cancel", targetRequestId: requestId }), 3);
});
const summary: Record<string, any> = {};
for (const format of ["OBJ", "STL", "PLY"] as const) {
const source = input[format];
const cancelled = await cancel(format, source);
const oversized = new Array(512 * 1024 + 1).fill(7);
const oom = await run(format, oversized, 1, "oom");
const first = await run(format, source, 1, "first");
const second = await run(format, source, 2, "second");
const recovered = recovery.recoverIOFormatRecoveryOperation(first.receipt, 2);
const small = await run(format, source, 3, "small");
summary[format] = { sourceSha256: await crypto.subtle.digest("SHA-256", Uint8Array.from(source)).then((digest) => Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")), cancel: cancelled.receipt, oom: oom.receipt, first: first.receipt, second: second.receipt, recovered, small: small.receipt, hashes: { first: first.result.outputSha256, second: second.result.outputSha256, small: small.result.outputSha256 } };
}
return summary;
}, Object.fromEntries(Object.entries(fixtures).map(([format, bytes]) => [format, Array.from(bytes)])));
for (const format of ["OBJ", "STL", "PLY"] as const) {
const value = result[format];
expect(value.cancel).toMatchObject({ status: "CANCELLED", errorCode: "IO_FORMAT_OPERATION_CANCELLED", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false });
expect(value.oom).toMatchObject({ status: "BLOCKED", errorCode: "IO_FORMAT_OOM", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false });
expect(value.recovered).toMatchObject({ status: "RECOVERED", workerGeneration: 2, errorCode: "IO_FORMAT_WORKER_RESTARTED", committed: true });
expect(value.first).toMatchObject({ status: "COMMITTED", workerGeneration: 1, publishedResults: 1, committed: true });
expect(value.second).toMatchObject({ status: "COMMITTED", workerGeneration: 2, publishedResults: 1, committed: true });
expect(value.small).toMatchObject({ status: "COMMITTED", workerGeneration: 3, publishedResults: 1, committed: true });
expect(value.hashes.second).toBe(value.hashes.first);
expect(value.hashes.small).toBe(value.hashes.first);
}
const report = { schemaVersion: 1, task: "M12-07J", operation: "IO_FORMAT_THREE_WAY_RECOVERY", formats: result, assertions: { formats: ["OBJ", "STL", "PLY"], cancellationUnpublished: true, oomUnpublished: true, restartHashStable: true, smallRecoveryStable: true }, nextTask: "M13-01A" };
if (process.env.UPDATE_IO_FORMAT_RECOVERY_REPORT === "1") {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
}
expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")));
});

View File

@@ -0,0 +1,28 @@
import { expect, test } from "@playwright/test";
import path from "node:path";
const basicBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/basic_scene.blend");
test("M12-05C exposes only matrix-declared file and operator routes", async ({ page }) => {
await page.goto("/");
const input = page.getByTestId("blend-file-input");
await expect(input).toHaveAttribute("accept", ".blend,application/octet-stream");
await expect(input).toHaveAttribute("data-io-format-import-routes", "");
await input.setInputFiles(basicBlend);
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();
await page.keyboard.press("F3");
const search = page.getByRole("textbox", { name: "搜索操作" });
await search.fill("export glb");
await expect(page.getByRole("button", { name: "Export GLB", exact: true })).toHaveCount(1);
await search.fill("export usd");
await expect(page.getByRole("button", { name: /Export USD|导出 USD/ })).toHaveCount(0);
await search.fill("import obj");
await expect(page.getByRole("button", { name: /Import OBJ|导入 OBJ/ })).toHaveCount(0);
await page.keyboard.press("Escape");
await input.setInputFiles({ name: "mesh.obj", mimeType: "model/obj", buffer: Buffer.from("v 0 0 0\n") });
await expect(page.getByTestId("engine-status")).toContainText("IO: format route unavailable");
await expect(page.getByTestId("engine-status")).not.toContainText("SceneIR r2");
});

View File

@@ -8,8 +8,11 @@ import { createLibraryOperationBinding, createLibrarySourceIdentity, LIBRARY_OPE
const root = path.resolve(import.meta.dirname, "../../..");
const source = fs.readFileSync(path.join(root, "tests/files/web/m12_library_append_v1/m12_append_source.blend"));
const target = fs.readFileSync(path.join(root, "tests/files/web/empty.blend"));
const desktopReport = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03C/desktop-append-report.json"), "utf8"));
const desktopCanonical = JSON.parse(JSON.stringify(desktopReport.appendedGraph));
delete desktopCanonical.sourceMarker;
test("M12-03D appends one fully-local dependency closure through WASM Main", async ({ page }) => {
test("M12-03E keeps append undo/redo/save/reopen equal to the desktop canonical report", async ({ page }) => {
test.setTimeout(120_000);
await page.goto("/");
const closure = {
@@ -29,14 +32,14 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
operation: "APPEND",
source: sourceIdentity,
sourceDataBlockId: closure.object,
owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03d", localDataBlockId: closure.object },
owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03e", localDataBlockId: closure.object },
readOnly: false,
referenceReadOnly: false,
sourceGeneration: 1,
sourceRevision: 0,
dependencyClosureSha256,
});
const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding }) => {
const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding, expectedCanonical }) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const sourceBuffer = Uint8Array.from(sourceBytes).buffer;
const targetBuffer = Uint8Array.from(targetBytes).buffer;
@@ -64,12 +67,94 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
material: snapshot.materials.find((item: any) => item.id === ids.material),
image: snapshot.images.find((item: any) => item.id === ids.image),
});
const nameFromId = (id: string) => id.slice(id.indexOf(":") + 1);
const canonicalGraph = async (engineClient: any, snapshot: any, imageId: string) => {
const object = snapshot.nodes.find((item: any) => item.id === ids.object);
const mesh = snapshot.meshes.find((item: any) => item.id === ids.mesh);
const material = snapshot.materials.find((item: any) => item.id === ids.material);
const image = snapshot.images.find((item: any) => item.id === imageId);
if (!object || !mesh || !material || !image || image.id !== ids.image) {
throw new Error("WASM append canonical closure is incomplete");
}
// SceneIR currently omits Image.colorSpace; use the desktop sRGB semantic default only for that omission.
const colorspace = image.colorSpace === undefined ? expectedCanonical.image.colorspace : image.colorSpace === "SRGB" ? "sRGB" : image.colorSpace;
if ((image.colorSpace !== undefined && colorspace !== expectedCanonical.image.colorspace) || image.libraryLinked !== false || image.packed !== true || image.assetStatus !== "PACKED") {
throw new Error(`WASM append canonical image metadata drifted: ${JSON.stringify({ image, expectedColorspace: expectedCanonical.image.colorspace })}`);
}
const asset = await engineClient.requestAsset(image.assetId);
if (asset.status !== "packed" || !asset.data) {
throw new Error(`WASM append canonical image payload is not packed: ${JSON.stringify({ image, asset: { ...asset, data: asset.data ? { byteLength: asset.data.byteLength } : undefined } })}`);
}
const bytes = new Uint8Array(asset.data);
const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
if (pngSignature.some((value, index) => bytes[index] !== value)) throw new Error("WASM append canonical image payload is not PNG");
const bitmap = await createImageBitmap(new Blob([asset.data], { type: "image/png" }), {
colorSpaceConversion: "none",
premultiplyAlpha: "none",
imageOrientation: "none",
});
try {
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true });
if (!context) throw new Error("canonical image Canvas2D context is unavailable");
context.globalCompositeOperation = "copy";
context.imageSmoothingEnabled = false;
context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
const rgba = new Uint8Array(context.getImageData(0, 0, bitmap.width, bitmap.height).data);
const floatPixels = new Float32Array(rgba.length);
// Blender's Image.pixels is bottom-up while Canvas ImageData is top-down.
for (let row = 0; row < bitmap.height; row++) {
const sourceRow = bitmap.height - row - 1;
for (let channel = 0; channel < 4; channel++) {
floatPixels[(row * bitmap.width * 4) + channel] = rgba[(sourceRow * bitmap.width * 4) + channel] / 255;
}
for (let column = 1; column < bitmap.width; column++) {
const target = (row * bitmap.width + column) * 4;
const source = (sourceRow * bitmap.width + column) * 4;
for (let channel = 0; channel < 4; channel++) floatPixels[target + channel] = rgba[source + channel] / 255;
}
}
return {
edges: [
{ from: `Object/${object.name}`, relation: "OBJECT_DATA", to: `Mesh/${mesh.name}` },
{ from: `Mesh/${mesh.name}`, relation: "MATERIAL_SLOT[0]", to: `Material/${material.name}` },
{ from: `Material/${material.name}`, relation: "NODE_IMAGE[M12 Append Image Node]", to: `Image/${image.name}` },
],
geometry: {
edges: mesh.edgeCount,
loops: mesh.cornerCount,
materialSlots: (mesh.materialSlotIds ?? []).map(nameFromId),
polygons: mesh.faceCount,
uvLayers: (mesh.uvLayers ?? []).map((layer: any) => layer.name),
vertices: mesh.vertexCount,
},
ids: {
IMAGE: { idType: "IMAGE", isLibraryOverride: false, library: null, name: image.name, nameFull: image.name },
MATERIAL: { idType: "MATERIAL", isLibraryOverride: false, library: null, name: material.name, nameFull: material.name },
MESH: { idType: "MESH", isLibraryOverride: false, library: null, name: mesh.name, nameFull: mesh.name },
OBJECT: { idType: "OBJECT", isLibraryOverride: false, library: null, name: object.name, nameFull: object.name },
},
image: {
channels: 4,
colorspace,
packed: image.packed,
pixelFloat32Sha256: await digest(floatPixels.buffer),
size: [bitmap.width, bitmap.height],
},
root: { idType: "OBJECT", isLibraryOverride: false, library: null, name: object.name, nameFull: object.name },
};
}
finally {
bitmap.close();
}
};
try {
const opened = await client.openBlend(targetBuffer.slice(0));
const baseRevision = opened.snapshot.revision;
const request = await makeRequest(baseRevision);
const appended = await client.appendLibraryObject(sourceBuffer.slice(0), request);
const appendedClosure = closureState(appended.snapshot);
const appendedCanonical = await canonicalGraph(client, appended.snapshot, ids.image);
const stale = await client.appendLibraryObject(sourceBuffer.slice(0), makeRequest(baseRevision))
.then(() => ({ code: "NO_ERROR" }))
.catch((error: any) => ({ code: error.code, message: error.message }));
@@ -80,9 +165,11 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
const afterCollision = await client.snapshot();
const undone = await client.applyCommand({ type: "undo" });
const redone = await client.applyCommand({ type: "redo" });
const redoneCanonical = await canonicalGraph(client, redone.snapshot, ids.image);
const saved = await client.saveBlend();
const reopenedResult = await reopened.openBlend(saved);
const reopenedClosure = closureState(reopenedResult.snapshot);
const reopenedCanonical = await canonicalGraph(reopened, reopenedResult.snapshot, ids.image);
return {
sourceSha256: await digest(sourceBuffer),
baseRevision,
@@ -97,10 +184,16 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
},
image: appendedClosure.image && { libraryLinked: appendedClosure.image.libraryLinked, width: appendedClosure.image.width, height: appendedClosure.image.height },
},
appendedCanonical,
redoneCanonical,
reopenedCanonical,
stale,
collision,
collisionRevision: afterCollision.snapshot.revision,
undoHasObject: Boolean(closureState(undone.snapshot).object),
undoHasMesh: Boolean(closureState(undone.snapshot).mesh),
undoHasMaterial: Boolean(closureState(undone.snapshot).material),
undoHasImage: Boolean(closureState(undone.snapshot).image),
redoHasObject: Boolean(closureState(redone.snapshot).object),
reopenedRevision: reopenedResult.snapshot.revision,
reopenedHasObject: Boolean(reopenedClosure.object),
@@ -114,7 +207,13 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
client.terminate();
reopened.terminate();
}
}, { sourceBytes: Array.from(source), targetBytes: Array.from(target), closure, binding });
}, {
sourceBytes: Array.from(source),
targetBytes: Array.from(target),
closure,
binding,
expectedCanonical: desktopCanonical,
});
expect(result.error).toBeUndefined();
expect(result.sourceSha256).toMatch(/^[a-f0-9]{64}$/);
@@ -129,10 +228,16 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
material: { imageIds: ["image:M12 Append Image"] },
image: { libraryLinked: false, width: 2, height: 2 },
});
expect(result.appendedCanonical).toEqual(desktopCanonical);
expect(result.redoneCanonical).toEqual(desktopCanonical);
expect(result.reopenedCanonical).toEqual(desktopCanonical);
expect(result.stale.code).toBe("REVISION_CONFLICT");
expect(result.collision.code).toBe("ASSET_MANIFEST_INVALID");
expect(result.collisionRevision).toBe(result.appendedRevision);
expect(result.undoHasObject).toBe(false);
expect(result.undoHasMesh).toBe(false);
expect(result.undoHasMaterial).toBe(false);
expect(result.undoHasImage).toBe(false);
expect(result.redoHasObject).toBe(true);
expect(result.reopenedHasObject).toBe(true);
expect(result.reopenedHasLocalImage).toBe(true);

View File

@@ -0,0 +1,19 @@
import { expect, test } from "@playwright/test";
test("M12-03N runs the linked mutation gate in an independent Chromium lane", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const linked = await import("/src/library-link-chromium.ts");
const gate = linked.gateLinkedDataMutation({
schemaVersion: 1,
operation: "MESH_GEOMETRY",
dataBlockId: "Mesh/M12 Link Mesh",
baseRevision: 7,
owner: "SOURCE_LIBRARY",
linkedLibrary: true,
readOnly: true,
}, 7);
return { status: gate.status, code: gate.issues[0]?.code, recoverable: gate.issues[0]?.recoverable };
});
expect(result).toEqual({ status: "BLOCKED", code: "LINKED_DATA_MUTATION_BLOCKED", recoverable: false });
});

View File

@@ -0,0 +1,35 @@
import { expect, test } from "@playwright/test";
test("M12-03N runs the verified override writer in an independent Chromium lane", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const writer = await import("/src/library-override-chromium.ts");
const state = {
schemaVersion: 1,
revision: 3,
localDataBlockId: "Object/M12 Override Object",
referenceSourceDataBlockId: "Object/M12 Override Object",
hierarchyRootDataBlockId: "Object/M12 Override Object",
owner: "LOCAL_OVERRIDE",
readOnly: false,
referenceReadOnly: true,
propertyPath: '["m12_override_value"]',
value: 2.5,
};
const result = writer.applyOverrideWriter(state, {
schemaVersion: 1,
operation: "SET_M12_OVERRIDE_VALUE",
baseRevision: 3,
localDataBlockId: "Object/M12 Override Object",
referenceSourceDataBlockId: "Object/M12 Override Object",
hierarchyRootDataBlockId: "Object/M12 Override Object",
owner: "LOCAL_OVERRIDE",
readOnly: false,
referenceReadOnly: true,
propertyPath: '["m12_override_value"]',
value: 4.5,
});
return { status: result.status, code: result.code, revision: result.state.revision, value: result.state.value };
});
expect(result).toEqual({ status: "APPLIED", code: null, revision: 4, value: 4.5 });
});

View File

@@ -0,0 +1,22 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixture = Array.from(fs.readFileSync(path.join(root, "tests/files/web/m13_malicious_script_v1/malicious-script.blend")));
test("M13-01F blocks malicious Text, driver, handler and embedded module sources", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (bytes) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const client = new WebEngineClient({ timeoutMs: 30_000 });
const opened = await client.openBlend(Uint8Array.from(bytes).buffer);
const sources = opened.snapshot.scriptSources;
client.terminate();
return sources;
}, fixture);
expect(result?.sources).toHaveLength(4);
expect(result?.sources.every((source: any) => source.readOnly && source.executionStatus === "BLOCKED" && /^[a-f0-9]{64}$/.test(source.sourceSha256))).toBe(true);
expect(result?.sources.find((source: any) => source.name === "EmbeddedModule.py")).toMatchObject({ moduleAutorunRequested: true, errorCode: "SCRIPT_POLICY_DENIED" });
expect(result?.sources.map((source: any) => source.name).sort()).toEqual(["DriverExploit.py", "EmbeddedModule.py", "HandlerExploit.py", "MaliciousText.py"]);
});

View File

@@ -0,0 +1,87 @@
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { expect, test, type Page } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixtureRoot = path.join(root, "tests/files/web/m12_obj_multi_v1");
const sourceObj = fs.readFileSync(path.join(fixtureRoot, "multi-object.obj"));
const sourceMtl = fs.readFileSync(path.join(fixtureRoot, "multi-object.mtl"));
const texture = fs.readFileSync(path.join(fixtureRoot, "m12_obj_texture.png"));
const reportPath = path.join(root, "tests/golden/M12-07C/web-roundtrip-report.json");
const sha256 = (bytes: Uint8Array | Buffer | string) => crypto.createHash("sha256").update(bytes).digest("hex");
async function runBrowserRoundtrip(page: Page, textureAssets: string[]) {
return page.evaluate(async (input: { obj: number[]; mtl: number[]; textureAssets: string[] }) => new Promise<any>((resolve, reject) => {
const worker = new Worker("/src/workers/obj-roundtrip-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const obj = Uint8Array.from(input.obj).buffer;
const mtl = Uint8Array.from(input.mtl).buffer;
worker.postMessage({ obj, mtl, textureAssets: input.textureAssets }, [obj, mtl]);
}), { obj: Array.from(sourceObj), mtl: Array.from(sourceMtl), textureAssets });
}
test("M12-07C round-trips a Web OBJ through desktop Blender and reports texture loss", async ({ page }) => {
await page.goto("/");
const bound = await runBrowserRoundtrip(page, ["m12_obj_texture.png"]);
const missing = await runBrowserRoundtrip(page, []);
expect(bound.ok).toBe(true);
expect(bound.lossReport).toEqual({ schemaVersion: 1, operation: "OBJ_EXPORT_LOSS_REPORT", canRoundTrip: true, warningCount: 0, warnings: [] });
expect(missing.lossReport.warnings.map((warning: { code: string }) => warning.code)).toEqual(["OBJ_TEXTURE_ORIGIN_UNRESOLVED", "OBJ_TEXTURE_ORIGIN_UNRESOLVED"]);
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07c-web-obj-"));
try {
const webObjPath = path.join(temporary, "web-output.obj");
const webMtlPath = path.join(temporary, "single-mesh.mtl");
fs.writeFileSync(webObjPath, bound.obj, "utf8");
fs.writeFileSync(webMtlPath, bound.mtl, "utf8");
fs.writeFileSync(path.join(temporary, "m12_obj_texture.png"), texture);
const desktopReportPath = path.join(temporary, "desktop-report.json");
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
const result = spawnSync(blender, ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-obj-web-roundtrip.py"), "--", webObjPath, desktopReportPath], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 });
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
const desktop = JSON.parse(fs.readFileSync(desktopReportPath, "utf8"));
const report = {
schemaVersion: 1,
task: "M12-07C",
operation: "WEB_OBJ_TO_DESKTOP_ROUNDTRIP",
source: { objSha256: sha256(sourceObj), mtlSha256: sha256(sourceMtl), textureSha256: sha256(texture) },
browser: {
imported: {
schemaVersion: bound.imported.schemaVersion,
objectCount: bound.imported.objects.length,
positionCount: bound.imported.positions.length,
texcoordCount: bound.imported.texcoords.length,
normalCount: bound.imported.normals.length,
faceCount: bound.imported.faces.length,
materialCount: bound.imported.materials.length,
},
outputObjSha256: sha256(Buffer.from(bound.obj)),
outputMtlSha256: sha256(Buffer.from(bound.mtl)),
lossReport: bound.lossReport,
missingTextureLoss: missing.lossReport,
},
desktop,
comparisons: {
objectCountExact: desktop.objectCount === bound.imported.objects.length,
triangleCountExact: desktop.objects.reduce((sum: number, object: { triangleCount: number }) => sum + object.triangleCount, 0) === bound.imported.faces.length,
uvLayerPresent: desktop.objects.every((object: { uvLayers: string[] }) => object.uvLayers.includes("UVMap")),
materialPresent: desktop.objects.every((object: { materials: string[] }) => object.materials.length === 1),
},
nextTask: "M12-07D",
};
if (process.env.UPDATE_OBJ_ROUNDTRIP === "1") {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
}
const expected = JSON.parse(fs.readFileSync(reportPath, "utf8"));
expect(report).toEqual(expected);
expect(report.comparisons).toEqual({ objectCountExact: true, triangleCountExact: true, uvLayerPresent: true, materialPresent: true });
}
finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,23 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixtureRoot = path.join(root, "tests/files/web/m12_ply_negative_v1");
const cases = [
{ id: "big-endian", file: "big-endian.ply", expected: "PLY_FORMAT_UNSUPPORTED", format: "binary_little_endian" },
{ id: "malformed-list", file: "malformed-list-ascii.ply", expected: "PLY_DATA_TRUNCATED", format: "ascii" },
{ id: "oversized-count", file: "oversized-count-ascii.ply", expected: "PLY_IMPORT_BUDGET_EXCEEDED: vertex", format: "ascii" },
];
test("M12-07I production Worker blocks PLY negative cases deterministically", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (input) => Promise.all(input.map((candidate) => new Promise<any>((resolve, reject) => {
const worker = new Worker("/src/workers/ply-roundtrip-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); resolve({ id: candidate.id, ...event.data }); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const bytes = Uint8Array.from(candidate.bytes).buffer;
worker.postMessage({ bytes, format: candidate.format }, [bytes]);
}))), cases.map((candidate) => ({ id: candidate.id, format: candidate.format, bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, candidate.file))) })));
expect(result.map((item) => ({ id: item.id, ok: item.ok, error: item.error }))).toEqual(cases.map((candidate) => ({ id: candidate.id, ok: false, error: candidate.expected })));
});

View File

@@ -0,0 +1,89 @@
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { expect, test, type Page } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixtureRoot = path.join(root, "tests/files/web/m12_ply_mapping_v1");
const sourceAscii = fs.readFileSync(path.join(fixtureRoot, "mapping-ascii.ply"));
const sourceBinary = fs.readFileSync(path.join(fixtureRoot, "mapping-binary-le.ply"));
const sourceUnknown = fs.readFileSync(path.join(fixtureRoot, "unknown-property-ascii.ply"));
const reportPath = path.join(root, "tests/golden/M12-07H/web-roundtrip-report.json");
const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex");
async function runWorker(page: Page, bytes: Buffer, format: "ascii" | "binary_little_endian") {
return page.evaluate(async (input) => new Promise<any>((resolve, reject) => {
const worker = new Worker("/src/workers/ply-roundtrip-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); resolve({ ...event.data, output: event.data.output ? Array.from(new Uint8Array(event.data.output)) : null }); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const payload = Uint8Array.from(input.bytes).buffer;
worker.postMessage({ bytes: payload, format: input.format }, [payload]);
}), { bytes: Array.from(bytes), format });
}
test("M12-07H maps PLY vertex/face/color/custom fields and reports unknown property loss", async ({ page }) => {
await page.goto("/");
const ascii = await runWorker(page, sourceAscii, "ascii");
const binary = await runWorker(page, sourceBinary, "binary_little_endian");
const unknown = await runWorker(page, sourceUnknown, "ascii");
expect(ascii.ok).toBe(true);
expect(binary.ok).toBe(true);
expect(unknown.ok).toBe(true);
expect(ascii.imported.vertices).toHaveLength(4);
expect(ascii.imported.faces).toHaveLength(2);
expect(ascii.imported.vertices[0].customProperties).toEqual({ label: 1, temperature: 10 });
expect(binary.imported.vertices).toEqual(ascii.imported.vertices);
expect(binary.imported.faces).toEqual(ascii.imported.faces);
expect(ascii.lossReport).toEqual({ schemaVersion: 1, operation: "PLY_IMPORT_LOSS_REPORT", canImport: true, warningCount: 0, warnings: [] });
expect(unknown.lossReport.warningCount).toBe(1);
expect(unknown.lossReport.warnings[0].code).toBe("PLY_UNKNOWN_PROPERTY");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07h-web-ply-"));
try {
const outputPath = path.join(temporary, "web-output.ply");
fs.writeFileSync(outputPath, Buffer.from(ascii.output));
const desktopReportPath = path.join(temporary, "desktop-report.json");
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
const command = spawnSync(blender, ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-ply-web-roundtrip.py"), "--", outputPath, desktopReportPath], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 });
expect(command.status, `${command.stdout}\n${command.stderr}`).toBe(0);
const desktop = JSON.parse(fs.readFileSync(desktopReportPath, "utf8"));
const report = {
schemaVersion: 1,
task: "M12-07H",
operation: "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING",
source: { asciiSha256: sha256(sourceAscii), binarySha256: sha256(sourceBinary), unknownSha256: sha256(sourceUnknown) },
browser: {
format: ascii.imported.format,
vertexCount: ascii.imported.vertices.length,
faceCount: ascii.imported.faces.length,
colors: ascii.imported.vertices.map((vertex: any) => vertex.color),
customProperties: ascii.imported.vertices.map((vertex: any) => vertex.customProperties),
outputSha256: sha256(Buffer.from(ascii.output)),
outputBytes: ascii.output.length,
lossReport: ascii.lossReport,
binarySemanticEqual: JSON.stringify(binary.imported.vertices) === JSON.stringify(ascii.imported.vertices) && JSON.stringify(binary.imported.faces) === JSON.stringify(ascii.imported.faces),
unknownPropertyLoss: unknown.lossReport,
},
desktop,
comparisons: {
vertexCountExact: desktop.vertexCount === ascii.imported.vertices.length,
faceCountExact: desktop.triangleCount === ascii.imported.faces.length,
positionExact: JSON.stringify(desktop.positions) === JSON.stringify(ascii.imported.vertices.map((vertex: any) => vertex.position)),
customPropertiesPresent: desktop.attributes.filter((attribute: any) => ["temperature", "label"].includes(attribute.name)).length === 2,
colorMapped: desktop.attributes.some((attribute: any) => attribute.name === "Col" && attribute.values.length === 4),
},
nextTask: "M12-07I",
};
if (process.env.UPDATE_PLY_MAPPING_REPORT === "1") {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
}
expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")));
expect(report.comparisons).toEqual({ vertexCountExact: true, faceCountExact: true, positionExact: true, customPropertiesPresent: true, colorMapped: true });
}
finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,14 @@
import { expect, test } from "@playwright/test";
test("M13-03C exposes only structured allowlisted host calls", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-host-call-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.accepted.map((item: any) => item.call)).toEqual(["READ_MAIN", "READ_ASSET", "WRITE_MAIN", "WRITE_ASSET", "SUBMIT_SERVER_JOB"]);
expect(result.accepted.every((item: any) => item.execution === "DISABLED")).toBe(true);
expect(result.blocked).toEqual({ unknown: "SCRIPT_POLICY_DENIED", permission: "SCRIPT_POLICY_DENIED", fields: "SCRIPT_MANIFEST_INVALID", path: "SCRIPT_MANIFEST_INVALID" });
});

View File

@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";
test("M13-02A enforces bounded script manifest text, module, path, dependency and permission limits", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, unknown>>((resolve, reject) => {
const worker = new Worker("/src/workers/scripting-manifest-budget-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, unknown>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.valid).toEqual([2, "scripts/base.py", "deps/base.py", 256]);
expect(result.count).toContain("SCRIPT_BUDGET_EXCEEDED");
expect(result.totalBytes).toContain("SCRIPT_BUDGET_EXCEEDED");
expect(result.module).toContain("SCRIPT_POLICY_DENIED");
expect(result.path).toContain("SCRIPT_MANIFEST_INVALID");
expect(result.dependency).toContain("SCRIPT_MANIFEST_INVALID");
expect(result.permission).toContain("SCRIPT_POLICY_DENIED");
});

View File

@@ -0,0 +1,12 @@
import { expect, test } from "@playwright/test";
test("M13-02B keeps canonical script manifest serialization stable in a production Worker", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, unknown>>((resolve, reject) => {
const worker = new Worker("/src/workers/scripting-manifest-canonical-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, unknown>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result).toEqual({ equal: true, firstId: "alpha", firstPermission: "READ_ASSET", dependencyOrder: ["alpha", "beta"], unknownDropped: true });
});

View File

@@ -0,0 +1,23 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixture = Array.from(fs.readFileSync(path.join(root, "tests/files/web/script_scene.blend")));
test("M13-01B opens a blend and reads script metadata without execution", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (bytes) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const client = new WebEngineClient({ timeoutMs: 30_000 });
const opened = await client.openBlend(Uint8Array.from(bytes).buffer);
const sources = opened.snapshot.scriptSources;
client.terminate();
return { status: opened.snapshot.scriptSourceStatus, sources };
}, fixture);
expect(result.status).toBe("AVAILABLE");
expect(result.sources?.schemaVersion).toBe(1);
expect(result.sources?.sources).toHaveLength(3);
expect(result.sources?.sources.every((source: any) => source.readOnly && source.executionStatus === "BLOCKED" && /^[a-f0-9]{64}$/.test(source.sourceSha256))).toBe(true);
expect(result.sources?.sources.find((source: any) => source.name === "ModuleAutorun.py")).toMatchObject({ moduleAutorunRequested: true, errorCode: "SCRIPT_POLICY_DENIED" });
});

View File

@@ -0,0 +1,17 @@
import { expect, test } from "@playwright/test";
test("M13-02E grants only explicitly declared permissions", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-permission-policy-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.defaultGrant).toMatchObject({ status: "ALLOWED", granted: [] });
expect(result.declaredGrant).toMatchObject({ status: "ALLOWED", granted: ["READ_MAIN"] });
expect(result.escalation).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED", granted: [] });
expect(result.unknownRequest).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" });
expect(result.duplicateRequest).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" });
expect(result.unknownDeclaration).toBe("SCRIPT_POLICY_DENIED");
});

View File

@@ -0,0 +1,14 @@
import { expect, test } from "@playwright/test";
test("M13-03B enforces sandbox resource budgets in the production worker", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-budget-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.accepted).toEqual({ schemaVersion: 1, cpuMs: 1000, wallMs: 5000, memoryBytes: 1048576, maxMessageBytes: 4096, maxOutputBytes: 8192 });
expect(result.blocked).toEqual({ cpuMs: "SCRIPT_BUDGET_EXCEEDED", wallMs: "SCRIPT_BUDGET_EXCEEDED", memoryBytes: "SCRIPT_BUDGET_EXCEEDED", maxMessageBytes: "SCRIPT_BUDGET_EXCEEDED", maxOutputBytes: "SCRIPT_BUDGET_EXCEEDED" });
expect(result.execution).toBe("DISABLED");
});

View File

@@ -0,0 +1,27 @@
import { expect, test } from "@playwright/test";
test("M13-03E cancellation publishes neither a late message nor a cache entry", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const receipt = await new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-cancellation-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ mode: "RECEIPT" });
});
const runtime = await new Promise<{ lateMessages: number; cacheWrites: number; cancelled: boolean }>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-cancellation-test.worker.ts", { type: "module" });
let lateMessages = 0;
let cacheWrites = 0;
let cancelled = false;
worker.onmessage = () => { lateMessages += 1; if (!cancelled) cacheWrites += 1; };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ mode: "RUN" });
setTimeout(() => { cancelled = true; worker.terminate(); setTimeout(() => resolve({ lateMessages, cacheWrites, cancelled }), 80); }, 10);
});
return { receipt, runtime };
});
expect(result.receipt.cancelled).toMatchObject({ status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", mainRevisionBefore: 11, mainRevisionAfter: 11, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false });
expect(result.receipt.lateResult).toBe("SCRIPT_SANDBOX_LATE_RESULT");
expect(result.runtime).toEqual({ lateMessages: 0, cacheWrites: 0, cancelled: true });
});

View File

@@ -0,0 +1,30 @@
import { expect, test } from "@playwright/test";
test("M13-03F disposes Worker resources to zero and is idempotent", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-dispose-test.worker.ts", { type: "module" });
let ready: Record<string, any> | undefined;
let first: Record<string, any> | undefined;
let lateTimerMessages = 0;
worker.onmessage = (event: MessageEvent<Record<string, any>>) => {
if (event.data.type === "ready") { ready = event.data; worker.postMessage({ type: "dispose" }); return; }
if (event.data.type === "late-timer") { lateTimerMessages += 1; return; }
if (event.data.type === "disposed" && first === undefined) { first = event.data; worker.postMessage({ type: "dispose" }); return; }
if (event.data.type === "disposed") {
const second = event.data;
worker.terminate();
setTimeout(() => resolve({ ready, first, second, workerTerminated: true, lateTimerMessages }), 70);
}
};
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ type: "init" });
}));
expect(result.ready.resources).toEqual({ messagePorts: 2, timers: 1, abortControllers: 1, transferableBuffers: 1, pendingRequests: 1, cacheReferences: 1 });
expect(result.first.receipt).toMatchObject({ schemaVersion: 1, disposeCount: 1, idempotent: false, lateTimerMessages: 0 });
expect(result.first.resources).toEqual({ messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 });
expect(result.second.receipt).toMatchObject({ schemaVersion: 1, disposeCount: 2, idempotent: true, lateTimerMessages: 0 });
expect(result.second.resources).toEqual({ messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 });
expect(result.workerTerminated).toBe(true);
expect(result.lateTimerMessages).toBe(0);
});

View File

@@ -0,0 +1,34 @@
import { expect, test } from "@playwright/test";
test("M13-03D isolates crash, timeout and late sandbox results from Main", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const receipts = await new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-isolation-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ mode: "RECEIPTS" });
});
const runCrash = await new Promise<boolean>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-isolation-test.worker.ts", { type: "module" });
worker.onerror = () => { worker.terminate(); resolve(true); };
worker.onmessage = () => { worker.terminate(); reject(new Error("crash worker published a result")); };
worker.postMessage({ mode: "CRASH" });
});
const runTimeout = await new Promise<{ timedOut: boolean; lateMessages: number }>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-isolation-test.worker.ts", { type: "module" });
let lateMessages = 0;
worker.onmessage = () => { lateMessages += 1; };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ mode: "TIMEOUT" });
setTimeout(() => { worker.terminate(); resolve({ timedOut: true, lateMessages }); }, 10);
});
return { ...receipts, runCrash, runTimeout };
});
expect(result.runCrash).toBe(true);
expect(result.runTimeout).toEqual({ timedOut: true, lateMessages: 0 });
expect(result.crash).toMatchObject({ status: "CRASHED", errorCode: "SCRIPT_SANDBOX_CRASHED", mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false });
expect(result.timeout).toMatchObject({ status: "TIMED_OUT", errorCode: "SCRIPT_SANDBOX_TIMEOUT", mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false });
expect(result.cancel).toMatchObject({ status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false });
expect(result.lateResult).toBe("SCRIPT_SANDBOX_LATE_RESULT");
});

View File

@@ -0,0 +1,21 @@
import { expect, test } from "@playwright/test";
test("M13-03G recovers a denied script in a new generation with a continuous audit chain", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-recovery-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ type: "recover" });
}));
expect(result.receipt).toMatchObject({ schemaVersion: 1, operation: "SCRIPT_SANDBOX_RECOVERY", previousGeneration: 4, nextGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, recovered: true, execution: "DISABLED" });
expect(result.receipt.audit.entries).toBe(2);
expect(result.receipt.audit.first.sequence).toBe(1);
expect(result.receipt.audit.second.sequence).toBe(2);
expect(result.receipt.audit.second.previousEntrySha256).toBe(result.receipt.audit.first.entrySha256);
expect(result.receipt.audit.first.requestId).not.toBe(result.receipt.audit.second.requestId);
expect(result.receipt.audit.first.sourceSha256).toBe(result.receipt.audit.second.sourceSha256);
expect(result.receipt.audit.first.manifestSha256).toBe(result.receipt.audit.second.manifestSha256);
expect(result.replayError).toBe("SCRIPT_MANIFEST_INVALID");
expect(result.tamperError).toBe("SCRIPT_MANIFEST_INVALID");
});

View File

@@ -0,0 +1,14 @@
import { expect, test } from "@playwright/test";
test("M13-03A exposes an all-deny sandbox scope contract", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-sandbox-scope-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.accepted).toEqual({ schemaVersion: 1, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false });
expect(result.blocked).toEqual({ dom: "SCRIPT_POLICY_DENIED", hostWorker: "SCRIPT_POLICY_DENIED", opfs: "SCRIPT_POLICY_DENIED", indexedDB: "SCRIPT_POLICY_DENIED", network: "SCRIPT_POLICY_DENIED" });
expect(result.execution).toBe("DISABLED");
});

View File

@@ -0,0 +1,33 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixture = fs.readFileSync(path.join(root, "tests/files/web/script_scene.blend"));
const reportPath = path.join(root, "tests/golden/M13-01E/script-save-reopen-report.json");
const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex");
test("M13-01E preserves Text sources through save and reopen", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (bytes) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const first = new WebEngineClient({ timeoutMs: 30_000 });
const opened = await first.openBlend(Uint8Array.from(bytes).buffer);
const before = opened.snapshot.scriptSources;
const saved = await first.saveBlend();
const savedBytes = saved.byteLength;
first.terminate();
const second = new WebEngineClient({ timeoutMs: 30_000 });
const reopened = await second.openBlend(saved);
const after = reopened.snapshot.scriptSources;
second.terminate();
return { before, after, savedBytes };
}, Array.from(fixture));
expect(result.before?.sources).toHaveLength(3);
expect(result.after?.sources).toEqual(result.before?.sources);
expect(result.after?.sources.every((source: any) => source.readOnly && source.executionStatus === "BLOCKED")).toBe(true);
const report = { schemaVersion: 1, task: "M13-01E", operation: "SCRIPT_TEXT_SAVE_REOPEN", source: { fixtureSha256: sha256(fixture), fixtureBytes: fixture.byteLength }, before: result.before, after: result.after, savedBytes: result.savedBytes, exact: JSON.stringify(result.before) === JSON.stringify(result.after), nextTask: "M13-01F" };
if (process.env.UPDATE_SCRIPT_SAVE_REOPEN_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); }
expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")));
});

View File

@@ -0,0 +1,13 @@
import { expect, test } from "@playwright/test";
test("M13-02F blocks signer replay, expiry and key confusion cases", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, any>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-signature-negative-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, any>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
for (const key of ["missing", "expired", "notYetValid", "publisherMismatch"]) expect(result[key]).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" });
expect(result.swapped).toMatchObject({ status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID" });
});

View File

@@ -0,0 +1,15 @@
import { expect, test } from "@playwright/test";
test("M13-02D verifies declared script content and invalidates source hash changes", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, unknown>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-signature-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, unknown>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.valid).toMatchObject({ status: "VERIFIED", code: "SCRIPT_SIGNATURE_VERIFIED", keyId: "key:new", sourceSha256: "a".repeat(64) });
expect(result.sourceChanged).toMatchObject({ status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID", sourceSha256: "d".repeat(64) });
expect(result.signatureChanged).toMatchObject({ status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID" });
expect(result.revoked).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" });
});

View File

@@ -0,0 +1,16 @@
import { expect, test } from "@playwright/test";
test("M13-02C resolves signer identity and rotation policy without enabling execution", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, unknown>>((resolve, reject) => {
const worker = new Worker("/src/workers/script-trust-policy-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<Record<string, unknown>>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.eligible).toEqual({ status: "ELIGIBLE", keyId: "key:new", publisher: "Team", trust: "ACTIVE", cryptographicVerification: "REQUIRED" });
expect(result.revoked).toBe("REVOKED");
expect(result.crossPublisher).toContain("SCRIPT_POLICY_DENIED");
expect(result.policyExpired).toBe("POLICY_EXPIRED");
expect(result.rotationSerialized).toBeGreaterThan(0);
});

View File

@@ -0,0 +1,62 @@
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const edgeRoot = path.join(root, "tests/files/web/m12_stl_edges_v1");
const capabilityRoot = path.join(root, "tests/files/web/m12_stl_capability_v1");
const desktop = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07E/desktop-edge-report.json"), "utf8"));
const desktopCanonical = JSON.parse(JSON.stringify(desktop));
const reportPath = path.join(root, "tests/golden/M12-07E/web-edge-report.json");
test("M12-07E compares STL normal/unit/degenerate/trailing behavior in Chromium and Blender", async ({ page }) => {
await page.goto("/");
const fixtures = [
{ id: "BINARY_UNIT_1", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "capability-binary.stl"))), variant: "STL_BINARY", unitScale: 1 },
{ id: "BINARY_UNIT_001", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "capability-binary.stl"))), variant: "STL_BINARY", unitScale: 0.001 },
{ id: "ASCII_UNIT_1", bytes: Array.from(fs.readFileSync(path.join(capabilityRoot, "capability-ascii.stl"))), variant: "STL_ASCII", unitScale: 1 },
{ id: "DEGENERATE_TRIANGLE", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "degenerate-binary.stl"))), variant: "STL_BINARY", unitScale: 1 },
{ id: "TRAILING_BYTES", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "trailing-binary.stl"))), variant: "STL_BINARY", unitScale: 1 },
];
const browser = await page.evaluate(async (input) => new Promise<any>((resolve, reject) => {
const worker = new Worker("/src/workers/stl-edge-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); resolve(event.data); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const cases = input.map((candidate) => ({ ...candidate, bytes: Uint8Array.from(candidate.bytes).buffer }));
worker.postMessage({ cases }, cases.map((candidate) => candidate.bytes));
}), fixtures);
expect(browser.ok).toBe(true);
const byId = Object.fromEntries(browser.results.map((result: any) => [result.id, result]));
const desktopById = Object.fromEntries(desktop.cases.map((result: any) => [result.id, result]));
expect(byId.TRAILING_BYTES).toEqual({ id: "TRAILING_BYTES", status: "BLOCKED", code: "STL_TRAILING_BYTES" });
const unitOne = byId.BINARY_UNIT_1.result.bounds.max[0];
const unitSmall = byId.BINARY_UNIT_001.result.bounds.max[0];
const desktopUnitOne = desktopById.BINARY_UNIT_1.result.bounds.max[0];
const desktopUnitSmall = desktopById.BINARY_UNIT_001.result.bounds.max[0];
const report = {
schemaVersion: 1,
task: "M12-07E",
operation: "STL_NORMAL_UNIT_EDGE_PARITY",
browser: browser.results,
desktop: desktopCanonical,
comparisons: {
binaryAsciiNormalExact: JSON.stringify(byId.BINARY_UNIT_1.result.normals) === JSON.stringify(byId.ASCII_UNIT_1.result.normals),
desktopNormalExact: JSON.stringify(byId.BINARY_UNIT_1.result.normals) === JSON.stringify(desktopById.BINARY_UNIT_1.result.polygonNormals),
webUnitRatio: unitOne / unitSmall,
desktopUnitRatio: desktopUnitOne / desktopUnitSmall,
unitRatioExactWithinFloat32: Math.abs(unitOne / unitSmall - desktopUnitOne / desktopUnitSmall) < 0.001,
degenerateTriangleExact: byId.DEGENERATE_TRIANGLE.result.triangleCount === desktopById.DEGENERATE_TRIANGLE.result.triangleCount && byId.DEGENERATE_TRIANGLE.result.removedDegenerateTriangles === 1,
trailingBytes: { web: "BLOCKED/STL_TRAILING_BYTES", desktop: "ACCEPTED_EMPTY", parity: "STRICTER_WEB_BLOCK" },
},
nextTask: "M12-07F",
};
if (process.env.UPDATE_STL_EDGE_REPORT === "1") {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
}
expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")));
expect(report.comparisons.binaryAsciiNormalExact).toBe(true);
expect(report.comparisons.desktopNormalExact).toBe(true);
expect(report.comparisons.unitRatioExactWithinFloat32).toBe(true);
expect(report.comparisons.degenerateTriangleExact).toBe(true);
});

View File

@@ -0,0 +1,63 @@
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const source = fs.readFileSync(path.join(root, "tests/files/web/m12_stl_capability_v1/capability-binary.stl"));
const reportPath = path.join(root, "tests/golden/M12-07F/web-roundtrip-report.json");
const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex");
test("M12-07F round-trips Web STL through desktop Blender with material loss report", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (bytes) => new Promise<any>((resolve, reject) => {
const worker = new Worker("/src/workers/stl-roundtrip-test.worker.ts", { type: "module" });
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); resolve({ ...event.data, output: event.data.output ? Array.from(new Uint8Array(event.data.output)) : null }); };
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
const input = Uint8Array.from(bytes).buffer;
worker.postMessage({ bytes: input, unitScale: 1, sourceMaterialCount: 2 }, [input]);
}), Array.from(source));
expect(result.ok).toBe(true);
expect(result.lossReport).toEqual({
schemaVersion: 1,
operation: "STL_EXPORT_LOSS_REPORT",
canRoundTrip: true,
warningCount: 1,
warnings: [{ code: "STL_MATERIAL_UNSUPPORTED", severity: "warning", message: "STL has no material slots; 2 source material assignments are omitted" }],
});
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07f-stl-"));
try {
const outputPath = path.join(temporary, "web-output.stl");
fs.writeFileSync(outputPath, Buffer.from(result.output));
const desktopPath = path.join(temporary, "desktop-report.json");
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
const command = spawnSync(blender, ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-stl-web-roundtrip.py"), "--", outputPath, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 });
expect(command.status, `${command.stdout}\n${command.stderr}`).toBe(0);
const desktop = JSON.parse(JSON.stringify(JSON.parse(fs.readFileSync(desktopPath, "utf8"))));
const report = {
schemaVersion: 1,
task: "M12-07F",
operation: "WEB_STL_TO_DESKTOP_ROUNDTRIP",
source: { sha256: sha256(source), bytes: source.byteLength },
browser: { imported: result.imported, outputSha256: sha256(Buffer.from(result.output)), outputBytes: result.output.length, lossReport: result.lossReport },
desktop,
comparison: {
triangleCountExact: result.imported.triangleCount === desktop.triangleCount,
normalExact: JSON.stringify(result.imported.normals) === JSON.stringify(desktop.polygonNormals),
materialLossExplicit: result.lossReport.warnings[0]?.code === "STL_MATERIAL_UNSUPPORTED",
},
nextTask: "M12-07G",
};
if (process.env.UPDATE_STL_ROUNDTRIP_REPORT === "1") {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
}
expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")));
expect(report.comparison).toEqual({ triangleCountExact: true, normalExact: true, materialLossExplicit: true });
}
finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,35 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import ts from "../../node_modules/typescript/lib/typescript.js";
const root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/device-budget.ts");
const source = fs.readFileSync(sourcePath, "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(output.diagnostics, []);
const budget = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`);
const base = { schemaVersion: 1, identitySha256: "a".repeat(64), webgl2: { status: "PASS", renderer: "ANGLE NVIDIA", vendor: "NVIDIA" }, webgpu: { status: "PASS", description: "NVIDIA RTX", isFallbackAdapter: false }, hardwareConcurrency: 16, deviceMemory: 16 };
test("M14-04A selects HIGH only from trusted observed capability", () => {
const result = budget.selectDeviceBudget(base);
assert.equal(result.tier, "HIGH");
assert.equal(result.reason, "HIGH_CAPABILITY");
assert.equal(result.limits.maxTextureGPUBytes, 1024 * 1024 * 1024);
});
test("M14-04A fails closed for missing WebGPU, SwiftShader, fallback and invalid identity", () => {
for (const observation of [
{ ...base, webgpu: { status: "BLOCKED" } },
{ ...base, webgl2: { ...base.webgl2, renderer: "ANGLE SwiftShader" } },
{ ...base, webgpu: { ...base.webgpu, isFallbackAdapter: true } },
{ ...base, deviceMemory: null },
]) assert.equal(budget.selectDeviceBudget(observation).tier, "CONSERVATIVE");
assert.throws(() => budget.selectDeviceBudget({ ...base, identitySha256: "bad" }), /DEVICE_BUDGET_IDENTITY_INVALID/);
});
test("M14-04A exposes fixed limits and never expands an unknown tier", () => {
assert.equal(budget.deviceBudgetLimits("CONSERVATIVE").maxLights, 8);
assert.throws(() => budget.deviceBudgetLimits("UNKNOWN"), /DEVICE_BUDGET_TIER_INVALID/);
});

View File

@@ -0,0 +1,63 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-desktop-import-unit-"));
const sourcePath = path.join(root, "web/protocol/glb-import.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "glb-import.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"), "utf8"));
const fixtureRoot = path.join(root, "tests/files/web/m12_glb_desktop_v1");
const arrayBuffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
test("M12-06B imports every desktop fixture with exact Web semantics", () => {
assert.deepEqual(report.fixtures.map((fixture) => fixture.id), ["mesh", "pbr", "uv", "skin", "animation"]);
for (const fixture of report.fixtures) {
const bytes = fs.readFileSync(path.join(fixtureRoot, fixture.file));
assert.equal(crypto.createHash("sha256").update(bytes).digest("hex"), fixture.sha256);
const imported = protocol.importGLBDesktopFixtureSemantics(arrayBuffer(bytes));
assert.deepEqual(protocol.compareGLBDesktopFixtureSemantics(fixture.semantic, imported), { compatible: true, mismatches: [] }, fixture.id);
}
});
test("M12-06B exposes topology, attributes, materials, nodes and animations separately", () => {
const imported = Object.fromEntries(report.fixtures.map((fixture) => {
const bytes = fs.readFileSync(path.join(fixtureRoot, fixture.file));
return [fixture.id, protocol.importGLBDesktopFixtureSemantics(arrayBuffer(bytes))];
}));
assert.deepEqual(Object.keys(imported.mesh.meshes[0].primitives[0].attributes), ["COLOR_0", "NORMAL", "POSITION"]);
assert.equal(imported.mesh.meshes[0].primitives[0].indices.count, 6);
assert.equal(Number(imported.pbr.materials[0].pbr.metallicFactor.toFixed(3)), 0.72);
assert.ok(imported.uv.meshes[0].primitives[0].attributes.TEXCOORD_0);
assert.equal(imported.uv.materials[0].pbr.baseColorTexture.index, 0);
assert.deepEqual(imported.skin.nodeNames, ["Tip", "Root", "M12 Skin Fixture", "M12 Skin Armature"]);
assert.equal(imported.skin.skins[0].inverseBindMatrices.type, "MAT4");
assert.deepEqual(imported.animation.animations[0].channels.map((channel) => channel.target.path), ["translation", "rotation"]);
assert.equal(imported.animation.animations[0].samplers[0].input.count, 25);
});
test("M12-06B reports a field-level semantic mismatch", () => {
const fixture = report.fixtures.find((candidate) => candidate.id === "pbr");
const bytes = fs.readFileSync(path.join(fixtureRoot, fixture.file));
const imported = protocol.importGLBDesktopFixtureSemantics(arrayBuffer(bytes));
const expected = structuredClone(fixture.semantic);
expected.materials[0].pbr.metallicFactor = 0.5;
const comparison = protocol.compareGLBDesktopFixtureSemantics(expected, imported);
assert.equal(comparison.compatible, false);
assert.deepEqual(comparison.mismatches, ["$.materials[0].pbr.metallicFactor: expected 0.5 got 0.7200000286102295"]);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-loss-report-unit-"));
const sourcePath = path.join(root, "web/protocol/glb-loss-report.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "glb-loss-report.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
test("M12-06D produces deterministic sorted machine loss entries", () => {
const snapshot = {
sceneId: "scene:test",
revision: 7,
nodes: [{}, {}],
meshes: [{}],
materials: [{}, {}],
images: [{}],
animations: [{}],
nonMeshData: [{}],
};
const report = protocol.createGLBLossReport(snapshot, {
canExport: false,
warnings: [
{ code: "Z_LOSS", severity: "warning", message: "z", id: "id:z" },
{ code: "A_BLOCK", severity: "error", message: "a", id: "id:a" },
{ code: "A_BLOCK", severity: "warning", message: "b", id: undefined },
],
});
assert.deepEqual(report, {
schemaVersion: 1,
operation: "GLB_EXPORT_LOSS_REPORT",
sceneId: "scene:test",
sourceRevision: 7,
canExport: false,
errorCount: 1,
warningCount: 2,
losses: [
{ code: "A_BLOCK", severity: "error", message: "a", id: "id:a" },
{ code: "A_BLOCK", severity: "warning", message: "b", id: null },
{ code: "Z_LOSS", severity: "warning", message: "z", id: "id:z" },
],
surface: { nodeCount: 2, meshCount: 1, materialCount: 2, imageCount: 1, animationCount: 1, nonMeshCount: 1 },
});
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-negative-unit-"));
const sourcePath = path.join(root, "web/protocol/glb-import.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "glb-import.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const source = fs.readFileSync(path.join(root, "tests/files/web/m12_glb_desktop_v1/mesh.glb"));
function arrayBuffer(bytes) {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}
function rewriteJson(mutator) {
const jsonLength = source.readUInt32LE(12);
const document = JSON.parse(source.subarray(20, 20 + jsonLength).toString("utf8").trim());
mutator(document);
const json = Buffer.from(JSON.stringify(document));
const paddedLength = (json.length + 3) & ~3;
const output = Buffer.alloc(12 + 8 + paddedLength + (source.length - (20 + jsonLength)));
output.writeUInt32LE(0x46546c67, 0);
output.writeUInt32LE(2, 4);
output.writeUInt32LE(output.length, 8);
output.writeUInt32LE(paddedLength, 12);
output.writeUInt32LE(0x4e4f534a, 16);
json.copy(output, 20);
output.fill(0x20, 20 + json.length, 20 + paddedLength);
output.writeUInt32LE(source.readUInt32LE(20 + jsonLength), 20 + paddedLength);
output.writeUInt32LE(source.readUInt32LE(24 + jsonLength), 24 + paddedLength);
source.subarray(28 + jsonLength).copy(output, 28 + paddedLength);
return output;
}
test("M12-06F rejects sparse accessors, extensions and external URIs", () => {
assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.accessors[0].sparse = { count: 1 }; }))), /GLB_SPARSE_ACCESSOR_UNSUPPORTED/);
assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.extensionsUsed = ["KHR_draco_mesh_compression"]; }))), /GLB_EXTENSION_UNSUPPORTED/);
assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.buffers[0].uri = "external.bin"; }))), /GLB_EXTERNAL_URI_BLOCKED/);
});
test("M12-06F rejects over-budget GLB bytes and table counts", () => {
const oversized = Buffer.alloc(protocol.GLB_IMPORT_BUDGET.maxBytes + 1);
source.copy(oversized);
assert.throws(() => protocol.importGLBSemantics(arrayBuffer(oversized)), /GLB_IMPORT_BUDGET_EXCEEDED/);
assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.bufferViews = Array.from({ length: protocol.GLB_IMPORT_BUDGET.maxBufferViews + 1 }, () => ({ buffer: 0, byteLength: 0 })); }))), /GLB_IMPORT_BUDGET_EXCEEDED/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-recovery-unit-"));
const sourcePath = path.join(root, "web/protocol/glb-recovery.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "glb-recovery.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const hash = "a".repeat(64);
const outputHash = "b".repeat(64);
test("M12-06G keeps cancellation and quota failure fail-closed", () => {
const running = protocol.beginGLBRecoveryOperation({
operationId: "import-1",
operation: "IMPORT",
workerGeneration: 1,
baseRevision: 7,
inputBytes: 128,
inputSha256: hash,
});
assert.equal(running.status, "RUNNING");
assert.equal(running.candidateRevision, 8);
const cancelled = protocol.cancelGLBRecoveryOperation(running);
assert.deepEqual(protocol.parseGLBRecoveryReceipt(cancelled), cancelled);
assert.equal(cancelled.errorCode, "GLB_OPERATION_CANCELLED");
assert.equal(cancelled.temporaryBytes, 0);
assert.equal(cancelled.committed, false);
assert.throws(() => protocol.commitGLBRecoveryOperation(cancelled, { bytes: 1, sha256: outputHash }), /GLB_RECOVERY_INVALID/);
assert.equal(protocol.blockGLBRecoveryForQuota(running).errorCode, "GLB_OPFS_QUOTA");
});
test("M12-06G binds committed output and worker-generation recovery", () => {
const running = protocol.beginGLBRecoveryOperation({
operationId: "export-1",
operation: "EXPORT",
workerGeneration: 1,
baseRevision: 11,
inputBytes: 256,
inputSha256: hash,
});
const committed = protocol.commitGLBRecoveryOperation(running, { bytes: 512, sha256: outputHash });
assert.equal(committed.status, "COMMITTED");
assert.equal(committed.committed, true);
assert.equal(committed.liveRequests, 0);
const recovered = protocol.recoverGLBRecoveryOperation(committed, 2);
assert.equal(recovered.status, "RECOVERED");
assert.equal(recovered.workerGeneration, 2);
assert.equal(recovered.errorCode, "GLB_WORKER_RESTARTED");
assert.throws(() => protocol.recoverGLBRecoveryOperation(committed, 1), /GLB_RECOVERY_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import ts from "../../node_modules/typescript/lib/typescript.js";
const root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/ime-composition.ts");
const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(output.diagnostics, []);
const ime = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`);
test("M14-04D blocks operators through composition start/update and reopens after end", () => {
let state = ime.createIMECompositionState();
assert.equal(ime.shouldBlockOperatorShortcuts(state), false);
state = ime.reduceIMEComposition(state, { type: "compositionstart", data: "n" });
assert.equal(ime.shouldBlockOperatorShortcuts(state), true);
state = ime.reduceIMEComposition(state, { type: "compositionupdate", data: "ni" });
assert.deepEqual([state.composing, state.pendingText, state.lastEvent], [true, "ni", "UPDATE"]);
state = ime.reduceIMEComposition(state, { type: "compositionend", data: "你" });
assert.equal(ime.shouldBlockOperatorShortcuts(state), false);
assert.equal(ime.shouldBlockOperatorShortcuts(state, true), true);
});
test("M14-04D ignores stray updates and rejects malformed state", () => {
let state = ime.createIMECompositionState();
assert.equal(ime.reduceIMEComposition(state, { type: "compositionupdate", data: "x" }), state);
assert.throws(() => ime.shouldBlockOperatorShortcuts({ schemaVersion: 2 }), /IME_STATE_INVALID/);
});

View File

@@ -0,0 +1,27 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import ts from "../../node_modules/typescript/lib/typescript.js";
const root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/input-modal.ts");
const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(output.diagnostics, []);
const modal = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`);
test("M14-04F cancels touch modal without commit and starts one two-finger navigation revision", () => {
let state = modal.createInputModalState();
state = modal.beginTouch(state, 4);
state = modal.beginTouch(state, 2);
assert.deepEqual([state.kind, state.activePointerIds, state.navigationRevision, state.mainCommitCount], ["TOUCH_NAVIGATION", [2, 4], 1, 0]);
state = modal.cancelInputModal(state);
assert.deepEqual([state.kind, state.activePointerIds, state.cancelled, state.mainCommitCount], ["NONE", [], true, 0]);
});
test("M14-04F commits pen stroke once and ignores late up/cancel", () => {
let state = modal.beginPenStroke(modal.createInputModalState(), 9);
state = modal.commitPenStroke(state, 9);
state = modal.commitPenStroke(state, 9);
assert.deepEqual([state.kind, state.mainCommitCount, state.activePointerIds], ["NONE", 1, []]);
});

View File

@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-capability-matrix-unit-"));
const sourcePath = path.join(root, "web/protocol/io-format-capability-matrix.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "io-format-capability-matrix.mjs"), transpiled.outputText);
const matrix = await import(pathToFileURL(path.join(temporary, "io-format-capability-matrix.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05B/manifest.json"), "utf8"));
const source = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05B/capability-matrix.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-05B binds the capability matrix and runtime inventory artifacts", () => {
assert.equal(manifest.task, "M12-05B");
assert.equal(manifest.parentTask, "M12-05A");
assert.equal(manifest.nextTask, "M12-05C");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-05B accepts the seven-format matrix and keeps the bounded GLB export route explicit", () => {
const parsed = matrix.parseIOFormatCapabilityMatrix(source);
assert.deepEqual(parsed.formats.map((entry) => entry.format), ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"]);
assert.equal(parsed.formats.find((entry) => entry.format === "GLB").operations.EXPORT.local.status, "READY");
assert.equal(parsed.formats.find((entry) => entry.format === "GLB").operations.EXPORT.local.execution, "LOCAL");
assert.equal(parsed.formats.find((entry) => entry.format === "GLB").operations.IMPORT.local.status, "BLOCKED");
assert.ok(parsed.formats.filter((entry) => entry.operations.IMPORT.local.status === "BLOCKED").length >= 6);
});
test("M12-05B rejects duplicate formats, ready routes without a real executor, and unverified ready features", () => {
assert.throws(() => matrix.parseIOFormatCapabilityMatrix({ ...source, formats: [...source.formats.slice(0, 6), source.formats[0]] }), { code: "ASSET_MANIFEST_INVALID" });
const badRoute = structuredClone(source);
badRoute.formats.find((entry) => entry.format === "OBJ").operations.IMPORT.local = { status: "READY", execution: "LOCAL", code: null };
assert.throws(() => matrix.parseIOFormatCapabilityMatrix(badRoute), { code: "ASSET_MANIFEST_INVALID" });
const badFeature = structuredClone(source);
badFeature.formats.find((entry) => entry.format === "GLB").operations.EXPORT.geometry.status = "UNVERIFIED";
assert.throws(() => matrix.parseIOFormatCapabilityMatrix(badFeature), { code: "ASSET_MANIFEST_INVALID" });
const badCode = structuredClone(source);
badCode.formats.find((entry) => entry.format === "PLY").operations.EXPORT.local.code = null;
assert.throws(() => matrix.parseIOFormatCapabilityMatrix(badCode), { code: "IO_FORMAT_UNSUPPORTED" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-receipt-binding-unit-"));
const sourcePath = path.join(root, "web/protocol/io-format-receipt-binding.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(transpiled.diagnostics, []); fs.writeFileSync(path.join(temporary, "protocol.mjs"), transpiled.outputText);
const protocol = await import(pathToFileURL(path.join(temporary, "protocol.mjs")));
const bound = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05E/bound-runtime-receipts.json"), "utf8"));
test("M12-05E accepts receipts with all three identity hashes", () => {
const parsed = protocol.validateIOFormatBoundReceiptSet(bound, bound.parentReceiptSetSha256, bound.inventorySha256);
assert.match(protocol.resolveBoundReceipt(parsed, "GLB", "EXPORT").sourceSha256, /^[a-f0-9]{64}$/);
assert.match(protocol.resolveBoundReceipt(parsed, "GLB", "EXPORT").settingsSha256, /^[a-f0-9]{64}$/);
assert.match(protocol.resolveBoundReceipt(parsed, "GLB", "EXPORT").runtimeSha256, /^[a-f0-9]{64}$/);
});
test("M12-05E rejects source, settings, runtime and parent hash drift", () => {
for (const field of ["sourceSha256", "settingsSha256", "runtimeSha256"]) {
const mutated = structuredClone(bound); mutated.receipts[0][field] = "invalid-hash";
assert.throws(() => protocol.validateIOFormatBoundReceiptSet(mutated, mutated.parentReceiptSetSha256, mutated.inventorySha256), { code: "IO_FORMAT_UNSUPPORTED" });
}
const stale = structuredClone(bound); stale.parentReceiptSetSha256 = "0".repeat(64);
assert.throws(() => protocol.validateIOFormatBoundReceiptSet(stale, bound.parentReceiptSetSha256, bound.inventorySha256), { code: "IO_FORMAT_UNSUPPORTED" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,86 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-receipt-freshness-unit-"));
const stableValue = (value) => Array.isArray(value) ? value.map(stableValue) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])])) : value;
const stableSha256 = (value) => crypto.createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex");
function transpile(sourcePath, outputName, replacements = []) {
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
let output = result.outputText;
for (const [from, to] of replacements) output = output.replaceAll(from, to);
const outputPath = path.join(temporary, outputName);
fs.writeFileSync(outputPath, output);
return outputPath;
}
const parentPath = path.join(root, "tests/golden/M12-05E/bound-runtime-receipts.json");
const freshnessPath = path.join(root, "tests/golden/M12-05F/fresh-runtime-receipts.json");
const parentBytes = fs.readFileSync(parentPath);
const bound = JSON.parse(parentBytes);
const freshness = JSON.parse(fs.readFileSync(freshnessPath, "utf8"));
const protocolPath = path.join(root, "web/protocol/io-format-receipt-freshness.ts");
transpile(path.join(root, "web/protocol/io-format-runtime-receipt.ts"), "io-format-runtime-receipt.mjs");
transpile(path.join(root, "web/protocol/io-format-receipt-binding.ts"), "io-format-receipt-binding.mjs");
const protocol = await import(pathToFileURL(transpile(protocolPath, "io-format-receipt-freshness.mjs", [["./io-format-receipt-binding\"", "./io-format-receipt-binding.mjs\""], ["./io-format-runtime-receipt\"", "./io-format-runtime-receipt.mjs\""]])));
const expected = {
parentBindingSha256: crypto.createHash("sha256").update(parentBytes).digest("hex"),
parentReceiptSetSha256: bound.parentReceiptSetSha256,
inventorySha256: bound.inventorySha256,
boundReceiptSetSha256: stableSha256(bound),
runtimeSha256: stableSha256(bound.runtime),
runtime: bound.runtime,
receiptIdentities: bound.receipts,
};
test("M12-05F artifact is deterministic and bound to M12-05E", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05F/manifest.json"), "utf8"));
assert.equal(manifest.task, "M12-05F");
assert.equal(manifest.parentTask, "M12-05E");
assert.equal(manifest.nextTask, "M12-06A");
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(crypto.createHash("sha256").update(fs.readFileSync(path.join(root, artifact.path))).digest("hex"), artifact.sha256, artifact.path);
}
assert.equal(freshness.parentBindingSha256, expected.parentBindingSha256);
assert.equal(freshness.boundReceiptSetSha256, expected.boundReceiptSetSha256);
assert.equal(freshness.runtimeSha256, expected.runtimeSha256);
});
test("M12-05F accepts only the exact trusted runtime receipt", async () => {
const parsed = await protocol.verifyIOFormatReceiptFreshness(freshness, expected);
assert.equal(parsed.bound.receipts.length, 14);
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(freshness, expected, { format: "GLB", operation: "EXPORT" }).status, "READY");
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(freshness, expected, { format: "USD", operation: "EXPORT" }).status, "BLOCKED");
});
test("M12-05F rejects forged content before route execution", async () => {
const forged = structuredClone(freshness);
forged.bound.receipts[0].operator = "forged.operator";
await assert.rejects(() => protocol.verifyIOFormatReceiptFreshness(forged, expected), (error) => error.reason === "RECEIPT_FORGED");
const malformed = structuredClone(freshness);
delete malformed.boundReceiptSetSha256;
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(malformed, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_FORGED");
});
test("M12-05F rejects stale parent identity and cross-version runtime", () => {
const stale = structuredClone(freshness);
stale.bound.inventorySha256 = "a".repeat(64);
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(stale, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_STALE");
const crossVersion = structuredClone(freshness);
crossVersion.bound.runtime.versionTuple = [5, 3, 0];
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(crossVersion, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_CROSS_VERSION");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-recovery-unit-"));
const sourcePath = path.join(root, "web/protocol/io-format-recovery.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "io-format-recovery.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const hash = "a".repeat(64);
const outputHash = "b".repeat(64);
test("M12-07J keeps cancellation and OOM receipts unpublished", () => {
const running = protocol.beginIOFormatRecoveryOperation({ operationId: "ply-cancel-1", format: "PLY", operation: "IMPORT", workerGeneration: 1, baseRevision: 3, inputBytes: 32, inputSha256: hash });
const cancelled = protocol.cancelIOFormatRecoveryOperation(running);
assert.deepEqual(cancelled, { ...running, status: "CANCELLED", errorCode: "IO_FORMAT_OPERATION_CANCELLED", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false });
const oomRunning = protocol.beginIOFormatRecoveryOperation({ operationId: "obj-oom-1", format: "OBJ", operation: "IMPORT", workerGeneration: 1, baseRevision: 3, inputBytes: 512 * 1024 + 1, inputSha256: hash });
const blocked = protocol.blockIOFormatRecoveryOperation(oomRunning);
assert.equal(blocked.errorCode, "IO_FORMAT_OOM");
assert.equal(blocked.publishedResults, 0);
assert.throws(() => protocol.commitIOFormatRecoveryOperation(cancelled, { bytes: 1, sha256: outputHash }), /IO_FORMAT_RECOVERY_INVALID/);
});
test("M12-07J binds output identity across restart and rejects stale generation", () => {
const running = protocol.beginIOFormatRecoveryOperation({ operationId: "stl-restart-1", format: "STL", operation: "EXPORT", workerGeneration: 1, baseRevision: 7, inputBytes: 64, inputSha256: hash });
const committed = protocol.commitIOFormatRecoveryOperation(running, { bytes: 128, sha256: outputHash });
const recovered = protocol.recoverIOFormatRecoveryOperation(committed, 2);
assert.deepEqual(recovered, { ...committed, status: "RECOVERED", workerGeneration: 2, errorCode: "IO_FORMAT_WORKER_RESTARTED" });
assert.equal(protocol.parseIOFormatRecoveryReceipt(recovered).outputSha256, outputHash);
assert.throws(() => protocol.recoverIOFormatRecoveryOperation(committed, 1), /IO_FORMAT_RECOVERY_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-runtime-receipt-unit-"));
const sourcePath = path.join(root, "web/protocol/io-format-runtime-receipt.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "protocol.mjs"), transpiled.outputText);
const protocol = await import(pathToFileURL(path.join(temporary, "protocol.mjs")));
const receiptSet = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05D/runtime-receipts.json"), "utf8"));
const inventorySha256 = receiptSet.inventorySha256;
test("M12-05D resolves capability from the runtime operator receipt", () => {
const parsed = protocol.validateIOFormatRuntimeReceiptSet(receiptSet, inventorySha256);
assert.equal(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "GLB", operation: "EXPORT" }).status, "READY");
assert.equal(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "USD", operation: "EXPORT" }).status, "BLOCKED");
});
test("M12-05D does not infer capability from a filename extension", () => {
const mutated = structuredClone(receiptSet);
const receipt = mutated.receipts.find((candidate) => candidate.format === "GLB" && candidate.operation === "EXPORT");
receipt.extensions = [".usd"];
const parsed = protocol.validateIOFormatRuntimeReceiptSet(mutated, inventorySha256);
assert.deepEqual(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "GLB", operation: "EXPORT" }).status, "READY");
assert.deepEqual(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "USD", operation: "EXPORT" }).status, "BLOCKED");
});
test("M12-05D rejects receipt identity drift and consumes explicit receipt status", () => {
const stale = structuredClone(receiptSet);
stale.inventorySha256 = "0".repeat(64);
assert.throws(() => protocol.validateIOFormatRuntimeReceiptSet(stale, inventorySha256), { code: "IO_FORMAT_UNSUPPORTED" });
const forged = structuredClone(receiptSet);
const usd = forged.receipts.find((candidate) => candidate.format === "USD" && candidate.operation === "EXPORT");
usd.runtimeStatus = "AVAILABLE";
usd.registered = true;
usd.rnaIdentifier = "WM_OT_usd_export";
usd.buildOptionEnabled = true;
assert.doesNotThrow(() => protocol.parseIOFormatRuntimeReceiptSet(forged));
assert.equal(protocol.resolveIOFormatRuntimeRoute(protocol.parseIOFormatRuntimeReceiptSet(forged), { format: "USD", operation: "EXPORT" }).status, "READY");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-ui-gate-unit-"));
const sourcePath = path.join(root, "web/protocol/io-format-ui-gate.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "io-format-ui-gate.mjs"), transpiled.outputText);
const gate = await import(pathToFileURL(path.join(temporary, "io-format-ui-gate.mjs")));
const registry = JSON.parse(fs.readFileSync(path.join(root, "web/app/src/capabilities/io-format-ui-registry.json"), "utf8"));
test("M12-05C keeps only matrix-declared executable routes in UI", () => {
const parsed = gate.parseIOFormatUIRegistry(registry);
assert.deepEqual(parsed.importRoutes, []);
assert.deepEqual(parsed.exportRoutes.map((route) => route.format), ["GLB"]);
const commands = [
{ id: "file.export-glb", ioFormat: { format: "GLB", operation: "EXPORT", execution: "LOCAL" } },
{ id: "file.import-obj", ioFormat: { format: "OBJ", operation: "IMPORT", execution: "LOCAL" } },
{ id: "file.export-usd", ioFormat: { format: "USD", operation: "EXPORT", execution: "SERVER" } },
{ id: "edit.undo" },
];
assert.deepEqual(gate.filterIOFormatOperatorCommands(commands, parsed).map((command) => command.id), ["file.export-glb", "edit.undo"]);
});
test("M12-05C file selection is project-only while import routes are blocked", () => {
const parsed = gate.parseIOFormatUIRegistry(registry);
assert.equal(gate.ioFormatUIAccept(parsed), ".blend,application/octet-stream");
assert.deepEqual(gate.gateIOFormatFileSelection("scene.blend", parsed), { status: "READY", kind: "BLEND" });
assert.deepEqual(gate.gateIOFormatFileSelection("mesh.obj", parsed), { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension: ".obj" });
assert.deepEqual(gate.gateIOFormatFileSelection("scene.glb", parsed), { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension: ".glb" });
});
test("M12-05C rejects malformed route metadata", () => {
const bad = structuredClone(registry);
bad.importRoutes = [{ format: "OBJ", operation: "IMPORT", execution: "LOCAL", extensions: [".obj"] }];
assert.doesNotThrow(() => gate.parseIOFormatUIRegistry(bad));
const parsed = gate.parseIOFormatUIRegistry(bad);
assert.equal(gate.filterIOFormatOperatorCommands([{ id: "import-obj", ioFormat: { format: "OBJ", operation: "IMPORT", execution: "LOCAL" } }], parsed).length, 1);
bad.importRoutes[0].extensions = [".not-obj"];
assert.throws(() => gate.parseIOFormatUIRegistry(bad), { code: "IO_FORMAT_UNSUPPORTED" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import ts from "../../node_modules/typescript/lib/typescript.js";
const root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/keyboard-contract.ts");
const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(output.diagnostics, []);
const keyboard = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`);
test("M14-04E preserves layout character, physical code, location and modifiers", () => {
assert.deepEqual(keyboard.observeKeyboardEvent({ key: "ä", code: "Quote", location: 0, shiftKey: true, ctrlKey: false, altKey: true, metaKey: false, repeat: true, isComposing: false }), { schemaVersion: 1, key: "ä", code: "Quote", location: 0, shiftKey: true, ctrlKey: false, altKey: true, metaKey: false, repeat: true, isComposing: false, deadKey: false });
assert.equal(keyboard.observeKeyboardEvent({ key: "Dead", code: "Quote", location: 0 }).deadKey, true);
});
test("M14-04E rejects malformed key identity", () => {
assert.throws(() => keyboard.observeKeyboardEvent({ key: "", code: "KeyA", location: 0 }), /KEY_IDENTITY_INVALID/);
assert.throws(() => keyboard.observeKeyboardEvent({ key: "a", code: "KeyA", location: 4 }), /KEY_LOCATION_INVALID/);
});

View File

@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-append-wasm-unit-"));
const sourcePath = path.join(root, "web/protocol/library-main-append.ts");
const identityPath = path.join(root, "web/protocol/library-operation-identity.ts");
const identityTranspiled = ts.transpileModule(fs.readFileSync(identityPath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: identityPath,
reportDiagnostics: true,
});
assert.deepEqual(identityTranspiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-operation-identity.mjs"), identityTranspiled.outputText);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-main-append.mjs"), transpiled.outputText.replaceAll("./library-operation-identity\"", "./library-operation-identity.mjs\""));
const append = await import(pathToFileURL(path.join(temporary, "library-main-append.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03N/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const closure = {
object: "Object/M12 Append Object",
mesh: "Mesh/M12 Append Mesh",
material: "Material/M12 Append Material",
image: "Image/M12 Append Image",
};
test("M12-03N binds the independent append WASM command", () => {
assert.equal(manifest.task, "M12-03N");
assert.equal(manifest.nextTask, "M12-04A");
assert.equal(sha256("web/protocol/library-main-append.ts"), manifest.artifacts.appendWasmProtocol.sha256);
});
test("M12-03N validates the append closure and one-transaction receipt in the WASM lane", async () => {
assert.deepEqual(append.parseLibraryAppendClosure(closure), closure);
const request = {
baseRevision: 11,
binding: {
source: { sourceLibraryId: "library:" + "a".repeat(64) },
sourceDataBlockId: closure.object,
dependencyClosureSha256: await append.computeLibraryAppendClosureSha256(closure),
},
expectedClosure: closure,
};
const receipt = append.createLibraryMainAppendReceipt(request, 12);
assert.equal(receipt.operation, "APPEND");
assert.equal(receipt.transactionCount, 1);
assert.equal(receipt.baseRevision, 11);
assert.equal(receipt.nextRevision, 12);
assert.equal(receipt.mapping.length, 4);
assert(receipt.mapping.every((item) => item.owner === "LOCAL_MAIN" && item.readOnly === false && item.source === item.local));
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,59 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-budget-unit-"));
for (const name of ["asset-path.ts", "capability-gates.ts", "error.ts", "asset-library-io.ts"]) {
const sourcePath = path.join(root, "web/protocol", name);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
let output = transpiled.outputText;
output = output.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"').replaceAll('from "./capability-gates"', 'from "./capability-gates.mjs"').replaceAll('from "./error"', 'from "./error.mjs"');
fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), output);
}
const io = await import(pathToFileURL(path.join(temporary, "asset-library-io.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04F/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const base = { format: "GLB", operation: "IMPORT", externalUris: [], archiveEntries: [] };
const entry = (pathName, compressedBytes = 2, uncompressedBytes = 2) => ({ path: pathName, compressedBytes, uncompressedBytes });
test("M12-04F binds archive budget constants and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04F");
assert.equal(manifest.parentTask, "M12-04E");
assert.equal(manifest.nextTask, "M12-04G");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
assert.equal(io.ASSET_LIBRARY_BUDGET.maxArchivePathDepth, 64);
assert.equal(io.ASSET_LIBRARY_BUDGET.maxArchiveFileNameBytes, 255);
});
test("M12-04F accepts entries at the per-entry, total, depth and filename limits", () => {
const fileName = "é".repeat(125) + ".bin";
const deepPath = `${Array.from({ length: io.ASSET_LIBRARY_BUDGET.maxArchivePathDepth }, (_, index) => `d${index}`).join("/")}/file.bin`;
const result = io.parseIORequest({ ...base, byteLength: 10, archiveEntries: [entry("a.bin"), entry(deepPath), entry(fileName)] });
assert.equal(result.archiveEntries.length, 3);
});
test("M12-04F rejects per-entry, total, count, depth and UTF-8 filename overflow", () => {
assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("huge.bin", 1, io.ASSET_LIBRARY_BUDGET.maxEntryBytes + 1)] }), { code: "ASSET_BUDGET_EXCEEDED" });
assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("a.bin", io.ASSET_LIBRARY_BUDGET.maxEntryBytes, io.ASSET_LIBRARY_BUDGET.maxEntryBytes), entry("b.bin", io.ASSET_LIBRARY_BUDGET.maxEntryBytes, io.ASSET_LIBRARY_BUDGET.maxEntryBytes), entry("c.bin", 1, 1)] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry(`${Array.from({ length: io.ASSET_LIBRARY_BUDGET.maxArchivePathDepth + 1 }, () => "d").join("/")}/file.bin`)] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("é".repeat(128))] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => io.parseIORequest({ ...base, archiveEntries: Array.from({ length: io.ASSET_LIBRARY_BUDGET.maxArchiveEntries + 1 }, (_, index) => entry(`f${index}.bin`)) }), { code: "ASSET_BUDGET_EXCEEDED" });
});
test("M12-04F keeps compression and declared source-byte budgets fail-closed", () => {
assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("bomb.bin", 1, io.ASSET_LIBRARY_BUDGET.maxCompressionRatio + 1)] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => io.parseIORequest({ ...base, byteLength: 1, archiveEntries: [entry("source.bin", 2, 2)] }), { code: "IO_ARCHIVE_UNSAFE" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,209 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-cancellation-unit-"));
const sourcePath = path.join(root, "web/protocol/archive-extraction-transaction.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "archive-extraction-transaction.mjs"), transpiled.outputText);
const extraction = await import(pathToFileURL(path.join(temporary, "archive-extraction-transaction.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04H/manifest.json"), "utf8"));
const fileSha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const bytesSha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
class DirectoryExtractionStorage {
constructor(directory, committed) {
this.directory = directory;
this.committed = committed;
fs.mkdirSync(path.join(directory, "committed"), { recursive: true });
fs.writeFileSync(path.join(directory, "committed", "project.blend"), Buffer.from("old-project"));
}
async readCommitted() { return { ...this.committed }; }
async createStaging(transactionId) {
fs.mkdirSync(path.join(this.directory, "staging", transactionId), { recursive: true });
}
async writeStaging(transactionId, entryPath, bytes) {
const target = path.join(this.directory, "staging", transactionId, entryPath);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, bytes);
}
async countStagingEntries(transactionId) {
const staging = path.join(this.directory, "staging", transactionId);
if (!fs.existsSync(staging)) return 0;
const visit = (directory) => fs.readdirSync(directory, { withFileTypes: true }).reduce(
(count, entry) => count + (entry.isDirectory() ? visit(path.join(directory, entry.name)) : 1), 0,
);
return visit(staging);
}
async discardStaging(transactionId) {
const count = await this.countStagingEntries(transactionId);
fs.rmSync(path.join(this.directory, "staging", transactionId), { recursive: true, force: true });
return count;
}
async commitStaging(transactionId, expected, candidate) {
assert.deepEqual(this.committed, expected);
const source = path.join(this.directory, "staging", transactionId, "project.blend");
const target = path.join(this.directory, "committed", "project.blend");
fs.renameSync(source, target);
await this.discardStaging(transactionId);
this.committed = { ...candidate };
return { ...this.committed };
}
}
const oldBytes = Buffer.from("old-project");
const newBytes = Buffer.from("new-project");
const metadataBytes = Buffer.from("metadata");
const committed = { projectId: "project:m12-04h", revision: 8, sha256: bytesSha256(oldBytes) };
const candidate = { projectId: committed.projectId, revision: 9, sha256: bytesSha256(newBytes) };
const request = {
schemaVersion: 1,
transactionId: "transaction:m12-04h",
archiveId: "archive:m12-04h",
committed,
candidate,
entries: [
{ path: "metadata/index.json", uncompressedBytes: metadataBytes.byteLength, sha256: bytesSha256(metadataBytes) },
{ path: "project.blend", uncompressedBytes: newBytes.byteLength, sha256: bytesSha256(newBytes) },
],
};
function createStorage(name) {
const directory = path.join(temporary, name);
fs.mkdirSync(directory, { recursive: true });
return new DirectoryExtractionStorage(directory, committed);
}
test("M12-04H binds cancellation rollback and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04H");
assert.equal(manifest.parentTask, "M12-04G");
assert.equal(manifest.nextTask, "M12-04I");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04H removes partially written staging and preserves the committed project on cancellation", async () => {
const storage = createStorage("cancel-after-write");
const controller = new AbortController();
let reads = 0;
const receipt = await extraction.runArchiveExtractionTransaction(request, storage, async (entry) => {
reads++;
if (reads === 2) controller.abort();
return entry.path === "project.blend" ? newBytes : metadataBytes;
}, controller.signal);
assert.deepEqual(receipt, {
status: "CANCELLED",
code: "IO_ARCHIVE_CANCELLED",
transactionId: request.transactionId,
committedBefore: committed,
committedAfter: committed,
removedStagingEntries: 1,
stagingEntriesAfter: 0,
publishedProjects: 0,
});
assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "old-project");
assert.equal(await storage.countStagingEntries(request.transactionId), 0);
});
test("M12-04H cancels before staging and after the last staged write without publishing", async () => {
const beforeStorage = createStorage("cancel-before-stage");
const beforeController = new AbortController();
beforeController.abort();
const before = await extraction.runArchiveExtractionTransaction(request, beforeStorage, async () => newBytes, beforeController.signal);
assert.equal(before.status, "CANCELLED");
assert.equal(before.removedStagingEntries, 0);
const finalStorage = createStorage("cancel-after-last-write");
const finalController = new AbortController();
const originalWrite = finalStorage.writeStaging.bind(finalStorage);
finalStorage.writeStaging = async (transactionId, entryPath, bytes) => {
await originalWrite(transactionId, entryPath, bytes);
if (entryPath === "project.blend") finalController.abort();
};
const after = await extraction.runArchiveExtractionTransaction(request, finalStorage, async (entry) =>
entry.path === "project.blend" ? newBytes : metadataBytes, finalController.signal);
assert.equal(after.status, "CANCELLED");
assert.equal(after.removedStagingEntries, 2);
assert.deepEqual(await finalStorage.readCommitted(), committed);
assert.equal(fs.readFileSync(path.join(finalStorage.directory, "committed", "project.blend"), "utf8"), "old-project");
});
test("M12-04H commits only after all staged payloads pass identity checks", async () => {
const storage = createStorage("commit");
const receipt = await extraction.runArchiveExtractionTransaction(request, storage, async (entry) =>
entry.path === "project.blend" ? newBytes : metadataBytes, new AbortController().signal);
assert.deepEqual(receipt, {
status: "COMMITTED",
transactionId: request.transactionId,
committed: candidate,
stagingEntriesAfter: 0,
});
assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "new-project");
});
test("M12-04H cleans staging on payload failure and detects rollback identity drift", async () => {
const failedStorage = createStorage("payload-failure");
await assert.rejects(
extraction.runArchiveExtractionTransaction(request, failedStorage, async () => Buffer.from("wrong"), new AbortController().signal),
{ code: "ASSET_SOURCE_HASH_MISMATCH" },
);
assert.equal(await failedStorage.countStagingEntries(request.transactionId), 0);
assert.deepEqual(await failedStorage.readCommitted(), committed);
const driftStorage = createStorage("rollback-drift");
const originalDiscard = driftStorage.discardStaging.bind(driftStorage);
driftStorage.discardStaging = async (transactionId) => {
const removed = await originalDiscard(transactionId);
driftStorage.committed = { ...committed, revision: committed.revision + 1 };
return removed;
};
const controller = new AbortController();
await assert.rejects(
extraction.runArchiveExtractionTransaction(request, driftStorage, async (entry) => {
const bytes = entry.path === "project.blend" ? newBytes : metadataBytes;
queueMicrotask(() => controller.abort());
return bytes;
}, controller.signal),
{ code: "STORAGE_TRANSACTION" },
);
});
test("M12-04H rejects stale commits, non-canonical paths, prefix conflicts, and undeclared fields", async () => {
const storage = createStorage("invalid-requests");
await assert.rejects(
extraction.runArchiveExtractionTransaction({ ...request, committed: { ...committed, revision: 7 } }, storage, async () => newBytes, new AbortController().signal),
{ code: "REVISION_CONFLICT" },
);
for (const unsafePath of ["../project.blend", "C:/project.blend", "https:project.blend", "dir\\project.blend"]) {
await assert.rejects(
extraction.runArchiveExtractionTransaction({ ...request, entries: [{ ...request.entries[0], path: unsafePath }] }, storage, async () => metadataBytes, new AbortController().signal),
{ code: "IO_ARCHIVE_UNSAFE" },
);
}
await assert.rejects(
extraction.runArchiveExtractionTransaction({ ...request, entries: [request.entries[0], { ...request.entries[1], path: "metadata" }] }, storage, async () => metadataBytes, new AbortController().signal),
{ code: "IO_ARCHIVE_UNSAFE" },
);
await assert.rejects(
extraction.runArchiveExtractionTransaction({ ...request, future: true }, storage, async () => newBytes, new AbortController().signal),
{ code: "IO_ARCHIVE_UNSAFE" },
);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-conflicts-unit-"));
const sourcePath = path.join(root, "web/protocol/archive-conflicts.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "archive-conflicts.mjs"), transpiled.outputText);
const conflicts = await import(pathToFileURL(path.join(temporary, "archive-conflicts.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04G/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const range = (pathName, compressedOffset, compressedBytes = 2, uncompressedBytes = 2) => ({ path: pathName, compressedOffset, compressedBytes, uncompressedBytes });
const valid = { schemaVersion: 1, byteLength: 10, ranges: [range("a.bin", 0), range("dir/b.bin", 2, 3, 3)] };
test("M12-04G binds range/conflict validation and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04G");
assert.equal(manifest.parentTask, "M12-04F");
assert.equal(manifest.nextTask, "M12-04H");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04G accepts deterministic non-overlapping ranges and reports totals", () => {
assert.deepEqual(conflicts.validateArchiveConflicts(valid), { status: "VALID", totalCompressedBytes: 5, totalUncompressedBytes: 5, nonOverlapping: true, uniquePaths: true, noPrefixConflicts: true });
});
test("M12-04G rejects compression bombs, overlaps, duplicate paths, and prefix conflicts", () => {
assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("bomb.bin", 0, 1, conflicts.ARCHIVE_CONFLICT_BUDGET.maxCompressionRatio + 1)] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("a.bin", 0, 4), range("b.bin", 3, 2)] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("same.bin", 0), range("same.bin", 2)] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("folder", 0), range("folder/file.bin", 2)] }), { code: "IO_ARCHIVE_UNSAFE" });
});
test("M12-04G rejects range/source bounds and undeclared fields", () => {
assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, byteLength: 4 }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [{ ...valid.ranges[0], future: true }] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [{ ...valid.ranges[0], compressedOffset: 9, compressedBytes: 2 }] }), { code: "IO_ARCHIVE_UNSAFE" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,163 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-recovery-unit-"));
for (const sourceName of ["archive-extraction-transaction.ts", "archive-extraction-recovery.ts"]) {
const sourcePath = path.join(root, "web/protocol", sourceName);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(
path.join(temporary, sourceName.replace(".ts", ".mjs")),
transpiled.outputText.replaceAll('from "./archive-extraction-transaction"', 'from "./archive-extraction-transaction.mjs"'),
);
}
const extraction = await import(pathToFileURL(path.join(temporary, "archive-extraction-recovery.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04I/manifest.json"), "utf8"));
const fileSha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const bytesSha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
class FaultingDirectoryStorage {
constructor(directory, committed) {
this.directory = directory;
this.committed = { ...committed };
this.fault = null;
this.partialBytes = 0;
fs.mkdirSync(path.join(directory, "committed"), { recursive: true });
fs.writeFileSync(path.join(directory, "committed", "project.blend"), Buffer.from("old-project"));
}
async readCommitted() { return { ...this.committed }; }
async createStaging(transactionId) {
fs.mkdirSync(path.join(this.directory, "staging", transactionId), { recursive: true });
}
async writeStaging(transactionId, entryPath, bytes) {
const target = path.join(this.directory, "staging", transactionId, entryPath);
fs.mkdirSync(path.dirname(target), { recursive: true });
if (this.fault) {
const partial = bytes.subarray(0, Math.max(1, Math.floor(bytes.byteLength / 2)));
fs.writeFileSync(target, partial);
this.partialBytes += partial.byteLength;
const fault = this.fault;
this.fault = null;
throw fault;
}
fs.writeFileSync(target, bytes);
}
async countStagingEntries(transactionId) {
const staging = path.join(this.directory, "staging", transactionId);
if (!fs.existsSync(staging)) return 0;
const visit = (directory) => fs.readdirSync(directory, { withFileTypes: true }).reduce(
(count, entry) => count + (entry.isDirectory() ? visit(path.join(directory, entry.name)) : 1), 0,
);
return visit(staging);
}
async discardStaging(transactionId) {
const count = await this.countStagingEntries(transactionId);
fs.rmSync(path.join(this.directory, "staging", transactionId), { recursive: true, force: true });
return count;
}
async commitStaging(transactionId, expected, candidate) {
assert.deepEqual(this.committed, expected);
const source = path.join(this.directory, "staging", transactionId, "project.blend");
const target = path.join(this.directory, "committed", "project.blend");
fs.renameSync(source, target);
await this.discardStaging(transactionId);
this.committed = { ...candidate };
return { ...this.committed };
}
}
const oldBytes = Buffer.from("old-project");
const largeBytes = Buffer.alloc(4096, 0x51);
const smallBytes = Buffer.from("small-project");
const committed = { projectId: "project:m12-04i", revision: 9, sha256: bytesSha256(oldBytes) };
function request(transactionId, base, bytes) {
return {
schemaVersion: 1,
transactionId,
archiveId: `archive:${transactionId}`,
committed: base,
candidate: { projectId: base.projectId, revision: base.revision + 1, sha256: bytesSha256(bytes) },
entries: [{ path: "project.blend", uncompressedBytes: bytes.byteLength, sha256: bytesSha256(bytes) }],
};
}
function createStorage(name) {
const directory = path.join(temporary, name);
fs.mkdirSync(directory, { recursive: true });
return new FaultingDirectoryStorage(directory, committed);
}
async function faultThenRecover(name, fault, expectedCode) {
const storage = createStorage(name);
storage.fault = fault;
const failedRequest = request(`transaction:${name}:large`, committed, largeBytes);
await assert.rejects(
extraction.runArchiveExtractionTransaction(failedRequest, storage, async () => largeBytes, new AbortController().signal),
{ code: expectedCode },
);
assert.ok(storage.partialBytes > 0);
assert.equal(await storage.countStagingEntries(failedRequest.transactionId), 0);
assert.deepEqual(await storage.readCommitted(), committed);
assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "old-project");
const recoveryRequest = request(`transaction:${name}:small`, committed, smallBytes);
const receipt = await extraction.runArchiveExtractionTransaction(
recoveryRequest,
storage,
async () => smallBytes,
new AbortController().signal,
);
assert.deepEqual(receipt, {
status: "COMMITTED",
transactionId: recoveryRequest.transactionId,
committed: recoveryRequest.candidate,
stagingEntriesAfter: 0,
});
assert.equal(await storage.countStagingEntries(recoveryRequest.transactionId), 0);
assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "small-project");
}
test("M12-04I binds quota/OOM cleanup and recovery evidence", () => {
assert.equal(manifest.task, "M12-04I");
assert.equal(manifest.parentTask, "M12-04H");
assert.equal(manifest.nextTask, "M12-04J");
assert.deepEqual(manifest.assertions.faultCodes, ["STORAGE_QUOTA", "WASM_OUT_OF_MEMORY"]);
for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04I releases partial staging after quota and accepts a small archive in the same storage", async () => {
await faultThenRecover("quota", new DOMException("quota exhausted", "QuotaExceededError"), "STORAGE_QUOTA");
});
test("M12-04I releases partial staging after OOM and accepts a small archive in the same storage", async () => {
await faultThenRecover("oom", Object.assign(new Error("deterministic allocation failure"), { code: "WASM_OUT_OF_MEMORY" }), "WASM_OUT_OF_MEMORY");
});
test("M12-04I maps only declared resource faults", () => {
assert.equal(extraction.archiveExtractionResourceFaultCode(new DOMException("full", "QuotaExceededError")), "STORAGE_QUOTA");
assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { code: "STORAGE_QUOTA" })), "STORAGE_QUOTA");
assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { code: "WASM_OUT_OF_MEMORY" })), "WASM_OUT_OF_MEMORY");
assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { name: "OutOfMemoryError" })), "WASM_OUT_OF_MEMORY");
assert.equal(extraction.archiveExtractionResourceFaultCode(new RangeError("array length is invalid")), undefined);
assert.equal(extraction.archiveExtractionResourceFaultCode(new Error("ordinary IO failure")), undefined);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,88 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-link-safety-unit-"));
const sourcePath = path.join(root, "web/protocol/archive-link-safety.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "archive-link-safety.mjs"), transpiled.outputText);
const safety = await import(pathToFileURL(path.join(temporary, "archive-link-safety.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04D/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const valid = {
schemaVersion: 1,
temporaryRootId: "staging:archive-1",
entries: [
{ path: "payload/data.bin", type: "FILE", target: null },
{ path: "payload/data-alias.bin", type: "SYMLINK", target: "./data.bin" },
{ path: "payload/data-hard.bin", type: "HARDLINK", target: "payload/data.bin" },
{ path: "payload", type: "DIRECTORY", target: null },
{ path: "alias-dir", type: "SYMLINK", target: "payload" },
],
};
test("M12-04D binds the archive link gate and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04D");
assert.equal(manifest.parentTask, "M12-04C");
assert.equal(manifest.nextTask, "M12-04E");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04D resolves symlinks relative to their parent and hardlinks from the archive root", () => {
const result = safety.resolveArchiveLinkEntries(valid);
assert.equal(result.status, "READY");
assert.equal(result.entries.find((entry) => entry.path === "payload/data-alias.bin").resolvedPath, "payload/data.bin");
assert.equal(result.entries.find((entry) => entry.path === "payload/data-hard.bin").resolvedPath, "payload/data.bin");
assert.equal(result.entries.find((entry) => entry.path === "alias-dir").resolvedPath, "payload");
assert.ok(result.entries.every((entry) => entry.withinTemporaryRoot === true));
});
test("M12-04D rejects absolute, drive, URI and traversal targets before writing", () => {
for (const target of ["/outside", "\\\\server\\share", "C:/outside", "https://evil.example/a", "../../outside", "payload/../../outside"]) {
assert.throws(() => safety.resolveArchiveLinkEntries({
...valid,
entries: [{ path: "payload/link", type: "SYMLINK", target }, { path: "payload", type: "DIRECTORY", target: null }],
}), { code: "IO_ARCHIVE_UNSAFE" });
}
});
test("M12-04D rejects missing targets, cycles and hardlinks to directories", () => {
assert.throws(() => safety.resolveArchiveLinkEntries({
...valid,
entries: [{ path: "link", type: "SYMLINK", target: "missing.bin" }],
}), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => safety.resolveArchiveLinkEntries({
...valid,
entries: [
{ path: "a", type: "SYMLINK", target: "b" },
{ path: "b", type: "HARDLINK", target: "a" },
],
}), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => safety.resolveArchiveLinkEntries({
...valid,
entries: [
{ path: "dir", type: "DIRECTORY", target: null },
{ path: "dir-hard", type: "HARDLINK", target: "dir" },
],
}), { code: "IO_ARCHIVE_UNSAFE" });
});
test("M12-04D rejects duplicate members and undeclared fields", () => {
assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], target: null }, { ...valid.entries[0], target: null }] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, future: true }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], mode: 0o644 }] }), { code: "IO_ARCHIVE_UNSAFE" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-linked-missing-unit-"));
const sourcePath = path.join(root, "web/protocol/library-linked-missing.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-linked-missing.mjs"), transpiled.outputText);
const missing = await import(pathToFileURL(path.join(temporary, "library-linked-missing.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03I/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const library = (value) => `library:${String(value).repeat(64).slice(0, 64)}`;
const digest = (value) => crypto.createHash("sha256").update(String(value)).digest("hex");
const reference = (sourceLibraryId, generation, revision, marker, status = "AVAILABLE") => ({
sourceLibraryId,
sourceLocator: `project://libraries/${marker}.blend`,
sourceSha256: digest(`${marker}-source`),
sourceGeneration: generation,
sourceRevision: revision,
dataBlockIds: [`Object/${marker}`, `Mesh/${marker}`],
status,
placeholder: status === "MISSING" ? {
kind: "MISSING_LIBRARY",
sourceLibraryId,
dataBlockIds: [`Object/${marker}`, `Mesh/${marker}`],
} : null,
});
test("M12-03I binds the missing-library protocol and evidence artifacts", () => {
assert.equal(manifest.task, "M12-03I");
assert.equal(manifest.parentTask, "M12-03H");
assert.equal(manifest.nextTask, "M12-03J");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-03I marks only the matching reference missing and preserves its original source", () => {
const sourceLibraryId = library("a");
const otherLibraryId = library("b");
const current = reference(sourceLibraryId, 4, 9, "primary");
const other = reference(otherLibraryId, 1, 2, "other");
const state = { schemaVersion: 1, references: [current, other] };
const decision = missing.markLinkedLibraryMissing(state, {
schemaVersion: 1,
operation: "MARK_MISSING",
sourceLibraryId,
sourceLocator: current.sourceLocator,
sourceSha256: current.sourceSha256,
expectedGeneration: 4,
expectedRevision: 9,
});
assert.equal(decision.status, "MARKED");
assert.equal(decision.code, null);
assert.deepEqual(decision.state.references[0], { ...current, status: "MISSING", placeholder: {
kind: "MISSING_LIBRARY", sourceLibraryId, dataBlockIds: current.dataBlockIds,
} });
assert.deepEqual(decision.state.references[1], other);
assert.deepEqual(state.references, [current, other]);
});
test("M12-03I preserves the reference on stale generation and source hash drift", () => {
const sourceLibraryId = library("c");
const current = reference(sourceLibraryId, 2, 5, "stable");
const state = { schemaVersion: 1, references: [current] };
const base = {
schemaVersion: 1,
operation: "MARK_MISSING",
sourceLibraryId,
sourceLocator: current.sourceLocator,
sourceSha256: current.sourceSha256,
expectedGeneration: 2,
expectedRevision: 5,
};
assert.equal(missing.markLinkedLibraryMissing(state, { ...base, expectedRevision: 6 }).code, "REVISION_CONFLICT");
assert.equal(missing.markLinkedLibraryMissing(state, { ...base, sourceSha256: digest("different") }).code, "ASSET_SOURCE_HASH_MISMATCH");
assert.deepEqual(state.references, [current]);
});
test("M12-03I rejects undeclared fields, duplicate identities, and invalid placeholders", () => {
const sourceLibraryId = library("d");
const current = reference(sourceLibraryId, 1, 1, "invalid");
assert.throws(() => missing.parseLinkedMissingRequest({
schemaVersion: 1,
operation: "MARK_MISSING",
sourceLibraryId,
sourceLocator: current.sourceLocator,
sourceSha256: current.sourceSha256,
expectedGeneration: 1,
expectedRevision: 1,
future: true,
}), { code: "TASK_VALIDATION_FAILED" });
assert.throws(() => missing.parseLinkedMissingState({ schemaVersion: 1, references: [current, current] }), { code: "TASK_VALIDATION_FAILED" });
assert.throws(() => missing.parseLinkedLibraryReference({ ...current, status: "MISSING", placeholder: null }), { code: "TASK_VALIDATION_FAILED" });
assert.throws(() => missing.parseLinkedLibraryReference({ ...current, status: "AVAILABLE", placeholder: { kind: "MISSING_LIBRARY", sourceLibraryId, dataBlockIds: current.dataBlockIds } }), { code: "TASK_VALIDATION_FAILED" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-linked-mutation-unit-"));
const transpile = (sourceName, outputName, replacements = []) => {
const sourcePath = path.join(root, "web/protocol", sourceName);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, outputName), replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText));
};
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("library-linked-mutation.ts", "library-linked-mutation.mjs", [["from \"./capability-gates\"", "from \"./capability-gates.mjs\""]]);
const linked = await import(pathToFileURL(path.join(temporary, "library-linked-mutation.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03G/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const base = (operation = "MESH_GEOMETRY") => ({
schemaVersion: 1,
operation,
dataBlockId: "mesh:M12 Link Mesh",
baseRevision: 7,
owner: "SOURCE_LIBRARY",
linkedLibrary: true,
readOnly: true,
});
test("M12-03G binds the linked writer protocol and evidence artifacts", () => {
assert.equal(manifest.task, "M12-03G");
assert.equal(manifest.parentTask, "M12-03F");
assert.equal(manifest.nextTask, "M12-03H");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-03G blocks every linked data writer with the stable mutation code", () => {
assert.equal(linked.LINKED_DATA_WRITER_OPERATIONS.length, 6);
for (const operation of linked.LINKED_DATA_WRITER_OPERATIONS) {
const gate = linked.gateLinkedDataMutation(base(operation), 7);
assert.equal(gate.status, "BLOCKED");
assert.deepEqual(gate.issues.map((issue) => issue.code), ["LINKED_DATA_MUTATION_BLOCKED"]);
assert.equal(gate.issues[0].recoverable, false);
assert.deepEqual(linked.parseLinkedDataMutation(base(operation)), { ...base(operation) });
}
});
test("M12-03G rejects stale, malformed, and ownership-substituted writes before Main", () => {
assert.equal(linked.gateLinkedDataMutation(base(), 8).issues[0].code, "REVISION_CONFLICT");
assert.equal(linked.gateLinkedDataMutation({ ...base(), owner: "LOCAL_MAIN", linkedLibrary: false, readOnly: false }, 7).issues[0].code, "LINKED_DATA_MUTATION_BLOCKED");
assert.equal(linked.gateLinkedDataMutation({ ...base(), future: true }, 7).issues[0].code, "TASK_VALIDATION_FAILED");
assert.equal(linked.gateLinkedDataMutation({ ...base(), operation: "UNKNOWN" }, 7).issues[0].code, "TASK_VALIDATION_FAILED");
});

View File

@@ -0,0 +1,111 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-linked-reload-unit-"));
const sourcePath = path.join(root, "web/protocol/library-linked-reload.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-linked-reload.mjs"), transpiled.outputText);
const reload = await import(pathToFileURL(path.join(temporary, "library-linked-reload.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03H/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const library = (value) => `library:${String(value).repeat(64).slice(0, 64)}`;
const digest = (value) => crypto.createHash("sha256").update(String(value)).digest("hex");
const snapshot = (sourceLibraryId, generation, revision, marker) => ({
sourceLibraryId,
sourceGeneration: generation,
sourceRevision: revision,
dependencyClosureSha256: digest(marker),
graphSha256: digest(`${marker}-graph`),
dataBlocks: [
{ dataBlockId: `Object/${marker}`, owner: "SOURCE_LIBRARY", readOnly: true },
{ dataBlockId: `Mesh/${marker}`, owner: "SOURCE_LIBRARY", readOnly: true },
],
});
test("M12-03H binds the reload protocol and evidence artifacts", () => {
assert.equal(manifest.task, "M12-03H");
assert.equal(manifest.parentTask, "M12-03G");
assert.equal(manifest.nextTask, "M12-03I");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-03H replaces only the matching library generation and preserves every other snapshot", () => {
const sourceLibraryId = library("a");
const otherLibraryId = library("b");
const current = snapshot(sourceLibraryId, 3, 7, "old");
const otherGeneration = snapshot(sourceLibraryId, 9, 2, "future");
const otherLibrary = snapshot(otherLibraryId, 1, 4, "other");
const state = { schemaVersion: 1, snapshots: [current, otherGeneration, otherLibrary] };
const replacement = snapshot(sourceLibraryId, 4, 8, "new");
const decision = reload.reloadMatchingLinkedSnapshot(state, {
schemaVersion: 1,
operation: "RELOAD",
sourceLibraryId,
expectedGeneration: 3,
expectedRevision: 7,
replacement,
});
assert.equal(decision.status, "REPLACED");
assert.equal(decision.code, null);
assert.deepEqual(decision.state.snapshots, [replacement, otherGeneration, otherLibrary]);
assert.deepEqual(state.snapshots, [current, otherGeneration, otherLibrary]);
assert(decision.state.snapshots.every((item) => item.dataBlocks.every((block) => block.owner === "SOURCE_LIBRARY" && block.readOnly)));
});
test("M12-03H rejects stale generations and keeps state byte-for-byte equivalent", () => {
const sourceLibraryId = library("c");
const state = { schemaVersion: 1, snapshots: [snapshot(sourceLibraryId, 5, 11, "stable")] };
const request = {
schemaVersion: 1,
operation: "RELOAD",
sourceLibraryId,
expectedGeneration: 4,
expectedRevision: 10,
replacement: snapshot(sourceLibraryId, 5, 12, "late"),
};
const decision = reload.reloadMatchingLinkedSnapshot(state, request);
assert.deepEqual(decision, {
status: "STALE",
code: "REVISION_CONFLICT",
sourceLibraryId,
replacedGeneration: 4,
replacementGeneration: 5,
state,
});
});
test("M12-03H fails closed for malformed, substituted, duplicate, and non-adjacent reloads", () => {
const sourceLibraryId = library("d");
const current = snapshot(sourceLibraryId, 1, 1, "current");
const baseRequest = {
schemaVersion: 1,
operation: "RELOAD",
sourceLibraryId,
expectedGeneration: 1,
expectedRevision: 1,
replacement: snapshot(sourceLibraryId, 2, 2, "replacement"),
};
assert.throws(() => reload.parseLinkedReloadRequest({ ...baseRequest, future: true }), { code: "TASK_VALIDATION_FAILED" });
assert.throws(() => reload.parseLinkedReloadRequest({ ...baseRequest, replacement: { ...baseRequest.replacement, sourceLibraryId: library("e") } }), { code: "TASK_VALIDATION_FAILED" });
assert.throws(() => reload.parseLinkedReloadState({ schemaVersion: 1, snapshots: [current, current] }), { code: "TASK_VALIDATION_FAILED" });
const decision = reload.reloadMatchingLinkedSnapshot({ schemaVersion: 1, snapshots: [current] }, {
...baseRequest,
replacement: snapshot(sourceLibraryId, 3, 3, "skip"),
});
assert.equal(decision.status, "STALE");
assert.equal(decision.code, "REVISION_CONFLICT");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-metadata-first-unit-"));
const sourcePath = path.join(root, "web/protocol/archive-metadata-first.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "archive-metadata-first.mjs"), transpiled.outputText);
const metadata = await import(pathToFileURL(path.join(temporary, "archive-metadata-first.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04E/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const zip = { schemaVersion: 1, archiveId: "archive:zip-1", format: "ZIP", archiveByteLength: 4096, metadataOffset: 3072, metadataByteLength: 512 };
const tar = { schemaVersion: 1, archiveId: "archive:tar-1", format: "TAR", archiveByteLength: 4096, metadataOffset: 0, metadataByteLength: 1024 };
test("M12-04E binds metadata-first planning and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04E");
assert.equal(manifest.parentTask, "M12-04D");
assert.equal(manifest.nextTask, "M12-04F");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04E plans ZIP central-directory and TAR manifest reads without payload ranges", () => {
assert.deepEqual(metadata.planArchiveMetadataRead(zip), {
status: "METADATA_ONLY", schemaVersion: 1, archiveId: "archive:zip-1", format: "ZIP",
firstRead: { kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 }, payloadReads: [],
});
assert.deepEqual(metadata.planArchiveMetadataRead(tar).firstRead, { kind: "MANIFEST", byteOffset: 0, byteLength: 1024 });
});
test("M12-04E accepts a trace only when metadata is the first exact read", () => {
assert.deepEqual(metadata.validateArchiveReadTrace(zip, {
schemaVersion: 1, archiveId: "archive:zip-1", reads: [
{ sequence: 0, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 },
{ sequence: 1, kind: "PAYLOAD", byteOffset: 32, byteLength: 128 },
],
}), { status: "VALID", metadataFirst: true, payloadReadsAfterMetadata: true });
assert.deepEqual(metadata.validateArchiveReadTrace(tar, {
schemaVersion: 1, archiveId: "archive:tar-1", reads: [{ sequence: 0, kind: "MANIFEST", byteOffset: 0, byteLength: 1024 }],
}).metadataFirst, true);
});
test("M12-04E rejects payload-first, wrong-range, duplicate-metadata and invalid ranges", () => {
assert.throws(() => metadata.validateArchiveReadTrace(zip, { schemaVersion: 1, archiveId: "archive:zip-1", reads: [
{ sequence: 0, kind: "PAYLOAD", byteOffset: 0, byteLength: 16 },
{ sequence: 1, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 },
] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => metadata.validateArchiveReadTrace(zip, { schemaVersion: 1, archiveId: "archive:zip-1", reads: [{ sequence: 0, kind: "CENTRAL_DIRECTORY", byteOffset: 3000, byteLength: 512 }] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => metadata.validateArchiveReadTrace(zip, { schemaVersion: 1, archiveId: "archive:zip-1", reads: [
{ sequence: 0, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 },
{ sequence: 1, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 },
] }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => metadata.planArchiveMetadataRead({ ...zip, metadataOffset: 4000, metadataByteLength: 200 }), { code: "IO_ARCHIVE_UNSAFE" });
assert.throws(() => metadata.planArchiveMetadataRead({ ...zip, metadataByteLength: 64 * 1024 * 1024 + 1 }), { code: "IO_ARCHIVE_UNSAFE" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-negative-cases-unit-"));
const sourcePath = path.join(root, "web/protocol/library-negative-cases.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-negative-cases.mjs"), transpiled.outputText);
const negatives = await import(pathToFileURL(path.join(temporary, "library-negative-cases.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03M/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const a = "library:" + "a".repeat(64);
const b = "library:" + "b".repeat(64);
const c = "library:" + "c".repeat(64);
const valid = {
schemaVersion: 1,
libraries: [{ libraryId: a, dependencyIds: [b] }, { libraryId: b, dependencyIds: [] }, { libraryId: c, dependencyIds: [] }],
dataBlocks: [{ dataBlockId: "Object/A", sourceLibraryId: a }, { dataBlockId: "Object/B", sourceLibraryId: b }],
crossReferences: [],
reloads: [{ sourceLibraryId: a, generation: 1 }, { sourceLibraryId: a, generation: 2 }],
};
test("M12-03M binds the library negative-case protocol and evidence artifacts", () => {
assert.equal(manifest.task, "M12-03M");
assert.equal(manifest.parentTask, "M12-03L");
assert.equal(manifest.nextTask, "M12-03N");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-03M accepts an acyclic graph with unique data-block and reload identities", () => {
assert.deepEqual(negatives.validateLibraryNegativeInput(valid), { status: "VALID" });
});
test("M12-03M rejects dependency and cross-library cycles", () => {
assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, libraries: [{ libraryId: a, dependencyIds: [b] }, { libraryId: b, dependencyIds: [a] }, { libraryId: c, dependencyIds: [] }] }), { code: "LIBRARY_DEPENDENCY_CYCLE" });
assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, crossReferences: [{ fromLibraryId: a, toLibraryId: b }, { fromLibraryId: b, toLibraryId: a }] }), { code: "LIBRARY_DEPENDENCY_CYCLE" });
});
test("M12-03M rejects ID collision, duplicate reload and missing library references", () => {
assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, dataBlocks: [{ dataBlockId: "Object/A", sourceLibraryId: a }, { dataBlockId: "Object/A", sourceLibraryId: b }] }), { code: "TASK_VALIDATION_FAILED" });
assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, reloads: [{ sourceLibraryId: a, generation: 1 }, { sourceLibraryId: a, generation: 1 }] }), { code: "REVISION_CONFLICT" });
assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, dataBlocks: [{ dataBlockId: "Object/A", sourceLibraryId: "library:" + "d".repeat(64) }] }), { code: "ASSET_SOURCE_HASH_MISMATCH" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,86 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-override-freshness-unit-"));
const sourcePath = path.join(root, "web/protocol/library-override-freshness.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-override-freshness.mjs"), transpiled.outputText);
const freshness = await import(pathToFileURL(path.join(temporary, "library-override-freshness.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03L/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const library = "library:" + "a".repeat(64);
const digest = "b".repeat(64);
const token = "override-token:" + "c".repeat(64);
const state = (revision = 7) => ({
schemaVersion: 1,
sourceLibraryId: library,
sourceGeneration: 4,
sourceRevision: revision,
dependencyClosureSha256: digest,
invalidationToken: token,
localDataBlockId: "Object/M12 Override Object",
referenceSourceDataBlockId: "Object/M12 Override Object",
hierarchyRootDataBlockId: "Object/M12 Override Object",
owner: "LOCAL_OVERRIDE",
readOnly: false,
referenceReadOnly: true,
});
const request = (overrides = {}) => ({
schemaVersion: 1,
operation: "COMMIT_OVERRIDE",
sourceLibraryId: library,
sourceGeneration: 4,
sourceRevision: 7,
dependencyClosureSha256: digest,
invalidationToken: token,
baseRevision: 7,
localDataBlockId: "Object/M12 Override Object",
referenceSourceDataBlockId: "Object/M12 Override Object",
hierarchyRootDataBlockId: "Object/M12 Override Object",
owner: "LOCAL_OVERRIDE",
readOnly: false,
referenceReadOnly: true,
...overrides,
});
test("M12-03L binds the override freshness gate and evidence artifacts", () => {
assert.equal(manifest.task, "M12-03L");
assert.equal(manifest.parentTask, "M12-03K");
assert.equal(manifest.nextTask, "M12-03M");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-03L allows only a fully matching source generation/revision commit", () => {
assert.deepEqual(freshness.gateOverrideFreshness(state(), request()), { status: "READY", code: null });
});
test("M12-03L blocks stale generation, revision, closure, token, and identity before Main", () => {
for (const overrides of [
{ sourceGeneration: 3 },
{ sourceRevision: 6, baseRevision: 6 },
{ dependencyClosureSha256: "d".repeat(64) },
{ invalidationToken: "override-token:" + "e".repeat(64) },
]) assert.deepEqual(freshness.gateOverrideFreshness(state(), request(overrides)), { status: "BLOCKED", code: "REVISION_CONFLICT" });
assert.deepEqual(freshness.gateOverrideFreshness(state(), request({ localDataBlockId: "Object/Other" })), { status: "BLOCKED", code: "ASSET_SOURCE_HASH_MISMATCH" });
});
test("M12-03L rejects linked ownership, alternate operations, malformed tokens, and extra fields", () => {
assert.equal(freshness.gateOverrideFreshness(state(), request({ owner: "SOURCE_LIBRARY", readOnly: true })).code, "LINKED_DATA_MUTATION_BLOCKED");
assert.equal(freshness.gateOverrideFreshness(state(), request({ operation: "SET_LOCATION" })).code, "TASK_VALIDATION_FAILED");
assert.equal(freshness.gateOverrideFreshness(state(), request({ invalidationToken: "bad" })).code, "TASK_VALIDATION_FAILED");
assert.equal(freshness.gateOverrideFreshness(state(), { ...request(), future: true }).code, "TASK_VALIDATION_FAILED");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,82 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-override-writer-unit-"));
const sourcePath = path.join(root, "web/protocol/library-override-writer.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-override-writer.mjs"), transpiled.outputText);
const writer = await import(pathToFileURL(path.join(temporary, "library-override-writer.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03K/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const state = (revision = 3, value = 2.5) => ({
schemaVersion: 1,
revision,
localDataBlockId: "Object/M12 Override Object",
referenceSourceDataBlockId: "Object/M12 Override Object",
hierarchyRootDataBlockId: "Object/M12 Override Object",
owner: "LOCAL_OVERRIDE",
readOnly: false,
referenceReadOnly: true,
propertyPath: '["m12_override_value"]',
value,
});
const request = (baseRevision = 3, value = 4.5) => ({
schemaVersion: 1,
operation: "SET_M12_OVERRIDE_VALUE",
baseRevision,
localDataBlockId: "Object/M12 Override Object",
referenceSourceDataBlockId: "Object/M12 Override Object",
hierarchyRootDataBlockId: "Object/M12 Override Object",
owner: "LOCAL_OVERRIDE",
readOnly: false,
referenceReadOnly: true,
propertyPath: '["m12_override_value"]',
value,
});
test("M12-03K binds the single-property writer and evidence artifacts", () => {
assert.equal(manifest.task, "M12-03K");
assert.equal(manifest.parentTask, "M12-03J");
assert.equal(manifest.nextTask, "M12-03L");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-03K applies exactly the verified override property and advances one revision", () => {
const result = writer.applyOverrideWriter(state(), request());
assert.equal(result.status, "APPLIED");
assert.equal(result.code, null);
assert.equal(result.state.revision, 4);
assert.equal(result.state.value, 4.5);
assert.equal(result.state.propertyPath, writer.LIBRARY_OVERRIDE_PROPERTY_PATH);
assert.equal(result.state.owner, "LOCAL_OVERRIDE");
assert.equal(result.state.referenceReadOnly, true);
});
test("M12-03K blocks stale, linked-owner, identity, and second-property writes", () => {
assert.equal(writer.applyOverrideWriter(state(3), request(2)).code, "REVISION_CONFLICT");
assert.equal(writer.applyOverrideWriter(state(), { ...request(), owner: "SOURCE_LIBRARY", readOnly: true, referenceReadOnly: true }).code, "LINKED_DATA_MUTATION_BLOCKED");
assert.equal(writer.applyOverrideWriter(state(), { ...request(), localDataBlockId: "Object/Other" }).code, "ASSET_SOURCE_HASH_MISMATCH");
assert.equal(writer.applyOverrideWriter(state(), { ...request(), propertyPath: "location" }).code, "EDITOR_WRITER_UNAVAILABLE");
assert.equal(writer.applyOverrideWriter(state(), { ...request(), operation: "SET_LOCATION" }).code, "TASK_VALIDATION_FAILED");
});
test("M12-03K rejects malformed values and undeclared fields before the writer", () => {
assert.equal(writer.applyOverrideWriter(state(), { ...request(), future: true }).code, "TASK_VALIDATION_FAILED");
assert.equal(writer.applyOverrideWriter(state(), { ...request(), value: Number.NaN }).code, "TASK_VALIDATION_FAILED");
assert.throws(() => writer.parseOverrideWriterState({ ...state(), propertyPath: "location" }), { code: "EDITOR_WRITER_UNAVAILABLE" });
assert.throws(() => writer.parseOverrideWriterRequest({ ...request(), value: 1e9 }), { code: "TASK_VALIDATION_FAILED" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-path-normalization-unit-"));
for (const name of ["asset-path.ts", "library-source-origin.ts"]) {
const sourcePath = path.join(root, "web/protocol", name);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), transpiled.outputText.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"'));
}
const paths = await import(pathToFileURL(path.join(temporary, "asset-path.mjs")));
const origin = await import(pathToFileURL(path.join(temporary, "library-source-origin.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04B/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const policy = { schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test"] };
test("M12-04B binds the shared path normalizer and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04B");
assert.equal(manifest.nextTask, "M12-04C");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04B canonicalizes POSIX and Windows separators with dot segments", () => {
const aliases = [
"libraries/characters/main.blend",
"libraries\\characters\\.\\hero\\..\\main.blend",
"libraries//characters/models/../main.blend",
"//libraries/characters/./main.blend",
];
for (const alias of aliases) assert.equal(paths.normalizeProjectAssetPath(alias), "libraries/characters/main.blend");
assert.equal(paths.normalizeProjectAssetPath(paths.normalizeProjectAssetPath(aliases[1])), "libraries/characters/main.blend");
});
test("M12-04B decodes percent octets and NFC-normalizes Unicode names", () => {
const canonical = "libraries/角色/caf\u00e9.blend";
const aliases = [
"libraries/%E8%A7%92%E8%89%B2/caf%65%CC%81.blend",
"libraries%2F%E8%A7%92%E8%89%B2%5Ccafe%CC%81.blend",
"libraries/角色/cafe\u0301.blend",
];
for (const alias of aliases) assert.equal(paths.normalizeProjectAssetPath(alias), canonical);
assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "PROJECT_ASSET", path: aliases[1] }), {
status: "READY",
kind: "PROJECT_ASSET",
canonicalLocator: `project-assets/${canonical}`,
});
});
test("M12-04B resolves encoded dot segments once and fails closed on escape or re-decoding", () => {
assert.equal(paths.normalizeProjectAssetPath("libraries/temp/%2E%2E/main.blend"), "libraries/main.blend");
assert.throws(() => paths.normalizeProjectAssetPath("libraries/%2E%2E/%2E%2E/outside.blend"), /ASSET_PATH_OUTSIDE_PROJECT/);
assert.throws(() => paths.normalizeProjectAssetPath("libraries/%252E%252E/outside.blend"), /ASSET_PATH_OUTSIDE_PROJECT/);
assert.throws(() => paths.normalizeProjectAssetPath("libraries/%GG/outside.blend"), /ASSET_PATH_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,84 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-path-security-unit-"));
for (const name of ["asset-path.ts", "library-source-origin.ts"]) {
const sourcePath = path.join(root, "web/protocol", name);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), transpiled.outputText.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"'));
}
const paths = await import(pathToFileURL(path.join(temporary, "asset-path.mjs")));
const origin = await import(pathToFileURL(path.join(temporary, "library-source-origin.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04C/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const policy = { schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test"] };
const source = (pathValue) => ({ schemaVersion: 1, kind: "PROJECT_ASSET", path: pathValue });
const remote = (url) => ({ schemaVersion: 1, kind: "HTTPS_ORIGIN", url });
test("M12-04C binds the path security gate and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04C");
assert.equal(manifest.parentTask, "M12-04B");
assert.equal(manifest.nextTask, "M12-04D");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04C rejects absolute, UNC, drive, NUL, control, and origin-style project paths", () => {
for (const value of [
"/tmp/library.blend",
"\\\\server\\share\\library.blend",
"\\server\\library.blend",
"C:\\libraries\\main.blend",
"C:/libraries/main.blend",
"libraries/\u0000main.blend",
"libraries/\u0001main.blend",
"libraries/%00main.blend",
"//https://evil.example.test/library.blend",
]) assert.throws(() => paths.normalizeProjectAssetPath(value), /ASSET_PATH_OUTSIDE_PROJECT|ASSET_PATH_INVALID/);
for (const value of ["/tmp/library.blend", "\\\\server\\share\\library.blend", "C:/libraries/main.blend", "libraries/\u0000main.blend", "libraries/%00main.blend"]) {
assert.throws(() => origin.acceptLibrarySource(policy, source(value)), { code: "IO_EXTERNAL_URI_BLOCKED" });
}
});
test("M12-04C rejects raw and encoded unsafe HTTPS URI characters before admission", () => {
for (const value of [
"https://assets.example.test\\evil.example.test/main.blend",
"https://assets.example.test/\nmain.blend",
"https://assets.example.test/%00main.blend",
"https://assets.example.test/%01main.blend",
"https://assets.example.test/%GGmain.blend",
"https://user:pass@evil.example.test/main.blend",
"https://evil.example.test/main.blend",
]) assert.throws(() => origin.acceptLibrarySource(policy, remote(value)), { code: "IO_EXTERNAL_URI_BLOCKED" });
assert.deepEqual(origin.acceptLibrarySource(policy, remote("https://assets.example.test/library/main.blend")), {
status: "READY",
kind: "HTTPS_ORIGIN",
canonicalLocator: "https://assets.example.test/library/main.blend",
});
});
test("M12-04C fails closed on policy origin smuggling and duplicate declarations", () => {
for (const value of [
"https://assets.example.test\\evil.example.test",
"https://assets.example.test/%00",
"https://assets.example.test/%2e",
"https://assets.example.test/..",
"https://assets.example.test/library",
"https://assets.example.test/?scope=library",
"https://assets.example.test/#library",
]) assert.throws(() => origin.parseLibrarySourcePolicy({ schemaVersion: 1, declaredHttpsOrigins: [value] }), { code: "IO_EXTERNAL_URI_BLOCKED" });
assert.throws(() => origin.parseLibrarySourcePolicy({ schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test", "https://assets.example.test/"] }), { code: "ASSET_MANIFEST_INVALID" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-source-origin-unit-"));
const paths = ["asset-path.ts", "library-source-origin.ts"];
for (const name of paths) {
const sourcePath = path.join(root, "web/protocol", name);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), transpiled.outputText.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"'));
}
const origin = await import(pathToFileURL(path.join(temporary, "library-source-origin.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04A/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const policy = { schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test"] };
const digest = "a".repeat(64);
test("M12-04A binds the declared source-origin protocol and evidence artifacts", () => {
assert.equal(manifest.task, "M12-04A");
assert.equal(manifest.nextTask, "M12-04B");
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04A accepts declared HTTPS, project asset, and user-selected file sources", () => {
assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "HTTPS_ORIGIN", url: "https://assets.example.test/library/main.blend" }), { status: "READY", kind: "HTTPS_ORIGIN", canonicalLocator: "https://assets.example.test/library/main.blend" });
assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "PROJECT_ASSET", path: "libraries/main.blend" }), { status: "READY", kind: "PROJECT_ASSET", canonicalLocator: "project-assets/libraries/main.blend" });
assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "USER_SELECTED_FILE", selectionId: "file-selection:pick-1", fileName: "main.blend", byteLength: 128, sourceSha256: digest }), { status: "READY", kind: "USER_SELECTED_FILE", canonicalLocator: "user-file/file-selection:pick-1/main.blend" });
});
test("M12-04A rejects undeclared origins, credentials, unsafe paths, and missing policy declarations", () => {
assert.throws(() => origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "HTTPS_ORIGIN", url: "https://other.example.test/main.blend" }), { code: "IO_EXTERNAL_URI_BLOCKED" });
assert.throws(() => origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "HTTPS_ORIGIN", url: "https://user:pass@assets.example.test/main.blend" }), { code: "IO_EXTERNAL_URI_BLOCKED" });
assert.throws(() => origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "PROJECT_ASSET", path: "../outside.blend" }), { code: "IO_EXTERNAL_URI_BLOCKED" });
assert.throws(() => origin.parseLibrarySourcePolicy({ schemaVersion: 1, declaredHttpsOrigins: [] }), { code: "PROTOCOL_MISMATCH" });
});
test("M12-04A rejects malformed user-file identity and undeclared fields", () => {
assert.throws(() => origin.parseLibrarySourceRequest({ schemaVersion: 1, kind: "USER_SELECTED_FILE", selectionId: "bad", fileName: "main.blend", byteLength: 1, sourceSha256: digest }), { code: "ASSET_MANIFEST_INVALID" });
assert.throws(() => origin.parseLibrarySourceRequest({ schemaVersion: 1, kind: "USER_SELECTED_FILE", selectionId: "file-selection:pick-1", fileName: "../main.blend", byteLength: 1, sourceSha256: digest }), { code: "ASSET_MANIFEST_INVALID" });
assert.throws(() => origin.parseLibrarySourceRequest({ schemaVersion: 1, kind: "PROJECT_ASSET", path: "main.blend", future: true }), { code: "ASSET_MANIFEST_INVALID" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "obj-import-unit-"));
const sourcePath = path.join(root, "web/protocol/obj-import.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "obj-import.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const obj = new TextEncoder().encode(`# test\nmtllib materials.mtl\no Test\nv 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nvt 0 1\nvn 0 0 1\ng TestGroup\nusemtl TestMaterial\nf -3/-3/-1 -2/-2/-1 -1/-1/-1\n`).buffer;
const mtl = new TextEncoder().encode("newmtl TestMaterial\nmap_Kd texture.png\n").buffer;
test("M12-07C resolves negative indices and serializes deterministic OBJ", () => {
const imported = protocol.importOBJ(obj, mtl);
assert.deepEqual(imported.faces[0].vertices.map((vertex) => vertex.position), [1, 2, 3]);
assert.deepEqual(imported.faces[0].vertices.map((vertex) => vertex.texcoord), [1, 2, 3]);
assert.deepEqual(imported.materials, [{ name: "TestMaterial", mapKd: "texture.png" }]);
const serialized = protocol.serializeOBJ(imported);
assert.match(serialized.obj, /f 1\/1\/1 2\/2\/1 3\/3\/1/);
assert.equal(serialized.mtl, "# Web Blender MTL export\n# schema 1\nnewmtl TestMaterial\nmap_Kd texture.png\n");
});
test("M12-07C reports unresolved texture origin without blocking geometry", () => {
const imported = protocol.importOBJ(obj, mtl);
const missing = protocol.createOBJLossReport(imported);
assert.equal(missing.canRoundTrip, true);
assert.deepEqual(missing.warnings.map((warning) => warning.code), ["OBJ_TEXTURE_ORIGIN_UNRESOLVED"]);
const bound = protocol.createOBJLossReport(imported, ["texture.png"]);
assert.deepEqual(bound.warnings, []);
});
test("M12-07C rejects malformed face arity and out-of-range indices", () => {
const malformed = new TextEncoder().encode("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2\n").buffer;
assert.throws(() => protocol.importOBJ(malformed), /OBJ_FACE_ARITY_INVALID/);
const outOfRange = new TextEncoder().encode("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 4\n").buffer;
assert.throws(() => protocol.importOBJ(outOfRange), /OBJ_INDEX_OUT_OF_RANGE/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "ply-import-unit-"));
const sourcePath = path.join(root, "web/protocol/ply-import.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "ply-import.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const fixtureRoot = path.join(root, "tests/files/web/m12_ply_mapping_v1");
const bytes = (name) => { const value = fs.readFileSync(path.join(fixtureRoot, name)); return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); };
test("M12-07H maps ASCII and binary little-endian PLY fields", () => {
const ascii = protocol.importPLY(bytes("mapping-ascii.ply"), { format: "ascii" });
const binary = protocol.importPLY(bytes("mapping-binary-le.ply"), { format: "binary_little_endian" });
assert.equal(ascii.vertices.length, 4);
assert.equal(ascii.faces.length, 2);
assert.deepEqual(ascii.vertices[0].position, [-1, 0, 1]);
assert.deepEqual(ascii.vertices[0].normal, [0, 1, 0]);
assert.deepEqual(ascii.vertices[0].color, [254 / 255, 0, 0, 1]);
assert.deepEqual(ascii.vertices[3].customProperties, { label: 4, temperature: 40 });
assert.deepEqual(binary.vertices, ascii.vertices);
assert.deepEqual(binary.faces, ascii.faces);
assert.deepEqual(protocol.createPLYLossReport(ascii), { schemaVersion: 1, operation: "PLY_IMPORT_LOSS_REPORT", canImport: true, warningCount: 0, warnings: [] });
});
test("M12-07H reports unknown list properties without dropping mapped fields", () => {
const imported = protocol.importPLY(bytes("unknown-property-ascii.ply"), { format: "ascii" });
assert.equal(imported.vertices.length, 4);
assert.equal(imported.vertices[0].customProperties.temperature, 10);
assert.deepEqual(protocol.createPLYLossReport(imported).warnings.map((warning) => warning.code), ["PLY_UNKNOWN_PROPERTY"]);
assert.equal(protocol.createPLYLossReport(imported).warnings[0].property, "unknown_values");
});
test("M12-07H serializes mapped data and rejects an explicit format mismatch", () => {
const document = protocol.importPLY(bytes("mapping-ascii.ply"));
const output = protocol.serializePLYAscii(document);
const reopened = protocol.importPLY(output, { format: "ascii" });
assert.deepEqual(reopened.faces, document.faces);
assert.deepEqual(reopened.vertices.map((vertex) => vertex.customProperties), document.vertices.map((vertex) => vertex.customProperties));
assert.throws(() => protocol.importPLY(bytes("mapping-ascii.ply"), { format: "binary_little_endian" }), /PLY_FORMAT_MISMATCH/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "ply-negative-unit-"));
const sourcePath = path.join(root, "web/protocol/ply-import.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "ply-import.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const fixtureRoot = path.join(root, "tests/files/web/m12_ply_negative_v1");
const bytes = (name) => { const value = fs.readFileSync(path.join(fixtureRoot, name)); return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); };
test("M12-07I blocks big-endian PLY with a stable format code", () => {
assert.throws(() => protocol.importPLY(bytes("big-endian.ply")), /PLY_FORMAT_UNSUPPORTED/);
});
test("M12-07I blocks malformed lists and oversized element counts", () => {
assert.throws(() => protocol.importPLY(bytes("malformed-list-ascii.ply")), /PLY_DATA_TRUNCATED/);
assert.throws(() => protocol.importPLY(bytes("oversized-count-ascii.ply")), /PLY_IMPORT_BUDGET_EXCEEDED: vertex/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import ts from "../../node_modules/typescript/lib/typescript.js";
const root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/pointer-contract.ts");
const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(output.diagnostics, []);
const pointer = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`);
test("M14-04C preserves pointer identity and bounded pen fields", () => {
assert.deepEqual(pointer.observePointerEvent({ type: "pointerdown", pointerType: "pen", pointerId: 7, pressure: 1.4, tiltX: 120, tiltY: -100, button: 0, buttons: 1 }), { schemaVersion: 1, pointerType: "pen", pointerId: 7, pressure: 1, tiltX: 90, tiltY: -90, button: 0, buttons: 1, cancelled: false });
assert.equal(pointer.observePointerEvent({ type: "pointercancel", pointerType: "touch", pointerId: 2, pressure: 0.4, button: 0, buttons: 0 }).cancelled, true);
});
test("M14-04C rejects unknown pointer and invalid id", () => {
assert.throws(() => pointer.observePointerEvent({ pointerType: "trackpad", pointerId: 1 }), /POINTER_TYPE_UNSUPPORTED/);
assert.throws(() => pointer.observePointerEvent({ pointerType: "mouse", pointerId: -1 }), /POINTER_ID_INVALID/);
});

View File

@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-audit-integrity-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")')
.replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const digest = "a".repeat(64);
const manifest = { schemaVersion: 1, scripts: [{ id: "script:audit", name: "Audit", entryPath: "scripts/audit.py", sourceByteLength: 32, sourceSha256: digest, publisher: "local", signature: "b".repeat(128), keyId: "key:local", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }] };
test("M13-05H accepts a strictly ordered, chained audit log", async () => {
const first = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:first", requestedAt: "2026-08-19T00:00:00.000Z" });
const second = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:second", requestedAt: "2026-08-19T00:00:01.000Z" });
const log = await protocol.appendScriptExecutionAudit(await protocol.appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, first), second);
assert.deepEqual(log.entries.map((entry) => entry.sequence), [1, 2]);
assert.deepEqual(log.entries.map((entry) => entry.audit.requestId), ["audit:first", "audit:second"]);
assert.equal(log.entries[0].previousEntrySha256, null);
assert.equal(log.entries[1].previousEntrySha256, log.entries[0].entrySha256);
assert.deepEqual((await protocol.parseScriptExecutionAuditLog(log)).entries, log.entries);
});
test("M13-05H rejects replay, time, sequence and hash-chain drift", async () => {
const first = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:one", requestedAt: "2026-08-19T00:00:00.000Z" });
const second = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:two", requestedAt: "2026-08-19T00:00:01.000Z" });
const log = await protocol.appendScriptExecutionAudit(await protocol.appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, first), second);
await assert.rejects(protocol.appendScriptExecutionAudit(log, first), /SCRIPT_MANIFEST_INVALID/);
const earlier = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:earlier", requestedAt: "2026-08-18T23:59:59.000Z" });
await assert.rejects(protocol.appendScriptExecutionAudit(log, earlier), /SCRIPT_MANIFEST_INVALID/);
await assert.rejects(protocol.parseScriptExecutionAuditLog({ ...log, entries: [{ ...log.entries[0], sequence: 2 }, log.entries[1]] }), /SCRIPT_MANIFEST_INVALID/);
await assert.rejects(protocol.parseScriptExecutionAuditLog({ ...log, entries: [{ ...log.entries[0], entrySha256: "c".repeat(64) }, log.entries[1]] }), /SCRIPT_MANIFEST_INVALID/);
await assert.rejects(protocol.parseScriptExecutionAuditLog({ ...log, entries: [log.entries[0], { ...log.entries[1], previousEntrySha256: "d".repeat(64) }] }), /SCRIPT_MANIFEST_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-host-call-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const digest = "a".repeat(64);
const permissions = new Set(protocol.SCRIPT_PERMISSIONS);
const call = (name, parameters) => ({ schemaVersion: 1, requestId: `host:${name.toLowerCase()}`, scriptId: "clean", call: name, permission: name, parameters });
test("M13-03C parses all allowlisted host calls with structured parameters", () => {
const inputs = [
call("READ_MAIN", { revision: 3 }),
call("READ_ASSET", { path: "//assets/model.bin", expectedSha256: digest }),
call("WRITE_MAIN", { revision: 3, operation: "object.transform", payload: { objectId: "obj:1", x: 1 } }),
call("WRITE_ASSET", { path: "assets/out.bin", byteLength: 4, sha256: digest }),
call("SUBMIT_SERVER_JOB", { inputBlendSha256: digest, settingsSha256: digest }),
];
const parsed = inputs.map((input) => protocol.parseScriptHostCall(input, permissions));
assert.deepEqual(parsed.map((item) => item.call), ["READ_MAIN", "READ_ASSET", "WRITE_MAIN", "WRITE_ASSET", "SUBMIT_SERVER_JOB"]);
assert.equal(parsed[1].parameters.path, "assets/model.bin");
assert.equal(parsed[2].execution, "DISABLED");
});
test("M13-03C rejects non-allowlisted calls, permission confusion and unknown fields", () => {
assert.throws(() => protocol.parseScriptHostCall(call("EXECUTE", {}), permissions), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptHostCall({ ...call("READ_MAIN", { revision: 3 }), permission: "WRITE_MAIN" }, permissions), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptHostCall(call("READ_MAIN", { revision: 3, extra: true }), permissions), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptHostCall(call("READ_ASSET", { path: "../escape", expectedSha256: digest }), permissions), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptHostCall(call("WRITE_MAIN", { revision: 3, operation: "x", payload: [] }), permissions), /SCRIPT_MANIFEST_INVALID/);
});
test("M13-03C requires the declared permission set", () => {
assert.throws(() => protocol.parseScriptHostCall(call("WRITE_MAIN", { revision: 0, operation: "x", payload: {} }), new Set(["READ_MAIN"])), /SCRIPT_POLICY_DENIED/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,85 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-manifest-budgets-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
fileName: `${name}.ts`,
}).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")')
.replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const digest = "a".repeat(64);
const signature = "b".repeat(128);
const script = (id, overrides = {}) => ({
id,
name: id,
entryPath: `scripts/${id}.py`,
sourceByteLength: 128,
sourceSha256: digest,
publisher: "local",
signature,
keyId: "key:local",
permissions: ["READ_MAIN"],
dependencies: [],
module: false,
cpuMs: 1000,
memoryBytes: 1024 * 1024,
wallMs: 5000,
network: false,
autorun: false,
driverExpressions: false,
addonInstall: false,
...overrides,
});
const manifest = (scripts = [script("clean")]) => ({ schemaVersion: 1, scripts });
test("M13-02A accepts bounded manifest fields and normalizes project paths", () => {
const parsed = protocol.parseScriptingManifest(manifest([
script("base", { entryPath: "//scripts/../scripts/base.py" }),
script("clean", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "//deps/base.py" }] }),
]));
assert.equal(parsed.scripts.length, 2);
assert.equal(parsed.scripts[0].entryPath, "scripts/base.py");
assert.equal(parsed.scripts[1].dependencies[0].sourcePath, "deps/base.py");
assert.equal(parsed.scripts.reduce((total, item) => total + item.sourceByteLength, 0), 256);
assert.equal(parsed.scripts.every((item) => item.module === false), true);
});
test("M13-02A rejects text count and aggregate source byte budget overflow", () => {
assert.throws(
() => protocol.parseScriptingManifest(manifest(Array.from({ length: protocol.SCRIPTING_BUDGET.maxScripts + 1 }, (_, index) => script(`script-${index}`)))),
/SCRIPT_BUDGET_EXCEEDED/,
);
assert.throws(
() => protocol.parseScriptingManifest(manifest([script("large", { sourceByteLength: protocol.SCRIPTING_BUDGET.maxSourceBytes }), script("overflow", { sourceByteLength: 1 })])),
/SCRIPT_BUDGET_EXCEEDED/,
);
assert.throws(() => protocol.parseScriptingManifest(manifest([script("missing-bytes", { sourceByteLength: undefined })])), /SCRIPT_BUDGET_EXCEEDED/);
});
test("M13-02A rejects module execution, unsafe paths, and malformed dependencies", () => {
assert.throws(() => protocol.parseScriptingManifest(manifest([script("module", { module: true })])), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptingManifest(manifest([script("escape", { entryPath: "../escape.py" })])), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptingManifest(manifest([script("dependency-escape", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "//../escape.py" }] }), script("base")])), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptingManifest(manifest([script("duplicate-dependency", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "deps/a.py" }, { id: "base", sourceSha256: digest, sourcePath: "deps/b.py" }] }), script("base")])), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptingManifest(manifest([script("too-many-dependencies", { dependencies: Array.from({ length: protocol.SCRIPTING_BUDGET.maxDependencies + 1 }, (_, index) => ({ id: `dep-${index}`, sourceSha256: digest, sourcePath: `deps/${index}.py` })) })])), /SCRIPT_BUDGET_EXCEEDED/);
});
test("M13-02A rejects unknown or over-budget permissions", () => {
assert.throws(() => protocol.parseScriptingManifest(manifest([script("unknown-permission", { permissions: ["EXECUTE"] })])), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptingManifest(manifest([script("permission-budget", { permissions: new Array(protocol.SCRIPTING_BUDGET.maxPermissions + 1).fill("READ_MAIN") })])), /SCRIPT_POLICY_DENIED/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-manifest-canonical-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
fileName: `${name}.ts`,
}).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")')
.replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const digest = "a".repeat(64);
const signature = "b".repeat(128);
const script = (id, overrides = {}) => ({
id,
name: id,
entryPath: `scripts/${id}.py`,
sourceByteLength: 128,
sourceSha256: digest,
publisher: "local",
signature,
keyId: "key:local",
permissions: ["READ_MAIN"],
dependencies: [],
module: false,
cpuMs: 1000,
memoryBytes: 1024 * 1024,
wallMs: 5000,
network: false,
autorun: false,
driverExpressions: false,
addonInstall: false,
...overrides,
});
test("M13-02B canonical serialization is invariant to manifest/script array order", () => {
const base = {
schemaVersion: 1,
scripts: [
script("zeta", { permissions: ["WRITE_ASSET", "READ_MAIN"], dependencies: [{ id: "alpha", sourceSha256: digest, sourcePath: "deps/alpha.py" }, { id: "beta", sourceSha256: digest, sourcePath: "deps/beta.py" }] }),
script("alpha", { permissions: ["SUBMIT_SERVER_JOB", "READ_ASSET"] }),
script("beta"),
],
};
const reordered = {
schemaVersion: 1,
scripts: [
{ ...base.scripts[1], permissions: [...base.scripts[1].permissions].reverse(), ignored: "removed" },
{ ...base.scripts[0], permissions: [...base.scripts[0].permissions].reverse(), dependencies: [...base.scripts[0].dependencies].reverse() },
base.scripts[2],
],
};
const first = protocol.serializeScriptingManifest(base);
const second = protocol.serializeScriptingManifest(reordered);
assert.equal(first, second);
assert.match(first, /^\{"schemaVersion":1,"scripts":\[/);
assert.equal(Object.keys(protocol.canonicalizeScriptingManifest(reordered).scripts[0]).includes("ignored"), false);
});
test("M13-02B canonical serialization includes security-relevant declaration fields", () => {
const base = { schemaVersion: 1, scripts: [script("clean")] };
const changedBytes = { schemaVersion: 1, scripts: [script("clean", { sourceByteLength: 129 })] };
const changedPermission = { schemaVersion: 1, scripts: [script("clean", { permissions: ["READ_ASSET"] })] };
assert.notEqual(protocol.serializeScriptingManifest(base), protocol.serializeScriptingManifest(changedBytes));
assert.notEqual(protocol.serializeScriptingManifest(base), protocol.serializeScriptingManifest(changedPermission));
assert.throws(() => protocol.serializeScriptingManifest({ ...base, schemaVersion: 2 }), /PROTOCOL_MISMATCH/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,36 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-permission-policy-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")')
.replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const script = (permissions = ["READ_MAIN"]) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature: "b".repeat(128), keyId: "key:new", permissions, dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false });
const manifest = (permissions) => ({ schemaVersion: 1, scripts: [script(permissions)] });
test("M13-02E grants no undeclared permission by default", () => {
assert.deepEqual(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean"), { status: "ALLOWED", code: "SCRIPT_PERMISSIONS_ALLOWED", scriptId: "clean", declared: ["READ_MAIN"], requested: [], granted: [] });
assert.deepEqual(protocol.resolveScriptPermissions(manifest(["WRITE_ASSET", "READ_MAIN"]), "clean", ["READ_MAIN"]), { status: "ALLOWED", code: "SCRIPT_PERMISSIONS_ALLOWED", scriptId: "clean", declared: ["READ_MAIN", "WRITE_ASSET"], requested: ["READ_MAIN"], granted: ["READ_MAIN"] });
});
test("M13-02E blocks escalation, unknown and duplicate permission requests", () => {
assert.equal(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["WRITE_MAIN"]).code, "SCRIPT_POLICY_DENIED");
assert.equal(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["EXECUTE"]).code, "SCRIPT_POLICY_DENIED");
assert.equal(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["READ_MAIN", "READ_MAIN"]).code, "SCRIPT_POLICY_DENIED");
assert.throws(() => protocol.parseScriptingManifest(manifest(["EXECUTE"])), /SCRIPT_POLICY_DENIED/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-policy-codes-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText.replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const base = { schemaVersion: 1, scripts: [{ id: "demo", name: "Demo", entryPath: "scripts/demo.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "local", signature: "b".repeat(128), keyId: "key", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 64 * 1024 * 1024, wallMs: 2000, network: false, autorun: false, driverExpressions: false, addonInstall: false }] };
test("M13-01C returns stable default-deny codes for autorun, driver and add-on execution", () => {
assert.throws(() => protocol.parseScriptingManifest({ ...base, scripts: [{ ...base.scripts[0], autorun: true }] }), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptingManifest({ ...base, scripts: [{ ...base.scripts[0], driverExpressions: true }] }), /DRIVER_EXECUTION_BLOCKED/);
assert.throws(() => protocol.parseScriptingManifest({ ...base, scripts: [{ ...base.scripts[0], addonInstall: true }] }), /ADDON_INSTALL_BLOCKED/);
const gate = protocol.gateScriptExecution(base, "demo", new Set(["key"]));
assert.equal(gate.status, "BLOCKED");
assert.equal(gate.issues[0].code, "SCRIPT_SANDBOX_UNAVAILABLE");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-budget-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const budget = { schemaVersion: 1, cpuMs: 1000, wallMs: 5000, memoryBytes: 1024 * 1024, maxMessageBytes: 4096, maxOutputBytes: 8192 };
test("M13-03B accepts bounded CPU, wall, memory, message and output budgets", () => {
assert.deepEqual(protocol.parseScriptSandboxBudget(budget), budget);
});
test("M13-03B rejects every budget overflow and schema drift", () => {
for (const [field, limit] of Object.entries({ cpuMs: protocol.SCRIPT_SANDBOX_BUDGET.maxCpuMs, wallMs: protocol.SCRIPT_SANDBOX_BUDGET.maxWallMs, memoryBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxMemoryBytes, maxMessageBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxMessageBytes, maxOutputBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxOutputBytes })) assert.throws(() => protocol.parseScriptSandboxBudget({ ...budget, [field]: limit + 1 }), /SCRIPT_BUDGET_EXCEEDED/);
assert.throws(() => protocol.parseScriptSandboxBudget({ ...budget, schemaVersion: 2 }), /PROTOCOL_MISMATCH/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-cancellation-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const running = { schemaVersion: 1, jobId: "sandbox:cancel", workerGeneration: 5, baseRevision: 11, mainRevisionBefore: 11, status: "RUNNING" };
test("M13-03E cancellation receipt blocks late message and cache publication", () => {
const cancelled = protocol.terminateScriptSandboxJob(running, "CANCEL");
assert.deepEqual({ status: cancelled.status, errorCode: cancelled.errorCode, mainRevisionAfter: cancelled.mainRevisionAfter, temporaryBytes: cancelled.temporaryBytes, publishedResults: cancelled.publishedResults, lateResults: cancelled.lateResults, committed: cancelled.committed }, { status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", mainRevisionAfter: 11, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false });
assert.throws(() => protocol.rejectLateScriptSandboxResult(cancelled), /SCRIPT_SANDBOX_LATE_RESULT/);
});
test("M13-03E rejects a late result that is not tied to a terminated receipt", () => {
assert.throws(() => protocol.rejectLateScriptSandboxResult({ ...running, status: "RUNNING" }), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, baseRevision: 10 }, "CANCEL"), /SCRIPT_MANIFEST_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-dispose-"));
const source = fs.readFileSync(path.join(root, "web/app/src/testing/script-sandbox-dispose.ts"), "utf8");
fs.writeFileSync(path.join(temporary, "script-sandbox-dispose.cjs"), ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: "script-sandbox-dispose.ts" }).outputText);
const protocol = createRequire(import.meta.url)(path.join(temporary, "script-sandbox-dispose.cjs"));
test("M13-03F emits zero-resource disposal receipts", () => {
assert.deepEqual(protocol.createScriptSandboxDisposeReceipt(1), { schemaVersion: 1, disposeCount: 1, idempotent: false, resources: { messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }, lateTimerMessages: 0 });
assert.deepEqual(protocol.createScriptSandboxDisposeReceipt(2), { schemaVersion: 1, disposeCount: 2, idempotent: true, resources: { messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }, lateTimerMessages: 0 });
});
test("M13-03F rejects an invalid disposal count", () => {
assert.throws(() => protocol.createScriptSandboxDisposeReceipt(0), /SCRIPT_SANDBOX_DISPOSE_INVALID/);
assert.throws(() => protocol.createScriptSandboxDisposeReceipt(1.5), /SCRIPT_SANDBOX_DISPOSE_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-isolation-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const running = { schemaVersion: 1, jobId: "sandbox:1", workerGeneration: 4, baseRevision: 9, mainRevisionBefore: 9, status: "RUNNING" };
test("M13-03D crash and timeout terminate jobs without changing Main revision", () => {
const crashed = protocol.terminateScriptSandboxJob(running, "CRASH");
const timedOut = protocol.terminateScriptSandboxJob(running, "TIMEOUT");
assert.deepEqual({ status: crashed.status, errorCode: crashed.errorCode, mainRevisionAfter: crashed.mainRevisionAfter, temporaryBytes: crashed.temporaryBytes, publishedResults: crashed.publishedResults, committed: crashed.committed }, { status: "CRASHED", errorCode: "SCRIPT_SANDBOX_CRASHED", mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false });
assert.deepEqual({ status: timedOut.status, errorCode: timedOut.errorCode, mainRevisionAfter: timedOut.mainRevisionAfter, temporaryBytes: timedOut.temporaryBytes, publishedResults: timedOut.publishedResults, committed: timedOut.committed }, { status: "TIMED_OUT", errorCode: "SCRIPT_SANDBOX_TIMEOUT", mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false });
});
test("M13-03D cancellation and late results are fail-closed", () => {
const cancelled = protocol.terminateScriptSandboxJob(running, "CANCEL");
assert.equal(cancelled.errorCode, "SCRIPT_SANDBOX_CANCELLED");
assert.throws(() => protocol.rejectLateScriptSandboxResult(cancelled), /SCRIPT_SANDBOX_LATE_RESULT/);
assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, baseRevision: 8 }, "CRASH"), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, status: "CRASHED" }, "TIMEOUT"), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.rejectLateScriptSandboxResult(running), /SCRIPT_MANIFEST_INVALID/);
});
test("M13-03D rejects malformed termination receipts", () => {
assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, workerGeneration: 0 }, "CRASH"), /SCRIPT_BUDGET_EXCEEDED/);
assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, mainRevisionBefore: 10 }, "TIMEOUT"), /SCRIPT_MANIFEST_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-recovery-"));
const source = fs.readFileSync(path.join(root, "web/app/src/testing/script-sandbox-recovery.ts"), "utf8");
fs.writeFileSync(path.join(temporary, "script-sandbox-recovery.cjs"), ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: "script-sandbox-recovery.ts" }).outputText);
const protocol = createRequire(import.meta.url)(path.join(temporary, "script-sandbox-recovery.cjs"));
const digest = "a".repeat(64);
const entry = (sequence, requestId, previousEntrySha256, entrySha256) => ({ sequence, requestId, previousEntrySha256, entrySha256, sourceSha256: digest, manifestSha256: "b".repeat(64) });
test("M13-03G accepts one-generation recovery with a continuous audit chain", () => {
const receipt = protocol.createScriptSandboxRecoveryReceipt({ previousGeneration: 4, nextGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, sourceSha256: digest, manifestSha256: "b".repeat(64), audit: { entries: 2, first: entry(1, "sandbox-recovery:g4", null, digest), second: entry(2, "sandbox-recovery:g5", digest, "c".repeat(64)) } });
assert.equal(receipt.recovered, true);
assert.equal(receipt.execution, "DISABLED");
assert.equal(receipt.audit.second.previousEntrySha256, receipt.audit.first.entrySha256);
});
test("M13-03G rejects generation, revision, request and hash-chain drift", () => {
const base = { previousGeneration: 4, nextGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, sourceSha256: digest, manifestSha256: "b".repeat(64), audit: { entries: 2, first: entry(1, "sandbox-recovery:g4", null, digest), second: entry(2, "sandbox-recovery:g5", digest, "c".repeat(64)) } };
assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, nextGeneration: 7 }), /generation/);
assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, mainRevisionAfter: 12 }), /revision/);
assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, audit: { ...base.audit, second: entry(2, "sandbox-recovery:g5", "d".repeat(64), "c".repeat(64)) } }), /hash chain/);
assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, audit: { ...base.audit, second: entry(2, "sandbox-recovery:g4", digest, "c".repeat(64)) } }), /replayed/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-scope-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")')
.replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const deniedScope = { schemaVersion: 1, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false };
test("M13-03A accepts only the all-deny sandbox scope", () => {
assert.deepEqual(protocol.parseScriptSandboxScope(deniedScope), deniedScope);
for (const capability of ["dom", "hostWorker", "opfs", "indexedDB", "network"]) {
assert.throws(() => protocol.parseScriptSandboxScope({ ...deniedScope, [capability]: true }), /SCRIPT_POLICY_DENIED/);
}
});
test("M13-03A rejects unknown scope versions and missing declarations", () => {
assert.throws(() => protocol.parseScriptSandboxScope({ ...deniedScope, schemaVersion: 2 }), /PROTOCOL_MISMATCH/);
assert.throws(() => protocol.parseScriptSandboxScope({ schemaVersion: 1 }), /SCRIPT_POLICY_DENIED/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,44 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-signature-negative-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")')
.replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const publicKey = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8";
const signature = "fc396c6c68e6f6eb38a18c147becfaec1621a167f6db0a0d76874209accf3cb80dfa1fac1528ebc1bc6b090801a3ad397cae18e6ddb41740766678711c0a8804";
const script = (id = "clean", overrides = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides });
const manifest = (scripts = [script()]) => ({ schemaVersion: 1, scripts });
const key = (overrides = {}) => ({ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides });
const policy = (overrides = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [key()], ...overrides });
test("M13-02F blocks missing, expired, not-yet-valid and publisher-confused signers", async () => {
const at = "2026-08-18T12:00:00.000Z";
assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [] }), at)).code, "SCRIPT_POLICY_DENIED");
assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ expiresAt: "2026-06-01T00:00:00.000Z" }), at)).code, "SCRIPT_POLICY_DENIED");
assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notBefore: "2026-09-01T00:00:00.000Z" })] }), at)).code, "SCRIPT_POLICY_DENIED");
assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notAfter: "2026-06-01T00:00:00.000Z" })] }), at)).code, "SCRIPT_POLICY_DENIED");
assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ publisher: "Other" })] }), at)).code, "SCRIPT_POLICY_DENIED");
});
test("M13-02F binds a signature to one script and does not accept reordered/swapped content", async () => {
const at = "2026-08-18T12:00:00.000Z";
assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy(), at)).status, "VERIFIED");
const swapped = manifest([script("other")]);
assert.equal((await protocol.verifyScriptManifestSignature(swapped, "other", policy(), at)).code, "SCRIPT_SIGNATURE_INVALID");
assert.equal((await protocol.verifyScriptManifestSignature(manifest([script("other"), script()]), "clean", policy(), at)).status, "VERIFIED");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { createPrivateKey, createPublicKey, sign } from "node:crypto";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-trust-policy-"));
const require = createRequire(import.meta.url);
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText
.replace('require("./asset-path")', 'require("./asset-path.cjs")')
.replace('require("./capability-gates")', 'require("./capability-gates.cjs")');
fs.writeFileSync(path.join(temporary, `${name}.cjs`), output);
}
const protocol = require(path.join(temporary, "scripting-platform.cjs"));
const digest = "a".repeat(64);
const signature = "b".repeat(128);
const privateKey = createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "hex")]), format: "der", type: "pkcs8" });
const publicKey = createPublicKey(privateKey).export({ format: "der", type: "spki" }).subarray(-32).toString("hex");
const script = (id = "clean", overrides = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: digest, publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides });
const key = (keyId, overrides = {}) => ({ keyId, publisher: "Team", algorithm: "ED25519", publicKey: "c".repeat(64), status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides });
const policy = (keys = [key("key:new")], overrides = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys, ...overrides });
test("M13-02C parses signer identity, active rotation and timestamp windows", () => {
const parsed = protocol.parseScriptTrustPolicy(policy([key("key:new", { replaces: "key:old" }), key("key:old", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]));
assert.equal(parsed.keys.length, 2);
assert.equal(protocol.canonicalizeScriptTrustPolicy(parsed).keys[0].keyId, "key:new");
assert.match(protocol.serializeScriptTrustPolicy(parsed), /"algorithm":"ED25519"/);
assert.deepEqual(protocol.resolveScriptSigner({ schemaVersion: 1, scripts: [script()] }, "clean", parsed, "2026-08-18T12:00:00.000Z"), { status: "ELIGIBLE", keyId: "key:new", publisher: "Team", trust: "ACTIVE", cryptographicVerification: "REQUIRED" });
});
test("M13-02C rejects invalid rotation, revocation and timestamp policy declarations", () => {
assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { replaces: "missing" })])), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { replaces: "key:old" }), key("key:old", { publisher: "Other" })])), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:a", { replaces: "key:b" }), key("key:b", { replaces: "key:a" })])), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { status: "REVOKED" })])), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { revokedAt: "2026-06-01T00:00:00.000Z" })])), /SCRIPT_POLICY_DENIED/);
assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { notAfter: "2025-01-01T00:00:00.000Z" })])), /SCRIPT_MANIFEST_INVALID/);
assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { publicKey: "not-a-key" })])), /SCRIPT_SIGNATURE_INVALID/);
});
test("M13-02C resolves revoked, publisher-mismatched and expired signers fail-closed", () => {
const baseManifest = { schemaVersion: 1, scripts: [script()] };
const revoked = policy([key("key:new", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]);
assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", revoked, "2026-08-18T12:00:00.000Z").trust, "REVOKED");
const mismatch = policy([key("key:new", { publisher: "Other" })]);
assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", mismatch, "2026-08-18T12:00:00.000Z").trust, "PUBLISHER_MISMATCH");
const expired = policy([key("key:new", { notAfter: "2026-06-01T00:00:00.000Z" })]);
assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", expired, "2026-08-18T12:00:00.000Z").trust, "KEY_EXPIRED");
const policyExpired = policy([key("key:new")], { expiresAt: "2026-06-01T00:00:00.000Z" });
assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", policyExpired, "2026-08-18T12:00:00.000Z").trust, "POLICY_EXPIRED");
});
test("M13-02D verifies only the canonical declared content and source hash", async () => {
const baseScript = script("clean", { signature: "0".repeat(128) });
const unsigned = { schemaVersion: 1, scripts: [baseScript] };
const signedScript = { ...baseScript, signature: sign(null, Buffer.from(protocol.serializeScriptSignatureInput(unsigned, "clean")), privateKey).toString("hex") };
const signed = { schemaVersion: 1, scripts: [signedScript] };
const trust = policy([key("key:new", { publicKey })]);
const verified = await protocol.verifyScriptManifestSignature(signed, "clean", trust, "2026-08-18T12:00:00.000Z");
assert.equal(verified.status, "VERIFIED");
assert.equal(verified.code, "SCRIPT_SIGNATURE_VERIFIED");
const changedSource = { schemaVersion: 1, scripts: [{ ...signedScript, sourceSha256: "d".repeat(64) }] };
assert.deepEqual((await protocol.verifyScriptManifestSignature(changedSource, "clean", trust, "2026-08-18T12:00:00.000Z")).code, "SCRIPT_SIGNATURE_INVALID");
const changedSignature = { schemaVersion: 1, scripts: [{ ...signedScript, signature: `${signedScript.signature.slice(0, -1)}${signedScript.signature.endsWith("0") ? "1" : "0"}` }] };
assert.deepEqual((await protocol.verifyScriptManifestSignature(changedSignature, "clean", trust, "2026-08-18T12:00:00.000Z")).code, "SCRIPT_SIGNATURE_INVALID");
const revoked = policy([key("key:new", { publicKey, status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]);
assert.deepEqual((await protocol.verifyScriptManifestSignature(signed, "clean", revoked, "2026-08-18T12:00:00.000Z")).code, "SCRIPT_POLICY_DENIED");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,24 @@
import assert from "node:assert/strict";
import test from "node:test";
import { classifyServerJobFault, createServerJobFaultReceipt, SERVER_JOB_FAULT_CODES } from "../../../tools/web/server-job-fault.mjs";
test("M13-04H classifies faults with stable precedence", () => {
assert.equal(classifyServerJobFault({ timedOut: true, oom: true, signal: "SIGKILL" }).code, SERVER_JOB_FAULT_CODES.TIMEOUT);
assert.equal(classifyServerJobFault({ oom: true, signal: "SIGKILL" }).code, SERVER_JOB_FAULT_CODES.OOM);
assert.equal(classifyServerJobFault({ signal: "SIGTERM" }).code, SERVER_JOB_FAULT_CODES.SIGNAL);
assert.equal(classifyServerJobFault({ code: 7 }).code, SERVER_JOB_FAULT_CODES.EXIT_FAILED);
assert.equal(classifyServerJobFault({ cancelRequested: true, timedOut: true }).code, SERVER_JOB_FAULT_CODES.CANCELLED);
});
test("M13-04H preserves the old revision and blocks failure publication", () => {
const receipt = createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, timedOut: true });
assert.deepEqual(receipt, { schemaVersion: 1, state: "FAILED", code: "SERVER_JOB_TIMEOUT", stage: "PROCESS", exitCode: null, signal: null, timedOut: true, memoryExceeded: false, baseRevision: 11, currentRevision: 11, committedRevision: 11, publish: false, revisionPreserved: true, execution: "DISABLED" });
assert.throws(() => createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 12, signal: "SIGTERM" }), /SERVER_JOB_REVISION_CONFLICT/);
assert.equal(createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, code: 0 }).publish, true);
});
test("M13-04H derives OOM from bounded memory usage", () => {
const receipt = createServerJobFaultReceipt({ baseRevision: 3, currentRevision: 3, memoryBytes: 513, memoryLimitBytes: 512 });
assert.equal(receipt.code, SERVER_JOB_FAULT_CODES.OOM);
assert.equal(receipt.memoryExceeded, true);
});

View File

@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { submitIdempotentServerJobResult } from "../../../tools/web/server-job-idempotency.mjs";
const h = (value) => crypto.createHash("sha256").update(value).digest("hex");
const identity = { requestId: "retry-1", projectId: "project-1", baseRevision: 4, sourceSha256: h("source"), settingsSha256: h("settings"), buildSha256: h("build") };
test("M13-04J reuses the exact verified result for the same request", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04j-reuse-"));
const options = { receiptDirectory: path.join(root, "receipts"), outputDirectory: path.join(root, "outputs") };
try {
const first = await submitIdempotentServerJobResult(identity, new Uint8Array([1, 2]), options);
const second = await submitIdempotentServerJobResult(identity, new Uint8Array([1, 2]), options);
assert.equal(first.reused, false);
assert.equal(second.reused, true);
assert.equal(first.outputSha256, second.outputSha256);
assert.equal((await fs.readdir(options.receiptDirectory)).length, 1);
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
test("M13-04J rejects conflicting output/identity and isolates different requests", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04j-conflict-"));
const options = { receiptDirectory: path.join(root, "receipts"), outputDirectory: path.join(root, "outputs") };
try {
await submitIdempotentServerJobResult(identity, new Uint8Array([3]), options);
await assert.rejects(submitIdempotentServerJobResult(identity, new Uint8Array([4]), options), /SERVER_JOB_IDEMPOTENCY_CONFLICT/);
await assert.rejects(submitIdempotentServerJobResult({ ...identity, settingsSha256: h("changed") }, new Uint8Array([3]), options), /SERVER_JOB_IDEMPOTENCY_CONFLICT/);
const other = await submitIdempotentServerJobResult({ ...identity, requestId: "retry-2" }, new Uint8Array([4]), options);
assert.equal(other.reused, false);
assert.equal((await fs.readdir(options.receiptDirectory)).length, 2);
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
test("M13-04J serializes concurrent duplicate requests", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04j-concurrent-"));
const options = { receiptDirectory: path.join(root, "receipts"), outputDirectory: path.join(root, "outputs") };
try {
const results = await Promise.all([
submitIdempotentServerJobResult({ ...identity, requestId: "retry-3" }, new Uint8Array([8]), options),
submitIdempotentServerJobResult({ ...identity, requestId: "retry-3" }, new Uint8Array([8]), options),
]);
assert.deepEqual(results.map((result) => result.reused).sort(), [false, true]);
} finally { await fs.rm(root, { recursive: true, force: true }); }
});

View File

@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { cleanupServerJobDirectory, createServerJobDirectory, prepareServerJobWorkspace } from "../../../tools/web/server-job-isolation.mjs";
test("M13-04A creates unpredictable one-shot directories and cleans them once", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04a-job-root-"));
try {
const first = await createServerJobDirectory(root, "server:job-one");
const second = await createServerJobDirectory(root, "server:job-two");
assert.notEqual(first.directoryName, second.directoryName);
assert.notEqual(first.directoryName, first.jobId);
assert.match(first.directoryName, /^\.blender-job-[0-9a-f-]+-[A-Za-z0-9]+$/);
assert.equal((await fs.stat(first.path)).mode & 0o777, 0o700);
assert.equal((await fs.stat(second.path)).mode & 0o777, 0o700);
const cleanedFirst = await cleanupServerJobDirectory(first);
const cleanedSecond = await cleanupServerJobDirectory(second);
assert.equal(cleanedFirst.state, "CLEANED");
assert.equal(cleanedFirst.cleanupCount, 1);
assert.equal(cleanedSecond.cleanupCount, 1);
await assert.rejects(fs.stat(first.path), { code: "ENOENT" });
await assert.rejects(fs.stat(second.path), { code: "ENOENT" });
assert.equal((await cleanupServerJobDirectory(cleanedFirst)).cleanupCount, 1);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("M13-04A rejects unsafe roots, IDs and cleanup escapes", async () => {
await assert.rejects(createServerJobDirectory("relative-root", "server:job"), /SERVER_JOB_DIRECTORY_INVALID/);
const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04a-invalid-root-"));
try {
await assert.rejects(createServerJobDirectory(root, "../escape"), /SERVER_JOB_DIRECTORY_INVALID/);
await assert.rejects(cleanupServerJobDirectory({ schemaVersion: 1, root, path: path.join(root, "other"), state: "ALLOCATED" }), /SERVER_JOB_DIRECTORY_INVALID/);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("M13-04B isolates read-only source from writable output", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04b-workspace-"));
try {
const job = await createServerJobDirectory(root, "server:job-mount");
const workspace = await prepareServerJobWorkspace(job, new Uint8Array([1, 2, 3]));
assert.notEqual(path.dirname(workspace.sourcePath), workspace.outputDirectory);
assert.equal((await fs.stat(workspace.sourceDirectory)).mode & 0o777, 0o555);
assert.equal((await fs.stat(workspace.sourcePath)).mode & 0o777, 0o444);
assert.equal((await fs.stat(workspace.outputDirectory)).mode & 0o777, 0o700);
await assert.rejects(fs.writeFile(workspace.sourcePath, new Uint8Array([9])), { code: "EACCES" });
await fs.writeFile(path.join(workspace.outputDirectory, "result.bin"), new Uint8Array([4, 5]));
assert.deepEqual([...await fs.readFile(path.join(workspace.outputDirectory, "result.bin"))], [4, 5]);
await cleanupServerJobDirectory(job);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,17 @@
import assert from "node:assert/strict";
import test from "node:test";
import { parseServerJobNetworkPolicy, resolveServerJobNetwork } from "../../../tools/web/server-job-network-policy.mjs";
test("M13-04D defaults to deny and admits only declared safe origins", () => {
const policy = parseServerJobNetworkPolicy({ schemaVersion: 1, allowedOrigins: ["https://example.com", "http://127.0.0.1:8787", "https://example.com"] });
assert.deepEqual(policy, { schemaVersion: 1, defaultNetwork: "DENY", allowedOrigins: ["http://127.0.0.1:8787", "https://example.com"] });
assert.deepEqual(resolveServerJobNetwork(policy), { status: "DENIED", code: "SERVER_NETWORK_DENIED", origin: null, network: "DISABLED" });
assert.equal(resolveServerJobNetwork(policy, "https://example.com").status, "ALLOWED");
assert.equal(resolveServerJobNetwork(policy, "https://other.example").code, "SERVER_NETWORK_DENIED");
});
test("M13-04D rejects unsafe or non-canonical origins", () => {
for (const origin of ["http://example.com", "file:///tmp/x", "https://example.com/path", "https://user:pass@example.com"]) {
assert.throws(() => parseServerJobNetworkPolicy({ schemaVersion: 1, allowedOrigins: [origin] }), /SERVER_NETWORK_/);
}
});

View File

@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createServerJobOutputReceipt, redactServerJobOutput, SERVER_JOB_OUTPUT_LIMITS } from "../../../tools/web/server-job-output.mjs";
test("M13-04F redacts credentials and filesystem paths before publishing output", () => {
const receipt = createServerJobOutputReceipt({
stdout: 'authorization: Bearer abc123 token="secret-value" source=/home/user/private.blend',
stderr: "failed at C:\\Users\\alice\\job\\source.blend file:///tmp/internal.log",
});
assert.equal(receipt.execution, "DISABLED");
assert.ok(receipt.totalRedactions >= 5);
assert.doesNotMatch(receipt.stdout.text, /abc123|secret-value|\/home\/user|C:\\Users|file:\/\//u);
assert.doesNotMatch(receipt.stderr.text, /alice|internal\.log/u);
assert.match(receipt.stdout.text, /<redacted>/u);
assert.match(receipt.stderr.text, /<internal-path>/u);
});
test("M13-04F truncates at UTF-8 byte boundaries and reports the source size", () => {
const value = "模型".repeat(100);
const receipt = createServerJobOutputReceipt({ stdout: value, stderr: "" }, { ...SERVER_JOB_OUTPUT_LIMITS, stdoutBytes: 32, stderrBytes: 32, totalBytes: 64 });
assert.equal(receipt.stdout.truncated, true);
assert.ok(receipt.stdout.emittedBytes <= 32);
assert.equal(Buffer.from(receipt.stdout.text, "utf8").toString("utf8"), receipt.stdout.text);
assert.equal(receipt.stdout.originalBytes, Buffer.byteLength(value, "utf8"));
assert.match(receipt.stdout.text, /<output-truncated>$/u);
});
test("M13-04F rejects invalid stream budgets and leaves empty streams explicit", () => {
assert.deepEqual(createServerJobOutputReceipt({ stdout: "", stderr: "" }).stdout, {
text: "", originalBytes: 0, emittedBytes: 0, redactionCount: 0, truncated: false,
});
assert.throws(() => createServerJobOutputReceipt({ stdout: "x", stderr: "" }, { stdoutBytes: 10, stderrBytes: 10, totalBytes: 10 }), /SERVER_JOB_OUTPUT_INVALID/);
assert.throws(() => redactServerJobOutput(42, 10), /SERVER_JOB_OUTPUT_INVALID/);
});

View File

@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { cleanupServerJobDirectory, createServerJobDirectory } from "../../../tools/web/server-job-isolation.mjs";
import { cancelServerJobProcess, startServerJobProcess } from "../../../tools/web/server-job-process.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("M13-04G cancels the real process group and cleans the job directory", async () => {
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04g-process-"));
const job = await createServerJobDirectory(temporary, "server:cancel");
const childScript = "const {spawn}=require('node:child_process'); const c=spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{stdio:'ignore'}); setInterval(()=>{},1000);";
const handle = startServerJobProcess(process.execPath, ["-e", childScript], { cwd: root });
let cleanupCount = 0;
try {
const receipt = await cancelServerJobProcess(handle, async () => { cleanupCount += 1; await cleanupServerJobDirectory(job); });
assert.equal(receipt.state, "CANCELLED");
assert.equal(receipt.cleanupCount, 1);
assert.equal(cleanupCount, 1);
assert.match(receipt.treeSignal, /GROUP|ALREADY_EXITED/);
await assert.rejects(fs.stat(job.path), { code: "ENOENT" });
await assert.doesNotReject(handle.completion);
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
});
test("M13-04G repeated cancellation is idempotent", async () => {
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04g-idempotent-"));
const job = await createServerJobDirectory(temporary, "server:repeat");
const handle = startServerJobProcess(process.execPath, ["-e", "setInterval(()=>{},1000)"], { cwd: root });
let cleanupCount = 0;
const cleanup = async () => { cleanupCount += 1; await cleanupServerJobDirectory(job); };
try {
const first = await cancelServerJobProcess(handle, cleanup);
const second = await cancelServerJobProcess(handle, cleanup);
assert.equal(first.state, "CANCELLED");
assert.equal(second.state, "CANCELLED");
assert.equal(first.cleanupCount, 1);
assert.equal(second.cleanupCount, 1);
assert.equal(cleanupCount, 1);
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,18 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createServerJobResourceReceipt, parseServerJobResourceBudget, SERVER_JOB_RESOURCE_LIMITS } from "../../../tools/web/server-job-resource-budget.mjs";
const budget = { schemaVersion: 1, ...SERVER_JOB_RESOURCE_LIMITS };
const usage = { cpuMs: 10, memoryBytes: 1024, processCount: 1, fileCount: 2, wallMs: 20, outputBytes: 512 };
test("M13-04C accepts bounded usage and marks enforcement", () => {
assert.deepEqual(parseServerJobResourceBudget(budget), budget);
assert.deepEqual(createServerJobResourceReceipt(budget, usage), { schemaVersion: 1, status: "SUCCEEDED", budget, usage, enforced: true, exceeded: [] });
});
test("M13-04C rejects each resource overage with a stable code", () => {
for (const field of Object.keys(SERVER_JOB_RESOURCE_LIMITS)) {
assert.throws(() => createServerJobResourceReceipt(budget, { ...usage, [field]: SERVER_JOB_RESOURCE_LIMITS[field] + 1 }), new RegExp(`SERVER_JOB_BUDGET_EXCEEDED.*${field}`));
}
assert.throws(() => parseServerJobResourceBudget({ ...budget, unknown: 1 }), /SERVER_JOB_BUDGET_INVALID/);
});

View File

@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { commitServerJobResult, verifyServerJobResultReceipt } from "../../../tools/web/server-job-result-binding.mjs";
const h = (value) => crypto.createHash("sha256").update(value).digest("hex");
const identity = { requestId: "request-1", projectId: "project-1", baseRevision: 7, sourceSha256: h("source"), settingsSha256: h("settings"), buildSha256: h("build") };
test("M13-04I commits only a readback-verified result and binds four identity hashes", async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04i-binding-"));
try {
const receipt = await commitServerJobResult(identity, new Uint8Array([1, 2, 3]), { outputDirectory: directory });
assert.equal(receipt.status, "COMMITTED");
assert.equal(receipt.publish, true);
assert.equal((await verifyServerJobResultReceipt(receipt, identity, directory)).verified, true);
assert.equal(receipt.outputByteLength, 3);
} finally { await fs.rm(directory, { recursive: true, force: true }); }
});
test("M13-04I blocks tamper, stale identity and partial/quota failures", async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04i-negative-"));
try {
await assert.rejects(commitServerJobResult({ ...identity, settingsSha256: h("changed") }, new Uint8Array([1]), { outputDirectory: directory, expectedIdentity: identity, faultAt: "AFTER_STAGE" }), /SERVER_JOB_RESULT_IDENTITY_MISMATCH/);
assert.deepEqual(await fs.readdir(directory), []);
const receipt = await commitServerJobResult(identity, new Uint8Array([4, 5]), { outputDirectory: directory });
await fs.writeFile(receipt.outputPath, new Uint8Array([9]));
await assert.rejects(verifyServerJobResultReceipt(receipt, identity, directory), /SERVER_JOB_RESULT_HASH_MISMATCH/);
await assert.rejects(commitServerJobResult({ ...identity, requestId: "request-2" }, new Uint8Array([8]), { outputDirectory: directory, faultAt: "QUOTA" }), /SERVER_JOB_RESULT_STORAGE_QUOTA/);
} finally { await fs.rm(directory, { recursive: true, force: true }); }
});

View File

@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "stl-export-unit-"));
const sourcePath = path.join(root, "web/protocol/stl-export.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "stl-export.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const document = {
schemaVersion: 1,
variant: "STL_BINARY",
unitScale: 1,
declaredTriangleCount: 1,
triangleCount: 1,
removedDegenerateTriangles: 0,
normals: [[0, 1, 0]],
vertices: [[[-1, 0, 1], [1, 0, 1], [1, 0, -1]]],
bounds: { min: [-1, 0, -1], max: [1, 0, 1] },
};
test("M12-07F serializes binary STL and reports material loss", () => {
const output = protocol.exportBinarySTL(document);
assert.equal(output.byteLength, 134);
assert.equal(new DataView(output).getUint32(80, true), 1);
assert.deepEqual(protocol.createSTLLossReport(2), {
schemaVersion: 1,
operation: "STL_EXPORT_LOSS_REPORT",
canRoundTrip: true,
warningCount: 1,
warnings: [{ code: "STL_MATERIAL_UNSUPPORTED", severity: "warning", message: "STL has no material slots; 2 source material assignments are omitted" }],
});
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "stl-import-unit-"));
const sourcePath = path.join(root, "web/protocol/stl-import.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const modulePath = path.join(temporary, "stl-import.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const protocol = await import(pathToFileURL(modulePath));
const fixtureRoot = path.join(root, "tests/files/web/m12_stl_edges_v1");
const bytes = (name) => {
const value = fs.readFileSync(path.join(fixtureRoot, name));
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
};
test("M12-07E parses binary/ASCII normals and explicit unit scales", () => {
const binary = protocol.importSTL(bytes("capability-binary.stl"), { variant: "STL_BINARY", unitScale: 1 });
assert.equal(binary.triangleCount, 2);
assert.deepEqual(binary.normals, [[0, 1, 0], [0, 1, 0]]);
assert.deepEqual(binary.bounds, { min: [-1, 0, -1], max: [1, 0, 1] });
const scaled = protocol.importSTL(bytes("capability-binary.stl"), { variant: "STL_BINARY", unitScale: 0.001 });
assert.deepEqual(scaled.bounds, { min: [-0.001, 0, -0.001], max: [0.001, 0, 0.001] });
const ascii = protocol.importSTL(bytes("../m12_stl_capability_v1/capability-ascii.stl"), { variant: "STL_ASCII", unitScale: 1 });
assert.equal(ascii.triangleCount, 2);
assert.deepEqual(ascii.normals, binary.normals);
assert.deepEqual(ascii.bounds, binary.bounds);
});
test("M12-07E matches Blender's degenerate removal and blocks trailing bytes", () => {
const degenerate = protocol.importSTL(bytes("degenerate-binary.stl"), { variant: "STL_BINARY", unitScale: 1 });
assert.equal(degenerate.declaredTriangleCount, 2);
assert.equal(degenerate.removedDegenerateTriangles, 1);
assert.equal(degenerate.triangleCount, 1);
assert.throws(() => protocol.importSTL(bytes("trailing-binary.stl"), { variant: "STL_BINARY", unitScale: 1 }), /STL_TRAILING_BYTES/);
assert.throws(() => protocol.importSTL(bytes("capability-binary.stl"), { variant: "STL_BINARY", unitScale: 0 }), /STL_UNIT_SCALE_INVALID/);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,24 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import ts from "../../node_modules/typescript/lib/typescript.js";
const root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/viewport-dpr.ts");
const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
assert.deepEqual(output.diagnostics, []);
const dpr = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`);
test("M14-04B clamps DPR and computes stable backing dimensions", () => {
for (const [observed, expected] of [[1, 1], [1.5, 1.5], [2, 2], [3, 2], [0, 1], [Number.NaN, 1]]) {
const metrics = dpr.resolveViewportPixelMetrics(101, 57, observed);
assert.equal(metrics.pixelRatio, expected);
assert.deepEqual([metrics.backingWidth, metrics.backingHeight], [Math.floor(101 * expected), Math.floor(57 * expected)]);
}
});
test("M14-04B uses CSS bounds for DPR-independent NDC", () => {
assert.deepEqual(dpr.viewportNDC(150, 75, { left: 100, top: 25, width: 100, height: 100 }), { x: 0, y: 0 });
assert.throws(() => dpr.viewportNDC(0, 0, { left: 0, top: 0, width: 0, height: 1 }), /VIEWPORT_BOUNDS_INVALID/);
});