tests/test_verifier.py
2.6 KB · 87 lines · python Raw
1 """Tests for TraceVerifier."""
2
3 from __future__ import annotations
4
5 import pytest
6
7 from pqc_reasoning_ledger import (
8 ReasoningRecorder,
9 SignatureVerificationError,
10 TraceVerifier,
11 )
12
13
14 def _seal_small(recorder: ReasoningRecorder):
15 recorder.record_observation("obs A")
16 recorder.record_deduction("ded B")
17 recorder.record_decision("final C")
18 return recorder.seal()
19
20
21 def test_verify_valid_sealed_trace(
22 sample_trace_started: ReasoningRecorder,
23 ) -> None:
24 sealed = _seal_small(sample_trace_started)
25 result = TraceVerifier.verify(sealed)
26 assert result.valid is True
27 assert result.fully_verified is True
28 assert result.error is None
29 assert result.step_count == 3
30
31
32 def test_tamper_step_content_fails_chain_check(
33 sample_trace_started: ReasoningRecorder,
34 ) -> None:
35 sealed = _seal_small(sample_trace_started)
36 # Flip content on step 1 - step_hash stays the same but would no longer match
37 # the recomputed hash because content_hash embedded in canonical_bytes changes.
38 # However content_hash is what's hashed, not content, so we have to tamper
39 # content_hash too to break the chain. Tamper content_hash directly.
40 sealed.steps[0].content_hash = "0" * 64
41 result = TraceVerifier.verify(sealed)
42 assert result.chain_intact is False
43 assert result.valid is False
44
45
46 def test_tamper_merkle_root_fails_merkle_check(
47 sample_trace_started: ReasoningRecorder,
48 ) -> None:
49 sealed = _seal_small(sample_trace_started)
50 sealed.merkle_root = "0" * 64
51 result = TraceVerifier.verify(sealed)
52 assert result.merkle_root_valid is False
53 assert result.valid is False
54
55
56 def test_tamper_signature_fails_sig_check(
57 sample_trace_started: ReasoningRecorder,
58 ) -> None:
59 sealed = _seal_small(sample_trace_started)
60 # flip one byte of the signature hex
61 orig = sealed.signature
62 flipped = ("0" if orig[0] != "0" else "f") + orig[1:]
63 sealed.signature = flipped
64 result = TraceVerifier.verify(sealed)
65 assert result.signature_valid is False
66 assert result.valid is False
67
68
69 def test_verify_or_raise_raises_on_invalid(
70 sample_trace_started: ReasoningRecorder,
71 ) -> None:
72 sealed = _seal_small(sample_trace_started)
73 sealed.merkle_root = "0" * 64
74 with pytest.raises(SignatureVerificationError):
75 TraceVerifier.verify_or_raise(sealed)
76
77
78 def test_missing_signature_returns_invalid(
79 sample_trace_started: ReasoningRecorder,
80 ) -> None:
81 sealed = _seal_small(sample_trace_started)
82 sealed.signature = ""
83 result = TraceVerifier.verify(sealed)
84 assert result.signature_valid is False
85 assert result.valid is False
86 assert result.error is not None
87