78 lines
8.4 KiB
TypeScript
78 lines
8.4 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-P11-003");
|
|
const linkedFixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P3-005-ASSET.LIBRARY_OPERATIONS.blend"));
|
|
const emptyFixture = fs.readFileSync(path.join(root, "tests/files/web/empty.blend"));
|
|
const sourceLibrary = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2P3005Library.blend"));
|
|
const sha256 = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
|
|
test("WBV2-P11-003 enforces linked ownership and local append through compiled WASM Main", 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 sourceSha256 = sha256(sourceLibrary);
|
|
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-p11-003", localDataBlockId: closure.object }, readOnly: false, referenceReadOnly: false, sourceGeneration: 1, sourceRevision: 0, dependencyClosureSha256: await computeLibraryAppendClosureSha256(closure) });
|
|
const result = await page.evaluate(async (input) => {
|
|
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 shapeError = (error: any) => ({ code: error?.code, message: error?.message });
|
|
const linkedClient = new WebEngineClient({ timeoutMs: 60_000 });
|
|
const appendClient = new WebEngineClient({ timeoutMs: 60_000 });
|
|
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
|
|
try {
|
|
const linked = await linkedClient.openBlend(buffer(input.linkedBytes));
|
|
const linkedObject: any = linked.snapshot.nodes.find((candidate: any) => candidate.id.includes("WBV2P3005LinkedObject"));
|
|
const linkedBefore = { id: linkedObject?.id ?? null, visible: linkedObject?.visible ?? null, objectPresent: Boolean(linkedObject), libraries: linked.snapshot.libraries ?? [], libraryStatus: linked.snapshot.libraryStatus };
|
|
const linkedMutation = linkedObject
|
|
? await linkedClient.applyCommand({ type: "setObjectVisibility", objectId: linkedObject.id, visible: !linkedObject.visible, baseRevision: linked.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(shapeError)
|
|
: { code: "LINKED_LIBRARY_REQUIRED", message: "linked object withheld while external library is unavailable" };
|
|
const linkedAfterSnapshot = await linkedClient.snapshot();
|
|
const linkedAfter = linkedObject ? linkedAfterSnapshot.snapshot.nodes.find((candidate: any) => candidate.id === linkedObject.id) : undefined;
|
|
|
|
const opened = await appendClient.openBlend(buffer(input.emptyBytes));
|
|
const baseRevision = opened.snapshot.revision;
|
|
const appended = await appendClient.appendLibraryObject(buffer(input.sourceBytes), { schemaVersion: 1, baseRevision, binding: input.binding, expectedClosure: input.closure });
|
|
const localClosure = (snapshot: any) => ({ object: snapshot.nodes.find((candidate: any) => candidate.id === "object:WBV2P3005AppendObject"), mesh: snapshot.meshes.find((candidate: any) => candidate.id === "mesh:WBV2P3005AppendMesh"), material: snapshot.materials.find((candidate: any) => candidate.id === "material:WBV2P3005AppendMaterial"), image: snapshot.images.find((candidate: any) => candidate.id === "image:WBV2P3005AppendImage") });
|
|
const local = localClosure(appended.snapshot);
|
|
const saved = await appendClient.saveBlend();
|
|
const savedSha256 = await digest(saved.slice(0));
|
|
const reopened = await reopenedClient.openBlend(saved.slice(0));
|
|
const reopenedLocal = localClosure(reopened.snapshot);
|
|
const stale = await appendClient.appendLibraryObject(buffer(input.sourceBytes), { schemaVersion: 1, baseRevision, binding: input.binding, expectedClosure: input.closure }).then(() => ({ code: "NO_ERROR" })).catch(shapeError);
|
|
const afterStale = (await appendClient.snapshot()).snapshot.revision;
|
|
return { linked: { before: linkedBefore, mutation: linkedMutation, after: { id: linkedAfter?.id ?? null, visible: linkedAfter?.visible ?? null, objectPresent: Boolean(linkedAfter) }, revisionBefore: linked.snapshot.revision, revisionAfter: linkedAfterSnapshot.snapshot.revision }, append: { baseRevision, appendedRevision: appended.snapshot.revision, receipt: appended.receipt, local: { object: local.object && { id: local.object.id, type: local.object.type, dataId: local.object.dataId }, mesh: local.mesh && { id: local.mesh.id, materialSlotIds: local.mesh.materialSlotIds, vertexCount: local.mesh.vertexCount }, material: local.material && { id: local.material.id, imageIds: local.material.imageIds ?? local.material.nodes?.flatMap((node: any) => node.imageId ? [node.imageId] : []) }, image: local.image && { id: local.image.id, libraryLinked: local.image.libraryLinked, packed: local.image.packed, assetStatus: local.image.assetStatus } }, reopened: { object: Boolean(reopenedLocal.object), mesh: Boolean(reopenedLocal.mesh), material: Boolean(reopenedLocal.material), image: reopenedLocal.image && { libraryLinked: reopenedLocal.image.libraryLinked, packed: reopenedLocal.image.packed } }, saveReopenExact: Boolean(reopenedLocal.object && reopenedLocal.mesh && reopenedLocal.material && reopenedLocal.image?.libraryLinked === false && reopenedLocal.image?.packed === true), savedSha256, sourceSha256: await digest(buffer(input.sourceBytes)), stale, revisionAfterStale: afterStale } };
|
|
}
|
|
finally { linkedClient.terminate(); appendClient.terminate(); reopenedClient.terminate(); }
|
|
}, { linkedBytes: Array.from(linkedFixture), emptyBytes: Array.from(emptyFixture), sourceBytes: Array.from(sourceLibrary), closure, binding });
|
|
fs.mkdirSync(evidenceRoot, { recursive: true });
|
|
const output = process.env.WBV2_ASSET_LINK_OVERRIDE_REPORT ?? "asset-link-override-production.json";
|
|
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P11-003", operation: "ASSET_LINK_OVERRIDE_RELOAD_FAMILY_CHROMIUM", browser: "chromium", queueMutation: false, ...result }, null, 2)}\n`);
|
|
expect(result.linked.before.libraryStatus).toBe("AVAILABLE");
|
|
expect(result.linked.before.libraries.length).toBeGreaterThan(0);
|
|
expect(result.linked.before.libraries.every((library: any) => library.status === "EXTERNAL_REQUIRED" && library.readOnly === true)).toBe(true);
|
|
expect(result.linked.mutation.code).not.toBe("NO_ERROR");
|
|
expect(result.linked.before.objectPresent).toBe(false);
|
|
expect(result.linked.after.objectPresent).toBe(false);
|
|
expect(result.linked.after.visible).toBe(result.linked.before.visible);
|
|
expect(result.linked.revisionAfter).toBe(result.linked.revisionBefore);
|
|
expect(result.append.appendedRevision).toBe(result.append.baseRevision + 1);
|
|
expect(result.append.receipt.operation).toBe("APPEND");
|
|
expect(result.append.receipt.mapping.every((entry: any) => entry.owner === "LOCAL_MAIN" && entry.readOnly === false && entry.source === entry.local)).toBe(true);
|
|
expect(result.append.local.object.type).toBe("MESH");
|
|
expect(result.append.local.mesh.vertexCount).toBe(4);
|
|
expect(result.append.local.image.libraryLinked).toBe(false);
|
|
expect(result.append.local.image.packed).toBe(true);
|
|
expect(result.append.saveReopenExact).toBe(true);
|
|
expect(result.append.stale.code).not.toBe("NO_ERROR");
|
|
expect(result.append.stale.message).toContain("REVISION_CONFLICT");
|
|
expect(result.append.revisionAfterStale).toBe(result.append.appendedRevision);
|
|
});
|