Skip to content

Commit 884a0f9

Browse files
authored
Create test_runtime_state.py
1 parent 34f1de1 commit 884a0f9

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

tests/test_runtime_state.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""
2+
IX-HapticSight — Tests for runtime state models.
3+
4+
These tests cover the new backend-agnostic runtime session layer introduced
5+
under `src/ohip_runtime/`. The goal is to verify that conservative runtime
6+
state transitions behave predictably before any ROS 2 or execution backend
7+
integration is added.
8+
"""
9+
10+
import os
11+
import sys
12+
13+
# Make both `ohip` and `ohip_runtime` importable without packaging
14+
sys.path.insert(0, os.path.abspath("src"))
15+
16+
from ohip.schemas import SafetyLevel # noqa: E402
17+
from ohip_runtime.state import ( # noqa: E402
18+
ExecutionState,
19+
FaultDisposition,
20+
FaultSeverity,
21+
InteractionSession,
22+
InteractionState,
23+
RuntimeFault,
24+
RuntimeHealth,
25+
SignalFreshness,
26+
)
27+
28+
29+
def test_signal_freshness_all_required_fresh():
30+
freshness = SignalFreshness(
31+
force_torque_fresh=True,
32+
tactile_fresh=False,
33+
proximity_fresh=True,
34+
thermal_fresh=True,
35+
scene_fresh=True,
36+
)
37+
38+
assert freshness.all_required_fresh(
39+
require_force_torque=True,
40+
require_proximity=True,
41+
require_thermal=True,
42+
require_scene=True,
43+
) is True
44+
45+
assert freshness.all_required_fresh(
46+
require_force_torque=True,
47+
require_tactile=True,
48+
) is False
49+
50+
51+
def test_runtime_fault_health_mapping():
52+
degraded = RuntimeFault(
53+
fault_id="f1",
54+
reason_code="sensor_degraded",
55+
severity=FaultSeverity.DEGRADED,
56+
disposition=FaultDisposition.NARROW_BEHAVIOR,
57+
source="test",
58+
)
59+
blocking = RuntimeFault(
60+
fault_id="f2",
61+
reason_code="consent_missing",
62+
severity=FaultSeverity.BLOCKING,
63+
disposition=FaultDisposition.REJECT_ACTION,
64+
source="test",
65+
)
66+
critical = RuntimeFault(
67+
fault_id="f3",
68+
reason_code="e_stop",
69+
severity=FaultSeverity.CRITICAL,
70+
disposition=FaultDisposition.LATCH,
71+
source="test",
72+
latched=True,
73+
)
74+
75+
assert degraded.runtime_health() == RuntimeHealth.DEGRADED
76+
assert blocking.runtime_health() == RuntimeHealth.BLOCKED
77+
assert critical.runtime_health() == RuntimeHealth.FAULTED
78+
79+
80+
def test_session_can_begin_approach_only_when_conditions_are_good():
81+
session = InteractionSession(session_id="s-1")
82+
session.interaction_state = InteractionState.IDLE
83+
session.safety_level = SafetyLevel.GREEN
84+
session.consent_valid = True
85+
session.consent_fresh = True
86+
87+
assert session.can_begin_approach() is True
88+
89+
session.interaction_state = InteractionState.CONTACT
90+
assert session.can_begin_approach() is False
91+
92+
session.interaction_state = InteractionState.VERIFY
93+
assert session.can_begin_approach() is True
94+
95+
session.safety_level = SafetyLevel.RED
96+
assert session.can_begin_approach() is False
97+
98+
99+
def test_non_latched_blocking_fault_blocks_new_actions_and_can_clear():
100+
session = InteractionSession(
101+
session_id="s-2",
102+
interaction_state=InteractionState.IDLE,
103+
execution_state=ExecutionState.READY,
104+
safety_level=SafetyLevel.GREEN,
105+
consent_valid=True,
106+
consent_fresh=True,
107+
)
108+
109+
fault = RuntimeFault(
110+
fault_id="f-block",
111+
reason_code="consent_denied",
112+
severity=FaultSeverity.BLOCKING,
113+
disposition=FaultDisposition.REJECT_ACTION,
114+
source="runtime_coordinator",
115+
)
116+
session.apply_fault(fault)
117+
118+
assert session.active_fault is not None
119+
assert session.runtime_health == RuntimeHealth.BLOCKED
120+
assert session.can_begin_approach() is False
121+
122+
cleared = session.clear_non_latched_fault()
123+
assert cleared is True
124+
assert session.active_fault is None
125+
assert session.runtime_health == RuntimeHealth.NOMINAL
126+
127+
128+
def test_retreat_fault_moves_session_to_retreating():
129+
session = InteractionSession(
130+
session_id="s-3",
131+
interaction_state=InteractionState.CONTACT,
132+
execution_state=ExecutionState.EXECUTING,
133+
safety_level=SafetyLevel.GREEN,
134+
consent_valid=True,
135+
consent_fresh=True,
136+
)
137+
138+
fault = RuntimeFault(
139+
fault_id="f-retreat",
140+
reason_code="overforce",
141+
severity=FaultSeverity.ABORT,
142+
disposition=FaultDisposition.RETREAT,
143+
source="safety",
144+
requires_retreat=True,
145+
)
146+
session.apply_fault(fault)
147+
148+
assert session.interaction_state == InteractionState.RETREAT
149+
assert session.execution_state == ExecutionState.RETREATING
150+
assert session.runtime_health == RuntimeHealth.FAULTED
151+
152+
153+
def test_safe_hold_fault_moves_session_to_safe_hold():
154+
session = InteractionSession(
155+
session_id="s-4",
156+
interaction_state=InteractionState.APPROACH,
157+
execution_state=ExecutionState.EXECUTING,
158+
safety_level=SafetyLevel.YELLOW,
159+
)
160+
161+
fault = RuntimeFault(
162+
fault_id="f-hold",
163+
reason_code="backend_unavailable",
164+
severity=FaultSeverity.ABORT,
165+
disposition=FaultDisposition.SAFE_HOLD,
166+
source="execution",
167+
requires_safe_hold=True,
168+
)
169+
session.apply_fault(fault)
170+
171+
assert session.interaction_state == InteractionState.SAFE_HOLD
172+
assert session.execution_state == ExecutionState.SAFE_HOLD
173+
assert session.runtime_health == RuntimeHealth.FAULTED
174+
175+
176+
def test_latched_fault_cannot_be_cleared_and_enters_fault_latched():
177+
session = InteractionSession(
178+
session_id="s-5",
179+
interaction_state=InteractionState.PRECONTACT,
180+
execution_state=ExecutionState.EXECUTING,
181+
safety_level=SafetyLevel.YELLOW,
182+
)
183+
184+
fault = RuntimeFault(
185+
fault_id="f-latched",
186+
reason_code="policy_integrity_failure",
187+
severity=FaultSeverity.CRITICAL,
188+
disposition=FaultDisposition.LATCH,
189+
source="integrity",
190+
latched=True,
191+
)
192+
session.apply_fault(fault)
193+
194+
assert session.interaction_state == InteractionState.FAULT_LATCHED
195+
assert session.execution_state == ExecutionState.FAULTED
196+
assert session.runtime_health == RuntimeHealth.FAULTED
197+
198+
cleared = session.clear_non_latched_fault()
199+
assert cleared is False
200+
assert session.active_fault is not None

0 commit comments

Comments
 (0)