import { describe, expect, it, vi } from "vitest"; import { createToolcraftModelAppearanceResourceRef, } from "../canonical/model-appearance-resource"; import { encodeToolcraftModelDocument } from "../canonical/model-document-codec"; import { digestToolcraftModelDocument } from "../canonical/model-document-digest"; import { createModelTexture, createValidModelDocumentV2, } from "../canonical/model-document-test-support"; import type { ToolcraftModelAppearanceResource } from "../model-import-types"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "../model-import-limits"; import { analyzeToolcraftModel } from "../topology/model-topology-analysis"; import { createToolcraftModelWorkerClient } from "./model-import-worker-client"; import { copyArrayBuffer, FakeWorker, importRequest, } from "./model-import-worker-client-test-support"; import { snapshotToolcraftModelWorkerAnalysisSummary } from "./model-import-worker-diagnostics"; import type { ToolcraftModelWorkerRequest, ToolcraftModelWorkerResponse, } from "./model-import-worker-protocol"; import { createToolcraftModelWorkerDraftReceiptDigest, createToolcraftModelWorkerReceiptDigest, digestToolcraftModelWorkerBytes, } from "./model-import-worker-receipt"; type DecodeRequest = Extract; function appearanceResource(values = [137, 80, 78, 71]): ToolcraftModelAppearanceResource { const bytes = new Uint8Array(values); const contentDigest = digestToolcraftModelWorkerBytes(bytes); return { bytes: bytes.buffer, contentDigest, mimeType: "image/png", resourceRef: createToolcraftModelAppearanceResourceRef(contentDigest), }; } function decodeResponses( request: Pick, resource: ToolcraftModelAppearanceResource, ): Readonly<{ draft: Extract; result: Extract; }> { const document = createValidModelDocumentV2({ textures: [createModelTexture({ contentDigest: resource.contentDigest, mimeType: resource.mimeType, resourceRef: resource.resourceRef, })], }); const canonicalDocument = copyArrayBuffer(encodeToolcraftModelDocument(document)); const canonicalDocumentDigest = digestToolcraftModelDocument(document); const draftWithoutReceipt = { appearanceResources: [{ ...resource, bytes: resource.bytes.slice(0) }], canonicalDocument: canonicalDocument.slice(0), canonicalDocumentDigest, }; const resultWithoutReceipt = { analysis: snapshotToolcraftModelWorkerAnalysisSummary(analyzeToolcraftModel( document, "realtime-mesh", TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, )), appearanceResources: [{ ...resource, bytes: resource.bytes.slice(0) }], canonicalDocument: canonicalDocument.slice(0), canonicalDocumentDigest, operation: "decode-and-analyze" as const, }; return { draft: { draft: { ...draftWithoutReceipt, receiptDigest: createToolcraftModelWorkerDraftReceiptDigest({ draft: draftWithoutReceipt, generation: request.generation, jobId: request.jobId, }), }, generation: request.generation, jobId: request.jobId, kind: "draft", }, result: { generation: request.generation, jobId: request.jobId, kind: "result", result: { ...resultWithoutReceipt, receiptDigest: createToolcraftModelWorkerReceiptDigest({ generation: request.generation, jobId: request.jobId, result: resultWithoutReceipt, }), }, }, }; } function emitDecodeResponses( worker: FakeWorker, request: DecodeRequest, responses: ReturnType, ): void { worker.emitMessage({ generation: request.generation, jobId: request.jobId, kind: "progress", phase: "decoding", progress: 0.9, }); worker.emitMessage(responses.draft); worker.emitMessage({ generation: request.generation, jobId: request.jobId, kind: "progress", phase: "analyzing", progress: 0.9, }); worker.emitMessage(responses.result); } describe("model worker appearance receipts", () => { it("publishes authenticated appearance bytes with the v2 document", async () => { const worker = new FakeWorker(); const onDraft = vi.fn(); const onResult = vi.fn(); const client = createToolcraftModelWorkerClient({ workerFactory: () => worker }); const pending = client.importModel(importRequest("appearance-authentic"), { onDraft, onResult, }); const request = worker.posts[0]!.message; if (request.kind !== "decode-and-analyze") { throw new Error("Expected decode request."); } const resource = appearanceResource(); emitDecodeResponses(worker, request, decodeResponses(request, resource)); await expect(pending).resolves.toMatchObject({ kind: "result", result: { appearanceResources: [{ contentDigest: resource.contentDigest, resourceRef: resource.resourceRef, }], operation: "decode-and-analyze", }, }); expect(onDraft).toHaveBeenCalledTimes(1); expect(onResult).toHaveBeenCalledTimes(1); }); it("rejects a one-byte resource substitution before publication", async () => { const worker = new FakeWorker(); const onResult = vi.fn(); const client = createToolcraftModelWorkerClient({ workerFactory: () => worker }); const pending = client.importModel(importRequest("appearance-tamper"), { onResult, }); const request = worker.posts[0]!.message; if (request.kind !== "decode-and-analyze") { throw new Error("Expected decode request."); } const responses = decodeResponses(request, appearanceResource()); const finalResource = responses.result.result.operation === "decode-and-analyze" ? responses.result.result.appearanceResources[0] : undefined; if (!finalResource) throw new Error("Expected appearance resource."); new Uint8Array(finalResource.bytes)[0] ^= 0xff; emitDecodeResponses(worker, request, responses); await expect(pending).resolves.toMatchObject({ feedback: { code: "model-worker-invalid-message" }, kind: "error", }); expect(onResult).not.toHaveBeenCalled(); expect(worker.terminate).toHaveBeenCalledTimes(1); }); it("rejects a final resource manifest that differs from its verified draft", async () => { const worker = new FakeWorker(); const onResult = vi.fn(); const client = createToolcraftModelWorkerClient({ workerFactory: () => worker }); const pending = client.importModel(importRequest("appearance-draft-mismatch"), { onResult, }); const request = worker.posts[0]!.message; if (request.kind !== "decode-and-analyze") { throw new Error("Expected decode request."); } const responses = decodeResponses(request, appearanceResource()); if (responses.result.result.operation !== "decode-and-analyze") { throw new Error("Expected decode result."); } const { receiptDigest: _receiptDigest, ...originalResult } = responses.result.result; const resultWithoutReceipt = { ...originalResult, appearanceResources: originalResult.appearanceResources.map((resource) => ({ ...resource, mimeType: "image/webp" as const, })), }; const mismatched = { ...responses, result: { ...responses.result, result: { ...resultWithoutReceipt, receiptDigest: createToolcraftModelWorkerReceiptDigest({ generation: request.generation, jobId: request.jobId, result: resultWithoutReceipt, }), }, }, }; emitDecodeResponses(worker, request, mismatched); await expect(pending).resolves.toMatchObject({ feedback: { code: "model-worker-invalid-message" }, kind: "error", }); expect(onResult).not.toHaveBeenCalled(); expect(worker.terminate).toHaveBeenCalledTimes(1); }); it("binds resource reference, MIME type, and byte length into the receipt", () => { const request = { generation: 1, jobId: "appearance-receipt", }; const resource = appearanceResource(); const response = decodeResponses(request, resource).result; if (response.result.operation !== "decode-and-analyze") { throw new Error("Expected decode result."); } const { receiptDigest: _receiptDigest, ...result } = response.result; const baseline = createToolcraftModelWorkerReceiptDigest({ generation: request.generation, jobId: request.jobId, result, }); const mutatedResources = [ { ...resource, resourceRef: `${resource.resourceRef}-changed` }, { ...resource, mimeType: "image/webp" as const }, { ...resource, bytes: new Uint8Array(resource.bytes.byteLength + 1).buffer }, ]; for (const mutated of mutatedResources) { expect(createToolcraftModelWorkerReceiptDigest({ generation: request.generation, jobId: request.jobId, result: { ...result, appearanceResources: [mutated] }, })).not.toBe(baseline); } }); });