import { afterEach, describe, expect, it, vi } from "vitest"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "../model-import-limits"; import { contextFor, createCoreTriangleDocumentFixture, createGlbTransfer, createHierarchyFixture, createIgnoredFeaturesFixture, createPrimitiveFixture, encodeGlb, REAL_DRACO_GLB, transferForGlbBytes, } from "../test-fixtures/model-fixtures"; import { canonicalizeGltfDocument } from "./gltf-document-canonicalizer"; import { toolcraftGlbModelFormatAdapter } from "./gltf-model-format-adapter"; const EMPTY_FEATURES = Object.freeze({ animationClips: 0, cameras: 0, ignoredAppearanceExtensions: 0, morphTargets: 0, nodeVisibility: 0, optionalExtensions: 0, skins: 0, }); afterEach(() => { vi.unstubAllGlobals(); }); describe("glTF model format adapters", () => { it("honors cancellation before decode and from inside canonical copy loops", async () => { const aborted = new AbortController(); aborted.abort(); await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(createPrimitiveFixture()), { signal: aborted.signal, }), ), ).rejects.toMatchObject({ name: "AbortError" }); let reads = 0; const loopSignal = { get aborted() { reads += 1; return reads > 8; }, get reason() { return new DOMException("cancelled in loop", "AbortError"); }, } as AbortSignal; const positions = Array.from({ length: 900 }, (_, index) => index % 3 === 2 ? 0 : index / 3, ); const indices = Array.from({ length: 300 }, (_, index) => index); await expect( toolcraftGlbModelFormatAdapter.decode( contextFor( createGlbTransfer(createPrimitiveFixture({ indices, positions })), { signal: loopSignal }, ), ), ).rejects.toMatchObject({ name: "AbortError" }); expect(reads).toBeGreaterThan(8); }); it.each([ ["vertices", { maxVertices: 2 }, "max-vertices-exceeded"], ["triangles", { maxTriangles: 1 }, "max-triangles-exceeded"], ["decoded bytes", { maxDecodedBytes: 32 }, "decoded-byte-limit-exceeded"], ["worker bytes", { maxEstimatedWorkerBytes: 64 }, "estimated-worker-memory-limit-exceeded"], ["nodes", { maxNodes: 1 }, "max-nodes-exceeded"], ["primitives", { maxPrimitives: 1 }, "max-primitives-exceeded"], ] as const)("enforces protected %s limits before allocation", async (name, limits, code) => { const fixture = name === "triangles" ? createPrimitiveFixture({ indices: [0, 1, 2, 0, 2, 3], positions: [0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0], }) : name === "nodes" ? createIgnoredFeaturesFixture() : name === "primitives" ? createHierarchyFixture() : createPrimitiveFixture(); await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(fixture), { limits }), ), ).rejects.toMatchObject({ category: "resource-limit", code }); }); it.each([ ["nodes", { maxNodes: 1 }, "max-nodes-exceeded"], ["primitives", { maxPrimitives: 1 }, "max-primitives-exceeded"], ] as const)( "rechecks canonical %s limits before reading accessor elements", (kind, limit, code) => { const fixture = createCoreTriangleDocumentFixture({ nodeCount: kind === "nodes" ? 2 : 1, primitiveCount: kind === "primitives" ? 2 : 1, }); const elementSpy = vi.spyOn(fixture.positions, "getElement"); expect(() => canonicalizeGltfDocument(fixture.document, { adapterVersion: "test@1", features: EMPTY_FEATURES, limits: { ...TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, ...limit }, missingAppearanceBindings: [], retainedWorkerBytes: 0, signal: new AbortController().signal, sourceFormat: "glb", }), ).toThrow(expect.objectContaining({ code })); expect(elementSpy).not.toHaveBeenCalled(); }, ); it("charges aggregate instanced primitive references before geometry or node arrays", () => { const fixture = createCoreTriangleDocumentFixture({ instanceMeshOnEveryNode: true, nodeCount: 100, }); const positionRead = vi.spyOn(fixture.positions, "getElement"); expect(() => canonicalizeGltfDocument(fixture.document, { adapterVersion: "test@1", features: EMPTY_FEATURES, limits: { ...TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, maxEstimatedWorkerBytes: 67_000, }, missingAppearanceBindings: [], retainedWorkerBytes: 0, signal: new AbortController().signal, sourceFormat: "glb", }), ).toThrow( expect.objectContaining({ code: "estimated-worker-memory-limit-exceeded", }), ); expect(positionRead).not.toHaveBeenCalled(); }); it("checks canonical index output bytes before allocating or copying", () => { const fixture = createCoreTriangleDocumentFixture({ indices: [0, 1, 2, 0, 2, 3], positions: [0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0], }); const positionRead = vi.spyOn(fixture.positions, "getElement"); const indexRead = vi.spyOn(fixture.indices, "getScalar"); const uint32Allocation = vi.fn(); const observedUint32Array = new Proxy(Uint32Array, { construct(target, argumentsList, newTarget) { uint32Allocation(); return Reflect.construct(target, argumentsList, newTarget); }, }); vi.stubGlobal("Uint32Array", observedUint32Array); expect(() => canonicalizeGltfDocument(fixture.document, { adapterVersion: "test@1", features: EMPTY_FEATURES, limits: { ...TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, maxDecodedBytes: 70, }, missingAppearanceBindings: [], retainedWorkerBytes: 0, signal: new AbortController().signal, sourceFormat: "glb", }), ).toThrow( expect.objectContaining({ code: "decoded-byte-limit-exceeded" }), ); expect(positionRead).not.toHaveBeenCalled(); expect(indexRead).not.toHaveBeenCalled(); expect(uint32Allocation).not.toHaveBeenCalled(); }); it("preflights compressed expansion before invoking a geometry decoder", async () => { await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(transferForGlbBytes(REAL_DRACO_GLB, "draco.glb"), { limits: { maxVertices: 2 }, }), ), ).rejects.toMatchObject({ category: "resource-limit", code: "max-vertices-exceeded", }); }); it("snapshots bundle bytes synchronously and emits no engine objects", async () => { const transfer = createGlbTransfer(createPrimitiveFixture()); const pending = toolcraftGlbModelFormatAdapter.decode(contextFor(transfer)); new Uint8Array(transfer.sourceFiles[0]!.bytes).fill(0); const result = await pending; expect(Object.keys(result.document).sort()).toEqual([ "bounds", "materials", "nodes", "primitives", "provenance", "rootNodeIds", "textures", "version", ]); expect(Object.keys(result.document.primitives[0]!).sort()).toEqual([ "bounds", "id", "indices", "positions", ]); expect(result.document.primitives[0]!.positions).toBeInstanceOf(Float32Array); expect(result.document.primitives[0]!.indices).toBeInstanceOf(Uint32Array); expect(JSON.stringify(result.document)).not.toMatch( /Object3D|BufferGeometry|Material|Scene/u, ); }); it("rejects malformed GLB chunk bounds deterministically", async () => { const malformed = encodeGlb(createPrimitiveFixture()).slice(); new DataView(malformed.buffer).setUint32(12, 0x7fff_ffff, true); await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(transferForGlbBytes(malformed)), ), ).rejects.toMatchObject({ category: "format", code: "malformed-glb" }); });});