agenticros 0.1.18 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/robot-config.test.d.ts +2 -0
- package/dist/__tests__/robot-config.test.d.ts.map +1 -0
- package/dist/__tests__/robot-config.test.js +374 -0
- package/dist/__tests__/robot-config.test.js.map +1 -0
- package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
- package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
- package/dist/__tests__/transport-shorthand.test.js +88 -0
- package/dist/__tests__/transport-shorthand.test.js.map +1 -0
- package/dist/commands/robots.d.ts +57 -0
- package/dist/commands/robots.d.ts.map +1 -0
- package/dist/commands/robots.js +678 -0
- package/dist/commands/robots.js.map +1 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +125 -2
- package/dist/commands/skills.js.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/menu.js +22 -4
- package/dist/menu.js.map +1 -1
- package/dist/util/marketplace.d.ts +57 -0
- package/dist/util/marketplace.d.ts.map +1 -0
- package/dist/util/marketplace.js +144 -0
- package/dist/util/marketplace.js.map +1 -0
- package/dist/util/mcp-discovery.d.ts +57 -0
- package/dist/util/mcp-discovery.d.ts.map +1 -0
- package/dist/util/mcp-discovery.js +153 -0
- package/dist/util/mcp-discovery.js.map +1 -0
- package/dist/util/robot-config.d.ts +167 -0
- package/dist/util/robot-config.d.ts.map +1 -0
- package/dist/util/robot-config.js +375 -0
- package/dist/util/robot-config.js.map +1 -0
- package/dist/util/skills.d.ts +6 -6
- package/dist/util/skills.d.ts.map +1 -1
- package/dist/util/skills.js +14 -9
- package/dist/util/skills.js.map +1 -1
- package/dist/util/transport-shorthand.d.ts +66 -0
- package/dist/util/transport-shorthand.d.ts.map +1 -0
- package/dist/util/transport-shorthand.js +102 -0
- package/dist/util/transport-shorthand.js.map +1 -0
- package/package.json +12 -10
- package/runtime/BUNDLE.json +1 -1
- package/runtime/README.md +62 -1
- package/runtime/package.json +6 -1
- package/runtime/packages/agenticros/openclaw.plugin.json +7 -1
- package/runtime/packages/agenticros/package.json +2 -1
- package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
- package/runtime/packages/agenticros/src/config-page.ts +1 -1
- package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
- package/runtime/packages/agenticros/src/service.ts +120 -2
- package/runtime/packages/agenticros/src/skill-loader.ts +109 -30
- package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
- package/runtime/packages/agenticros/src/tools/index.ts +49 -9
- package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
- package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
- package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
- package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
- package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
- package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
- package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
- package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
- package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
- package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
- package/runtime/packages/agenticros-claude-code/README.md +52 -9
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts +2 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js +773 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts +2 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js +97 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts +2 -2
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js +5 -5
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts +109 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js +392 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts +8 -3
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js +30 -12
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/index.js +5 -5
- package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts +16 -0
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.js +19 -0
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/tools.d.ts +9 -0
- package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/tools.js +700 -54
- package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/transport.d.ts +28 -8
- package/runtime/packages/agenticros-claude-code/dist/transport.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/transport.js +37 -34
- package/runtime/packages/agenticros-claude-code/dist/transport.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/package.json +2 -1
- package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +965 -0
- package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
- package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
- package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
- package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
- package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
- package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
- package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
- package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
- package/runtime/packages/agenticros-gemini/package.json +1 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
- package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
- package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
- package/runtime/packages/core/LICENSE +192 -0
- package/runtime/packages/core/README.md +88 -0
- package/runtime/packages/core/package.json +36 -3
- package/runtime/packages/core/src/__tests__/capabilities.test.ts +266 -0
- package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
- package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
- package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
- package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
- package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
- package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
- package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
- package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
- package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
- package/runtime/packages/core/src/capabilities.ts +314 -0
- package/runtime/packages/core/src/config.ts +221 -39
- package/runtime/packages/core/src/discovery.ts +193 -0
- package/runtime/packages/core/src/find-robots-for.ts +164 -0
- package/runtime/packages/core/src/index.ts +67 -1
- package/runtime/packages/core/src/mission-registry.ts +142 -0
- package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
- package/runtime/packages/core/src/mission.ts +574 -0
- package/runtime/packages/core/src/planner/index.ts +564 -0
- package/runtime/packages/core/src/robots.ts +282 -0
- package/runtime/packages/core/src/topic-utils.ts +47 -9
- package/runtime/packages/core/src/transport-pool.ts +171 -0
- package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
- package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
- package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
- package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
- package/runtime/scripts/configure_agenticros.sh +0 -0
- package/runtime/scripts/configure_for_sim.sh +0 -0
- package/runtime/scripts/fix-openclaw-control-ui-path.sh +0 -0
- package/runtime/scripts/install_cli.sh +0 -0
- package/runtime/scripts/install_rosbridge_from_source.sh +0 -0
- package/runtime/scripts/onboard_robot.sh +0 -0
- package/runtime/scripts/pack-runtime.mjs +0 -0
- package/runtime/scripts/refresh-skill-deps.mjs +381 -0
- package/runtime/scripts/run_demo_native.sh +0 -0
- package/runtime/scripts/run_nemoclaw_host_stack.sh +0 -0
- package/runtime/scripts/run_robot_rosbridge.sh +0 -0
- package/runtime/scripts/setup_gateway_plugin.sh +32 -5
- package/runtime/scripts/setup_robot.sh +0 -0
- package/runtime/scripts/setup_workspace.sh +0 -0
- package/runtime/scripts/sim/run_sim.sh +0 -0
- package/runtime/scripts/smoke_test_nemoclaw.sh +0 -0
- package/runtime/scripts/start_demo.sh +0 -0
- package/runtime/scripts/sync-skill-tools.mjs +69 -12
- package/runtime/scripts/use-openclaw-2026.2.26.sh +0 -0
- package/runtime/scripts/use-openclaw-2026.3.11.sh +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the Phase 1.g rule-based mission planner.
|
|
3
|
+
*
|
|
4
|
+
* These tests pin the contract for `compileGoalToMission`:
|
|
5
|
+
*
|
|
6
|
+
* - Recognised verbs (find / take / measure / follow / drive / list)
|
|
7
|
+
* map to the right capability ids and extract structured inputs.
|
|
8
|
+
* - Compound "find X and drive toward it" emits a 2-step plan with
|
|
9
|
+
* the canonical template ref so the runner steers via the
|
|
10
|
+
* detected horizontal_offset.
|
|
11
|
+
* - Skill capabilities the runtime doesn't have are NOT fabricated
|
|
12
|
+
* — if `find_object` isn't in the registry, "find a chair" fails
|
|
13
|
+
* gracefully instead of pretending to run.
|
|
14
|
+
* - Failures surface a useful error + suggestions list so the
|
|
15
|
+
* agent can self-correct without an extra round-trip.
|
|
16
|
+
* - Multi-segment goals ("X and Y") compile each segment
|
|
17
|
+
* independently; segments that don't map are recorded as
|
|
18
|
+
* `unmatched_verbs` for explainability.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { test } from "node:test";
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
|
|
24
|
+
import { compileGoalToMission } from "../planner/index.js";
|
|
25
|
+
import type { Capability } from "../capabilities.js";
|
|
26
|
+
|
|
27
|
+
const BASE_CAPS: Capability[] = [
|
|
28
|
+
{ id: "drive_base", verb: "drive", description: "drive", source: { kind: "builtin" } },
|
|
29
|
+
{ id: "take_snapshot", verb: "see", description: "see", source: { kind: "builtin" } },
|
|
30
|
+
{ id: "measure_depth", verb: "measure", description: "measure", source: { kind: "builtin" } },
|
|
31
|
+
{ id: "list_topics", verb: "introspect", description: "list", source: { kind: "builtin" } },
|
|
32
|
+
{
|
|
33
|
+
id: "find_object",
|
|
34
|
+
verb: "find",
|
|
35
|
+
description: "find",
|
|
36
|
+
source: { kind: "skill", skillId: "find", package: "agenticros-skill-find" },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "follow_person",
|
|
40
|
+
verb: "follow",
|
|
41
|
+
description: "follow",
|
|
42
|
+
source: { kind: "skill", skillId: "followme", package: "agenticros-skill-followme" },
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// --- Recognition: single-verb goals ---
|
|
47
|
+
|
|
48
|
+
test("planner: 'find a chair' compiles to a single find_object step", () => {
|
|
49
|
+
const res = compileGoalToMission("find a chair", BASE_CAPS);
|
|
50
|
+
assert.ok(res.mission, `expected mission, got error: ${res.error}`);
|
|
51
|
+
assert.equal(res.mission!.steps.length, 1);
|
|
52
|
+
assert.equal(res.mission!.steps[0].capability, "find_object");
|
|
53
|
+
assert.equal((res.mission!.steps[0].inputs as { target: string }).target, "chair");
|
|
54
|
+
assert.equal(res.mission!.goal, "find a chair");
|
|
55
|
+
assert.ok(res.mission!.name?.includes("find a chair"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("planner: 'where is the laptop' compiles to find_object with target=laptop", () => {
|
|
59
|
+
const res = compileGoalToMission("Where is the laptop?", BASE_CAPS);
|
|
60
|
+
assert.ok(res.mission);
|
|
61
|
+
assert.equal(res.mission!.steps[0].capability, "find_object");
|
|
62
|
+
assert.equal((res.mission!.steps[0].inputs as { target: string }).target, "laptop");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("planner: 'look for a bottle' uses the 'look for' synonym", () => {
|
|
66
|
+
const res = compileGoalToMission("Look for a bottle", BASE_CAPS);
|
|
67
|
+
assert.ok(res.mission);
|
|
68
|
+
assert.equal((res.mission!.steps[0].inputs as { target: string }).target, "bottle");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("planner: 'take a picture' maps to take_snapshot", () => {
|
|
72
|
+
const res = compileGoalToMission("take a picture", BASE_CAPS);
|
|
73
|
+
assert.ok(res.mission);
|
|
74
|
+
assert.equal(res.mission!.steps.length, 1);
|
|
75
|
+
assert.equal(res.mission!.steps[0].capability, "take_snapshot");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("planner: 'what do you see' maps to take_snapshot", () => {
|
|
79
|
+
const res = compileGoalToMission("What do you see?", BASE_CAPS);
|
|
80
|
+
assert.ok(res.mission);
|
|
81
|
+
assert.equal(res.mission!.steps[0].capability, "take_snapshot");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("planner: 'measure depth' maps to measure_depth", () => {
|
|
85
|
+
const res = compileGoalToMission("Measure depth", BASE_CAPS);
|
|
86
|
+
assert.ok(res.mission);
|
|
87
|
+
assert.equal(res.mission!.steps[0].capability, "measure_depth");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("planner: 'how far away is the wall' maps to measure_depth", () => {
|
|
91
|
+
const res = compileGoalToMission("How far away is the wall?", BASE_CAPS);
|
|
92
|
+
assert.ok(res.mission);
|
|
93
|
+
assert.equal(res.mission!.steps[0].capability, "measure_depth");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("planner: 'follow me' maps to follow_person", () => {
|
|
97
|
+
const res = compileGoalToMission("follow me", BASE_CAPS);
|
|
98
|
+
assert.ok(res.mission);
|
|
99
|
+
assert.equal(res.mission!.steps[0].capability, "follow_person");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("planner: 'list topics' maps to list_topics", () => {
|
|
103
|
+
const res = compileGoalToMission("list topics", BASE_CAPS);
|
|
104
|
+
assert.ok(res.mission);
|
|
105
|
+
assert.equal(res.mission!.steps[0].capability, "list_topics");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// --- Drive parsing ---
|
|
109
|
+
|
|
110
|
+
test("planner: 'drive forward' produces positive linear_x with default speed", () => {
|
|
111
|
+
const res = compileGoalToMission("drive forward", BASE_CAPS);
|
|
112
|
+
assert.ok(res.mission);
|
|
113
|
+
const inputs = res.mission!.steps[0].inputs as { linear_x: number; angular_z: number };
|
|
114
|
+
assert.ok(inputs.linear_x > 0, `linear_x should be positive, got ${inputs.linear_x}`);
|
|
115
|
+
assert.equal(inputs.angular_z, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("planner: 'drive forward at 0.5 m/s' extracts the explicit speed", () => {
|
|
119
|
+
const res = compileGoalToMission("drive forward at 0.5 m/s", BASE_CAPS);
|
|
120
|
+
assert.ok(res.mission);
|
|
121
|
+
const inputs = res.mission!.steps[0].inputs as { linear_x: number };
|
|
122
|
+
assert.equal(inputs.linear_x, 0.5);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("planner: 'drive backward' produces NEGATIVE linear_x", () => {
|
|
126
|
+
const res = compileGoalToMission("drive backward", BASE_CAPS);
|
|
127
|
+
assert.ok(res.mission);
|
|
128
|
+
const inputs = res.mission!.steps[0].inputs as { linear_x: number; angular_z: number };
|
|
129
|
+
assert.ok(inputs.linear_x < 0, `linear_x should be negative, got ${inputs.linear_x}`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("planner: 'turn left' produces positive angular_z", () => {
|
|
133
|
+
const res = compileGoalToMission("turn left", BASE_CAPS);
|
|
134
|
+
assert.ok(res.mission);
|
|
135
|
+
const inputs = res.mission!.steps[0].inputs as { linear_x: number; angular_z: number };
|
|
136
|
+
assert.equal(inputs.linear_x, 0);
|
|
137
|
+
assert.ok(inputs.angular_z > 0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("planner: 'turn right' produces NEGATIVE angular_z", () => {
|
|
141
|
+
const res = compileGoalToMission("turn right", BASE_CAPS);
|
|
142
|
+
assert.ok(res.mission);
|
|
143
|
+
const inputs = res.mission!.steps[0].inputs as { angular_z: number };
|
|
144
|
+
assert.ok(inputs.angular_z < 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("planner: 'stop' produces a zero-velocity drive_base step", () => {
|
|
148
|
+
const res = compileGoalToMission("stop", BASE_CAPS);
|
|
149
|
+
assert.ok(res.mission);
|
|
150
|
+
const inputs = res.mission!.steps[0].inputs as { linear_x: number; angular_z: number };
|
|
151
|
+
assert.equal(inputs.linear_x, 0);
|
|
152
|
+
assert.equal(inputs.angular_z, 0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("planner: bare 'drive' (no direction) does NOT compile (avoids no-op steps)", () => {
|
|
156
|
+
// A drive without direction is ambiguous — the planner must NOT
|
|
157
|
+
// emit a useless zero-velocity step that's identical to "stop".
|
|
158
|
+
const res = compileGoalToMission("drive", BASE_CAPS);
|
|
159
|
+
assert.equal(res.mission, null);
|
|
160
|
+
assert.ok(res.error);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// --- Compound: find then approach ---
|
|
164
|
+
|
|
165
|
+
test("planner: 'find a chair and drive toward it' compiles to a 2-step plan with template ref", () => {
|
|
166
|
+
const res = compileGoalToMission("find a chair and drive toward it", BASE_CAPS);
|
|
167
|
+
assert.ok(res.mission, `expected mission, got error: ${res.error}`);
|
|
168
|
+
assert.equal(res.mission!.steps.length, 2);
|
|
169
|
+
// Step 1: find_object with target=chair
|
|
170
|
+
assert.equal(res.mission!.steps[0].id, "find");
|
|
171
|
+
assert.equal(res.mission!.steps[0].capability, "find_object");
|
|
172
|
+
assert.equal((res.mission!.steps[0].inputs as { target: string }).target, "chair");
|
|
173
|
+
// Step 2: drive_base with templated angular_z so the runner steers
|
|
174
|
+
// via the detected horizontal_offset.
|
|
175
|
+
assert.equal(res.mission!.steps[1].id, "approach");
|
|
176
|
+
assert.equal(res.mission!.steps[1].capability, "drive_base");
|
|
177
|
+
const driveInputs = res.mission!.steps[1].inputs as { linear_x: number; angular_z: string };
|
|
178
|
+
assert.ok(driveInputs.linear_x > 0, "approach should move forward");
|
|
179
|
+
assert.equal(
|
|
180
|
+
driveInputs.angular_z,
|
|
181
|
+
"{{find.outputs.horizontal_offset}}",
|
|
182
|
+
"angular_z must reference find_object's horizontal_offset output via the runner template",
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("planner: 'find a bottle and approach it' is the same 2-step pattern (approach synonym)", () => {
|
|
187
|
+
const res = compileGoalToMission("find a bottle and approach it", BASE_CAPS);
|
|
188
|
+
assert.ok(res.mission);
|
|
189
|
+
assert.equal(res.mission!.steps.length, 2);
|
|
190
|
+
assert.equal((res.mission!.steps[0].inputs as { target: string }).target, "bottle");
|
|
191
|
+
assert.equal(res.mission!.steps[1].capability, "drive_base");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// --- Multi-segment compounds (independent steps) ---
|
|
195
|
+
|
|
196
|
+
test("planner: 'take a picture and then measure depth' compiles to 2 independent steps", () => {
|
|
197
|
+
const res = compileGoalToMission("take a picture and then measure depth", BASE_CAPS);
|
|
198
|
+
assert.ok(res.mission, `expected mission, got error: ${res.error}`);
|
|
199
|
+
assert.equal(res.mission!.steps.length, 2);
|
|
200
|
+
assert.equal(res.mission!.steps[0].capability, "take_snapshot");
|
|
201
|
+
assert.equal(res.mission!.steps[1].capability, "measure_depth");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("planner: 'follow me then take a picture' compiles to 2 sequential steps", () => {
|
|
205
|
+
const res = compileGoalToMission("follow me then take a picture", BASE_CAPS);
|
|
206
|
+
assert.ok(res.mission);
|
|
207
|
+
assert.equal(res.mission!.steps.length, 2);
|
|
208
|
+
assert.equal(res.mission!.steps[0].capability, "follow_person");
|
|
209
|
+
assert.equal(res.mission!.steps[1].capability, "take_snapshot");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// --- Capability availability (no fabrication) ---
|
|
213
|
+
|
|
214
|
+
test("planner: 'find a chair' fails when find_object isn't in the registry (no fabricated calls)", () => {
|
|
215
|
+
const noFind = BASE_CAPS.filter((c) => c.id !== "find_object");
|
|
216
|
+
const res = compileGoalToMission("find a chair", noFind);
|
|
217
|
+
assert.equal(res.mission, null);
|
|
218
|
+
assert.ok(res.error);
|
|
219
|
+
// The planner mentions available capabilities so the agent can pivot.
|
|
220
|
+
const allSuggestions = res.suggestions.join(" ");
|
|
221
|
+
assert.ok(allSuggestions.includes("Available capabilities"), `expected suggestions to list capabilities; got: ${allSuggestions}`);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("planner: 'follow me' fails when follow_person isn't installed", () => {
|
|
225
|
+
const noFollow = BASE_CAPS.filter((c) => c.id !== "follow_person");
|
|
226
|
+
const res = compileGoalToMission("follow me", noFollow);
|
|
227
|
+
assert.equal(res.mission, null);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// --- Failure paths ---
|
|
231
|
+
|
|
232
|
+
test("planner: empty goal returns a clean error", () => {
|
|
233
|
+
const res = compileGoalToMission("", BASE_CAPS);
|
|
234
|
+
assert.equal(res.mission, null);
|
|
235
|
+
assert.ok(res.error?.toLowerCase().includes("empty"));
|
|
236
|
+
// Suggestions should still surface so the agent can recover.
|
|
237
|
+
assert.ok(res.suggestions.length > 0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("planner: whitespace-only goal is treated as empty", () => {
|
|
241
|
+
const res = compileGoalToMission(" \n\t ", BASE_CAPS);
|
|
242
|
+
assert.equal(res.mission, null);
|
|
243
|
+
assert.ok(res.error);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("planner: 'paint the wall blue' (unrecognised verb) fails gracefully with suggestions", () => {
|
|
247
|
+
const res = compileGoalToMission("paint the wall blue", BASE_CAPS);
|
|
248
|
+
assert.equal(res.mission, null);
|
|
249
|
+
assert.ok(res.error);
|
|
250
|
+
assert.ok(res.suggestions.length > 0, "must surface suggestions on failure");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// --- Options pass-through ---
|
|
254
|
+
|
|
255
|
+
test("planner: options.robot_id is propagated onto the compiled mission", () => {
|
|
256
|
+
const res = compileGoalToMission("take a picture", BASE_CAPS, { robot_id: "robotA" });
|
|
257
|
+
assert.ok(res.mission);
|
|
258
|
+
assert.equal(res.mission!.robot_id, "robotA");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("planner: options.mission_name overrides the default 'Goal: <goal>' label", () => {
|
|
262
|
+
const res = compileGoalToMission("take a picture", BASE_CAPS, { mission_name: "Snap" });
|
|
263
|
+
assert.ok(res.mission);
|
|
264
|
+
assert.equal(res.mission!.name, "Snap");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// --- Candidates surface ---
|
|
268
|
+
|
|
269
|
+
test("planner: candidates list contains the matched capability ids in declaration order", () => {
|
|
270
|
+
const res = compileGoalToMission("find a chair and drive toward it", BASE_CAPS);
|
|
271
|
+
assert.ok(res.mission);
|
|
272
|
+
assert.equal(res.candidates.length, 2);
|
|
273
|
+
assert.equal(res.candidates[0].capability_id, "find_object");
|
|
274
|
+
assert.equal(res.candidates[1].capability_id, "drive_base");
|
|
275
|
+
for (const c of res.candidates) {
|
|
276
|
+
assert.ok(c.confidence > 0 && c.confidence <= 1, `confidence should be in (0,1]; got ${c.confidence}`);
|
|
277
|
+
assert.ok(c.rationale.length > 0);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the Phase 1.d multi-robot resolver.
|
|
3
|
+
*
|
|
4
|
+
* What these tests pin down (each one corresponds to a real failure mode
|
|
5
|
+
* the resolver has to defend against in production):
|
|
6
|
+
*
|
|
7
|
+
* - Backwards compat: existing single-robot configs (no `robots`
|
|
8
|
+
* array) must yield a synthesized one-entry list so older configs
|
|
9
|
+
* don't break and the chat agent doesn't see "no robots configured".
|
|
10
|
+
* - Synthesized fallback id derives from namespace when non-empty,
|
|
11
|
+
* defaults to "default" otherwise — important because the active id
|
|
12
|
+
* is what tool calls will key off in the next iteration.
|
|
13
|
+
* - Explicit `robots: []` takes precedence over the legacy single
|
|
14
|
+
* robot when non-empty.
|
|
15
|
+
* - `default: true` flag wins regardless of position. First-entry wins
|
|
16
|
+
* when no default flag.
|
|
17
|
+
* - `resolveRobot(robotId)` returns the matching entry, OR a
|
|
18
|
+
* descriptive error containing the known ids — the error text shape
|
|
19
|
+
* matters because the LLM uses it to self-correct.
|
|
20
|
+
* - `resolveRobot()` with no id returns the active robot, matching
|
|
21
|
+
* getActiveRobotId().
|
|
22
|
+
* - Edge: an explicit override on getActiveRobotId returns it
|
|
23
|
+
* verbatim (even if the id doesn't exist — discovery happens at
|
|
24
|
+
* resolveRobot time, not here).
|
|
25
|
+
*/
|
|
26
|
+
import { test } from "node:test";
|
|
27
|
+
import assert from "node:assert/strict";
|
|
28
|
+
|
|
29
|
+
import { parseConfig } from "../config.js";
|
|
30
|
+
import {
|
|
31
|
+
listRobots,
|
|
32
|
+
resolveRobot,
|
|
33
|
+
getActiveRobotId,
|
|
34
|
+
getTransportConfigForRobot,
|
|
35
|
+
hasRobotTransportOverride,
|
|
36
|
+
type ResolvedRobot,
|
|
37
|
+
} from "../robots.js";
|
|
38
|
+
|
|
39
|
+
test("robots: empty config yields one synthesized 'default' entry (legacy fallback)", () => {
|
|
40
|
+
const cfg = parseConfig({});
|
|
41
|
+
const robots = listRobots(cfg);
|
|
42
|
+
assert.equal(robots.length, 1);
|
|
43
|
+
assert.equal(robots[0].id, "default");
|
|
44
|
+
assert.equal(robots[0].name, "Robot");
|
|
45
|
+
assert.equal(robots[0].namespace, "");
|
|
46
|
+
assert.equal(robots[0].source, "legacy");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("robots: legacy single-robot config synthesizes id from namespace", () => {
|
|
50
|
+
const cfg = parseConfig({
|
|
51
|
+
robot: { name: "Spot", namespace: "robot-spot-1", cameraTopic: "/cam" },
|
|
52
|
+
});
|
|
53
|
+
const robots = listRobots(cfg);
|
|
54
|
+
assert.equal(robots.length, 1);
|
|
55
|
+
assert.equal(robots[0].id, "robot-spot-1");
|
|
56
|
+
assert.equal(robots[0].name, "Spot");
|
|
57
|
+
assert.equal(robots[0].namespace, "robot-spot-1");
|
|
58
|
+
assert.equal(robots[0].cameraTopic, "/cam");
|
|
59
|
+
assert.equal(robots[0].source, "legacy");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("robots: explicit robots[] array overrides the legacy single-robot fallback", () => {
|
|
63
|
+
const cfg = parseConfig({
|
|
64
|
+
robot: { name: "Ignored", namespace: "ignored-ns" },
|
|
65
|
+
robots: [
|
|
66
|
+
{ id: "alpha", name: "Alpha", namespace: "ns-alpha" },
|
|
67
|
+
{ id: "beta", name: "Beta", namespace: "ns-beta" },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const robots = listRobots(cfg);
|
|
71
|
+
assert.equal(robots.length, 2);
|
|
72
|
+
assert.deepEqual(
|
|
73
|
+
robots.map((r) => r.id),
|
|
74
|
+
["alpha", "beta"],
|
|
75
|
+
);
|
|
76
|
+
for (const r of robots) assert.equal(r.source, "config");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("robots: getActiveRobotId returns first entry when no default flag is set", () => {
|
|
80
|
+
const cfg = parseConfig({
|
|
81
|
+
robots: [
|
|
82
|
+
{ id: "alpha", name: "Alpha" },
|
|
83
|
+
{ id: "beta", name: "Beta" },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
assert.equal(getActiveRobotId(cfg), "alpha");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("robots: getActiveRobotId picks the default-flagged entry regardless of position", () => {
|
|
90
|
+
const cfg = parseConfig({
|
|
91
|
+
robots: [
|
|
92
|
+
{ id: "alpha", name: "Alpha" },
|
|
93
|
+
{ id: "beta", name: "Beta", default: true },
|
|
94
|
+
{ id: "gamma", name: "Gamma" },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
assert.equal(getActiveRobotId(cfg), "beta");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("robots: getActiveRobotId honours explicit override", () => {
|
|
101
|
+
const cfg = parseConfig({
|
|
102
|
+
robots: [
|
|
103
|
+
{ id: "alpha", name: "Alpha" },
|
|
104
|
+
{ id: "beta", name: "Beta", default: true },
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
assert.equal(getActiveRobotId(cfg, "alpha"), "alpha");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("robots: getActiveRobotId trims whitespace from the override", () => {
|
|
111
|
+
const cfg = parseConfig({
|
|
112
|
+
robots: [{ id: "alpha", name: "Alpha" }],
|
|
113
|
+
});
|
|
114
|
+
assert.equal(getActiveRobotId(cfg, " alpha "), "alpha");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("robots: getActiveRobotId ignores empty-string overrides (falls through to default rules)", () => {
|
|
118
|
+
const cfg = parseConfig({
|
|
119
|
+
robots: [
|
|
120
|
+
{ id: "alpha", name: "Alpha" },
|
|
121
|
+
{ id: "beta", name: "Beta", default: true },
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
assert.equal(getActiveRobotId(cfg, ""), "beta");
|
|
125
|
+
assert.equal(getActiveRobotId(cfg, " "), "beta");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("robots: resolveRobot() with no id returns the active robot", () => {
|
|
129
|
+
const cfg = parseConfig({
|
|
130
|
+
robots: [
|
|
131
|
+
{ id: "alpha", name: "Alpha" },
|
|
132
|
+
{ id: "beta", name: "Beta", default: true },
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
const r = resolveRobot(cfg);
|
|
136
|
+
assert.equal(r.id, "beta");
|
|
137
|
+
assert.equal(r.name, "Beta");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("robots: resolveRobot(id) returns the matching entry", () => {
|
|
141
|
+
const cfg = parseConfig({
|
|
142
|
+
robots: [
|
|
143
|
+
{ id: "alpha", name: "Alpha", namespace: "ns-a" },
|
|
144
|
+
{ id: "beta", name: "Beta", namespace: "ns-b" },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
const r: ResolvedRobot = resolveRobot(cfg, "beta");
|
|
148
|
+
assert.equal(r.id, "beta");
|
|
149
|
+
assert.equal(r.namespace, "ns-b");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("robots: resolveRobot(unknown id) throws with known ids listed in the error message", () => {
|
|
153
|
+
const cfg = parseConfig({
|
|
154
|
+
robots: [
|
|
155
|
+
{ id: "alpha", name: "Alpha" },
|
|
156
|
+
{ id: "beta", name: "Beta" },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
assert.throws(
|
|
160
|
+
() => resolveRobot(cfg, "gamma"),
|
|
161
|
+
(err: Error) => {
|
|
162
|
+
assert.ok(
|
|
163
|
+
err.message.includes("gamma"),
|
|
164
|
+
`error should mention the unknown id (got: ${err.message})`,
|
|
165
|
+
);
|
|
166
|
+
assert.ok(
|
|
167
|
+
err.message.includes("alpha") && err.message.includes("beta"),
|
|
168
|
+
`error should list known ids (got: ${err.message})`,
|
|
169
|
+
);
|
|
170
|
+
assert.ok(
|
|
171
|
+
err.message.toLowerCase().includes("ros2_list_robots"),
|
|
172
|
+
`error should hint at ros2_list_robots for self-correction (got: ${err.message})`,
|
|
173
|
+
);
|
|
174
|
+
return true;
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("robots: resolveRobot trims whitespace from id arg", () => {
|
|
180
|
+
const cfg = parseConfig({
|
|
181
|
+
robots: [{ id: "alpha", name: "Alpha" }],
|
|
182
|
+
});
|
|
183
|
+
assert.equal(resolveRobot(cfg, " alpha ").id, "alpha");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("robots: source tag distinguishes config vs legacy entries", () => {
|
|
187
|
+
const cfgLegacy = parseConfig({ robot: { namespace: "ns" } });
|
|
188
|
+
const cfgExplicit = parseConfig({ robots: [{ id: "x" }] });
|
|
189
|
+
assert.equal(listRobots(cfgLegacy)[0].source, "legacy");
|
|
190
|
+
assert.equal(listRobots(cfgExplicit)[0].source, "config");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
// Phase 1.d-resolve: per-robot transport override tests.
|
|
195
|
+
//
|
|
196
|
+
// These pin down a contract that matters for the multi-host fleet story —
|
|
197
|
+
// once adopted by adapters, a single host can simultaneously drive a local
|
|
198
|
+
// sim (mode: "local") AND a remote real robot reached via a router
|
|
199
|
+
// (mode: "zenoh" / "rosbridge"). The resolver alone doesn't materialise
|
|
200
|
+
// transports — it surfaces the right TransportConfig per robot id so an
|
|
201
|
+
// adapter pool can call `createTransport` on it.
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
test("transport-override: schema accepts a per-robot override and round-trips", () => {
|
|
205
|
+
const cfg = parseConfig({
|
|
206
|
+
robots: [
|
|
207
|
+
{
|
|
208
|
+
id: "sim",
|
|
209
|
+
namespace: "sim_robot",
|
|
210
|
+
transport: { mode: "local", local: { domainId: 7 } },
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
// Parsed shape preserves the override discriminant + sub-section.
|
|
215
|
+
const raw = cfg.robots[0]?.transport;
|
|
216
|
+
assert.ok(raw, "override must survive parseConfig");
|
|
217
|
+
assert.equal(raw!.mode, "local");
|
|
218
|
+
// Discriminant-narrowed access.
|
|
219
|
+
if (raw!.mode === "local") {
|
|
220
|
+
assert.equal(raw!.local?.domainId, 7);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("transport-override: schema rejects a sub-section that doesn't match the mode", () => {
|
|
225
|
+
// A `mode: "zenoh"` override with a `local: {...}` sub-section is a
|
|
226
|
+
// typo we want to catch at parse time — otherwise the override would
|
|
227
|
+
// silently fall back to the global zenoh config and the user's
|
|
228
|
+
// intent would be lost.
|
|
229
|
+
assert.throws(() =>
|
|
230
|
+
parseConfig({
|
|
231
|
+
robots: [
|
|
232
|
+
{
|
|
233
|
+
id: "sim",
|
|
234
|
+
transport: { mode: "zenoh", local: { domainId: 1 } } as unknown as Record<string, unknown>,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("transport-override: getTransportConfigForRobot returns the override when set", () => {
|
|
242
|
+
const cfg = parseConfig({
|
|
243
|
+
transport: { mode: "rosbridge" },
|
|
244
|
+
rosbridge: { url: "ws://default:9090" },
|
|
245
|
+
robots: [
|
|
246
|
+
{ id: "real", namespace: "real" },
|
|
247
|
+
{
|
|
248
|
+
id: "sim",
|
|
249
|
+
namespace: "sim_robot",
|
|
250
|
+
transport: { mode: "local", local: { domainId: 7 } },
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
const t = getTransportConfigForRobot(cfg, "sim");
|
|
255
|
+
assert.equal(t.mode, "local");
|
|
256
|
+
if (t.mode === "local") {
|
|
257
|
+
assert.equal(t.local?.domainId, 7, "override domainId must win");
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("transport-override: getTransportConfigForRobot falls back to the global transport when no override", () => {
|
|
262
|
+
const cfg = parseConfig({
|
|
263
|
+
transport: { mode: "zenoh" },
|
|
264
|
+
zenoh: { routerEndpoint: "ws://global:10000" },
|
|
265
|
+
robots: [{ id: "real", namespace: "real" }],
|
|
266
|
+
});
|
|
267
|
+
const t = getTransportConfigForRobot(cfg, "real");
|
|
268
|
+
assert.equal(t.mode, "zenoh");
|
|
269
|
+
if (t.mode === "zenoh") {
|
|
270
|
+
assert.equal(t.zenoh.routerEndpoint, "ws://global:10000");
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("transport-override: sub-section omitted in override inherits from top-level config", () => {
|
|
275
|
+
// The user wants `sim` to use zenoh BUT didn't repeat the
|
|
276
|
+
// routerEndpoint inside the override. The resolver must use the
|
|
277
|
+
// top-level zenoh config so they don't have to duplicate it.
|
|
278
|
+
const cfg = parseConfig({
|
|
279
|
+
transport: { mode: "rosbridge" }, // global mode is different — irrelevant here
|
|
280
|
+
zenoh: { routerEndpoint: "ws://shared:10000", domainId: 3 },
|
|
281
|
+
robots: [
|
|
282
|
+
{
|
|
283
|
+
id: "sim",
|
|
284
|
+
namespace: "sim_robot",
|
|
285
|
+
transport: { mode: "zenoh" }, // no `zenoh: {...}` sub-section
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
const t = getTransportConfigForRobot(cfg, "sim");
|
|
290
|
+
assert.equal(t.mode, "zenoh");
|
|
291
|
+
if (t.mode === "zenoh") {
|
|
292
|
+
assert.equal(t.zenoh.routerEndpoint, "ws://shared:10000");
|
|
293
|
+
assert.equal(t.zenoh.domainId, 3);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("transport-override: partial sub-section in override merges over top-level (per-field precedence)", () => {
|
|
298
|
+
// The override only customises routerEndpoint — domainId should
|
|
299
|
+
// continue to come from the top-level config. This is the most
|
|
300
|
+
// common real-world shape (override just the endpoint, keep
|
|
301
|
+
// everything else).
|
|
302
|
+
const cfg = parseConfig({
|
|
303
|
+
transport: { mode: "rosbridge" },
|
|
304
|
+
zenoh: { routerEndpoint: "ws://shared:10000", domainId: 3 },
|
|
305
|
+
robots: [
|
|
306
|
+
{
|
|
307
|
+
id: "field",
|
|
308
|
+
namespace: "field_robot",
|
|
309
|
+
transport: {
|
|
310
|
+
mode: "zenoh",
|
|
311
|
+
zenoh: { routerEndpoint: "ws://field-router:10000" },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
});
|
|
316
|
+
const t = getTransportConfigForRobot(cfg, "field");
|
|
317
|
+
assert.equal(t.mode, "zenoh");
|
|
318
|
+
if (t.mode === "zenoh") {
|
|
319
|
+
assert.equal(t.zenoh.routerEndpoint, "ws://field-router:10000", "override wins per-field");
|
|
320
|
+
assert.equal(t.zenoh.domainId, 3, "non-overridden fields inherit from global");
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("transport-override: legacy single-robot config still returns the global transport", () => {
|
|
325
|
+
// Backwards compat: when robots[] is empty, there's no override
|
|
326
|
+
// possible — fast path returns the global transport unmodified.
|
|
327
|
+
const cfg = parseConfig({
|
|
328
|
+
transport: { mode: "rosbridge" },
|
|
329
|
+
rosbridge: { url: "ws://default:9090" },
|
|
330
|
+
robot: { namespace: "legacy" },
|
|
331
|
+
});
|
|
332
|
+
const t = getTransportConfigForRobot(cfg, "legacy");
|
|
333
|
+
assert.equal(t.mode, "rosbridge");
|
|
334
|
+
if (t.mode === "rosbridge") {
|
|
335
|
+
assert.equal(t.rosbridge.url, "ws://default:9090");
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("transport-override: getTransportConfigForRobot() without id uses the active robot", () => {
|
|
340
|
+
const cfg = parseConfig({
|
|
341
|
+
transport: { mode: "local" },
|
|
342
|
+
robots: [
|
|
343
|
+
{ id: "alpha", namespace: "alpha" },
|
|
344
|
+
{
|
|
345
|
+
id: "beta",
|
|
346
|
+
namespace: "beta",
|
|
347
|
+
default: true,
|
|
348
|
+
transport: { mode: "zenoh", zenoh: { routerEndpoint: "ws://beta:10000" } },
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
const t = getTransportConfigForRobot(cfg); // no id ⇒ active = beta
|
|
353
|
+
assert.equal(t.mode, "zenoh");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("transport-override: getTransportConfigForRobot throws on unknown robot_id (same contract as resolveRobot)", () => {
|
|
357
|
+
const cfg = parseConfig({
|
|
358
|
+
robots: [{ id: "alpha", namespace: "alpha" }],
|
|
359
|
+
});
|
|
360
|
+
assert.throws(
|
|
361
|
+
() => getTransportConfigForRobot(cfg, "gamma"),
|
|
362
|
+
/Unknown robot_id "gamma"/,
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("transport-override: hasRobotTransportOverride() reflects presence", () => {
|
|
367
|
+
const cfg = parseConfig({
|
|
368
|
+
robots: [
|
|
369
|
+
{ id: "alpha", namespace: "alpha" },
|
|
370
|
+
{
|
|
371
|
+
id: "beta",
|
|
372
|
+
namespace: "beta",
|
|
373
|
+
transport: { mode: "local" },
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
});
|
|
377
|
+
assert.equal(hasRobotTransportOverride(cfg, "alpha"), false);
|
|
378
|
+
assert.equal(hasRobotTransportOverride(cfg, "beta"), true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("transport-override: hasRobotTransportOverride() returns false (not throws) for unknown ids", () => {
|
|
382
|
+
// Robust against arbitrary tool args — adapters often want a cheap
|
|
383
|
+
// "should I take the pool path?" check before validating robot ids.
|
|
384
|
+
const cfg = parseConfig({
|
|
385
|
+
robots: [{ id: "alpha", namespace: "alpha" }],
|
|
386
|
+
});
|
|
387
|
+
assert.equal(hasRobotTransportOverride(cfg, "no-such"), false);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("transport-override: ResolvedRobot is unchanged by overrides (transport is read via the helper, not the resolved record)", () => {
|
|
391
|
+
// Intentional design choice — ResolvedRobot stays lean. Tools that
|
|
392
|
+
// care about transport call getTransportConfigForRobot explicitly.
|
|
393
|
+
// This test guards that surface from accidental coupling.
|
|
394
|
+
const cfg = parseConfig({
|
|
395
|
+
robots: [
|
|
396
|
+
{
|
|
397
|
+
id: "sim",
|
|
398
|
+
namespace: "sim",
|
|
399
|
+
transport: { mode: "local", local: { domainId: 7 } },
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
const r = resolveRobot(cfg, "sim");
|
|
404
|
+
assert.equal(r.id, "sim");
|
|
405
|
+
assert.equal((r as ResolvedRobot & { transport?: unknown }).transport, undefined);
|
|
406
|
+
});
|