Skip to content

Commit 0c2274a

Browse files
authored
Add tests for review workflow functionality
1 parent 9f25517 commit 0c2274a

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

tests/test_review_workflow.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from ix_autonomy_assurance_case_runtime.authority import ReviewActor
6+
from ix_autonomy_assurance_case_runtime.contracts import ContractValueError, ReviewDisposition
7+
from ix_autonomy_assurance_case_runtime.review_workflow import (
8+
ReviewAuthorityBinding,
9+
ReviewAuthorityScope,
10+
ReviewDissentRecord,
11+
ReviewDissentSeverity,
12+
ReviewFinding,
13+
ReviewFindingSeverity,
14+
ReviewFindingStatus,
15+
ReviewSignoffRecord,
16+
ReviewWorkflowRecord,
17+
ReviewWorkflowStatus,
18+
)
19+
20+
21+
def _actor(actor_id: str = "reviewer-001") -> ReviewActor:
22+
return ReviewActor(
23+
actor_id=actor_id,
24+
role="assurance-reviewer",
25+
display_name="Assurance Reviewer",
26+
)
27+
28+
29+
def _binding() -> ReviewAuthorityBinding:
30+
return ReviewAuthorityBinding(
31+
binding_id="binding-reviewer-001",
32+
actor=_actor(),
33+
authority_scopes=(
34+
ReviewAuthorityScope.ASSURANCE_CASE,
35+
ReviewAuthorityScope.SCENARIO_CAMPAIGN,
36+
),
37+
can_sign=True,
38+
can_waive=True,
39+
)
40+
41+
42+
def _finding(
43+
*,
44+
status: ReviewFindingStatus = ReviewFindingStatus.CLOSED,
45+
severity: ReviewFindingSeverity = ReviewFindingSeverity.HIGH,
46+
waiver_id: str | None = None,
47+
) -> ReviewFinding:
48+
return ReviewFinding(
49+
finding_id="finding-runtime-evidence-001",
50+
scope=ReviewAuthorityScope.SCENARIO_CAMPAIGN,
51+
severity=severity,
52+
status=status,
53+
title="Runtime evidence reviewed",
54+
rationale="Campaign evidence supports the bounded runtime behavior claim.",
55+
opened_by_actor_id="reviewer-001",
56+
opened_at_utc="2026-05-12T12:00:00Z",
57+
requirement_ids=("req-runtime-boundary",),
58+
hazard_ids=("hazard-runtime-boundary",),
59+
evidence_bundle_ids=("ev-review-finding-001",),
60+
source_record_ids=("campaign-run-001",),
61+
waiver_id=waiver_id,
62+
)
63+
64+
65+
def _signoff(
66+
*,
67+
disposition: ReviewDisposition = ReviewDisposition.APPROVED,
68+
condition_ids: tuple[str, ...] = (),
69+
) -> ReviewSignoffRecord:
70+
return ReviewSignoffRecord(
71+
signoff_id="signoff-reviewer-001",
72+
workflow_id="workflow-review-001",
73+
actor=_actor(),
74+
scope=ReviewAuthorityScope.ASSURANCE_CASE,
75+
disposition=disposition,
76+
rationale="Evidence, campaign, monitoring, and provenance posture reviewed.",
77+
signed_at_utc="2026-05-12T13:00:00Z",
78+
evidence_bundle_ids=("ev-review-signoff-001",),
79+
condition_ids=condition_ids,
80+
)
81+
82+
83+
def _dissent(
84+
*,
85+
severity: ReviewDissentSeverity = ReviewDissentSeverity.CONCERN,
86+
) -> ReviewDissentRecord:
87+
return ReviewDissentRecord(
88+
dissent_id="dissent-reviewer-001",
89+
workflow_id="workflow-review-001",
90+
actor=_actor("reviewer-002"),
91+
scope=ReviewAuthorityScope.MONITORING,
92+
severity=severity,
93+
rationale="Monitoring confidence should be reviewed again before wider claims.",
94+
recorded_at_utc="2026-05-12T13:10:00Z",
95+
evidence_bundle_ids=("ev-review-dissent-001",),
96+
related_finding_ids=("finding-runtime-evidence-001",),
97+
)
98+
99+
100+
def _workflow(
101+
*,
102+
status: ReviewWorkflowStatus = ReviewWorkflowStatus.COMPLETED,
103+
finding: ReviewFinding | None = None,
104+
signoff: ReviewSignoffRecord | None = None,
105+
dissents: tuple[ReviewDissentRecord, ...] = (),
106+
) -> ReviewWorkflowRecord:
107+
return ReviewWorkflowRecord(
108+
workflow_id="workflow-review-001",
109+
case_id="case-runtime-001",
110+
title="Runtime assurance review",
111+
status=status,
112+
authority_bindings=(_binding(),),
113+
findings=(finding if finding is not None else _finding(),),
114+
signoffs=(signoff if signoff is not None else _signoff(),),
115+
dissents=dissents,
116+
evidence_bundle_ids=("ev-review-workflow-001",),
117+
system_id="system-runtime-001",
118+
deployment_id="deploy-runtime-001",
119+
)
120+
121+
122+
def test_review_workflow_records_acceptance_ready_signoff_state() -> None:
123+
workflow = _workflow()
124+
125+
assert workflow.can_support_acceptance()
126+
assert workflow.unresolved_finding_ids() == ()
127+
assert workflow.accepted_signoff_ids() == ("signoff-reviewer-001",)
128+
assert workflow.required_evidence_bundle_ids() == (
129+
"ev-review-workflow-001",
130+
"ev-review-finding-001",
131+
"ev-review-signoff-001",
132+
)
133+
134+
135+
def test_review_workflow_blocks_unresolved_medium_or_higher_findings() -> None:
136+
workflow = _workflow(finding=_finding(status=ReviewFindingStatus.OPEN))
137+
138+
assert not workflow.can_support_acceptance()
139+
assert workflow.unresolved_finding_ids() == ("finding-runtime-evidence-001",)
140+
141+
142+
def test_review_workflow_records_blocking_dissent() -> None:
143+
workflow = _workflow(dissents=(_dissent(severity=ReviewDissentSeverity.BLOCKING_OBJECTION),))
144+
145+
assert not workflow.can_support_acceptance()
146+
assert workflow.blocking_dissent_ids() == ("dissent-reviewer-001",)
147+
assert workflow.dissent_ids() == ("dissent-reviewer-001",)
148+
149+
150+
def test_review_finding_requires_evidence_for_medium_or_higher_severity() -> None:
151+
with pytest.raises(ContractValueError, match="medium, high, or critical findings"):
152+
ReviewFinding(
153+
finding_id="finding-missing-evidence",
154+
scope=ReviewAuthorityScope.POLICY,
155+
severity=ReviewFindingSeverity.MEDIUM,
156+
status=ReviewFindingStatus.OPEN,
157+
title="Policy evidence missing",
158+
rationale="The reviewer needs policy evidence.",
159+
opened_by_actor_id="reviewer-001",
160+
opened_at_utc="2026-05-12T12:00:00Z",
161+
)
162+
163+
164+
def test_critical_review_finding_requires_hazard_links() -> None:
165+
with pytest.raises(ContractValueError, match="critical review findings"):
166+
_finding(severity=ReviewFindingSeverity.CRITICAL).__class__(
167+
finding_id="finding-critical-no-hazard",
168+
scope=ReviewAuthorityScope.SAFETY_GATE,
169+
severity=ReviewFindingSeverity.CRITICAL,
170+
status=ReviewFindingStatus.OPEN,
171+
title="Critical finding without hazard",
172+
rationale="Critical review finding lacks hazard trace.",
173+
opened_by_actor_id="reviewer-001",
174+
opened_at_utc="2026-05-12T12:00:00Z",
175+
evidence_bundle_ids=("ev-review-finding-002",),
176+
)
177+
178+
179+
def test_waived_finding_requires_explicit_waiver_id() -> None:
180+
with pytest.raises(ContractValueError, match="waived review findings require waiver_id"):
181+
_finding(status=ReviewFindingStatus.WAIVED, waiver_id=None)
182+
183+
waived = _finding(status=ReviewFindingStatus.WAIVED, waiver_id="waiver-review-001")
184+
185+
assert waived.status.requires_waiver_reference()
186+
assert waived.waiver_id == "waiver-review-001"
187+
188+
189+
def test_signoff_enforces_acceptance_evidence_and_conditions() -> None:
190+
with pytest.raises(ContractValueError, match="accepting review signoffs"):
191+
ReviewSignoffRecord(
192+
signoff_id="signoff-no-evidence",
193+
workflow_id="workflow-review-001",
194+
actor=_actor(),
195+
scope=ReviewAuthorityScope.ASSURANCE_CASE,
196+
disposition=ReviewDisposition.APPROVED,
197+
rationale="No evidence attached.",
198+
signed_at_utc="2026-05-12T13:00:00Z",
199+
)
200+
201+
with pytest.raises(ContractValueError, match="approved_with_conditions"):
202+
_signoff(disposition=ReviewDisposition.APPROVED_WITH_CONDITIONS)
203+
204+
conditional = _signoff(
205+
disposition=ReviewDisposition.APPROVED_WITH_CONDITIONS,
206+
condition_ids=("condition-review-001",),
207+
)
208+
209+
assert conditional.supports_acceptance()
210+
211+
212+
def test_dissent_preserves_blocking_objection_traceability() -> None:
213+
with pytest.raises(ContractValueError, match="blocking dissent requires"):
214+
ReviewDissentRecord(
215+
dissent_id="dissent-blocking-no-finding",
216+
workflow_id="workflow-review-001",
217+
actor=_actor("reviewer-002"),
218+
scope=ReviewAuthorityScope.MONITORING,
219+
severity=ReviewDissentSeverity.BLOCKING_OBJECTION,
220+
rationale="Blocking objection without linked finding.",
221+
recorded_at_utc="2026-05-12T13:10:00Z",
222+
)
223+
224+
225+
def test_review_workflow_rejects_duplicate_and_mismatched_records() -> None:
226+
finding = _finding()
227+
228+
with pytest.raises(ContractValueError, match="review finding IDs"):
229+
_workflow(finding=finding).__class__(
230+
workflow_id="workflow-review-001",
231+
case_id="case-runtime-001",
232+
title="Runtime assurance review",
233+
status=ReviewWorkflowStatus.COMPLETED,
234+
authority_bindings=(_binding(),),
235+
findings=(finding, finding),
236+
signoffs=(_signoff(),),
237+
)
238+
239+
bad_signoff = ReviewSignoffRecord(
240+
signoff_id="signoff-other-workflow",
241+
workflow_id="workflow-other",
242+
actor=_actor(),
243+
scope=ReviewAuthorityScope.ASSURANCE_CASE,
244+
disposition=ReviewDisposition.APPROVED,
245+
rationale="Wrong workflow.",
246+
signed_at_utc="2026-05-12T13:00:00Z",
247+
evidence_bundle_ids=("ev-review-signoff-002",),
248+
)
249+
250+
with pytest.raises(ContractValueError, match="signoff workflow_id"):
251+
_workflow(signoff=bad_signoff)

0 commit comments

Comments
 (0)