import { describe, expect, it } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import { createToolcraftState } from "../state/create-template-state"; import { toolcraftReducer } from "../state/reducer"; import type { ToolcraftModelAsset } from "../state/types"; import { createToolcraftDefaultModelFiles, isToolcraftDefaultModelPlaceholder, } from "./default-model-source-assets"; const limits = { maxBundleFiles: 4, maxSourceBytes: 64, }; function modelApp() { return defineToolcraft({ canvas: { enabled: true, upload: true }, media: { defaultAssets: [ { assetKind: "model", fileName: "scene.gltf", id: "default-model", layerId: "default-model-layer", sourceFiles: [ { dataUrl: "data:model/gltf+json,%7B%7D", path: "bundle/scene.gltf", }, { dataUrl: "data:application/octet-stream;base64,AAEC", path: "bundle/scene.bin", }, ], sourceTarget: "source.model", }, ], }, panels: { controls: { sections: [ { controls: { model: { assetKind: "model", defaultValue: null, target: "source.model", type: "fileDrop", }, }, title: "Model", }, ], title: "Controls", }, }, }); } describe("default model source assets", () => { it("restores nested glTF paths as browser path evidence", async () => { const schemaAsset = modelApp().media.defaultAssets[0]; if (schemaAsset?.assetKind !== "model") { throw new Error("Expected a default model schema asset."); } const files = createToolcraftDefaultModelFiles(schemaAsset, limits); expect(files.map((file) => file.name)).toEqual(["scene.gltf", "scene.bin"]); expect( files.map((file) => (file as File & { webkitRelativePath?: string }).webkitRelativePath ), ).toEqual(["bundle/scene.gltf", "bundle/scene.bin"]); expect(new Uint8Array(await files[1]?.arrayBuffer())).toEqual( new Uint8Array([0, 1, 2]), ); }); it("rejects invalid, duplicate, and oversized serialized sources before import", () => { const base = { assetKind: "model" as const, fileName: "scene.glb", sourceTarget: "source.model", }; expect(() => createToolcraftDefaultModelFiles( { ...base, sourceFiles: [{ dataUrl: "https://example.com/a", path: "scene.glb" }] }, limits, ) ).toThrow(/data URLs/u); expect(() => createToolcraftDefaultModelFiles( { ...base, sourceFiles: [ { dataUrl: "data:application/octet-stream;base64,AA==", path: "scene.glb" }, { dataUrl: "data:application/octet-stream;base64,AA==", path: "scene.glb" }, ], }, limits, ) ).toThrow(/duplicated/u); expect(() => createToolcraftDefaultModelFiles( { ...base, sourceFiles: [{ dataUrl: `data:application/octet-stream,${"a".repeat(65)}`, path: "scene.glb", }], }, limits, ) ).toThrow(/source byte limit/u); }); it("creates a serializable restoring attachment and restores it on reset", () => { const initial = createToolcraftState(modelApp()); const placeholder = initial.mediaAssets[0]; if (placeholder?.assetKind !== "model") { throw new Error("Expected a default model placeholder."); } const deleted = toolcraftReducer(initial, { mediaId: placeholder.id, type: "media.delete", }); const reset = toolcraftReducer(deleted, { type: "controls.reset" }); expect(isToolcraftDefaultModelPlaceholder(placeholder)).toBe(true); expect(placeholder).toMatchObject({ id: "default-model", layerId: "default-model-layer", lifecycle: "restoring", sourceTarget: "source.model", }); expect(deleted.mediaAssets).toEqual([]); expect(reset.mediaAssets).toHaveLength(1); expect( reset.mediaAssets[0]?.assetKind === "model" && isToolcraftDefaultModelPlaceholder(reset.mediaAssets[0]), ).toBe(true); }); it("respects an explicitly persisted empty media slice", () => { const restored = createToolcraftState(modelApp(), { layers: [], mediaAssets: [], selectedLayerId: null, }); expect(restored.mediaAssets).toEqual([]); expect(restored.layers).toEqual([]); expect(restored.selectedLayerId).toBeNull(); }); it("commits a default hydration only while the exact placeholder exists", () => { const state = createToolcraftState(modelApp()); const placeholder = state.mediaAssets[0]; if (placeholder?.assetKind !== "model") { throw new Error("Expected a default model placeholder."); } const hydratedAsset: ToolcraftModelAsset = { ...placeholder, activeDocumentRef: "document:ready", analysis: { boundaryEdges: 0, diagnostics: [], disconnectedComponents: 1, nonManifoldEdges: 0, outcome: "clean", triangles: 12, vertices: 8, }, lifecycle: "clean", originalAnalysis: { boundaryEdges: 0, diagnostics: [], disconnectedComponents: 1, nonManifoldEdges: 0, outcome: "clean", triangles: 12, vertices: 8, }, originalDocumentRef: "document:ready", sourceBundleDigest: "sha256:ready", sourceBundleRef: "bundle:ready", }; const command = { asset: hydratedAsset, expectedPlaceholderRef: placeholder.sourceBundleRef, type: "media.hydrateDefaultModel" as const, }; const hydrated = toolcraftReducer(state, command); const deleted = toolcraftReducer(state, { mediaId: placeholder.id, type: "media.delete", }); expect(hydrated.history).toEqual(state.history); expect(hydrated.mediaAssets[0]).toMatchObject({ activeDocumentRef: "document:ready", id: "default-model", layerId: "default-model-layer", lifecycle: "clean", }); expect(toolcraftReducer(deleted, command)).toBe(deleted); }); });