import { describe, expect, it } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import type { ToolcraftModelAsset } from "./types"; import { createToolcraftState } from "./create-template-state"; import { createToolcraftPersistenceSnapshot } from "./persistence-snapshot"; import { readMediaAssets } from "./persistence-reader-media"; function persistentMediaSchema() { return defineToolcraft({ canvas: { enabled: true, upload: true }, panels: { controls: { sections: [ { controls: { source: { defaultValue: null, target: "source.media", type: "fileDrop", }, }, }, ], title: "Controls", }, }, persistence: { include: ["media", "panels"], key: "toolcraft:model-media:state:v1", storage: "localStorage", version: 1, }, }); } function modelAsset(): ToolcraftModelAsset { return { activeDocumentRef: "model-document:active", analysis: { boundaryEdges: 0, diagnostics: [], disconnectedComponents: 1, nonManifoldEdges: 0, outcome: "clean", triangles: 12, vertices: 8, }, assetKind: "model", fileName: "cube.glb", id: "model-1", layerId: "model-layer-1", lifecycle: "clean", mimeType: "model/gltf-binary", originalAnalysis: { boundaryEdges: 0, diagnostics: [], disconnectedComponents: 1, nonManifoldEdges: 0, outcome: "clean", triangles: 12, vertices: 8, }, originalDocumentRef: "model-document:active", position: { x: 0, y: 0 }, size: { height: 512, unit: "px", width: 512 }, sourceBundleDigest: "sha256:bundle", sourceBundleRef: "model-bundle:source", sourceTarget: "source.media", topologyProfile: "realtime-mesh", }; } describe("media discriminated union", () => { it("keeps default model source bytes out of synchronous live state", () => { const schema = defineToolcraft({ canvas: { enabled: true, upload: true }, media: { defaultAssets: [ { assetKind: "model", fileName: "default.glb", sourceFiles: [ { dataUrl: "data:model/gltf-binary;base64,AAAA", path: "default.glb", }, ], sourceTarget: "source.model", }, ], }, panels: { controls: { sections: [ { controls: { model: { assetKind: "model", defaultValue: null, target: "source.model", type: "fileDrop", }, }, title: "Model", }, ], title: "Controls", }, }, }); expect(schema.media.defaultAssets).toHaveLength(1); const [placeholder] = createToolcraftState(schema).mediaAssets; expect(placeholder).toMatchObject({ assetKind: "model", fileName: "default.glb", lifecycle: "restoring", sourceTarget: "source.model", }); expect(placeholder).not.toHaveProperty("dataUrl"); expect(placeholder).not.toHaveProperty("sourceFiles"); }); it("migrates legacy persisted media without assetKind to image", () => { expect( readMediaAssets([ { dataUrl: "data:image/png;base64,AAAA", fileName: "legacy.png", id: "legacy-image", layerId: "legacy-layer", mimeType: "image/png", position: { x: 0, y: 0 }, }, ]), ).toEqual([ expect.objectContaining({ assetKind: "image", fileName: "legacy.png", }), ]); }); it("reads a model record without requiring image-only fields", () => { expect(readMediaAssets([modelAsset()])).toEqual([ { ...modelAsset(), lifecycle: "restoring" }, ]); }); it("serializes only compact model metadata and resource refs", () => { const schema = persistentMediaSchema(); const unsafeModel = { ...modelAsset(), analysis: { ...modelAsset().analysis, diagnostics: [ { affectedCount: 1, binaryPayload: new Uint8Array([1, 2, 3]), code: "unsafe", explanation: "Unsafe extra data", severity: "warning", }, ], }, dataUrl: "data:model/gltf-binary;base64,AAAA", indices: new Uint32Array([0, 1, 2]), lastRepairError: { binaryPayload: new Uint8Array([4, 5, 6]), category: "repair", code: "unsafe-error", message: "Unsafe error payload", }, originalAnalysis: { ...modelAsset().originalAnalysis, diagnostics: [ { affectedCount: 1, code: "unsafe-original", explanation: "Unsafe original data", rawPath: ["source", "mesh"], severity: "warning", }, ], }, position: { x: 1, y: 2 }, positions: new Float32Array([0, 0, 0]), } as unknown as ToolcraftModelAsset; const state = createToolcraftState(schema, { mediaAssets: [unsafeModel], }); const serialized = JSON.stringify( createToolcraftPersistenceSnapshot(state, schema.persistence), ); expect(serialized).toContain('"assetKind":"model"'); expect(serialized).toContain('"activeDocumentRef":"model-document:active"'); expect(serialized).toContain('"position":{"x":1,"y":2}'); expect(serialized).not.toMatch( /binaryPayload|dataUrl|indices|positions|rawPath/u, ); }); });