"""Tests for Master Capability Registry v0 (Phase 2C, Compliance Execution domain). Acceptance: a registry mints stable MCAP ids, stores typed relations + versioned policy + lifecycle events + provenance, and DERIVES confidence/status on demand (never stored). Hard rule: a certification alone can never yield CONFIRMED. No real ISO/cert mappings here — only synthetic relations (mappings are not part of v0; they are Execution data, injected later). """ from __future__ import annotations import pytest from compliance.capability import ( DEFAULT_POLICY, AssertionStatus, CapabilityCandidate, CapabilityRegistry, CapabilityRelation, Confidence, EvidenceKind, LifecycleEventType, PolicyRule, PolicyVersion, RelationType, assert_no_certification_confirms, deprecate_capability, evaluate_relation, merge_capabilities, mint_capability, resolve, ) def _rel(rt, ek, target="MCAP-00001", src="x"): return CapabilityRelation( relation_id="r", source=src, target_capability_id=target, relationship_type=rt, evidence_kind=ek ) # 1. minting assigns stable, incrementing MCAP ids. def test_mint_stable_mcap_id(): reg = CapabilityRegistry() a = mint_capability(reg, CapabilityCandidate(raw_term="Patch Management")) b = mint_capability(reg, CapabilityCandidate(raw_term="Incident Response")) assert a.capability_id == "MCAP-00001" assert b.capability_id == "MCAP-00002" assert reg.capabilities["MCAP-00001"].name == "Patch Management" # 2. minted capability carries provenance. def test_mint_records_provenance(): reg = CapabilityRegistry() cap = mint_capability(reg, CapabilityCandidate(raw_term="Secure Development")) assert cap.provenance.author == "system" assert "Secure Development" in cap.provenance.basis # 3. all seven relationship types exist. def test_relation_types_complete(): assert {t.value for t in RelationType} == { "equivalent", "supports", "requires", "confirms", "broader_than", "narrower_than", "related_to", } # 4. confidence is COMPUTED from relation_type + evidence + policy_version, not stored. def test_confidence_computed_and_policy_referenced(): rel = _rel(RelationType.SUPPORTS, EvidenceKind.CERTIFICATION) a = evaluate_relation(rel, DEFAULT_POLICY) assert a.status == AssertionStatus.INFERRED and a.confidence == Confidence.LOW assert a.policy_version == "capability-policy-v0" # the registry itself stores NO confidence/coverage assert "confidence" not in CapabilityRegistry.model_fields assert "coverage" not in CapabilityRegistry.model_fields # 4b. a DIFFERENT policy version yields a different result for the SAME relation # -> "why did you say X last year" needs the policy-as-of-then. def test_policy_versioning_changes_outcome(): rel = _rel(RelationType.SUPPORTS, EvidenceKind.CERTIFICATION) v0b = PolicyVersion( policy_version="capability-policy-v0b", rules=[PolicyRule( relationship_type=RelationType.SUPPORTS, evidence_kind=EvidenceKind.CERTIFICATION, status=AssertionStatus.INFERRED, confidence=Confidence.MEDIUM, )], ) assert evaluate_relation(rel, DEFAULT_POLICY).confidence == Confidence.LOW assert evaluate_relation(rel, v0b).confidence == Confidence.MEDIUM assert evaluate_relation(rel, v0b).policy_version == "capability-policy-v0b" # 5 + 8. HARD RULE: a certification alone can NEVER be CONFIRMED. def test_certification_never_confirmed(): for rt in (RelationType.SUPPORTS, RelationType.EQUIVALENT, RelationType.BROADER_THAN, RelationType.RELATED_TO): a = evaluate_relation(_rel(rt, EvidenceKind.CERTIFICATION), DEFAULT_POLICY) assert a.status != AssertionStatus.CONFIRMED # only a concrete artifact via a CONFIRMS relation reaches CONFIRMED assert evaluate_relation(_rel(RelationType.CONFIRMS, EvidenceKind.ARTIFACT)).status == AssertionStatus.CONFIRMED # the shipped policy structurally satisfies the hard rule assert_no_certification_confirms(DEFAULT_POLICY) # 8b. a policy that maps a certification to CONFIRMED is rejected. def test_bad_policy_rejected(): bad = PolicyVersion( policy_version="bad", rules=[PolicyRule( relationship_type=RelationType.EQUIVALENT, evidence_kind=EvidenceKind.CERTIFICATION, status=AssertionStatus.CONFIRMED, confidence=Confidence.HIGH, )], ) with pytest.raises(ValueError): assert_no_certification_confirms(bad) # 6. provenance on a curated relation atom. def test_relation_carries_provenance(): rel = CapabilityRelation( relation_id="r1", source="certification:ISO27001", target_capability_id="MCAP-00001", relationship_type=RelationType.SUPPORTS, evidence_kind=EvidenceKind.CERTIFICATION, ) assert rel.relationship_type == RelationType.SUPPORTS assert hasattr(rel, "provenance") # 9. merge keeps a redirect: resolve(old) follows it to the new capability. def test_merge_keeps_redirect(): reg = CapabilityRegistry() old = mint_capability(reg, CapabilityCandidate(raw_term="Update Management")) new = mint_capability(reg, CapabilityCandidate(raw_term="Software Update Management")) event = merge_capabilities(reg, old.capability_id, new.capability_id) assert event.event_type == LifecycleEventType.MERGE assert reg.capabilities[old.capability_id].redirect_to == new.capability_id # resolve follows the redirect to the canonical capability assert resolve(reg, old.capability_id).capability_id == new.capability_id assert reg.lifecycle_events[-1].from_ids == [old.capability_id] # 9b. deprecate without a redirect resolves to None (no canonical target). def test_deprecate_without_redirect_resolves_none(): reg = CapabilityRegistry() cap = mint_capability(reg, CapabilityCandidate(raw_term="Legacy Capability")) deprecate_capability(reg, cap.capability_id) assert resolve(reg, cap.capability_id) is None assert reg.capabilities[cap.capability_id].state.value == "deprecated" # requires = an obligation NEEDS a capability (relevance), not possession -> unknown. def test_requires_is_relevance_not_possession(): a = evaluate_relation(_rel(RelationType.REQUIRES, EvidenceKind.NONE), DEFAULT_POLICY) assert a.status == AssertionStatus.UNKNOWN