import { describe, expect, it, vi } from "vitest"; import type { ToolcraftControlSchema } from "../schema/types"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "./model-import-limit-values"; import type { ToolcraftModelWorkerAnalysisSummary } from "./model-import-types"; import { createToolcraftModelSourceAssetHandler } from "./model-source-asset-handler"; import { createToolcraftModelAppearanceResourceRef, createToolcraftModelDocumentResourceRef, TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, } from "./model-source-asset-handler-resources"; import type { ToolcraftModelWorkerClient } from "./worker/model-import-worker-client"; const DRAFT_DIGEST = `sha256:${"a".repeat(64)}`; const FINAL_DIGEST = `sha256:${"b".repeat(64)}`; const TEXTURE_DIGEST = `sha256:${"c".repeat(64)}`; function analysis(): ToolcraftModelWorkerAnalysisSummary { return { analyzerVersion: "toolcraft-topology-v1", diagnostics: [], limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, outcome: "clean", profile: "realtime-mesh", statistics: { boundaryEdgeCount: 0, componentCount: 1, decodedBytes: 256, edgeCount: 3, estimatedPeakWorkerBytes: 512, estimatedRepairBytes: 0, nodeCount: 1, nonManifoldEdgeCount: 0, nonManifoldVertexCount: 0, primitiveCount: 1, triangleCount: 1, unusedVertexCount: 0, vertexCount: 3, }, }; } function modelControl(): ToolcraftControlSchema { return { assetKind: "model", modelFormats: ["glb"], modelLimits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, multiple: false, target: "source.model", topologyProfile: "realtime-mesh", type: "fileDrop", }; } describe("Toolcraft model source appearance staging", () => { it("stages appearance bytes once and binds draft and final documents to them", async () => { const resourceRef = createToolcraftModelAppearanceResourceRef(TEXTURE_DIGEST); const appearanceResources = [{ bytes: new Uint8Array([137, 80, 78, 71]).buffer, contentDigest: TEXTURE_DIGEST, mimeType: "image/png" as const, resourceRef, }]; const importModel = vi.fn(async (request, observers = {}) => { observers.onDraft?.({ draft: { appearanceResources, canonicalDocument: new Uint8Array([1, 2, 3]).buffer, canonicalDocumentDigest: DRAFT_DIGEST, receiptDigest: FINAL_DIGEST, }, generation: 1, jobId: request.jobId, kind: "draft", }); observers.onDiagnostic?.({ diagnostic: { affectedCount: 1, code: "missing-appearance-resource", explanation: "A local base-color texture is unavailable.", severity: "warning", }, generation: 1, jobId: request.jobId, kind: "diagnostic", }); return { generation: 1, jobId: request.jobId, kind: "result" as const, result: { analysis: analysis(), appearanceResources, canonicalDocument: new Uint8Array([9, 8, 7]).buffer, canonicalDocumentDigest: FINAL_DIGEST, operation: "decode-and-analyze" as const, receiptDigest: DRAFT_DIGEST, }, }; }); const workerClient: ToolcraftModelWorkerClient = { cancel: vi.fn(), dispose: vi.fn(), extractModelPackage: vi.fn(), importModel, repair: vi.fn(), reset: vi.fn(), }; const handler = createToolcraftModelSourceAssetHandler({ workerClient }); const control = modelControl(); const batch = { files: [new File([new Uint8Array([0x67, 0x6c, 0x54, 0x46])], "scene.glb", { type: "model/gltf-binary", })], origin: "panel" as const, target: control.target, }; const claim = handler.match(batch, control); if (!claim) throw new Error("Expected model source claim."); const stageResource = vi.fn(async ( _ref: string, _bytes: Uint8Array, _options: unknown, ) => undefined); const prepared = await handler.prepare({ batch, canvasFrame: { kind: "finite", size: { height: 512, unit: "px", width: 512 }, }, claim, control, jobId: "appearance-model", plan: handler.plan(batch, control, claim), reportOperation: vi.fn(), signal: new AbortController().signal, stageResource, }); expect(prepared.assets[0]?.analysis.diagnostics).toContainEqual({ affectedCount: 1, code: "missing-appearance-resource", explanation: "A local base-color texture is unavailable.", severity: "warning", }); expect(stageResource.mock.calls.filter(([ref]) => ref === resourceRef)) .toEqual([[ resourceRef, expect.any(Uint8Array), { contentType: "image/png", durable: false }, ]]); for (const documentRef of [ createToolcraftModelDocumentResourceRef(DRAFT_DIGEST), createToolcraftModelDocumentResourceRef(FINAL_DIGEST), ]) { expect(stageResource).toHaveBeenCalledWith( documentRef, expect.any(Uint8Array), { contentType: TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, dependencies: [resourceRef], durable: false, }, ); } }); });