import { describe, expect, it } from "vitest"; import { KdlStructuredError } from "../../src/kdl/rpc.js"; import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js"; import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js"; import { loadRobotFromUrdfModel } from "../../src/robot/urdfParser.js"; const SIMPLE_URDF = ` `; describe("URDF to NormalizedRobotModel", () => { it("parses links, joints, origins, axes, limits, stable active joint names, and source hash", () => { const model = loadRobotFromUrdfModel(SIMPLE_URDF, { robotId: "r1", baseLink: "base_link", tipLink: "tool0" }); expect(model).toMatchObject({ robotId: "r1", name: "simple6", baseLink: "base_link", tipLink: "tool0", activeJointNames: ["joint_1", "joint_2"], source: { type: "urdf" } }); expect(model.source.urdfHash).toHaveLength(64); expect(model.links.map((link) => link.name)).toEqual(["base_link", "link_1", "link_2", "tool0"]); expect(model.joints[0]).toMatchObject({ name: "joint_1", type: "revolute", parent: "base_link", child: "link_1", origin: { xyz: [0, 0, 0.1], rpy: [0, 0, 0] }, axis: [0, 0, 1] }); expect(model.limits).toEqual([ { name: "joint_1", lower: -3.14, upper: 3.14, velocity: 2.5, acceleration: 5 }, { name: "joint_2", lower: 0, upper: 0.4, velocity: 0.3, acceleration: 1.2 } ]); }); it("applies joint order and limit overrides", () => { const model = loadRobotFromUrdfModel(SIMPLE_URDF, { robotId: "r1", baseLink: "base_link", tipLink: "tool0", jointOrder: ["joint_2", "joint_1"], overrideLimits: [{ name: "joint_2", velocity: 0.2 }] }); expect(model.activeJointNames).toEqual(["joint_2", "joint_1"]); expect(model.limits[0]).toMatchObject({ name: "joint_2", velocity: 0.2 }); }); it("returns structured diagnostics for disconnected base and tip links", () => { let thrown: unknown; try { loadRobotFromUrdfModel(SIMPLE_URDF, { robotId: "r1", baseLink: "tool0", tipLink: "base_link" }); } catch (error) { thrown = error; } expect(thrown).toBeInstanceOf(KdlStructuredError); expect(thrown).toMatchObject({ code: "KDL_INVALID_MODEL", diagnostics: [ { severity: "error", code: "KDL_INVALID_MODEL" } ] }); }); it("rejects unsupported joint types", () => { const urdf = SIMPLE_URDF.replace('type="prismatic"', 'type="floating"'); let thrown: unknown; try { loadRobotFromUrdfModel(urdf, { robotId: "r1", baseLink: "base_link", tipLink: "tool0" }); } catch (error) { thrown = error; } expect(thrown).toBeInstanceOf(KdlStructuredError); expect(thrown).toMatchObject({ code: "KDL_INVALID_MODEL", message: expect.stringContaining("Unsupported joint type") }); }); it("supports RobotHandle lifecycle through the worker runtime", async () => { const runtime = createKdlWorkerRuntime(); await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{}] }); const createResponse = await dispatchKdlRpcRequest(runtime, { id: 2, method: "loadRobotFromUrdf", payload: [ SIMPLE_URDF, { robotId: "r1", baseLink: "base_link", tipLink: "tool0" } ] }); expect(createResponse).toMatchObject({ ok: true, result: 1 }); const infoResponse = await dispatchKdlRpcRequest(runtime, { id: 3, method: "getRobotInfo", payload: [1] }); expect(infoResponse.result).toMatchObject({ handle: 1, robotId: "r1", name: "simple6", dof: 2, jointNames: ["joint_1", "joint_2"] }); const limitsResponse = await dispatchKdlRpcRequest(runtime, { id: 4, method: "getJointLimits", payload: [1] }); expect(limitsResponse.result).toEqual([ { name: "joint_1", lower: -3.14, upper: 3.14, velocity: 2.5, acceleration: 5 }, { name: "joint_2", lower: 0, upper: 0.4, velocity: 0.3, acceleration: 1.2 } ]); const destroyResponse = await dispatchKdlRpcRequest(runtime, { id: 5, method: "destroyRobot", payload: [1] }); expect(destroyResponse.ok).toBe(true); const afterDestroy = await dispatchKdlRpcRequest(runtime, { id: 6, method: "getRobotInfo", payload: [1] }); expect(afterDestroy).toMatchObject({ ok: false, error: { code: "KDL_INVALID_HANDLE" } }); }); });