| """ |
| Tests for src/primordial_os/audit_log.py — Phase 6 Audit Log |
| """ |
|
|
| import dataclasses |
| import inspect |
|
|
| import pytest |
|
|
| from primordial_os.audit_log import ( |
| AuditEvent, |
| append_to_audit_chain, |
| compute_event_hash, |
| create_audit_event, |
| create_runtime_audit_event, |
| verify_audit_chain, |
| ) |
| from primordial_os.hir_kernel import HIRInput |
| from primordial_os.oam_detector import OAMSignals |
| from primordial_os.runtime import run_evaluation |
|
|
|
|
| |
| |
| |
|
|
| def _green_decision(): |
| return run_evaluation( |
| HIRInput(0.9, 0.9, 0.9, 0.9, pressure=0.0), |
| OAMSignals( |
| certainty_level=0.5, |
| decision_made_for_user=False, |
| context_preservation=0.9, |
| dignity_preserved=0.9, |
| identity_assigned=False, |
| session_load_ratio=0.2, |
| uncertainty_disclosed=True, |
| autonomy_scope=0.2, |
| consent_present=True, |
| clinical_language_present=False, |
| memory_accessed_without_consent=False, |
| end_of_life_decision_attempted=False, |
| human_override_available=True, |
| ), |
| ) |
|
|
|
|
| def _red_decision(): |
| return run_evaluation( |
| HIRInput(0.9, 0.9, 0.9, 0.9, pressure=0.0), |
| OAMSignals( |
| certainty_level=0.5, |
| decision_made_for_user=True, |
| context_preservation=0.9, |
| dignity_preserved=0.9, |
| identity_assigned=False, |
| session_load_ratio=0.2, |
| uncertainty_disclosed=True, |
| autonomy_scope=0.2, |
| consent_present=True, |
| clinical_language_present=False, |
| memory_accessed_without_consent=False, |
| end_of_life_decision_attempted=False, |
| human_override_available=True, |
| ), |
| ) |
|
|
|
|
| def _fixed_hash_args(**overrides) -> dict: |
| defaults = dict( |
| timestamp="2026-05-02T12:00:00+00:00", |
| event_type="runtime_evaluation", |
| final_gate_state="GREEN", |
| hard_stop=False, |
| resonance=0.9, |
| pressure_adjusted_stability=0.9, |
| max_oam_severity=0.0, |
| summary="Gate: GREEN. Resonance=0.900. PAS=0.900. OAM severity=0.000.", |
| previous_hash=None, |
| ) |
| defaults.update(overrides) |
| return defaults |
|
|
|
|
| |
| |
| |
|
|
| class TestComputeEventHash: |
| def test_returns_64_char_hex_string(self): |
| h = compute_event_hash(**_fixed_hash_args()) |
| assert len(h) == 64 |
| assert all(c in "0123456789abcdef" for c in h) |
|
|
| def test_deterministic_same_inputs(self): |
| args = _fixed_hash_args() |
| assert compute_event_hash(**args) == compute_event_hash(**args) |
|
|
| def test_different_summary_produces_different_hash(self): |
| h1 = compute_event_hash(**_fixed_hash_args(summary="Gate: GREEN.")) |
| h2 = compute_event_hash(**_fixed_hash_args(summary="Gate: RED.")) |
| assert h1 != h2 |
|
|
| def test_different_gate_state_produces_different_hash(self): |
| h1 = compute_event_hash(**_fixed_hash_args(final_gate_state="GREEN")) |
| h2 = compute_event_hash(**_fixed_hash_args(final_gate_state="RED")) |
| assert h1 != h2 |
|
|
| def test_different_resonance_produces_different_hash(self): |
| h1 = compute_event_hash(**_fixed_hash_args(resonance=0.9)) |
| h2 = compute_event_hash(**_fixed_hash_args(resonance=0.5)) |
| assert h1 != h2 |
|
|
| def test_previous_hash_affects_current_hash(self): |
| h_no_prev = compute_event_hash(**_fixed_hash_args(previous_hash=None)) |
| h_with_prev = compute_event_hash(**_fixed_hash_args(previous_hash="abc123")) |
| assert h_no_prev != h_with_prev |
|
|
| def test_different_previous_hashes_produce_different_hashes(self): |
| h1 = compute_event_hash(**_fixed_hash_args(previous_hash="aaa")) |
| h2 = compute_event_hash(**_fixed_hash_args(previous_hash="bbb")) |
| assert h1 != h2 |
|
|
| def test_hard_stop_bool_affects_hash(self): |
| h_false = compute_event_hash(**_fixed_hash_args(hard_stop=False)) |
| h_true = compute_event_hash(**_fixed_hash_args(hard_stop=True)) |
| assert h_false != h_true |
|
|
| def test_timestamp_affects_hash(self): |
| h1 = compute_event_hash(**_fixed_hash_args(timestamp="2026-05-02T12:00:00+00:00")) |
| h2 = compute_event_hash(**_fixed_hash_args(timestamp="2026-05-02T13:00:00+00:00")) |
| assert h1 != h2 |
|
|
|
|
| |
| |
| |
|
|
| class TestAuditEventStructure: |
| def test_audit_event_is_frozen(self): |
| event = create_audit_event(_green_decision()) |
| with pytest.raises((AttributeError, TypeError)): |
| event.summary = "tampered" |
|
|
| def test_create_returns_audit_event_instance(self): |
| event = create_audit_event(_green_decision()) |
| assert isinstance(event, AuditEvent) |
|
|
| def test_current_hash_is_64_chars(self): |
| event = create_audit_event(_green_decision()) |
| assert len(event.current_hash) == 64 |
|
|
| def test_previous_hash_none_for_first_event(self): |
| event = create_audit_event(_green_decision()) |
| assert event.previous_hash is None |
|
|
| def test_previous_hash_propagated_when_supplied(self): |
| sentinel = "a" * 64 |
| event = create_audit_event(_green_decision(), previous_hash=sentinel) |
| assert event.previous_hash == sentinel |
|
|
| def test_gate_state_stored_as_string(self): |
| event = create_audit_event(_green_decision()) |
| assert event.final_gate_state == "GREEN" |
|
|
| def test_hard_stop_matches_decision(self): |
| green = create_audit_event(_green_decision()) |
| red = create_audit_event(_red_decision()) |
| assert green.hard_stop is False |
| assert red.hard_stop is True |
|
|
| def test_resonance_matches_decision(self): |
| decision = _green_decision() |
| event = create_audit_event(decision) |
| assert event.resonance == pytest.approx(decision.hir_result.resonance) |
|
|
| def test_event_type_default(self): |
| event = create_audit_event(_green_decision()) |
| assert event.event_type == "runtime_evaluation" |
|
|
| def test_event_type_custom(self): |
| event = create_audit_event(_green_decision(), event_type="manual_review") |
| assert event.event_type == "manual_review" |
|
|
| def test_timestamp_is_non_empty_string(self): |
| event = create_audit_event(_green_decision()) |
| assert isinstance(event.timestamp, str) |
| assert len(event.timestamp) > 0 |
|
|
|
|
| |
| |
| |
|
|
| class TestVerifyAuditChain: |
| def test_empty_chain_is_valid(self): |
| assert verify_audit_chain([]) is True |
|
|
| def test_single_event_chain_is_valid(self): |
| event = create_audit_event(_green_decision()) |
| assert verify_audit_chain([event]) is True |
|
|
| def test_two_event_chain_is_valid(self): |
| e1 = create_audit_event(_green_decision()) |
| e2 = create_audit_event(_red_decision(), previous_hash=e1.current_hash) |
| assert verify_audit_chain([e1, e2]) is True |
|
|
| def test_three_event_chain_is_valid(self): |
| e1 = create_audit_event(_green_decision()) |
| e2 = create_audit_event(_red_decision(), previous_hash=e1.current_hash) |
| e3 = create_audit_event(_green_decision(), previous_hash=e2.current_hash) |
| assert verify_audit_chain([e1, e2, e3]) is True |
|
|
| def test_tampered_summary_breaks_chain(self): |
| event = create_audit_event(_green_decision()) |
| tampered = dataclasses.replace(event, summary="TAMPERED") |
| assert verify_audit_chain([tampered]) is False |
|
|
| def test_tampered_gate_state_breaks_chain(self): |
| event = create_audit_event(_green_decision()) |
| tampered = dataclasses.replace(event, final_gate_state="RED") |
| assert verify_audit_chain([tampered]) is False |
|
|
| def test_tampered_resonance_breaks_chain(self): |
| event = create_audit_event(_green_decision()) |
| tampered = dataclasses.replace(event, resonance=0.0) |
| assert verify_audit_chain([tampered]) is False |
|
|
| def test_tampered_hard_stop_breaks_chain(self): |
| event = create_audit_event(_green_decision()) |
| tampered = dataclasses.replace(event, hard_stop=True) |
| assert verify_audit_chain([tampered]) is False |
|
|
| def test_tampered_previous_hash_breaks_chain(self): |
| e1 = create_audit_event(_green_decision()) |
| e2 = create_audit_event(_red_decision(), previous_hash=e1.current_hash) |
| |
| tampered_e2 = dataclasses.replace(e2, previous_hash="wrong_hash") |
| assert verify_audit_chain([e1, tampered_e2]) is False |
|
|
| def test_broken_chain_link_breaks_verification(self): |
| e1 = create_audit_event(_green_decision()) |
| |
| e2 = create_audit_event(_red_decision(), previous_hash=None) |
| assert verify_audit_chain([e1, e2]) is False |
|
|
| def test_middle_event_tamper_breaks_chain(self): |
| e1 = create_audit_event(_green_decision()) |
| e2 = create_audit_event(_red_decision(), previous_hash=e1.current_hash) |
| e3 = create_audit_event(_green_decision(), previous_hash=e2.current_hash) |
| tampered_e2 = dataclasses.replace(e2, summary="TAMPERED") |
| assert verify_audit_chain([e1, tampered_e2, e3]) is False |
|
|
| def test_reordered_chain_breaks_verification(self): |
| e1 = create_audit_event(_green_decision()) |
| e2 = create_audit_event(_red_decision(), previous_hash=e1.current_hash) |
| |
| assert verify_audit_chain([e2, e1]) is False |
|
|
|
|
| |
| |
| |
|
|
| class TestCreateRuntimeAuditEvent: |
| def test_returns_audit_event_instance(self): |
| event = create_runtime_audit_event(_green_decision()) |
| assert isinstance(event, AuditEvent) |
|
|
| def test_previous_hash_is_none(self): |
| event = create_runtime_audit_event(_green_decision()) |
| assert event.previous_hash is None |
|
|
| def test_default_event_type_is_runtime_evaluation(self): |
| event = create_runtime_audit_event(_green_decision()) |
| assert event.event_type == "runtime_evaluation" |
|
|
| def test_custom_event_type_propagated(self): |
| event = create_runtime_audit_event(_green_decision(), event_type="test_event") |
| assert event.event_type == "test_event" |
|
|
| def test_hash_is_64_chars(self): |
| event = create_runtime_audit_event(_green_decision()) |
| assert len(event.current_hash) == 64 |
|
|
| def test_hash_verifies_as_single_chain(self): |
| event = create_runtime_audit_event(_green_decision()) |
| assert verify_audit_chain([event]) is True |
|
|
|
|
| |
| |
| |
|
|
| class TestAppendToAuditChain: |
| def test_append_to_empty_tuple_sets_previous_hash_none(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| assert chain[0].previous_hash is None |
|
|
| def test_append_to_empty_list_sets_previous_hash_none(self): |
| chain = append_to_audit_chain([], _green_decision()) |
| assert chain[0].previous_hash is None |
|
|
| def test_append_to_nonempty_chain_links_previous_hash(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| chain2 = append_to_audit_chain(chain, _red_decision()) |
| assert chain2[1].previous_hash == chain[0].current_hash |
|
|
| def test_append_does_not_mutate_original_list(self): |
| original: list = [] |
| append_to_audit_chain(original, _green_decision()) |
| assert len(original) == 0 |
|
|
| def test_append_does_not_mutate_original_tuple(self): |
| first = append_to_audit_chain((), _green_decision()) |
| _ = append_to_audit_chain(first, _red_decision()) |
| assert len(first) == 1 |
|
|
| def test_result_is_tuple(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| assert isinstance(chain, tuple) |
|
|
| def test_chain_length_grows_by_one_per_append(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| chain2 = append_to_audit_chain(chain, _red_decision()) |
| assert len(chain) == 1 |
| assert len(chain2) == 2 |
|
|
| def test_single_append_chain_verifies_true(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| assert verify_audit_chain(chain) is True |
|
|
| def test_double_append_chain_verifies_true(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| chain = append_to_audit_chain(chain, _red_decision()) |
| assert verify_audit_chain(chain) is True |
|
|
| def test_triple_append_chain_verifies_true(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| chain = append_to_audit_chain(chain, _red_decision()) |
| chain = append_to_audit_chain(chain, _green_decision()) |
| assert verify_audit_chain(chain) is True |
|
|
| def test_tampered_appended_event_breaks_verification(self): |
| chain = append_to_audit_chain((), _green_decision()) |
| tampered = dataclasses.replace(chain[0], summary="TAMPERED") |
| assert verify_audit_chain((tampered,)) is False |
|
|
|
|
| |
| |
| |
|
|
| class TestSourceHygiene: |
| def test_no_coherence_terminology_in_source(self): |
| import primordial_os.audit_log as mod |
| source = inspect.getsource(mod) |
| assert "coherence" not in source.lower(), ( |
| "Banned term 'coherence' found in audit_log source" |
| ) |
|
|