import { describe, expect, it } from "vitest"; import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js"; import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js"; import type { TrapProfileResult, TrapSample } from "../../src/kdl/types.js"; async function createRuntime() { const runtime = createKdlWorkerRuntime(); await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{}] }); return runtime; } function expectMonotonic(samples: TrapSample[]) { for (let index = 1; index < samples.length; index += 1) { expect(samples[index]!.time).toBeGreaterThan(samples[index - 1]!.time); expect(samples[index]!.s).toBeGreaterThanOrEqual(samples[index - 1]!.s); } } describe("trapezoid velocity profile API", () => { it("creates a trapezoid profile when the path can reach max velocity", async () => { const runtime = await createRuntime(); const response = await dispatchKdlRpcRequest(runtime, { id: 2, method: "makeTrapProfile", payload: [ 2, { maxVelocity: 1, maxAcceleration: 1, sampleTime: 0.25 } ] }); expect(response.ok).toBe(true); const profile = response.result as TrapProfileResult; expect(profile).toMatchObject({ ok: true, type: "trapezoid", length: 2, duration: 3, tAccel: 1, tConst: 1, tDecel: 1, vPeak: 1, diagnostics: [] }); expect(profile.samples[0]).toMatchObject({ index: 0, time: 0, s: 0 }); expect(profile.samples.at(-1)).toMatchObject({ time: 3, s: 1 }); expect(profile.samples.find((sample) => sample.time === 1)?.s).toBeCloseTo(0.25); expect(profile.samples.find((sample) => sample.time === 1)?.sd).toBeCloseTo(0.5); expectMonotonic(profile.samples); }); it("falls back to a triangle profile for short paths", async () => { const runtime = await createRuntime(); const response = await dispatchKdlRpcRequest(runtime, { id: 3, method: "makeTrapProfile", payload: [ 0.5, { maxVelocity: 2, maxAcceleration: 1, sampleTime: 0.1 } ] }); expect(response.ok).toBe(true); const profile = response.result as TrapProfileResult; expect(profile.type).toBe("triangle"); expect(profile.tConst).toBe(0); expect(profile.vPeak).toBeCloseTo(Math.sqrt(0.5)); expect(profile.duration).toBeCloseTo(2 * Math.sqrt(0.5)); expect(profile.samples[0]?.s).toBe(0); expect(profile.samples.at(-1)?.s).toBe(1); expect(profile.diagnostics).toMatchObject([ { severity: "info", code: "KDL_TRAP_TRIANGLE_PROFILE" } ]); expectMonotonic(profile.samples); }); it("returns samples from sampleTrapProfile with strict endpoint samples", async () => { const runtime = await createRuntime(); const response = await dispatchKdlRpcRequest(runtime, { id: 4, method: "sampleTrapProfile", payload: [ 1, { maxVelocity: 1, maxAcceleration: 2, sampleTime: 0.2 } ] }); expect(response.ok).toBe(true); const samples = response.result as TrapSample[]; expect(samples[0]).toMatchObject({ index: 0, time: 0, s: 0 }); expect(samples.at(-1)?.s).toBe(1); expect(samples.at(-1)?.time).toBeCloseTo(1.5); expectMonotonic(samples); }); it("returns a structured diagnostic for invalid trap profile inputs", async () => { const runtime = await createRuntime(); const response = await dispatchKdlRpcRequest(runtime, { id: 5, method: "makeTrapProfile", payload: [ 1, { maxVelocity: 0, maxAcceleration: 1, sampleTime: 0.01 } ] }); expect(response).toMatchObject({ ok: false, error: { code: "KDL_INVALID_TRAP_PROFILE", diagnostics: [ { severity: "error", code: "KDL_INVALID_TRAP_PROFILE" } ] } }); }); });