import { describe, expect, it } from "vitest";
import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js";
import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js";
import type { PathPlanRequest, PathPlanResult, PathValidationResult } from "../../src/kdl/types.js";
const PLANAR_URDF = `
`;
async function createRobot() {
const runtime = createKdlWorkerRuntime();
await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{}] });
const response = await dispatchKdlRpcRequest(runtime, {
id: 2,
method: "loadRobotFromUrdf",
payload: [
PLANAR_URDF,
{
robotId: "path",
baseLink: "base_link",
tipLink: "tool0"
}
]
});
expect(response.ok).toBe(true);
return { runtime, handle: response.result as number };
}
function pathRequest(overrides: Partial = {}): PathPlanRequest {
return {
startJoints: [0, 0],
sampleTime: 0.05,
segments: [
{
id: "move-home",
motion: "MOVEJ",
target: {
id: "joint_goal",
joints: [Math.PI / 2, 0.2]
},
speed: { kind: "joint_abs", velocity: 1, acceleration: 4 },
zone: { kind: "fine" },
sourceMap: { line: 10, column: 5 }
},
{
id: "line-out",
motion: "MOVEL",
target: {
id: "line_goal",
pose: {
position: [0, 0.4, 0],
quaternion: [0, 0, 0, 1]
}
},
speed: { kind: "linear", velocity: 0.2, acceleration: 1 },
zone: { kind: "fine" },
sourceMap: { line: 11, column: 5 }
}
],
...overrides
};
}
describe("planPath and validatePath", () => {
it("plans multiple motion segments and merges points with segment metadata", async () => {
const { runtime, handle } = await createRobot();
const response = await dispatchKdlRpcRequest(runtime, {
id: 3,
method: "planPath",
payload: [handle, pathRequest()]
});
expect(response.ok).toBe(true);
const result = response.result as PathPlanResult;
expect(result.ok).toBe(true);
expect(result.segments).toHaveLength(2);
expect(result.points.length).toBeGreaterThan(result.segments[0]!.points.length);
expect(result.points[0]).toMatchObject({
index: 0,
time: 0,
segmentId: "move-home",
targetId: "joint_goal",
sourceMap: { line: 10 }
});
expect(result.points.at(-1)).toMatchObject({
segmentId: "line-out",
targetId: "line_goal",
sourceMap: { line: 11 }
});
expect(result.points.at(-1)?.tcp.position[0]).toBeCloseTo(0, 5);
expect(result.points.at(-1)?.tcp.position[1]).toBeCloseTo(0.4, 5);
expect(result.duration).toBeCloseTo(result.segments[0]!.duration + result.segments[1]!.duration);
for (let index = 1; index < result.points.length; index += 1) {
expect(result.points[index]!.time).toBeGreaterThan(result.points[index - 1]!.time);
expect(result.points[index]!.index).toBe(index);
}
});
it("validates a path and returns per-segment reports", async () => {
const { runtime, handle } = await createRobot();
const response = await dispatchKdlRpcRequest(runtime, {
id: 4,
method: "validatePath",
payload: [handle, pathRequest()]
});
expect(response.ok).toBe(true);
const result = response.result as PathValidationResult;
expect(result.ok).toBe(true);
expect(result.reachable).toBe(true);
expect(result.cycleTime).toBeGreaterThan(0);
expect(result.segmentReports.map((report) => report.segmentId)).toEqual(["move-home", "line-out"]);
expect(result.segmentReports[0]).toMatchObject({
ok: true,
motion: "MOVEJ"
});
expect(result.segmentReports[1]).toMatchObject({
ok: true,
motion: "MOVEL",
maxCartesianError: 0
});
});
it("returns KDL_PATH_EMPTY for empty path requests", async () => {
const { runtime, handle } = await createRobot();
const response = await dispatchKdlRpcRequest(runtime, {
id: 5,
method: "planPath",
payload: [
handle,
pathRequest({
segments: []
})
]
});
expect(response.ok).toBe(true);
expect(response.result).toMatchObject({
ok: false,
duration: 0,
segments: [],
points: [],
diagnostics: [
{
severity: "error",
code: "KDL_PATH_EMPTY"
}
]
});
});
});