Skip to content

Commit 3dc60e8

Browse files
authored
Create test_simulated_execution_adapter.py
1 parent 5d2a3b8 commit 3dc60e8

1 file changed

Lines changed: 330 additions & 0 deletions

File tree

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
"""
2+
IX-HapticSight — Tests for the in-memory simulated execution adapter.
3+
4+
These tests verify that the simulated adapter:
5+
- exposes stable capabilities
6+
- accepts supported bounded execution requests
7+
- tracks per-session execution updates
8+
- supports abort and safe-hold transitions
9+
- rejects unsupported command kinds cleanly
10+
"""
11+
12+
import os
13+
import sys
14+
15+
# Make project packages importable without packaging/install
16+
sys.path.insert(0, os.path.abspath("src"))
17+
18+
from ohip.schemas import ( # noqa: E402
19+
ConsentMode,
20+
ContactPlan,
21+
ImpedanceProfile,
22+
Pose,
23+
RPY,
24+
SafetyLevel,
25+
Vector3,
26+
)
27+
from ohip_interfaces.execution_adapter import ( # noqa: E402
28+
BoundedExecutionRequest,
29+
ExecutionCommandKind,
30+
ExecutionResultStatus,
31+
)
32+
from ohip_interfaces.simulated_execution_adapter import ( # noqa: E402
33+
SimulatedExecutionAdapter,
34+
)
35+
36+
37+
POSE_TARGET = Pose(
38+
frame="W",
39+
xyz=Vector3(0.42, -0.18, 1.36),
40+
rpy=RPY(0.0, 0.0, 1.57),
41+
)
42+
43+
44+
def make_plan() -> ContactPlan:
45+
return ContactPlan(
46+
target=POSE_TARGET,
47+
contact_normal=Vector3(0.0, 0.8, 0.6),
48+
peak_force_N=1.2,
49+
dwell_ms=1500,
50+
approach_speed_mps=0.15,
51+
release_speed_mps=0.20,
52+
impedance=ImpedanceProfile(
53+
normal_N_per_mm=(0.3, 0.6),
54+
tangential_N_per_mm=(0.1, 0.3),
55+
),
56+
rationale="test support contact",
57+
consent_mode=ConsentMode.EXPLICIT,
58+
)
59+
60+
61+
def test_simulated_adapter_reports_capabilities():
62+
adapter = SimulatedExecutionAdapter(
63+
backend_name="sim-backend",
64+
support_pose_targets=True,
65+
support_plan_execution=True,
66+
support_progress_updates=True,
67+
)
68+
69+
caps = adapter.capabilities()
70+
71+
assert caps.backend_name == "sim-backend"
72+
assert caps.supports_plan_execution is True
73+
assert caps.supports_pose_targets is True
74+
assert caps.supports_abort is True
75+
assert caps.supports_retreat is True
76+
assert caps.supports_safe_hold is True
77+
assert caps.supports_progress_updates is True
78+
79+
80+
def test_submit_plan_request_and_advance_to_completion():
81+
adapter = SimulatedExecutionAdapter()
82+
83+
request = BoundedExecutionRequest(
84+
request_id="exec-req-1",
85+
session_id="sess-1",
86+
command_kind=ExecutionCommandKind.PLAN,
87+
safety_level=SafetyLevel.GREEN,
88+
plan=make_plan(),
89+
max_speed_scale=0.75,
90+
timeout_s=3.0,
91+
reason_code="runtime_approved_plan",
92+
)
93+
94+
response = adapter.submit(request)
95+
assert response.status == ExecutionResultStatus.ACCEPTED
96+
assert response.accepted is True
97+
assert response.backend_execution_id is not None
98+
99+
update = adapter.current_update(session_id="sess-1")
100+
assert update is not None
101+
assert update.status == ExecutionResultStatus.ACCEPTED
102+
assert update.progress == 0.0
103+
104+
update = adapter.advance(
105+
session_id="sess-1",
106+
progress=0.4,
107+
reason_code="running",
108+
)
109+
assert update.status == ExecutionResultStatus.RUNNING
110+
assert update.progress == 0.4
111+
assert update.reason_code == "running"
112+
113+
update = adapter.advance(
114+
session_id="sess-1",
115+
progress=1.0,
116+
complete=True,
117+
reason_code="completed",
118+
)
119+
assert update.status == ExecutionResultStatus.COMPLETED
120+
assert update.progress == 1.0
121+
assert update.reason_code == "completed"
122+
123+
124+
def test_submit_pose_target_request():
125+
adapter = SimulatedExecutionAdapter()
126+
127+
request = BoundedExecutionRequest(
128+
request_id="exec-req-2",
129+
session_id="sess-2",
130+
command_kind=ExecutionCommandKind.POSE_TARGET,
131+
safety_level=SafetyLevel.YELLOW,
132+
target_pose=POSE_TARGET,
133+
max_speed_scale=0.5,
134+
timeout_s=2.0,
135+
reason_code="move_to_rest_pose",
136+
)
137+
138+
response = adapter.submit(request)
139+
assert response.status == ExecutionResultStatus.ACCEPTED
140+
assert response.accepted is True
141+
142+
update = adapter.advance(
143+
session_id="sess-2",
144+
progress=0.25,
145+
reason_code="running",
146+
)
147+
assert update.status == ExecutionResultStatus.RUNNING
148+
assert update.progress == 0.25
149+
150+
151+
def test_retreat_safe_hold_and_abort_requests_become_terminal_states():
152+
adapter = SimulatedExecutionAdapter()
153+
154+
retreat_request = BoundedExecutionRequest(
155+
request_id="exec-retreat-1",
156+
session_id="sess-3",
157+
command_kind=ExecutionCommandKind.RETREAT,
158+
safety_level=SafetyLevel.RED,
159+
target_pose=POSE_TARGET,
160+
reason_code="retreat_required",
161+
)
162+
retreat_response = adapter.submit(retreat_request)
163+
assert retreat_response.status == ExecutionResultStatus.RETREATING
164+
assert retreat_response.accepted is True
165+
166+
retreat_update = adapter.current_update(session_id="sess-3")
167+
assert retreat_update is not None
168+
assert retreat_update.status == ExecutionResultStatus.RETREATING
169+
170+
hold_request = BoundedExecutionRequest(
171+
request_id="exec-hold-1",
172+
session_id="sess-4",
173+
command_kind=ExecutionCommandKind.SAFE_HOLD,
174+
safety_level=SafetyLevel.RED,
175+
reason_code="backend_unavailable",
176+
)
177+
hold_response = adapter.submit(hold_request)
178+
assert hold_response.status == ExecutionResultStatus.SAFE_HOLD
179+
assert hold_response.accepted is True
180+
181+
hold_update = adapter.current_update(session_id="sess-4")
182+
assert hold_update is not None
183+
assert hold_update.status == ExecutionResultStatus.SAFE_HOLD
184+
assert hold_update.progress == 1.0
185+
186+
abort_request = BoundedExecutionRequest(
187+
request_id="exec-abort-1",
188+
session_id="sess-5",
189+
command_kind=ExecutionCommandKind.ABORT,
190+
safety_level=SafetyLevel.RED,
191+
reason_code="hazard_abort",
192+
)
193+
abort_response = adapter.submit(abort_request)
194+
assert abort_response.status == ExecutionResultStatus.ABORTED
195+
assert abort_response.accepted is True
196+
197+
abort_update = adapter.current_update(session_id="sess-5")
198+
assert abort_update is not None
199+
assert abort_update.status == ExecutionResultStatus.ABORTED
200+
assert abort_update.progress == 1.0
201+
202+
203+
def test_abort_and_safe_hold_can_override_existing_session_state():
204+
adapter = SimulatedExecutionAdapter()
205+
206+
request = BoundedExecutionRequest(
207+
request_id="exec-req-6",
208+
session_id="sess-6",
209+
command_kind=ExecutionCommandKind.PLAN,
210+
safety_level=SafetyLevel.GREEN,
211+
plan=make_plan(),
212+
reason_code="runtime_approved_plan",
213+
)
214+
adapter.submit(request)
215+
216+
abort_response = adapter.abort(
217+
session_id="sess-6",
218+
reason_code="operator_abort",
219+
)
220+
assert abort_response.status == ExecutionResultStatus.ABORTED
221+
assert abort_response.accepted is True
222+
assert abort_response.reason_code == "operator_abort"
223+
224+
update = adapter.current_update(session_id="sess-6")
225+
assert update is not None
226+
assert update.status == ExecutionResultStatus.ABORTED
227+
assert update.progress == 1.0
228+
229+
# Re-submit to same session and then drive to safe hold.
230+
adapter.submit(request)
231+
hold_response = adapter.safe_hold(
232+
session_id="sess-6",
233+
reason_code="force_safe_hold",
234+
)
235+
assert hold_response.status == ExecutionResultStatus.SAFE_HOLD
236+
assert hold_response.accepted is True
237+
assert hold_response.reason_code == "force_safe_hold"
238+
239+
update = adapter.current_update(session_id="sess-6")
240+
assert update is not None
241+
assert update.status == ExecutionResultStatus.SAFE_HOLD
242+
assert update.progress == 1.0
243+
244+
245+
def test_abort_or_safe_hold_unknown_session_returns_unavailable():
246+
adapter = SimulatedExecutionAdapter()
247+
248+
abort_response = adapter.abort(
249+
session_id="missing-session",
250+
reason_code="unknown_session_abort",
251+
)
252+
assert abort_response.status == ExecutionResultStatus.UNAVAILABLE
253+
assert abort_response.accepted is False
254+
255+
hold_response = adapter.safe_hold(
256+
session_id="missing-session",
257+
reason_code="unknown_session_hold",
258+
)
259+
assert hold_response.status == ExecutionResultStatus.UNAVAILABLE
260+
assert hold_response.accepted is False
261+
262+
263+
def test_unsupported_command_types_are_rejected():
264+
no_plan_adapter = SimulatedExecutionAdapter(
265+
support_plan_execution=False,
266+
support_pose_targets=True,
267+
)
268+
no_pose_adapter = SimulatedExecutionAdapter(
269+
support_plan_execution=True,
270+
support_pose_targets=False,
271+
)
272+
273+
plan_request = BoundedExecutionRequest(
274+
request_id="exec-plan-reject",
275+
session_id="sess-7",
276+
command_kind=ExecutionCommandKind.PLAN,
277+
safety_level=SafetyLevel.GREEN,
278+
plan=make_plan(),
279+
reason_code="plan_request",
280+
)
281+
plan_response = no_plan_adapter.submit(plan_request)
282+
assert plan_response.status == ExecutionResultStatus.REJECTED
283+
assert plan_response.accepted is False
284+
assert plan_response.reason_code == "plan_execution_not_supported"
285+
286+
pose_request = BoundedExecutionRequest(
287+
request_id="exec-pose-reject",
288+
session_id="sess-8",
289+
command_kind=ExecutionCommandKind.POSE_TARGET,
290+
safety_level=SafetyLevel.YELLOW,
291+
target_pose=POSE_TARGET,
292+
reason_code="pose_request",
293+
)
294+
pose_response = no_pose_adapter.submit(pose_request)
295+
assert pose_response.status == ExecutionResultStatus.REJECTED
296+
assert pose_response.accepted is False
297+
assert pose_response.reason_code == "pose_target_not_supported"
298+
299+
300+
def test_advance_can_fault_and_terminal_states_do_not_change_afterward():
301+
adapter = SimulatedExecutionAdapter()
302+
303+
request = BoundedExecutionRequest(
304+
request_id="exec-req-9",
305+
session_id="sess-9",
306+
command_kind=ExecutionCommandKind.PLAN,
307+
safety_level=SafetyLevel.GREEN,
308+
plan=make_plan(),
309+
reason_code="runtime_approved_plan",
310+
)
311+
adapter.submit(request)
312+
313+
update = adapter.advance(
314+
session_id="sess-9",
315+
progress=0.5,
316+
fault=True,
317+
reason_code="backend_fault",
318+
)
319+
assert update.status == ExecutionResultStatus.FAULTED
320+
assert update.progress == 0.5
321+
assert update.reason_code == "backend_fault"
322+
323+
later = adapter.advance(
324+
session_id="sess-9",
325+
progress=1.0,
326+
complete=True,
327+
reason_code="should_not_override_fault",
328+
)
329+
assert later.status == ExecutionResultStatus.FAULTED
330+
assert later.reason_code == "backend_fault"

0 commit comments

Comments
 (0)