Skip to content

Commit 84c5ec5

Browse files
authored
Create test_invariants.py
1 parent 6b986b0 commit 84c5ec5

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

tests/test_invariants.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import replace
4+
from datetime import UTC, datetime, timedelta
5+
6+
from ix_style.core import (
7+
AuthState,
8+
CommandSource,
9+
ControlPayload,
10+
FreshnessMetadata,
11+
FreshnessState,
12+
FunctionClass,
13+
IntegrityMetadata,
14+
IntegrityState,
15+
MessageClass,
16+
MessageEnvelope,
17+
MissionPhase,
18+
OrderingMetadata,
19+
ReplayState,
20+
SafetyPosture,
21+
)
22+
from ix_style.verification import (
23+
InvariantChecker,
24+
ScenarioRunner,
25+
VerificationExpectation,
26+
VerificationScenario,
27+
build_nav_spoof_transition_scenario,
28+
build_power_fault_clamp_scenario,
29+
build_recovery_deferred_scenario,
30+
)
31+
32+
33+
def _now() -> datetime:
34+
return datetime.now(tz=UTC)
35+
36+
37+
def _make_operator_recovery_scenario_with_weak_comms() -> VerificationScenario:
38+
now = _now()
39+
envelope = MessageEnvelope(
40+
schema_version="0.1.0",
41+
message_id="MSG-INV-RECOVERY-000001",
42+
message_class=MessageClass.CONTROL,
43+
message_type="control.recovery_action_request",
44+
source_id="operator.console",
45+
source_kind="operator",
46+
created_at=now,
47+
freshness=FreshnessMetadata(
48+
issued_at=now,
49+
expires_at=now + timedelta(seconds=5),
50+
freshness_state=FreshnessState.FRESH,
51+
),
52+
ordering=OrderingMetadata(
53+
sequence_number=1,
54+
session_id="SES-INV-RECOVERY-000001",
55+
replay_state=ReplayState.ACCEPTABLE,
56+
),
57+
integrity=IntegrityMetadata(
58+
integrity_state=IntegrityState.INTEGRITY_VALID,
59+
auth_state=AuthState.INTEGRITY_VALID,
60+
),
61+
payload=ControlPayload(
62+
function_class=FunctionClass.RECOVERY_ACTION,
63+
requested_action="request_recovery_review",
64+
command_source=CommandSource.OPERATOR,
65+
policy_context={"override_requested": False},
66+
requested_scope="vehicle.primary",
67+
requested_duration_ms=100,
68+
),
69+
)
70+
return VerificationScenario(
71+
scenario_id="TEST-INV-RECOVERY-000001",
72+
name="recovery invariant scenario",
73+
purpose="exercise recovery-gate invariants under weak comms",
74+
linked_requirements=("IXS-SYS-034",),
75+
linked_hazards=("IXS-HZ-010",),
76+
envelope=envelope,
77+
mission_phase=MissionPhase.ACTIVE,
78+
safety_posture=SafetyPosture.COMMS_DEGRADED,
79+
active_degradation_flags=("comms_link_intermittent",),
80+
expectations=VerificationExpectation(
81+
required_active_degradation_flags=("comms_link_intermittent",),
82+
),
83+
)
84+
85+
86+
def test_invariant_checker_passes_power_fault_scenario() -> None:
87+
result = ScenarioRunner().run(build_power_fault_clamp_scenario())
88+
report = InvariantChecker().evaluate(result)
89+
90+
assert result.passed is True
91+
assert report.passed is True
92+
assert all(check.passed for check in report.checks)
93+
94+
95+
def test_invariant_checker_passes_nav_spoof_scenario() -> None:
96+
result = ScenarioRunner().run(build_nav_spoof_transition_scenario())
97+
report = InvariantChecker().evaluate(result)
98+
99+
assert result.passed is True
100+
assert report.passed is True
101+
assert all(check.passed for check in report.checks)
102+
103+
104+
def test_invariant_checker_passes_recovery_deferred_scenario() -> None:
105+
result = ScenarioRunner().run(build_recovery_deferred_scenario())
106+
report = InvariantChecker().evaluate(result)
107+
108+
assert result.passed is True
109+
assert report.passed is True
110+
assert all(check.passed for check in report.checks)
111+
assert result.pipeline_trace["recovery_gate_status"] == "DEFERRED"
112+
113+
114+
def test_invariant_checker_detects_tampered_bundle() -> None:
115+
result = ScenarioRunner().run(build_power_fault_clamp_scenario())
116+
tampered_bundle = dict(result.evidence_package.evidence_bundle)
117+
tampered_items = list(tampered_bundle["items"])
118+
tampered_items[0] = dict(tampered_items[0])
119+
tampered_items[0]["data"] = dict(tampered_items[0]["data"])
120+
tampered_items[0]["data"]["rationale_summary"] = "tampered after export"
121+
tampered_bundle["items"] = tampered_items
122+
123+
tampered_evidence = replace(result.evidence_package, evidence_bundle=tampered_bundle)
124+
tampered_result = replace(result, evidence_package=tampered_evidence)
125+
126+
report = InvariantChecker().evaluate(tampered_result)
127+
128+
assert report.passed is False
129+
assert any(check.invariant_id == "IXS-INV-006" and not check.passed for check in report.checks)
130+
131+
132+
def test_recovery_gate_invariant_shows_no_authority_progression_after_deferral() -> None:
133+
result = ScenarioRunner().run(_make_operator_recovery_scenario_with_weak_comms())
134+
report = InvariantChecker().evaluate(result)
135+
136+
assert result.pipeline_trace["recovery_gate_status"] == "DEFERRED"
137+
assert result.pipeline_trace["authority_decision_present"] is False
138+
assert result.pipeline_trace["guard_decision_present"] is False
139+
assert report.passed is True

0 commit comments

Comments
 (0)