import { describe, expect, it } from "vitest"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "./model-import-limit-values"; import type { ToolcraftModelDiagnostic, ToolcraftModelTopologyDiagnostic, ToolcraftModelWorkerAnalysisSummary, } from "./model-import-types"; import { mergeToolcraftModelAnalysisDiagnostics, projectToolcraftModelAnalysisSummary, } from "./model-analysis-summary"; function diagnostic( severity: ToolcraftModelTopologyDiagnostic["severity"], ): ToolcraftModelTopologyDiagnostic { return { affectedCount: 2, code: "boundary-edges", explanation: `${severity} topology finding`, primitiveId: "primitive:0", primitiveIndex: 0, severity, }; } function workerSummary( outcome: ToolcraftModelWorkerAnalysisSummary["outcome"], diagnostics: readonly ToolcraftModelTopologyDiagnostic[], ): ToolcraftModelWorkerAnalysisSummary { return { analyzerVersion: "toolcraft-topology@test", diagnostics, limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, outcome, profile: "realtime-mesh", statistics: { boundaryEdgeCount: 7, componentCount: 3, decodedBytes: 4096, edgeCount: 12, estimatedPeakWorkerBytes: 8192, estimatedRepairBytes: 2048, nodeCount: 2, nonManifoldEdgeCount: 1, nonManifoldVertexCount: 1, primitiveCount: 1, triangleCount: 4, unusedVertexCount: 0, vertexCount: 6, }, }; } function expectProjectionFailure( summary: ToolcraftModelWorkerAnalysisSummary, repairPlanRef?: string, ): void { let rejection: unknown; try { projectToolcraftModelAnalysisSummary(summary, { repairPlanRef }); } catch (error) { rejection = error; } expect(rejection).toMatchObject({ category: "topology", code: "model-analysis-projection-invalid", }); } describe("Toolcraft model analysis summary projection", () => { it("maps an accepted warning result to clean while preserving compact diagnostics and statistics", () => { const sourceDiagnostic = diagnostic("warning"); const result = projectToolcraftModelAnalysisSummary( workerSummary("warning", [sourceDiagnostic]), ); expect(result).toEqual({ boundaryEdges: 7, diagnostics: [{ affectedCount: 2, code: "boundary-edges", explanation: "warning topology finding", primitiveId: "primitive:0", severity: "warning", }], disconnectedComponents: 3, nonManifoldEdges: 1, outcome: "clean", triangles: 4, vertices: 6, }); expect(Object.isFrozen(result)).toBe(true); expect(Object.isFrozen(result.diagnostics)).toBe(true); expect(Object.isFrozen(result.diagnostics[0])).toBe(true); expect(result.diagnostics[0]).not.toBe(sourceDiagnostic); }); it("requires and preserves a repair plan ref only for repairable outcomes", () => { const result = projectToolcraftModelAnalysisSummary( workerSummary("repairable", [diagnostic("repairable")]), { repairPlanRef: "toolcraft:model-repair-plan:sha256:plan" }, ); expect(result).toMatchObject({ outcome: "repairable", repairPlanRef: "toolcraft:model-repair-plan:sha256:plan", }); expectProjectionFailure( workerSummary("repairable", [diagnostic("repairable")]), ); expectProjectionFailure( workerSummary("clean", [diagnostic("info")]), "toolcraft:model-repair-plan:sha256:plan", ); expectProjectionFailure( workerSummary("warning", [diagnostic("warning")]), "toolcraft:model-repair-plan:sha256:plan", ); expectProjectionFailure( workerSummary("fatal", [diagnostic("fatal")]), "toolcraft:model-repair-plan:sha256:plan", ); expectProjectionFailure( workerSummary("repairable", [diagnostic("repairable")]), " repair-plan ", ); }); it("projects clean and fatal outcomes without changing their lifecycle meaning", () => { expect(projectToolcraftModelAnalysisSummary( workerSummary("clean", [diagnostic("info")]), ).outcome).toBe("clean"); expect(projectToolcraftModelAnalysisSummary( workerSummary("fatal", [diagnostic("fatal")]), ).outcome).toBe("fatal"); }); it("merges unique decode warnings without duplicating topology diagnostics", () => { const topology = diagnostic("warning"); const projected = projectToolcraftModelAnalysisSummary( workerSummary("warning", [topology]), ); const missingTexture: ToolcraftModelDiagnostic = { affectedCount: 1, code: "missing-appearance-resource", explanation: "A local base-color texture could not be restored.", severity: "warning", }; const merged = mergeToolcraftModelAnalysisDiagnostics(projected, [ missingTexture, topology, missingTexture, ]); expect(merged.diagnostics.map(({ code }) => code)).toEqual([ "missing-appearance-resource", "boundary-edges", ]); expect(merged.outcome).toBe("clean"); expect(Object.isFrozen(merged.diagnostics[0])).toBe(true); }); it.each([ ["clean with warning", "clean", "warning"], ["clean with repairable", "clean", "repairable"], ["warning without warning", "warning", "info"], ["warning with repairable", "warning", "repairable"], ["repairable without repairable", "repairable", "warning"], ["repairable with fatal", "repairable", "fatal"], ["fatal without fatal", "fatal", "repairable"], ] as const)("rejects %s diagnostics", (_name, outcome, severity) => { expectProjectionFailure( workerSummary(outcome, [diagnostic(severity)]), outcome === "repairable" ? "repair-plan:valid" : undefined, ); }); });