import { describe, expect, it, vi } from "vitest"; import { createMemoryToolcraftBinaryAssetRepository } from "../source-assets/repository/memory-binary-asset-repository"; import { encodeToolcraftModelDocument } from "./canonical/model-document-codec"; import { digestToolcraftModelDocument } from "./canonical/model-document-digest"; import { createModelMaterial, createValidModelDocumentV2, } from "./canonical/model-document-test-support"; import { TOOLCRAFT_PRODUCTION_MODEL_FORMAT_ADAPTER_REGISTRY } from "./formats/production-model-format-registry"; import { createToolcraftModelDocumentResourceRef, TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, } from "./model-source-asset-handler-resources"; import { createToolcraftModelSourceBundleDescriptorResource } from "./model-source-bundle-codec"; import { createToolcraftModelSourceBundleSnapshot } from "./model-source-bundle"; import { hydrateToolcraftPersistedModels } from "./model-persistence-hydration"; import { createHydrationModelAsset, createHydrationStateHarness, createHydrationWorkerAnalysis, createHydrationWorkerClient, } from "./model-persistence-hydration-test-support"; import type { ToolcraftModelWorkerClient } from "./worker/model-import-worker-client"; describe("persisted model appearance hydration", () => { it("regenerates a missing texture as a per-channel fallback with typed warning", async () => { const repository = createMemoryToolcraftBinaryAssetRepository(); const source = await createToolcraftModelSourceBundleSnapshot([ new File([new Uint8Array([0x67, 0x6c, 0x54, 0x46])], "scene.glb", { type: "model/gltf-binary", }), new File([new Uint8Array([137, 80, 78, 71])], "albedo.png", { type: "image/png", }), ], { registry: TOOLCRAFT_PRODUCTION_MODEL_FORMAT_ADAPTER_REGISTRY }); const descriptor = createToolcraftModelSourceBundleDescriptorResource( source.bundle, ); const original = createValidModelDocumentV2(); const originalBytes = encodeToolcraftModelDocument(original); const originalRef = createToolcraftModelDocumentResourceRef( digestToolcraftModelDocument(original), ); const rootMetadata = source.bundle.sourceFiles.find( ({ path }) => path === source.bundle.rootPath, ); const rootTransfer = source.transfer.sourceFiles.find( ({ path }) => path === source.bundle.rootPath, ); if (!rootMetadata || !rootTransfer) throw new Error("Expected root source."); const lease = await repository.beginLease("appearance-hydration-source"); await lease.put(rootMetadata.resourceRef, new Uint8Array(rootTransfer.bytes), { contentType: rootMetadata.mimeType, durable: true, }); await lease.put( descriptor.sourceBundleRef, descriptor.bytes, descriptor.stageOptions, ); await lease.put(originalRef, originalBytes, { contentType: TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, dependencies: original.textures.map(({ resourceRef }) => resourceRef), durable: false, }); await lease.commit(); const { baseColorTexture: _missingSlot, ...fallbackMaterial } = createModelMaterial(); const regenerated = createValidModelDocumentV2({ materials: [fallbackMaterial], textures: [], }); const regeneratedBytes = encodeToolcraftModelDocument(regenerated); const regeneratedDigest = digestToolcraftModelDocument(regenerated); const importModel: ToolcraftModelWorkerClient["importModel"] = vi.fn( async ( request: Parameters[0], observers: NonNullable< Parameters[1] > = {}, ) => { expect(request.bundle.sourceFiles.map(({ path }) => path)).toEqual([ source.bundle.rootPath, ]); observers.onDiagnostic?.({ diagnostic: { affectedCount: 1, code: "missing-appearance-resource", explanation: "The saved base-color texture could not be restored.", severity: "warning", }, generation: 1, jobId: request.jobId, kind: "diagnostic", }); return { generation: 1, jobId: request.jobId, kind: "result" as const, result: { analysis: createHydrationWorkerAnalysis(), appearanceResources: [], canonicalDocument: regeneratedBytes.slice().buffer, canonicalDocumentDigest: regeneratedDigest, operation: "decode-and-analyze" as const, receiptDigest: `sha256:${"3".repeat(64)}`, }, }; }, ); const harness = createHydrationStateHarness(createHydrationModelAsset( originalRef, { sourceBundleDigest: source.bundle.aggregateDigest, sourceBundleRef: descriptor.sourceBundleRef, }, )); await hydrateToolcraftPersistedModels({ ...harness, repository, workerClient: createHydrationWorkerClient(importModel), }).promise; const regeneratedRef = createToolcraftModelDocumentResourceRef( regeneratedDigest, ); expect(harness.getState().mediaAssets[0]).toMatchObject({ activeDocumentRef: regeneratedRef, analysis: { diagnostics: [{ affectedCount: 1, code: "missing-appearance-resource", explanation: "The saved base-color texture could not be restored.", severity: "warning", }], }, lifecycle: "clean", }); await expect(repository.get(regeneratedRef)).resolves.toMatchObject({ dependencies: [], }); }); });