gabriel / muse public
test_attestation_interface.py python
416 lines 16.1 KB
Raw
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
1 """Tests for muse.core.attestation — Phase 7.1 framework interface.
2
3 Covers Rule #0 tiers that apply to a pure framework module:
4
5 * unit — Protocol conformance, registry thread-safety, error hierarchy.
6 * integration — Stub provider compute → anchor → verify round-trip.
7 * security — Misuse-resistance: partial verification raises, anchor()
8 is write-once, registry isolation between domains, error
9 type hierarchy is the only path for failure signalling.
10
11 The "stress" / "performance" tiers are intentionally omitted at this layer —
12 the framework has no I/O, so latency/throughput tests would only measure
13 Python interpreter overhead and add noise without value. Stress at the
14 provider implementations (Phase 7.2 / 7.4) is where it belongs.
15 """
16
17 from __future__ import annotations
18
19 import threading
20 from typing import cast
21
22 import pytest
23
24 from muse.core.attestation import (
25 AnchorReceipt,
26 AttestationError,
27 AttestationRecord,
28 AttestationRequiredError,
29 AttestationVerifyError,
30 ImmutableReceiptError,
31 MuseAttestationProvider,
32 VerifyResult,
33 _registered_domains,
34 get_attestation_provider,
35 register_attestation_provider,
36 unregister_attestation_provider,
37 )
38
39
40 # ---------------------------------------------------------------------------
41 # Stub providers — minimal, deterministic, used across every tier.
42 # ---------------------------------------------------------------------------
43
44
45 class _StubProvider:
46 """Deterministic in-memory stub that satisfies MuseAttestationProvider."""
47
48 def __init__(self, provider_id: str = "stub") -> None:
49 self.provider_id = provider_id
50 self._anchored: dict[str, AnchorReceipt] = {}
51 self._lock = threading.Lock()
52
53 def compute_attestation(
54 self, snapshot_id: str, commit_meta: dict[str, object]
55 ) -> AttestationRecord:
56 path = str(commit_meta.get("path", ""))
57 return {
58 "id": f"{snapshot_id}:{path}",
59 "action": str(commit_meta.get("action", "write")),
60 "path": path,
61 "timestamp": "2026-05-13T00:00:00Z",
62 "content_hash": str(commit_meta.get("content_hash", "")),
63 "sig": "stub-sig",
64 "algorithm": "stub",
65 "provider_id": self.provider_id,
66 }
67
68 def anchor(self, record: AttestationRecord) -> AnchorReceipt:
69 rid = record.get("id", "")
70 with self._lock:
71 if rid in self._anchored:
72 raise ImmutableReceiptError(
73 f"record {rid!r} already anchored (write-once)"
74 )
75 receipt: AnchorReceipt = {
76 "tx_id": f"tx-{len(self._anchored) + 1}",
77 "anchor_url": f"https://stub.example/attest/{rid}",
78 "anchored_at": "2026-05-13T00:00:01Z",
79 "provider_id": self.provider_id,
80 }
81 self._anchored[rid] = receipt
82 return receipt
83
84 def verify(
85 self, record: AttestationRecord, receipt: AnchorReceipt
86 ) -> VerifyResult:
87 if record.get("provider_id") != receipt.get("provider_id"):
88 raise AttestationVerifyError(
89 "provider_id mismatch between record and receipt"
90 )
91 with self._lock:
92 anchored = self._anchored.get(record.get("id", ""))
93 if anchored is None:
94 raise AttestationVerifyError(
95 "no receipt on file — cannot complete verification",
96 code="NO_RECEIPT",
97 )
98 if anchored != receipt:
99 return {
100 "valid": False,
101 "reason": "receipt does not match anchored copy",
102 "depth": 0,
103 "provider_id": self.provider_id,
104 }
105 return {
106 "valid": True,
107 "reason": "",
108 "depth": 0,
109 "provider_id": self.provider_id,
110 }
111
112
113 class _NotAProvider:
114 """Object that does NOT implement the protocol (missing methods)."""
115
116 def compute_attestation(self, snapshot_id: str, commit_meta: dict[str, object]) -> AttestationRecord: # noqa: ARG002
117 return cast(AttestationRecord, {})
118
119 # Intentionally missing anchor() and verify().
120
121
122 # ---------------------------------------------------------------------------
123 # Auto-cleanup fixture — keep the registry pristine between tests.
124 # ---------------------------------------------------------------------------
125
126
127 @pytest.fixture(autouse=True)
128 def _clean_registry() -> None:
129 """Snapshot and restore registered providers around every test."""
130 before = _registered_domains()
131 yield
132 for d in _registered_domains():
133 if d not in before:
134 unregister_attestation_provider(d)
135
136
137 # ===========================================================================
138 # UNIT TIER
139 # ===========================================================================
140
141
142 class TestProtocolConformance:
143 """Verify Protocol structural typing works as documented."""
144
145 def test_stub_provider_satisfies_protocol(self) -> None:
146 """Stub provider with all three methods is recognised as the Protocol."""
147 provider = _StubProvider()
148 assert isinstance(provider, MuseAttestationProvider)
149
150 def test_incomplete_object_does_not_satisfy_protocol(self) -> None:
151 """Objects missing required methods fail isinstance — registration rejects."""
152 with pytest.raises(TypeError, match="MuseAttestationProvider"):
153 register_attestation_provider("nope", cast(MuseAttestationProvider, _NotAProvider()))
154
155
156 class TestErrorHierarchy:
157 """Every typed error derives from AttestationError; codes are stable."""
158
159 def test_required_error_subclasses_base(self) -> None:
160 """AttestationRequiredError is catchable via the base class."""
161 with pytest.raises(AttestationError):
162 raise AttestationRequiredError("required but missing")
163
164 def test_verify_error_subclasses_base(self) -> None:
165 """AttestationVerifyError is catchable via the base class."""
166 with pytest.raises(AttestationError):
167 raise AttestationVerifyError("cannot verify")
168
169 def test_immutable_receipt_subclasses_base(self) -> None:
170 """ImmutableReceiptError is catchable via the base class."""
171 with pytest.raises(AttestationError):
172 raise ImmutableReceiptError("write-once violation")
173
174 def test_required_error_code_constant(self) -> None:
175 """code attribute is the published constant — used by CLI/MCP layers."""
176 try:
177 raise AttestationRequiredError("x")
178 except AttestationRequiredError as exc:
179 assert exc.code == "ATTESTATION_REQUIRED"
180
181 def test_verify_error_default_and_override_codes(self) -> None:
182 """code defaults to VERIFY_INCOMPLETE; overrides are honoured."""
183 try:
184 raise AttestationVerifyError("default")
185 except AttestationVerifyError as exc:
186 assert exc.code == "VERIFY_INCOMPLETE"
187 try:
188 raise AttestationVerifyError("custom", code="ICP_UNREACHABLE")
189 except AttestationVerifyError as exc:
190 assert exc.code == "ICP_UNREACHABLE"
191
192 def test_immutable_receipt_code_constant(self) -> None:
193 """ImmutableReceiptError carries its stable code."""
194 try:
195 raise ImmutableReceiptError("dup")
196 except ImmutableReceiptError as exc:
197 assert exc.code == "IMMUTABLE_RECEIPT"
198
199
200 class TestRegistryBasics:
201 """register/get/unregister behave per documented contract."""
202
203 def test_register_then_get_returns_same_instance(self) -> None:
204 """get() returns the exact instance passed to register()."""
205 provider = _StubProvider()
206 register_attestation_provider("stubdom", provider)
207 assert get_attestation_provider("stubdom") is provider
208
209 def test_get_unregistered_domain_returns_none(self) -> None:
210 """Looking up a domain with no provider returns None, not a fallback."""
211 assert get_attestation_provider("never-registered") is None
212
213 def test_register_replaces_previous(self) -> None:
214 """Re-registering the same domain replaces the prior provider."""
215 first = _StubProvider("first")
216 second = _StubProvider("second")
217 register_attestation_provider("stubdom", first)
218 register_attestation_provider("stubdom", second)
219 assert get_attestation_provider("stubdom") is second
220
221 def test_unregister_returns_true_when_present(self) -> None:
222 """unregister returns True when a provider was removed."""
223 register_attestation_provider("stubdom", _StubProvider())
224 assert unregister_attestation_provider("stubdom") is True
225
226 def test_unregister_returns_false_when_absent(self) -> None:
227 """unregister returns False on a domain that was never registered."""
228 assert unregister_attestation_provider("never-here") is False
229
230 def test_register_rejects_empty_domain(self) -> None:
231 """Empty domain names are rejected at registration time."""
232 with pytest.raises(ValueError, match="non-empty string"):
233 register_attestation_provider("", _StubProvider())
234
235 def test_register_rejects_non_string_domain(self) -> None:
236 """Non-string domain identifiers are rejected at registration time."""
237 with pytest.raises(ValueError, match="non-empty string"):
238 register_attestation_provider(cast(str, 42), _StubProvider())
239
240
241 class TestRegistryThreadSafety:
242 """Concurrent registers/gets must not lose entries."""
243
244 def test_concurrent_registrations_visible_to_all_getters(self) -> None:
245 """Spin 16 threads registering distinct domains; every entry survives."""
246 N = 16
247 barrier = threading.Barrier(N)
248
249 def _worker(idx: int) -> None:
250 barrier.wait()
251 register_attestation_provider(f"dom-{idx}", _StubProvider(f"p{idx}"))
252
253 threads = [
254 threading.Thread(target=_worker, args=(i,)) for i in range(N)
255 ]
256 for t in threads:
257 t.start()
258 for t in threads:
259 t.join()
260
261 for i in range(N):
262 provider = get_attestation_provider(f"dom-{i}")
263 assert provider is not None
264 assert provider.provider_id == f"p{i}" # type: ignore[attr-defined]
265
266 def test_concurrent_get_during_register_does_not_crash(self) -> None:
267 """Hammer get() while register() is running — must never raise."""
268 stop = threading.Event()
269 errors: list[BaseException] = []
270
271 def _registrar() -> None:
272 for i in range(200):
273 try:
274 register_attestation_provider("hot", _StubProvider(f"r{i}"))
275 except Exception as e: # pragma: no cover — only fires on bug
276 errors.append(e)
277
278 def _reader() -> None:
279 while not stop.is_set():
280 try:
281 get_attestation_provider("hot")
282 except Exception as e: # pragma: no cover — only fires on bug
283 errors.append(e)
284
285 readers = [threading.Thread(target=_reader) for _ in range(4)]
286 for r in readers:
287 r.start()
288 registrar = threading.Thread(target=_registrar)
289 registrar.start()
290 registrar.join()
291 stop.set()
292 for r in readers:
293 r.join()
294
295 assert not errors
296
297
298 # ===========================================================================
299 # INTEGRATION TIER
300 # ===========================================================================
301
302
303 class TestRoundTrip:
304 """Stub provider → register → compute → anchor → verify."""
305
306 def test_full_round_trip_returns_valid(self) -> None:
307 """Happy path: registered provider produces a valid result end to end."""
308 provider = _StubProvider("rt")
309 register_attestation_provider("rtdom", provider)
310
311 looked_up = get_attestation_provider("rtdom")
312 assert looked_up is provider
313
314 record = looked_up.compute_attestation(
315 "snapshot-abc",
316 {"path": "notes/a.md", "action": "write", "content_hash": "deadbeef"},
317 )
318 assert record["id"] == "snapshot-abc:notes/a.md"
319 assert record["provider_id"] == "rt"
320
321 receipt = looked_up.anchor(record)
322 assert receipt["tx_id"]
323 assert receipt["anchor_url"].startswith("https://")
324
325 result = looked_up.verify(record, receipt)
326 assert result["valid"] is True
327 assert result["reason"] == ""
328 assert result["depth"] == 0
329
330 def test_compute_is_deterministic(self) -> None:
331 """Same (snapshot_id, commit_meta) → same record id every time."""
332 provider = _StubProvider()
333 meta = {"path": "x.md", "action": "write", "content_hash": "abc"}
334 a = provider.compute_attestation("s1", meta)
335 b = provider.compute_attestation("s1", meta)
336 assert a == b
337
338
339 # ===========================================================================
340 # SECURITY TIER (mandatory per Phase 7 spec)
341 # ===========================================================================
342
343
344 class TestSecurity:
345 """Hardening contracts that must hold against misuse and adversarial input."""
346
347 def test_partial_verification_is_a_hard_error(self) -> None:
348 """verify() with no anchored record raises, never returns valid=True."""
349 provider = _StubProvider()
350 record = provider.compute_attestation(
351 "s", {"path": "p", "content_hash": "c"}
352 )
353 bogus_receipt: AnchorReceipt = {
354 "tx_id": "fake",
355 "anchor_url": "https://fake.example/x",
356 "anchored_at": "2026-05-13T00:00:00Z",
357 "provider_id": "stub",
358 }
359 with pytest.raises(AttestationVerifyError) as exc_info:
360 provider.verify(record, bogus_receipt)
361 assert exc_info.value.code == "NO_RECEIPT"
362
363 def test_known_bad_record_returns_valid_false_not_raises(self) -> None:
364 """A *provably-bad* record returns valid=False — distinct from incomplete."""
365 provider = _StubProvider()
366 record = provider.compute_attestation(
367 "s", {"path": "p", "content_hash": "c"}
368 )
369 receipt = provider.anchor(record)
370 tampered_receipt: AnchorReceipt = dict(receipt) # type: ignore[assignment]
371 tampered_receipt["tx_id"] = "tampered-tx"
372
373 result = provider.verify(record, tampered_receipt)
374 assert result["valid"] is False
375 assert result["reason"]
376
377 def test_anchor_is_write_once(self) -> None:
378 """Re-anchoring the same record raises ImmutableReceiptError."""
379 provider = _StubProvider()
380 record = provider.compute_attestation(
381 "s", {"path": "p", "content_hash": "c"}
382 )
383 provider.anchor(record)
384 with pytest.raises(ImmutableReceiptError) as exc_info:
385 provider.anchor(record)
386 assert exc_info.value.code == "IMMUTABLE_RECEIPT"
387
388 def test_registry_isolation_between_domains(self) -> None:
389 """Provider for domain A is never returned for domain B."""
390 provider_a = _StubProvider("A")
391 register_attestation_provider("dom-a", provider_a)
392 assert get_attestation_provider("dom-b") is None
393 assert get_attestation_provider("dom-A") is None # case-sensitive
394
395 def test_verify_provider_id_mismatch_is_hard_error(self) -> None:
396 """Cross-provider receipts trigger AttestationVerifyError, not False."""
397 provider = _StubProvider("legit")
398 record = provider.compute_attestation(
399 "s", {"path": "p", "content_hash": "c"}
400 )
401 spoofed_receipt: AnchorReceipt = {
402 "tx_id": "x",
403 "anchor_url": "https://x.example/x",
404 "anchored_at": "2026-05-13T00:00:00Z",
405 "provider_id": "evil",
406 }
407 with pytest.raises(AttestationVerifyError):
408 provider.verify(record, spoofed_receipt)
409
410 def test_verify_error_codes_are_stable_strings(self) -> None:
411 """Codes are stable enough for CLI/MCP layers to map without regex."""
412 try:
413 raise AttestationVerifyError("x", code="ICP_UNREACHABLE")
414 except AttestationVerifyError as e:
415 assert isinstance(e.code, str)
416 assert e.code.isupper() and e.code.replace("_", "").isalnum()
File History 1 commit
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago