Skip to content

Commit bcafd61

Browse files
authored
Create test_claim_guardrails_validation.py
1 parent 4c9a1ba commit bcafd61

1 file changed

Lines changed: 370 additions & 0 deletions

File tree

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from ix_autonomy_assurance_case_runtime.claim_guardrails import (
6+
ClaimAudience,
7+
ClaimEvidenceReference,
8+
ClaimEvidenceStrength,
9+
ClaimProhibitedPhraseRule,
10+
ClaimReleasePackage,
11+
ClaimReviewStatus,
12+
ClaimRiskLevel,
13+
ClaimStatementType,
14+
EvidenceBackedClaim,
15+
)
16+
from ix_autonomy_assurance_case_runtime.claim_guardrails_validation import (
17+
ClaimGuardrailValidationFinding,
18+
ClaimGuardrailValidationFindingSeverity,
19+
ClaimGuardrailValidationFindingSource,
20+
ClaimGuardrailValidator,
21+
)
22+
from ix_autonomy_assurance_case_runtime.contracts import ContractValueError, EvidenceStatus
23+
from ix_autonomy_assurance_case_runtime.evidence import EvidenceBundle, EvidenceRecord
24+
25+
26+
def _evidence_reference(
27+
*,
28+
reference_id: str = "claim-evidence-ref-001",
29+
evidence_bundle_id: str = "ev-claim-001",
30+
artifact_ids: tuple[str, ...] = ("artifact-readiness-rollup-001",),
31+
capability_ids: tuple[str, ...] = ("assurance-dossier",),
32+
) -> ClaimEvidenceReference:
33+
return ClaimEvidenceReference(
34+
reference_id=reference_id,
35+
evidence_bundle_id=evidence_bundle_id,
36+
artifact_ids=artifact_ids,
37+
capability_ids=capability_ids,
38+
)
39+
40+
41+
def _rule(
42+
*,
43+
rule_id: str = "rule-no-certified",
44+
phrase: str = "certified",
45+
blocks_release: bool = True,
46+
allowed_context_markers: tuple[str, ...] = ("not certified",),
47+
) -> ClaimProhibitedPhraseRule:
48+
return ClaimProhibitedPhraseRule(
49+
rule_id=rule_id,
50+
phrase=phrase,
51+
rationale="Prototype language must not imply official certification.",
52+
blocks_release=blocks_release,
53+
allowed_context_markers=allowed_context_markers,
54+
)
55+
56+
57+
def _claim(
58+
*,
59+
claim_id: str = "claim-serious-prototype-001",
60+
statement_type: ClaimStatementType = ClaimStatementType.MATURITY,
61+
risk_level: ClaimRiskLevel = ClaimRiskLevel.MODERATE,
62+
evidence_strength: ClaimEvidenceStrength = ClaimEvidenceStrength.TRACE_CLOSED,
63+
review_status: ClaimReviewStatus = ClaimReviewStatus.APPROVED_WITH_LIMITATIONS,
64+
text: str = "The project is a serious open-source prototype with trace-closed evidence.",
65+
evidence_reference_ids: tuple[str, ...] = ("claim-evidence-ref-001",),
66+
limitation_text: str | None = "Prototype maturity is not deployment approval.",
67+
reviewer_ids: tuple[str, ...] = ("reviewer-001",),
68+
related_capability_ids: tuple[str, ...] = ("assurance-dossier",),
69+
related_artifact_ids: tuple[str, ...] = ("artifact-readiness-rollup-001",),
70+
) -> EvidenceBackedClaim:
71+
return EvidenceBackedClaim(
72+
claim_id=claim_id,
73+
statement_type=statement_type,
74+
risk_level=risk_level,
75+
evidence_strength=evidence_strength,
76+
review_status=review_status,
77+
text=text,
78+
evidence_reference_ids=evidence_reference_ids,
79+
limitation_text=limitation_text,
80+
reviewer_ids=reviewer_ids,
81+
related_capability_ids=related_capability_ids,
82+
related_artifact_ids=related_artifact_ids,
83+
)
84+
85+
86+
def _limitation_claim() -> EvidenceBackedClaim:
87+
return EvidenceBackedClaim(
88+
claim_id="claim-non-endorsement-001",
89+
statement_type=ClaimStatementType.NON_ENDORSEMENT,
90+
risk_level=ClaimRiskLevel.LOW,
91+
evidence_strength=ClaimEvidenceStrength.NONE,
92+
review_status=ClaimReviewStatus.APPROVED,
93+
text=(
94+
"This package is not certified, not an authority-to-operate decision, "
95+
"and not agency acceptance."
96+
),
97+
evidence_reference_ids=(),
98+
)
99+
100+
101+
def _package(
102+
*,
103+
claims: tuple[EvidenceBackedClaim, ...] | None = None,
104+
evidence_references: tuple[ClaimEvidenceReference, ...] | None = None,
105+
prohibited_phrase_rules: tuple[ClaimProhibitedPhraseRule, ...] | None = None,
106+
review_status: ClaimReviewStatus = ClaimReviewStatus.APPROVED_WITH_LIMITATIONS,
107+
audience: ClaimAudience = ClaimAudience.FEDERAL_EVALUATION,
108+
disclaimer: str | None = None,
109+
) -> ClaimReleasePackage:
110+
return ClaimReleasePackage(
111+
package_id="claim-package-runtime-001",
112+
audience=audience,
113+
review_status=review_status,
114+
created_at_utc="2026-05-12T16:00:00Z",
115+
claims=claims if claims is not None else (_claim(), _limitation_claim()),
116+
evidence_references=(
117+
evidence_references if evidence_references is not None else (_evidence_reference(),)
118+
),
119+
prohibited_phrase_rules=(
120+
prohibited_phrase_rules if prohibited_phrase_rules is not None else (_rule(),)
121+
),
122+
release_notes=("Bounded prototype language only.",),
123+
disclaimer=disclaimer
124+
or (
125+
"Local prototype claim package only; not a certification, "
126+
"authority-to-operate decision, deployment approval, official endorsement, "
127+
"or agency acceptance."
128+
),
129+
)
130+
131+
132+
def _bundle(bundle_id: str = "ev-claim-001", *, hashed: bool = True) -> EvidenceBundle:
133+
bundle = EvidenceBundle(
134+
bundle_id=bundle_id,
135+
case_id="case-runtime-001",
136+
records=(
137+
EvidenceRecord(
138+
evidence_id=f"record-{bundle_id}",
139+
kind="claim-guardrail",
140+
source="unit-test",
141+
payload={"bundle_id": bundle_id},
142+
status=EvidenceStatus.ACCEPTED,
143+
),
144+
),
145+
)
146+
if hashed:
147+
return bundle.with_computed_hashes()
148+
return bundle
149+
150+
151+
def _validator(
152+
*,
153+
evidence_bundles: tuple[EvidenceBundle, ...] | None = None,
154+
known_capability_ids: tuple[str, ...] = ("assurance-dossier",),
155+
known_artifact_ids: tuple[str, ...] = ("artifact-readiness-rollup-001",),
156+
reviewer_ids: tuple[str, ...] = ("reviewer-001",),
157+
) -> ClaimGuardrailValidator:
158+
return ClaimGuardrailValidator(
159+
evidence_bundles=(_bundle(),) if evidence_bundles is None else evidence_bundles,
160+
known_capability_ids=known_capability_ids,
161+
known_artifact_ids=known_artifact_ids,
162+
reviewer_ids=reviewer_ids,
163+
)
164+
165+
166+
def test_claim_guardrail_validator_accepts_grounded_claim_package() -> None:
167+
report = _validator().validate(_package())
168+
169+
assert report.is_claim_release_ready()
170+
assert report.blocker_count == 0
171+
assert report.warning_count == 0
172+
assert report.summary() == (
173+
"claim-guardrail-validation: claim-package-runtime-001 "
174+
"(2 claim(s), 1 evidence reference(s), 1 prohibited rule(s), "
175+
"1 evidence bundle(s), 0 blocker(s), 0 warning(s))"
176+
)
177+
178+
179+
def test_claim_guardrail_validator_blocks_unreleased_package_review_status() -> None:
180+
report = _validator().validate(
181+
_package(
182+
review_status=ClaimReviewStatus.IN_REVIEW,
183+
audience=ClaimAudience.LOCAL_DEVELOPMENT,
184+
)
185+
)
186+
187+
assert not report.is_claim_release_ready()
188+
assert any(
189+
finding.finding_id == "package-claim-package-runtime-001-review-not-releaseable"
190+
for finding in report.findings
191+
)
192+
193+
194+
def test_claim_guardrail_validator_blocks_strict_audience_without_limitation_claim() -> None:
195+
report = _validator().validate(
196+
_package(
197+
claims=(_claim(),),
198+
review_status=ClaimReviewStatus.APPROVED,
199+
audience=ClaimAudience.FEDERAL_EVALUATION,
200+
)
201+
)
202+
203+
assert not report.is_claim_release_ready()
204+
assert any(
205+
finding.finding_id == "package-claim-package-runtime-001-missing-limitation-claim"
206+
for finding in report.findings
207+
)
208+
209+
210+
def test_claim_guardrail_validator_blocks_weak_or_prohibited_claims() -> None:
211+
weak_claim = _claim(
212+
claim_id="claim-weak-evidence-001",
213+
evidence_strength=ClaimEvidenceStrength.DESCRIBED,
214+
)
215+
prohibited_claim = _claim(
216+
claim_id="claim-prohibited-001",
217+
risk_level=ClaimRiskLevel.PROHIBITED,
218+
)
219+
report = _validator().validate(
220+
_package(claims=(weak_claim, prohibited_claim, _limitation_claim()))
221+
)
222+
223+
assert not report.is_claim_release_ready()
224+
assert report.findings_for_claim("claim-weak-evidence-001")
225+
assert report.findings_for_claim("claim-prohibited-001")
226+
227+
228+
def test_claim_guardrail_validator_blocks_prohibited_language_outside_allowed_context() -> None:
229+
report = _validator().validate(
230+
_package(
231+
claims=(
232+
_claim(text="This runtime is certified for use."),
233+
_limitation_claim(),
234+
)
235+
)
236+
)
237+
238+
assert not report.is_claim_release_ready()
239+
assert report.findings_for_rule("rule-no-certified")[0].source is (
240+
ClaimGuardrailValidationFindingSource.LANGUAGE
241+
)
242+
243+
244+
def test_claim_guardrail_validator_blocks_missing_claim_evidence_reference() -> None:
245+
report = _validator().validate(
246+
_package(
247+
claims=(
248+
_claim(evidence_reference_ids=("claim-evidence-ref-missing",)),
249+
_limitation_claim(),
250+
)
251+
)
252+
)
253+
254+
assert not report.is_claim_release_ready()
255+
assert report.findings_for_evidence_reference("claim-evidence-ref-missing")
256+
257+
258+
def test_claim_guardrail_validator_blocks_unsupported_capability_and_artifact_links() -> None:
259+
report = _validator().validate(
260+
_package(
261+
evidence_references=(
262+
_evidence_reference(
263+
artifact_ids=("artifact-other",),
264+
capability_ids=("capability-other",),
265+
),
266+
)
267+
)
268+
)
269+
270+
assert not report.is_claim_release_ready()
271+
assert any(
272+
finding.capability_id == "assurance-dossier"
273+
for finding in report.findings_for_claim("claim-serious-prototype-001")
274+
)
275+
assert any(
276+
finding.artifact_id == "artifact-readiness-rollup-001"
277+
for finding in report.findings_for_claim("claim-serious-prototype-001")
278+
)
279+
280+
281+
def test_claim_guardrail_validator_blocks_unknown_reviewer_capability_and_artifact() -> None:
282+
report = _validator(
283+
known_capability_ids=("other-capability",),
284+
known_artifact_ids=("other-artifact",),
285+
reviewer_ids=("reviewer-other",),
286+
).validate(_package())
287+
288+
assert not report.is_claim_release_ready()
289+
assert report.findings_for_reviewer("reviewer-001")
290+
assert any(finding.capability_id == "assurance-dossier" for finding in report.findings)
291+
assert any(finding.artifact_id == "artifact-readiness-rollup-001" for finding in report.findings)
292+
293+
294+
def test_claim_guardrail_validator_blocks_missing_evidence_bundle() -> None:
295+
report = _validator(evidence_bundles=()).validate(_package())
296+
297+
assert not report.is_claim_release_ready()
298+
assert report.findings_for_evidence_bundle("ev-claim-001")[0].source is (
299+
ClaimGuardrailValidationFindingSource.EVIDENCE
300+
)
301+
302+
303+
def test_claim_guardrail_validator_warns_for_unhashed_evidence_bundle() -> None:
304+
report = _validator(evidence_bundles=(_bundle(hashed=False),)).validate(_package())
305+
306+
assert report.is_claim_release_ready()
307+
assert report.warning_count == 2
308+
assert report.findings_for_evidence_bundle("ev-claim-001")
309+
310+
311+
def test_claim_guardrail_validator_warns_for_unused_evidence_reference() -> None:
312+
report = _validator(
313+
evidence_bundles=(_bundle(), _bundle("ev-unused-claim-001")),
314+
).validate(
315+
_package(
316+
evidence_references=(
317+
_evidence_reference(),
318+
_evidence_reference(
319+
reference_id="claim-evidence-ref-unused",
320+
evidence_bundle_id="ev-unused-claim-001",
321+
),
322+
)
323+
)
324+
)
325+
326+
assert report.is_claim_release_ready()
327+
assert report.warning_count == 1
328+
assert report.findings_for_evidence_reference("claim-evidence-ref-unused")
329+
330+
331+
def test_claim_guardrail_validator_blocks_weak_disclaimer() -> None:
332+
report = _validator().validate(_package(disclaimer="Ready for review."))
333+
334+
assert not report.is_claim_release_ready()
335+
assert any(
336+
finding.source is ClaimGuardrailValidationFindingSource.DISCLAIMER
337+
for finding in report.findings
338+
)
339+
340+
341+
def test_claim_guardrail_validator_rejects_duplicate_inputs() -> None:
342+
bundle = _bundle()
343+
344+
with pytest.raises(ContractValueError, match="Duplicate claim guardrail evidence"):
345+
ClaimGuardrailValidator(evidence_bundles=(bundle, bundle))
346+
347+
with pytest.raises(ContractValueError, match="known_capability_ids"):
348+
ClaimGuardrailValidator(known_capability_ids=("capability-001", "capability-001"))
349+
350+
with pytest.raises(ContractValueError, match="reviewer_ids"):
351+
ClaimGuardrailValidator(reviewer_ids=("reviewer-001", "reviewer-001"))
352+
353+
354+
def test_claim_guardrail_validation_finding_validates_optional_identifiers() -> None:
355+
with pytest.raises(ContractValueError, match="needs a message"):
356+
ClaimGuardrailValidationFinding(
357+
finding_id="finding-claim-validation-001",
358+
severity=ClaimGuardrailValidationFindingSeverity.BLOCKER,
359+
source=ClaimGuardrailValidationFindingSource.CLAIM,
360+
message="",
361+
)
362+
363+
with pytest.raises(ContractValueError, match="claim_id must not be blank"):
364+
ClaimGuardrailValidationFinding(
365+
finding_id="finding-claim-validation-001",
366+
severity=ClaimGuardrailValidationFindingSeverity.BLOCKER,
367+
source=ClaimGuardrailValidationFindingSource.CLAIM,
368+
message="Bad claim.",
369+
claim_id="",
370+
)

0 commit comments

Comments
 (0)