Skip to content

Commit b938f57

Browse files
authored
Create test_execution_adapter_contracts.py
1 parent a171f35 commit b938f57

1 file changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
"""
2+
IX-HapticSight — Tests for execution adapter contracts.
3+
4+
These tests verify the backend-agnostic execution request/response models that
5+
sit between runtime coordination and a future concrete backend adapter.
6+
"""
7+
8+
import os
9+
import sys
10+
11+
# Make project packages importable without packaging/install
12+
sys.path.insert(0, os.path.abspath("src"))
13+
14+
from ohip.schemas import ( # noqa: E402
15+
ConsentMode,
16+
ContactPlan,
17+
ImpedanceProfile,
18+
Pose,
19+
RPY,
20+
SafetyLevel,
21+
Vector3,
22+
)
23+
from ohip_interfaces.execution_adapter import ( # noqa: E402
24+
BoundedExecutionRequest,
25+
ExecutionBackendCapabilities,
26+
ExecutionCommandKind,
27+
ExecutionResponse,
28+
ExecutionResultStatus,
29+
ExecutionUpdate,
30+
)
31+
32+
33+
POSE_TARGET = Pose(
34+
frame="W",
35+
xyz=Vector3(0.42, -0.18, 1.36),
36+
rpy=RPY(0.0, 0.0, 1.57),
37+
)
38+
39+
40+
def make_plan() -> ContactPlan:
41+
return ContactPlan(
42+
target=POSE_TARGET,
43+
contact_normal=Vector3(0.0, 0.8, 0.6),
44+
peak_force_N=1.2,
45+
dwell_ms=1500,
46+
approach_speed_mps=0.15,
47+
release_speed_mps=0.20,
48+
impedance=ImpedanceProfile(
49+
normal_N_per_mm=(0.3, 0.6),
50+
tangential_N_per_mm=(0.1, 0.3),
51+
),
52+
rationale="test support contact",
53+
consent_mode=ConsentMode.EXPLICIT,
54+
)
55+
56+
57+
def test_execution_backend_capabilities_to_dict():
58+
caps = ExecutionBackendCapabilities(
59+
backend_name="sim-backend",
60+
supports_plan_execution=True,
61+
supports_pose_targets=True,
62+
supports_abort=True,
63+
supports_retreat=True,
64+
supports_safe_hold=True,
65+
supports_progress_updates=True,
66+
supports_collision_aware_execution=True,
67+
supports_velocity_scaling=True,
68+
supports_force_limited_execution=False,
69+
)
70+
71+
data = caps.to_dict()
72+
73+
assert data["backend_name"] == "sim-backend"
74+
assert data["supports_plan_execution"] is True
75+
assert data["supports_pose_targets"] is True
76+
assert data["supports_progress_updates"] is True
77+
assert data["supports_collision_aware_execution"] is True
78+
assert data["supports_force_limited_execution"] is False
79+
80+
81+
def test_bounded_execution_request_with_plan_validates_and_serializes():
82+
request = BoundedExecutionRequest(
83+
request_id="exec-req-1",
84+
session_id="sess-1",
85+
command_kind=ExecutionCommandKind.PLAN,
86+
safety_level=SafetyLevel.GREEN,
87+
plan=make_plan(),
88+
max_speed_scale=0.75,
89+
timeout_s=3.0,
90+
reason_code="runtime_approved_plan",
91+
)
92+
93+
request.validate()
94+
data = request.to_dict()
95+
96+
assert data["request_id"] == "exec-req-1"
97+
assert data["command_kind"] == "PLAN"
98+
assert data["safety_level"] == "GREEN"
99+
assert data["plan"] is not None
100+
assert data["target_pose"] is None
101+
assert data["max_speed_scale"] == 0.75
102+
assert data["timeout_s"] == 3.0
103+
assert data["reason_code"] == "runtime_approved_plan"
104+
105+
106+
def test_bounded_execution_request_with_pose_target_validates_and_serializes():
107+
request = BoundedExecutionRequest(
108+
request_id="exec-req-2",
109+
session_id="sess-1",
110+
command_kind=ExecutionCommandKind.POSE_TARGET,
111+
safety_level=SafetyLevel.YELLOW,
112+
target_pose=POSE_TARGET,
113+
max_speed_scale=0.5,
114+
timeout_s=2.5,
115+
reason_code="move_to_rest_pose",
116+
)
117+
118+
request.validate()
119+
data = request.to_dict()
120+
121+
assert data["command_kind"] == "POSE_TARGET"
122+
assert data["plan"] is None
123+
assert data["target_pose"] is not None
124+
assert data["target_pose"]["frame"] == "W"
125+
assert data["max_speed_scale"] == 0.5
126+
127+
128+
def test_abort_and_safe_hold_requests_can_omit_plan_and_pose():
129+
abort_request = BoundedExecutionRequest(
130+
request_id="exec-abort-1",
131+
session_id="sess-1",
132+
command_kind=ExecutionCommandKind.ABORT,
133+
safety_level=SafetyLevel.RED,
134+
reason_code="hazard_abort",
135+
)
136+
safe_hold_request = BoundedExecutionRequest(
137+
request_id="exec-hold-1",
138+
session_id="sess-1",
139+
command_kind=ExecutionCommandKind.SAFE_HOLD,
140+
safety_level=SafetyLevel.RED,
141+
reason_code="backend_unavailable",
142+
)
143+
144+
abort_request.validate()
145+
safe_hold_request.validate()
146+
147+
assert abort_request.to_dict()["plan"] is None
148+
assert safe_hold_request.to_dict()["target_pose"] is None
149+
150+
151+
def test_retreat_request_requires_plan_or_target_pose():
152+
bad_request = BoundedExecutionRequest(
153+
request_id="exec-retreat-bad",
154+
session_id="sess-1",
155+
command_kind=ExecutionCommandKind.RETREAT,
156+
safety_level=SafetyLevel.RED,
157+
reason_code="retreat_required",
158+
)
159+
160+
try:
161+
bad_request.validate()
162+
raised = False
163+
except ValueError:
164+
raised = True
165+
166+
assert raised is True
167+
168+
169+
def test_pose_target_request_requires_target_pose():
170+
bad_request = BoundedExecutionRequest(
171+
request_id="exec-pose-bad",
172+
session_id="sess-1",
173+
command_kind=ExecutionCommandKind.POSE_TARGET,
174+
safety_level=SafetyLevel.YELLOW,
175+
reason_code="missing_pose",
176+
)
177+
178+
try:
179+
bad_request.validate()
180+
raised = False
181+
except ValueError:
182+
raised = True
183+
184+
assert raised is True
185+
186+
187+
def test_request_rejects_both_plan_and_pose_and_invalid_scalars():
188+
try:
189+
BoundedExecutionRequest(
190+
request_id="exec-bad-both",
191+
session_id="sess-1",
192+
command_kind=ExecutionCommandKind.PLAN,
193+
safety_level=SafetyLevel.GREEN,
194+
plan=make_plan(),
195+
target_pose=POSE_TARGET,
196+
reason_code="bad_both",
197+
).validate()
198+
raised_both = False
199+
except ValueError:
200+
raised_both = True
201+
202+
try:
203+
BoundedExecutionRequest(
204+
request_id="exec-bad-speed",
205+
session_id="sess-1",
206+
command_kind=ExecutionCommandKind.PLAN,
207+
safety_level=SafetyLevel.GREEN,
208+
plan=make_plan(),
209+
max_speed_scale=0.0,
210+
reason_code="bad_speed",
211+
).validate()
212+
raised_speed = False
213+
except ValueError:
214+
raised_speed = True
215+
216+
try:
217+
BoundedExecutionRequest(
218+
request_id="exec-bad-timeout",
219+
session_id="sess-1",
220+
command_kind=ExecutionCommandKind.PLAN,
221+
safety_level=SafetyLevel.GREEN,
222+
plan=make_plan(),
223+
timeout_s=0.0,
224+
reason_code="bad_timeout",
225+
).validate()
226+
raised_timeout = False
227+
except ValueError:
228+
raised_timeout = True
229+
230+
assert raised_both is True
231+
assert raised_speed is True
232+
assert raised_timeout is True
233+
234+
235+
def test_execution_response_to_dict():
236+
response = ExecutionResponse(
237+
request_id="exec-req-1",
238+
session_id="sess-1",
239+
status=ExecutionResultStatus.ACCEPTED,
240+
accepted=True,
241+
backend_name="sim-backend",
242+
reason_code="accepted_for_execution",
243+
backend_execution_id="sim-123",
244+
)
245+
246+
data = response.to_dict()
247+
248+
assert data["status"] == "ACCEPTED"
249+
assert data["accepted"] is True
250+
assert data["backend_name"] == "sim-backend"
251+
assert data["backend_execution_id"] == "sim-123"
252+
253+
254+
def test_execution_update_validate_and_to_dict():
255+
update = ExecutionUpdate(
256+
request_id="exec-req-1",
257+
session_id="sess-1",
258+
status=ExecutionResultStatus.RUNNING,
259+
backend_name="sim-backend",
260+
progress=0.4,
261+
reason_code="moving",
262+
backend_execution_id="sim-123",
263+
)
264+
265+
update.validate()
266+
data = update.to_dict()
267+
268+
assert data["status"] == "RUNNING"
269+
assert data["progress"] == 0.4
270+
assert data["reason_code"] == "moving"
271+
assert data["backend_execution_id"] == "sim-123"
272+
273+
try:
274+
ExecutionUpdate(
275+
request_id="exec-req-1",
276+
session_id="sess-1",
277+
status=ExecutionResultStatus.RUNNING,
278+
backend_name="sim-backend",
279+
progress=1.5,
280+
).validate()
281+
raised = False
282+
except ValueError:
283+
raised = True
284+
285+
assert raised is True

0 commit comments

Comments
 (0)