import { describe, expect, it } from "vitest"; import type { ToolcraftModelDocument, ToolcraftModelNode, } from "./model-document"; import { createModelBounds, createModelNode, createModelPrimitive, createValidModelDocument, IDENTITY_MODEL_MATRIX, } from "./model-document-test-support"; import { assertValidToolcraftModelDocument, ToolcraftModelDocumentValidationError, type ToolcraftModelDocumentValidationFailureCode, validateToolcraftModelDocument, } from "./model-document-validation"; function expectFailure( document: unknown, code: ToolcraftModelDocumentValidationFailureCode, ): void { expect(validateToolcraftModelDocument(document)).toMatchObject({ code, ok: false, }); } function documentWithPrimitive( primitive: ReturnType, ): ToolcraftModelDocument { return createValidModelDocument({ bounds: primitive.bounds, primitives: [primitive], }); } describe("canonical model document validation", () => { it("accepts a structurally renderable version 1 document", () => { expect(validateToolcraftModelDocument(createValidModelDocument())).toEqual({ ok: true, }); }); it.each([ [ "document version", { ...createValidModelDocument(), version: 3 }, "unsupported-document-version", ], [ "canonical schema version", { ...createValidModelDocument(), provenance: { ...createValidModelDocument().provenance, canonicalSchemaVersion: 2, }, }, "invalid-canonical-schema-version", ], [ "source format", { ...createValidModelDocument(), provenance: { ...createValidModelDocument().provenance, sourceFormat: "usd", }, }, "invalid-source-format", ], ] as const)("rejects an invalid %s", (_label, document, code) => { expectFailure(document, code); }); it.each([ [ "positions must use Float32Array", createModelPrimitive({ positions: [0, 0, 0, 2, 0, 0, 0, 3, 0] as unknown as Float32Array, }), "invalid-positions-type", ], [ "positions must contain XYZ triples", createModelPrimitive({ positions: new Float32Array(8) }), "invalid-position-cardinality", ], [ "positions must be finite", createModelPrimitive({ positions: new Float32Array([ Number.NaN, 0, 0, 2, 0, 0, 0, 3, 0, ]), }), "non-finite-position", ], [ "indices must use Uint32Array", createModelPrimitive({ indices: new Uint16Array([0, 1, 2]) as unknown as Uint32Array, }), "invalid-indices-type", ], [ "an empty index buffer is not renderable", createModelPrimitive({ indices: new Uint32Array() }), "empty-primitive", ], [ "indices must contain triangle triples", createModelPrimitive({ indices: new Uint32Array([0, 1, 2, 0]) }), "invalid-triangle-cardinality", ], [ "indices must resolve within the primitive", createModelPrimitive({ indices: new Uint32Array([0, 1, 9]) }), "index-out-of-range", ], [ "at least one triangle must have nonzero area", createModelPrimitive({ bounds: createModelBounds([0, 0, 0], [2, 0, 0]), positions: new Float32Array([ 0, 0, 0, 1, 0, 0, 2, 0, 0, ]), }), "no-renderable-triangle", ], [ "normals must use Float32Array", createModelPrimitive({ normals: new Uint32Array(9) as unknown as Float32Array, }), "invalid-normals-type", ], [ "normal count must match positions", createModelPrimitive({ normals: new Float32Array(6) }), "normal-count-mismatch", ], [ "normals must be finite", createModelPrimitive({ normals: new Float32Array([ 0, 0, Number.POSITIVE_INFINITY, 0, 0, 1, 0, 0, 1, ]), }), "non-finite-normal", ], ] satisfies readonly [ string, ReturnType, ToolcraftModelDocumentValidationFailureCode, ][])("rejects unsafe primitive geometry: %s", (_label, primitive, code) => { expectFailure(documentWithPrimitive(primitive), code); }); it("rejects a document with no primitives", () => { expectFailure( createValidModelDocument({ primitives: [] }), "empty-document-primitives", ); }); it.each([ [ "non-finite primitive bound", createValidModelDocument({ primitives: [ createModelPrimitive({ bounds: createModelBounds( [0, 0, 0], [Number.POSITIVE_INFINITY, 3, 0], ), }), ], }), "non-finite-bound", ], [ "unordered primitive bound", createValidModelDocument({ primitives: [ createModelPrimitive({ bounds: createModelBounds([3, 0, 0], [2, 3, 0]), }), ], }), "unordered-bound", ], [ "primitive bound inconsistent with positions", createValidModelDocument({ primitives: [ createModelPrimitive({ bounds: createModelBounds([0, 0, 0], [4, 3, 0]), }), ], }), "primitive-bounds-mismatch", ], [ "non-finite document bound", createValidModelDocument({ bounds: createModelBounds( [0, Number.NaN, 0], [2, 3, 0], ), }), "non-finite-bound", ], [ "document bound inconsistent with primitives", createValidModelDocument({ bounds: createModelBounds([0, 0, 0], [4, 3, 0]), }), "document-bounds-mismatch", ], ] satisfies readonly [ string, ToolcraftModelDocument, ToolcraftModelDocumentValidationFailureCode, ][])("rejects invalid bounds: %s", (_label, document, code) => { expectFailure(document, code); }); it.each([ [ "matrix cardinality", createModelNode({ localMatrix: [1, 0] as unknown as ToolcraftModelNode["localMatrix"] }), "invalid-node-matrix", ], [ "finite matrix components", createModelNode({ localMatrix: IDENTITY_MODEL_MATRIX.map((value, index) => index === 7 ? Number.NaN : value, ) as unknown as ToolcraftModelNode["localMatrix"], }), "non-finite-node-matrix", ], ] satisfies readonly [ string, ToolcraftModelNode, ToolcraftModelDocumentValidationFailureCode, ][])("rejects an invalid node %s", (_label, node, code) => { expectFailure(createValidModelDocument({ nodes: [node] }), code); }); it.each([ [ "empty node id", [createModelNode({ id: "" })], [""], "empty-id", ], [ "duplicate node id", [createModelNode(), createModelNode({ name: "Duplicate" })], ["node-1"], "duplicate-id", ], [ "missing root", [createModelNode()], ["missing"], "missing-node-reference", ], [ "duplicate root", [createModelNode()], ["node-1", "node-1"], "duplicate-root", ], [ "duplicate child", [ createModelNode({ children: ["child", "child"] }), createModelNode({ id: "child", name: "Child", primitiveIds: [] }), ], ["node-1"], "duplicate-child", ], [ "missing child", [createModelNode({ children: ["missing"] })], ["node-1"], "missing-node-reference", ], [ "duplicate primitive reference", [createModelNode({ primitiveIds: ["primitive-1", "primitive-1"] })], ["node-1"], "duplicate-primitive-reference", ], [ "missing primitive reference", [createModelNode({ primitiveIds: ["missing"] })], ["node-1"], "missing-primitive-reference", ], [ "hierarchy cycle", [ createModelNode({ children: ["child"] }), createModelNode({ children: ["node-1"], id: "child", name: "Child", primitiveIds: [], }), ], ["node-1"], "hierarchy-cycle", ], [ "multiple parents", [ createModelNode({ children: ["parent-a", "parent-b"] }), createModelNode({ children: ["leaf"], id: "parent-a", name: "Parent A", primitiveIds: [], }), createModelNode({ children: ["leaf"], id: "parent-b", name: "Parent B", primitiveIds: [], }), createModelNode({ id: "leaf", name: "Leaf", primitiveIds: [] }), ], ["node-1"], "multiple-parents", ], [ "root with a parent", [ createModelNode(), createModelNode({ children: ["node-1"], id: "other-root", name: "Other root", primitiveIds: [], }), ], ["node-1", "other-root"], "root-has-parent", ], [ "unreachable node", [ createModelNode(), createModelNode({ id: "detached", name: "Detached", primitiveIds: [], }), ], ["node-1"], "unreachable-node", ], ] satisfies readonly [ string, readonly ToolcraftModelNode[], readonly string[], ToolcraftModelDocumentValidationFailureCode, ][])("rejects an ambiguous hierarchy: %s", (_label, nodes, roots, code) => { expectFailure( createValidModelDocument({ nodes, rootNodeIds: roots }), code, ); }); it("rejects duplicate primitive ids", () => { expectFailure( createValidModelDocument({ primitives: [createModelPrimitive(), createModelPrimitive()], }), "duplicate-id", ); }); it("rejects a primitive that no reachable node references", () => { expectFailure( createValidModelDocument({ primitives: [ createModelPrimitive(), createModelPrimitive({ id: "primitive-2" }), ], }), "unreferenced-primitive", ); }); it.each([ [ "maxNodes", createValidModelDocument({ nodes: [ createModelNode({ children: ["child"] }), createModelNode({ id: "child", name: "Child", primitiveIds: [] }), ], }), { maxNodes: 1 }, "max-nodes-exceeded", ], [ "maxPrimitives", createValidModelDocument({ nodes: [ createModelNode({ primitiveIds: ["primitive-1", "primitive-2"] }), ], primitives: [ createModelPrimitive(), createModelPrimitive({ id: "primitive-2" }), ], }), { maxPrimitives: 1 }, "max-primitives-exceeded", ], [ "maxVertices", createValidModelDocument(), { maxVertices: 2 }, "max-vertices-exceeded", ], [ "maxTriangles", documentWithPrimitive( createModelPrimitive({ bounds: createModelBounds([0, 0, 0], [2, 3, 0]), indices: new Uint32Array([0, 1, 2, 0, 2, 3]), normals: new Float32Array([ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ]), positions: new Float32Array([ 0, 0, 0, 2, 0, 0, 0, 3, 0, 2, 3, 0, ]), }), ), { maxTriangles: 1 }, "max-triangles-exceeded", ], ] as const)( "honors a caller-narrowed %s limit", (_label, document, limits, code) => { expect(validateToolcraftModelDocument(document, limits)).toMatchObject({ code, ok: false, }); }, ); it("rejects an attempt to widen a protected hard ceiling", () => { expect( validateToolcraftModelDocument(createValidModelDocument(), { maxNodes: Number.MAX_SAFE_INTEGER, }), ).toMatchObject({ code: "invalid-limit-options", ok: false, path: "limits.maxNodes", }); }); it("throws a typed validation error with the same failure details", () => { const document = documentWithPrimitive( createModelPrimitive({ indices: new Uint32Array([0, 1, 9]) }), ); expect(() => assertValidToolcraftModelDocument(document)).toThrow( ToolcraftModelDocumentValidationError, ); try { assertValidToolcraftModelDocument(document); } catch (error) { expect(error).toMatchObject({ code: "index-out-of-range", path: "primitives[0].indices[2]", }); } }); });