import { afterEach, describe, expect, it, vi } from "vitest"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "../model-import-limits"; import { contextFor, createAppearanceFixture, createExtensionOnlyBehaviorFixture, createGlbTransfer, createIgnoredFeaturesFixture, createNormalizedQuantizedFixture, createPrimitiveFixture, createSparsePositionFixture, REAL_DRACO_GLB, REAL_MESHOPT_GLB, rewriteGlbJson, transferForGlbBytes, } from "../test-fixtures/model-fixtures"; import { toolcraftGlbModelFormatAdapter } from "./gltf-model-format-adapter"; import { preflightGltfJsonDocument } from "./gltf-json-preflight"; afterEach(() => { vi.unstubAllGlobals(); }); describe("glTF model format adapters", () => { it("rejects invalid accessor declarations and ranges before WebIO allocates", async () => { const wrongType = createPrimitiveFixture(); wrongType.json.accessors![0]!.type = "VEC2"; await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(wrongType)), ), ).rejects.toMatchObject({ category: "geometry", code: "invalid-position-accessor", }); const invalidRange = createPrimitiveFixture(); invalidRange.json.bufferViews![0]!.byteLength = 1_000_000; await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(invalidRange)), ), ).rejects.toMatchObject({ category: "format", code: "invalid-buffer-view", }); }); it.each([ ["integer POSITION without required quantization", 5122, false, undefined, "invalid-position-accessor"], ["quantization listed but not required", 5122, false, "used-only", "invalid-gltf-extensions"], ["UNSIGNED_INT POSITION with quantization", 5125, false, "required", "invalid-position-accessor"], ["normalized FLOAT POSITION", 5126, true, undefined, "invalid-gltf-accessor"], ["half-float POSITION without its required extension", 5131, false, undefined, "invalid-position-accessor"], ] as const)( "rejects %s before WebIO", async (_name, componentType, normalized, extensionMode, code) => { const fixture = createPrimitiveFixture(); const position = fixture.json.accessors![0]! as unknown as Record< string, unknown >; position.componentType = componentType; position.normalized = normalized; if (extensionMode) { fixture.json.extensionsUsed = ["KHR_mesh_quantization"]; if (extensionMode === "required") { fixture.json.extensionsRequired = ["KHR_mesh_quantization"]; } } await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(fixture)), ), ).rejects.toMatchObject({ code }); }, ); it.each([ ["unnormalized signed BYTE", 5120, false], ["normalized UNSIGNED_BYTE", 5121, true], ] as const)( "rejects quantized NORMAL encoded as %s before WebIO", async (_name, componentType, normalized) => { const fixture = createNormalizedQuantizedFixture(); const normal = fixture.json.accessors![1]! as unknown as Record< string, unknown >; normal.componentType = componentType; normal.normalized = normalized; await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(fixture)), ), ).rejects.toMatchObject({ category: "geometry", code: "invalid-normal-accessor", }); }, ); it.each([ ["out-of-range", [0, 3]], ["duplicate", [1, 1]], ["unordered", [2, 1]], ] as const)( "rejects %s sparse accessor indices during bounded preflight", (_name, sparseIndices) => { const fixture = createSparsePositionFixture(sparseIndices); expect(() => preflightGltfJsonDocument( fixture.json, { "mesh.bin": new Uint8Array(fixture.bin) }, 0, TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, new AbortController().signal, ), ).toThrow( expect.objectContaining({ category: "format", code: "invalid-sparse-indices", }), ); }, ); it("rejects unknown required extensions before GLTF Transform", async () => { const fixture = createPrimitiveFixture(); fixture.json.extensionsUsed = ["VENDOR_unknown_geometry"]; fixture.json.extensionsRequired = ["VENDOR_unknown_geometry"]; await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(fixture)), ), ).rejects.toMatchObject({ category: "format", code: "unsupported-required-extension", }); }); it.each(["EXT_mesh_gpu_instancing", "KHR_mesh_primitive_restart"])( "rejects decoder-registered but semantically unsupported required extension %s", async (extensionName) => { const fixture = createPrimitiveFixture(); fixture.json.extensionsUsed = [extensionName]; fixture.json.extensionsRequired = [extensionName]; await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(fixture)), ), ).rejects.toMatchObject({ category: "format", code: "unsupported-required-extension", }); }, ); it("decodes genuine Draco and Meshopt GLBs with bundled dependencies and no fetch", async () => { const fetchSpy = vi.fn(() => Promise.reject(new Error("network disabled"))); vi.stubGlobal("fetch", fetchSpy); const [draco, meshopt] = await Promise.all([ toolcraftGlbModelFormatAdapter.decode( contextFor(transferForGlbBytes(REAL_DRACO_GLB, "draco.glb")), ), toolcraftGlbModelFormatAdapter.decode( contextFor(transferForGlbBytes(REAL_MESHOPT_GLB, "meshopt.glb")), ), ]); expect(draco.document.primitives[0]).toMatchObject({ indices: new Uint32Array([0, 1, 2]), positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), }); expect(meshopt.document.primitives[0]).toMatchObject({ indices: new Uint32Array([0, 1, 2]), positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), }); expect(fetchSpy).not.toHaveBeenCalled(); }); it("validates real Draco decoded counts against accessor declarations immediately after decode", async () => { const mismatched = rewriteGlbJson(REAL_DRACO_GLB, (json) => { json.accessors![1]!.count = 2; json.accessors![2]!.count = 2; }); await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(transferForGlbBytes(mismatched, "draco-count.glb")), ), ).rejects.toMatchObject({ category: "geometry", code: "decoded-accessor-count-mismatch", }); }); it("rejects remote appearance resources without fetching or dropping material factors", async () => { const fetchSpy = vi.fn(() => Promise.reject(new Error("network disabled"))); vi.stubGlobal("fetch", fetchSpy); const result = await toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(createAppearanceFixture())), ); expect(result.diagnostics).toEqual(expect.arrayContaining([ expect.objectContaining({ affectedCount: 1, code: "ignored-appearance-extensions", severity: "info", }), expect.objectContaining({ affectedCount: 1, code: "missing-appearance-resource", explanation: expect.stringContaining("https://example.invalid"), severity: "warning", }), ])); expect(result.document.version).toBe(2); expect(fetchSpy).not.toHaveBeenCalled(); }); it("diagnoses extension-only appearance, node visibility, and cameras that are stripped", async () => { const result = await toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(createExtensionOnlyBehaviorFixture())), ); expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ affectedCount: 1, code: "ignored-appearance-extensions", }), expect.objectContaining({ affectedCount: 1, code: "ignored-node-visibility", }), expect.objectContaining({ affectedCount: 1, code: "ignored-cameras" }), ]), ); }); it("diagnoses optional extension semantics stripped from fallback geometry", async () => { const fixture = createPrimitiveFixture(); fixture.json.extensionsUsed = ["EXT_mesh_features"]; fixture.json.meshes![0]!.primitives[0]!.extensions = { EXT_mesh_features: {}, }; const result = await toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(fixture)), ); expect(result.diagnostics).toContainEqual( expect.objectContaining({ affectedCount: 1, code: "ignored-optional-extensions", severity: "info", }), ); }); it("keeps static base geometry and diagnoses ignored skins, rigs, morphs, and clips", async () => { const result = await toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(createIgnoredFeaturesFixture())), ); expect(result.document.primitives[0]!.positions).toEqual( new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), ); expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "ignored-skins-and-rigs", severity: "info" }), expect.objectContaining({ code: "ignored-morph-targets", severity: "info" }), expect.objectContaining({ code: "ignored-animation-clips", severity: "info" }), ]), ); }); });