import { describe, expect, it, vi } from "vitest"; import { createToolcraftModelSourceAssetHandler } from "./model-source-asset-handler"; import { createToolcraftModelDocumentResourceRef, createToolcraftModelRepairPlanResourceRef, TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, TOOLCRAFT_MODEL_REPAIR_PLAN_CONTENT_TYPE, } from "./model-source-asset-handler-resources"; import { TOOLCRAFT_MODEL_SOURCE_BUNDLE_DESCRIPTOR_CONTENT_TYPE } from "./model-source-bundle-codec"; import { glbFile, gltfBundle, modelControl, prepare, SHA_A, SHA_B, SHA_C, workerClient, } from "./model-source-asset-handler.test-support"; describe("Toolcraft model source lifecycle", () => { it("stages source blobs, descriptor, draft, and final document before one clean item", async () => { const { prepared, reportOperation, stageResource, worker, } = await prepare("clean", gltfBundle()); const sourceRefs = prepared.stagedResourceRefs.filter((ref) => ref.startsWith("toolcraft:model-source:sha256:"), ); const bundleRef = prepared.stagedResourceRefs.find((ref) => ref.startsWith("toolcraft:model-source-bundle:sha256:"), ); const draftRef = createToolcraftModelDocumentResourceRef(SHA_A); const finalRef = createToolcraftModelDocumentResourceRef(SHA_B); expect(prepared.assets).toHaveLength(1); expect(prepared.assets[0]).toMatchObject({ activeDocumentRef: finalRef, assetKind: "model", fileName: "scene.gltf", lifecycle: "clean", originalDocumentRef: finalRef, sourceBundleRef: bundleRef, topologyProfile: "realtime-mesh", }); expect(sourceRefs).toHaveLength(2); expect(stageResource).toHaveBeenCalledWith( sourceRefs[0], expect.anything(), expect.objectContaining({ durable: true }), ); expect(stageResource).toHaveBeenCalledWith( bundleRef, expect.anything(), { contentType: TOOLCRAFT_MODEL_SOURCE_BUNDLE_DESCRIPTOR_CONTENT_TYPE, dependencies: [...sourceRefs].sort(), durable: true, }, ); expect(stageResource).toHaveBeenCalledWith( draftRef, expect.anything(), { contentType: TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, dependencies: [], durable: false, }, ); expect(stageResource).toHaveBeenCalledWith( finalRef, expect.anything(), { contentType: TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, dependencies: [], durable: false, }, ); expect(reportOperation).toHaveBeenCalledWith({ phase: "analyzing", stagedDocumentRef: draftRef, }); expect(worker.importModel).toHaveBeenCalledWith( expect.objectContaining({ format: "gltf", jobId: "model-job-1", topologyProfile: "realtime-mesh", }), expect.objectContaining({ signal: expect.any(AbortSignal), }), ); }); it("maps warning analysis to a clean committed lifecycle", async () => { const { prepared } = await prepare("warning"); expect(prepared.assets[0]).toMatchObject({ analysis: { diagnostics: [ expect.objectContaining({ severity: "warning" }), ], outcome: "clean", }, lifecycle: "clean", }); }); it("stages the repair envelope and projects its ref for repairable models", async () => { const { prepared, stageResource } = await prepare("repairable"); const repairPlanRef = createToolcraftModelRepairPlanResourceRef({ envelopeDigest: SHA_C, planDigest: SHA_A, }); expect(prepared.assets[0]).toMatchObject({ analysis: { outcome: "repairable", repairPlanRef, }, lifecycle: "repairable", originalAnalysis: { repairPlanRef }, }); expect(stageResource).toHaveBeenCalledWith( repairPlanRef, expect.anything(), { contentType: TOOLCRAFT_MODEL_REPAIR_PLAN_CONTENT_TYPE, durable: false, }, ); }); it("rejects fatal topology after staging so the coordinator can roll it back", async () => { const worker = workerClient("fatal"); const handler = createToolcraftModelSourceAssetHandler({ workerClient: worker, }); const control = modelControl(); const batch = { files: [glbFile()], origin: "panel" as const, }; const claim = handler.match(batch, control)!; const stageResource = vi.fn(async () => undefined); await expect( handler.prepare({ batch, canvasFrame: { kind: "finite", size: { height: 512, unit: "px", width: 512 }, }, claim, control, jobId: "fatal-model", plan: handler.plan(batch, control, claim), reportOperation: vi.fn(), signal: new AbortController().signal, stageResource, }), ).rejects.toMatchObject({ category: "topology", code: "model-topology-fatal", }); expect(stageResource).toHaveBeenCalled(); }); });