import { describe, expect, it } from "vitest"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "../model-import-limits"; import { TOOLCRAFT_MODEL_REPAIR_ALGORITHM_VERSION, type ToolcraftModelRepairPlan, } from "./model-repair-plan"; import { decodeToolcraftModelRepairPlanEnvelope, encodeToolcraftModelRepairPlanEnvelope, } from "./model-repair-plan-codec"; import { TOOLCRAFT_MODEL_TOPOLOGY_ANALYZER_VERSION } from "./model-topology-analysis"; import { digestToolcraftRepairPlanPayload } from "./model-topology-digest"; function repairPlan(indexCount = 20_000): ToolcraftModelRepairPlan { const payload = { algorithmVersion: TOOLCRAFT_MODEL_REPAIR_ALGORITHM_VERSION, analyzerVersion: TOOLCRAFT_MODEL_TOPOLOGY_ANALYZER_VERSION, operations: [{ expectedCountDelta: { triangles: -indexCount, vertices: 0 }, primitiveId: "primitive-0", primitiveIndex: 0, safetyPreconditions: ["indices are in range"], triangleIndices: Array.from({ length: indexCount }, (_, index) => index), type: "remove-invalid-triangles" as const, }], profile: "realtime-mesh" as const, sourceDocumentDigest: `sha256:${"2".repeat(64)}`, }; const planDigest = digestToolcraftRepairPlanPayload(payload); return { ...payload, planDigest, recipeId: `${TOOLCRAFT_MODEL_REPAIR_ALGORITHM_VERSION}:${planDigest}`, }; } describe("repair plan transferable envelope", () => { it("round-trips a large plan through a bounded versioned envelope in the worker", () => { const plan = repairPlan(); const envelope = encodeToolcraftModelRepairPlanEnvelope(plan); expect(envelope).toBeInstanceOf(Uint8Array); expect(envelope.byteLength).toBeGreaterThan(100_000); const decoded = decodeToolcraftModelRepairPlanEnvelope(envelope, { expectedPlanDigest: plan.planDigest, expectedProfile: plan.profile, limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, }); expect(decoded).toEqual(plan); expect(Object.isFrozen(decoded)).toBe(true); expect(Object.isFrozen(decoded.operations)).toBe(true); expect(Object.isFrozen(decoded.operations[0])).toBe(true); expect(Object.isFrozen(decoded.operations[0]!.safetyPreconditions)).toBe(true); expect(Object.isFrozen( (decoded.operations[0] as { triangleIndices: readonly number[] }).triangleIndices, )).toBe(true); }); it("keeps semantic digests stable across object insertion order", () => { const first = { operations: [{ expectedCountDelta: { triangles: 0, vertices: -2 }, primitiveId: "primitive-0", primitiveIndex: 0, removedVertexIndices: [4, 5], retainedVertexIndices: [0, 1, 2, 3], safetyPreconditions: ["stable remap"], type: "compact-unused-vertices", }], profile: "realtime-mesh", }; const second = { profile: "realtime-mesh", operations: [{ type: "compact-unused-vertices", safetyPreconditions: ["stable remap"], retainedVertexIndices: [0, 1, 2, 3], removedVertexIndices: [4, 5], primitiveIndex: 0, primitiveId: "primitive-0", expectedCountDelta: { vertices: -2, triangles: 0 }, }], }; expect(digestToolcraftRepairPlanPayload(first)).toBe( digestToolcraftRepairPlanPayload(second), ); }); it("rejects malformed framing and a forged semantic plan digest", () => { const plan = repairPlan(3); const malformed = encodeToolcraftModelRepairPlanEnvelope(plan); malformed[0] ^= 0xff; expect(() => decodeToolcraftModelRepairPlanEnvelope(malformed, { expectedPlanDigest: plan.planDigest, expectedProfile: plan.profile, limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, })).toThrow(/envelope/u); const forged = { ...plan, planDigest: `sha256:${"0".repeat(64)}`, recipeId: `${TOOLCRAFT_MODEL_REPAIR_ALGORITHM_VERSION}:sha256:${"0".repeat(64)}`, }; const forgedEnvelope = encodeToolcraftModelRepairPlanEnvelope(forged); expect(() => decodeToolcraftModelRepairPlanEnvelope(forgedEnvelope, { expectedPlanDigest: forged.planDigest, expectedProfile: forged.profile, limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, })).toThrow(/digest/u); }); it("enforces the envelope byte limit before JSON parsing", () => { const plan = repairPlan(100); const envelope = encodeToolcraftModelRepairPlanEnvelope(plan); expect(() => decodeToolcraftModelRepairPlanEnvelope(envelope, { expectedPlanDigest: plan.planDigest, expectedProfile: plan.profile, limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, maxByteLength: 64, })).toThrow(/byte limit/u); }); });