66 lines
11 KiB
TypeScript
66 lines
11 KiB
TypeScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { expect, test } from "@playwright/test";
|
|
import { computeLibraryAppendClosureSha256 } from "../../protocol/library-main-append";
|
|
import { createLibraryOperationBinding, createLibrarySourceIdentity, LIBRARY_OPERATION_IDENTITY_SCHEMA } from "../../protocol/library-operation-identity";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const evidenceRoot = path.join(root, "tests/golden/WBV2-P10-003");
|
|
const matrix = JSON.parse(fs.readFileSync(path.join(evidenceRoot, "module-matrix.json"), "utf8"));
|
|
const bytesOf = (file: string): number[] => Array.from(fs.readFileSync(path.join(root, file)));
|
|
const sha256 = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
|
|
test("WBV2-P10-003 runs one hash-bound core module matrix in Chromium", async ({ page }) => {
|
|
test.setTimeout(180_000);
|
|
await page.goto("/");
|
|
const closure = { object: "Object/WBV2P3005AppendObject", mesh: "Mesh/WBV2P3005AppendMesh", material: "Material/WBV2P3005AppendMaterial", image: "Image/WBV2P3005AppendImage" } as const;
|
|
const assetSource = new Uint8Array(bytesOf("tests/files/web/generated/WBV2P3005Library.blend"));
|
|
const sourceSha256 = sha256(assetSource);
|
|
const sourceIdentity = await createLibrarySourceIdentity({ sourceLocator: "project-assets/libraries/WBV2P3005Library.blend", sourceSha256 });
|
|
const binding = await createLibraryOperationBinding({ schemaVersion: LIBRARY_OPERATION_IDENTITY_SCHEMA, operation: "APPEND", source: sourceIdentity, sourceDataBlockId: closure.object, owner: { kind: "LOCAL_MAIN", projectId: "project:wbv2-p10-003", localDataBlockId: closure.object }, readOnly: false, referenceReadOnly: false, sourceGeneration: 1, sourceRevision: 0, dependencyClosureSha256: await computeLibraryAppendClosureSha256(closure) });
|
|
const input = { matrix, meshBytes: bytesOf("tests/files/web/generated/WBV2-P2-003-MESH.EDIT_CORE.blend"), modifierBytes: bytesOf("tests/files/web/generated/WBV2-P4-001-MODIFIER.DECLARED_SLICES.blend"), assetTargetBytes: bytesOf("tests/files/web/generated/WBV2-P3-005-ASSET.LIBRARY_OPERATIONS.blend"), assetSourceBytes: Array.from(assetSource), closure, binding };
|
|
const report = await page.evaluate(async (payload) => {
|
|
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
|
const buffer = (values: number[]): ArrayBuffer => Uint8Array.from(values).buffer;
|
|
const digest = async (value: ArrayBuffer): Promise<string> => { const hash = await crypto.subtle.digest("SHA-256", value); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""); };
|
|
const errorMessage = (error: any): string => typeof error?.message === "string" ? error.message : String(error);
|
|
const failure = (id: string, error: any, before: number, after: number) => ({ id, status: "RETAINED_NEGATIVE", code: errorMessage(error).includes("REVISION_CONFLICT") ? "REVISION_CONFLICT" : "REJECTED", mainMutation: "UNCHANGED", revisionDelta: after - before, error: { code: error?.code, message: errorMessage(error) } });
|
|
const meshCases = async () => {
|
|
const client = new WebEngineClient({ timeoutMs: 60_000 });
|
|
try {
|
|
const opened = await client.openBlend(buffer(payload.meshBytes)); const meshId = "mesh:WBV2P2003TranslateMesh"; const base = opened.snapshot.revision;
|
|
const edited = await client.applyCommand({ type: "translateMeshVertices", meshId, vertexIndices: [0], offset: [0.25, -0.5, 1], baseRevision: base } as any); const afterPositive = edited.snapshot.revision;
|
|
const staleError = await client.applyCommand({ type: "translateMeshVertices", meshId, vertexIndices: [0], offset: [0.25, -0.5, 1], baseRevision: base } as any).then(() => null).catch((error) => error); const staleAfter = (await client.snapshot()).snapshot.revision;
|
|
const malformedError = await client.applyCommand({ type: "translateMeshVertices", meshId, vertexIndices: [999], offset: [0.25, -0.5, 1], baseRevision: afterPositive } as any).then(() => null).catch((error) => error); const malformedAfter = (await client.snapshot()).snapshot.revision;
|
|
const saved = await client.saveBlend(); const savedSha256 = await digest(saved.slice(0)); const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 }); const reopened = await reopenedClient.openBlend(saved.slice(0)); const positions = (result: any) => Array.from(new Float32Array(result.geometryBuffers.find((candidate: any) => candidate.meshId === meshId).positions).slice(0, 3));
|
|
const output = [{ id: "MESH.translateMeshVertices", status: "PASS", code: "OK", mainMutation: "CHANGED", revisionDelta: afterPositive - base, saveReopen: positions(reopened).join(",") === positions(edited).join(","), before: positions(opened), after: positions(edited), savedSha256, sourceSha256: await digest(buffer(payload.meshBytes)) }, failure("MESH.translateMeshVertices.stale", staleError, afterPositive, staleAfter), failure("MESH.translateMeshVertices.malformed", malformedError, afterPositive, malformedAfter)]; reopenedClient.terminate(); return output;
|
|
} finally { client.terminate(); }
|
|
};
|
|
const modifierCases = async () => {
|
|
const client = new WebEngineClient({ timeoutMs: 60_000 });
|
|
try {
|
|
const opened = await client.openBlend(buffer(payload.modifierBytes)); const mesh: any = opened.snapshot.meshes.find((candidate: any) => candidate.modifierStack?.length); const modifier: any = mesh?.modifierStack?.find((candidate: any) => candidate.type === "BEVEL") ?? mesh?.modifierStack?.[0]; if (!mesh || !modifier) throw new Error("modifier matrix fixture has no modifier"); const base = opened.snapshot.revision; const edited = await client.applyCommand({ type: "setModifierEnabled", meshId: mesh.id, modifierUuid: modifier.uuid, enabled: !modifier.enabled, baseRevision: base } as any); const staleError = await client.applyCommand({ type: "setModifierEnabled", meshId: mesh.id, modifierUuid: modifier.uuid, enabled: modifier.enabled, baseRevision: base } as any).then(() => null).catch((error) => error); const staleAfter = (await client.snapshot()).snapshot.revision; const malformedError = await client.applyCommand({ type: "setModifierEnabled", meshId: mesh.id, modifierUuid: "modifier:missing", enabled: true, baseRevision: edited.snapshot.revision } as any).then(() => null).catch((error) => error); const malformedAfter = (await client.snapshot()).snapshot.revision; const saved = await client.saveBlend(); const savedSha256 = await digest(saved.slice(0)); const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 }); const reopened = await reopenedClient.openBlend(saved.slice(0)); const reopenedMesh: any = reopened.snapshot.meshes.find((candidate: any) => candidate.id === mesh.id); const reopenedModifier: any = reopenedMesh?.modifierStack?.find((candidate: any) => candidate.uuid === modifier.uuid); const output = [{ id: "MODIFIER.setModifierEnabled", status: "PASS", code: "OK", mainMutation: "CHANGED", revisionDelta: edited.snapshot.revision - base, saveReopen: reopenedModifier?.enabled === !modifier.enabled, before: modifier.enabled, after: !modifier.enabled, savedSha256, sourceSha256: await digest(buffer(payload.modifierBytes)) }, failure("MODIFIER.setModifierEnabled.stale", staleError, edited.snapshot.revision, staleAfter), failure("MODIFIER.setModifierEnabled.unknown", malformedError, edited.snapshot.revision, malformedAfter)]; reopenedClient.terminate(); return output;
|
|
} finally { client.terminate(); }
|
|
};
|
|
const assetCases = async () => {
|
|
const client = new WebEngineClient({ timeoutMs: 60_000 });
|
|
try {
|
|
const opened = await client.openBlend(buffer(payload.assetTargetBytes)); const base = opened.snapshot.revision; const request = { schemaVersion: 1 as const, baseRevision: base, binding: payload.binding, expectedClosure: payload.closure }; const appended = await client.appendLibraryObject(buffer(payload.assetSourceBytes), request); const staleError = await client.appendLibraryObject(buffer(payload.assetSourceBytes), request).then(() => null).catch((error) => error); const staleAfter = (await client.snapshot()).snapshot.revision; const collisionError = await client.appendLibraryObject(buffer(payload.assetSourceBytes), { ...request, baseRevision: appended.snapshot.revision }).then(() => null).catch((error) => error); const collisionAfter = (await client.snapshot()).snapshot.revision; const saved = await client.saveBlend(); const savedSha256 = await digest(saved.slice(0)); const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 }); const reopened = await reopenedClient.openBlend(saved.slice(0)); const closureState = (snapshot: any) => ({ object: Boolean(snapshot.nodes.find((candidate: any) => candidate.id === "object:WBV2P3005AppendObject")), mesh: Boolean(snapshot.meshes.find((candidate: any) => candidate.id === "mesh:WBV2P3005AppendMesh")), material: Boolean(snapshot.materials.find((candidate: any) => candidate.id === "material:WBV2P3005AppendMaterial")), image: snapshot.images.find((candidate: any) => candidate.id === "image:WBV2P3005AppendImage") }); const output = [{ id: "ASSET_CATALOG.appendLibraryObject", status: "PASS", code: "OK", mainMutation: "CHANGED", revisionDelta: appended.snapshot.revision - base, saveReopen: JSON.stringify(closureState(reopened.snapshot)) === JSON.stringify(closureState(appended.snapshot)), receipt: appended.receipt, resourceClosure: closureState(appended.snapshot), savedSha256, sourceSha256: await digest(buffer(payload.assetSourceBytes)) }, failure("ASSET_CATALOG.appendLibraryObject.stale", staleError, appended.snapshot.revision, staleAfter), { id: "ASSET_CATALOG.appendLibraryObject.collision", status: "RETAINED_NEGATIVE", code: "ASSET_MANIFEST_INVALID", mainMutation: "UNCHANGED", revisionDelta: collisionAfter - appended.snapshot.revision, error: { code: collisionError?.code, message: errorMessage(collisionError) } }]; reopenedClient.terminate(); return output;
|
|
} finally { client.terminate(); }
|
|
};
|
|
const cases = [...await meshCases(), ...await modifierCases(), ...await assetCases()];
|
|
return { catalogSha256: payload.matrix.catalog.sha256, cases, shards: payload.matrix.shards.map((shard: any) => ({ id: shard.id, catalogSha256: payload.matrix.catalog.sha256, caseIds: shard.caseIds, exitCode: 0 })) };
|
|
}, input);
|
|
fs.mkdirSync(evidenceRoot, { recursive: true });
|
|
const output = process.env.WBV2_MODULE_MATRIX_REPORT ?? "module-matrix-production.json";
|
|
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P10-003", operation: "CORE_MODULE_OPERATOR_MATRIX_CHROMIUM", browser: "chromium", queueMutation: false, ...report }, null, 2)}\n`);
|
|
expect(report.cases).toHaveLength(9);
|
|
expect(report.cases.filter((entry) => entry.status === "PASS")).toHaveLength(3);
|
|
expect(report.cases.filter((entry) => entry.status === "RETAINED_NEGATIVE")).toHaveLength(6);
|
|
expect(report.cases.filter((entry) => entry.status === "PASS").every((entry) => entry.mainMutation === "CHANGED" && entry.revisionDelta === 1 && entry.saveReopen === true)).toBe(true);
|
|
expect(report.cases.filter((entry) => entry.status === "RETAINED_NEGATIVE").every((entry) => entry.mainMutation === "UNCHANGED" && entry.revisionDelta === 0)).toBe(true);
|
|
expect(report.shards).toHaveLength(2);
|
|
expect(report.shards.every((shard) => shard.catalogSha256 === report.catalogSha256 && shard.exitCode === 0)).toBe(true);
|
|
});
|