From b17900cb332bb34d666fdb2dfaedc0fe730d3a57 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 10 May 2026 12:44:13 -0400 Subject: [PATCH] tests/pi_star: 21 tests for protocol + registry (foundation, was untested) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit arborist/pi_star/protocol.py + arborist/pi_star/registry.py are the foundation every concrete π* kernel rides on. Both shipped in #000015 Phase 1 with zero direct tests; concrete kernels (arithmetic@v1, logic-kernel@v1, algebra-symbolic@v1, ...) have their own test files but the protocol contract + registry mutation discipline weren't pinned. Coverage: protocol.py - PiStar @runtime_checkable: instances satisfying the duck-type pass isinstance check; missing-method instances are rejected - registry_key returns "name@version" exactly; distinct versions yield distinct keys - equivalence_class_id is sha256 over canonicalize() output; invariant under pre-canonical form (two raws that canonicalize to the same bytes get the same eclass id); distinguishes different canonicals - assert_round_trip passes on idempotent π*; raises AssertionError naming the kernel on non-idempotent; propagates PiStarError when canonicalize raises on the test input itself registry.py - register inserts; get retrieves - register IS idempotent for the same instance at the same key (no error) - register REJECTS a different instance at the same key (cache_key invariant: name@version content-pinned) - same name with different versions coexist - get raises KeyError on unknown key - list_keys returns sorted keys (deterministic for cache_key derivation downstream) - domains() groups by domain; per-domain key lists are sorted; empty registry → empty dict isolated_registry fixture monkeypatches REGISTRY to {} for mutation tests so the global registry stays untouched (matches the module docstring's no-public-unregister discipline). Substrate-paper-spec'd primitives (arborist/substrate/* and arborist/pi_star/protocol.py + registry.py) all directly tested now. --- tests/test_pi_star_protocol_and_registry.py | 280 ++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 tests/test_pi_star_protocol_and_registry.py diff --git a/tests/test_pi_star_protocol_and_registry.py b/tests/test_pi_star_protocol_and_registry.py new file mode 100644 index 0000000..4ff9bf3 --- /dev/null +++ b/tests/test_pi_star_protocol_and_registry.py @@ -0,0 +1,280 @@ +"""Tests for ``arborist.pi_star.protocol`` + ``arborist.pi_star.registry`` +(#000015 Phase 1 substrate). + +Both modules previously had zero direct tests despite being the +foundation every π* kernel rides on. Every concrete kernel +(``arithmetic@v1``, ``logic-kernel@v1``, ``algebra-symbolic@v1``, +…) implements the :class:`PiStar` Protocol; the registry holds the +``name@version`` → instance map that ``cache_key`` derives from. + +Test discipline: + - Use unique names per test to avoid polluting the global + REGISTRY (no public unregister; mutation discipline per the + module docstring). + - Test the Protocol contract via tiny stub classes that satisfy + the duck-type without inheriting. +""" + +from __future__ import annotations + +import hashlib + +import pytest + +from arborist.pi_star import registry as reg +from arborist.pi_star.protocol import ( + PiStar, + PiStarError, + assert_round_trip, + equivalence_class_id, + registry_key, +) + + +# --- stub π* implementations ---------------------------------------- + + +class _IdentityStub: + """π* that passes raw bytes through unchanged. Trivially + idempotent. Used as a baseline for the Protocol contract.""" + + def __init__(self, name: str = "identity-stub", version: str = "v1", + domain: str = "text"): + self.name = name + self.version = version + self.domain = domain + + def canonicalize(self, raw: bytes) -> bytes: + return raw + + +class _StripWhitespaceStub: + """π* that strips whitespace. Idempotent: stripping twice = + stripping once.""" + + name = "strip-ws-stub" + version = "v1" + domain = "text" + + def canonicalize(self, raw: bytes) -> bytes: + return raw.strip() + + +class _NonIdempotentStub: + """π* that doubles its input. NOT idempotent — used only to + test that assert_round_trip catches the violation.""" + + name = "double-stub" + version = "v1" + domain = "text" + + def canonicalize(self, raw: bytes) -> bytes: + return raw + raw + + +class _DomainGuardStub: + """π* that raises PiStarError on bytes that don't start with 'X:'. + Used to test that the protocol's contract honors the + PiStarError-on-out-of-domain rule.""" + + name = "domain-guard-stub" + version = "v1" + domain = "text" + + def canonicalize(self, raw: bytes) -> bytes: + if not raw.startswith(b"X:"): + raise PiStarError(f"out-of-domain input: {raw!r}") + return raw[2:] + + +# --- protocol — runtime checkable ----------------------------------- + + +def test_pistar_runtime_checkable(): + """@runtime_checkable Protocol → instances satisfying the + duck-type pass `isinstance(x, PiStar)` without inheriting.""" + assert isinstance(_IdentityStub(), PiStar) + assert isinstance(_StripWhitespaceStub(), PiStar) + # An object with all 3 required attrs + canonicalize method + # passes the runtime check. + + +def test_pistar_runtime_check_rejects_missing_method(): + """Object missing `canonicalize` → not a PiStar.""" + class _Incomplete: + name = "x" + version = "v1" + domain = "text" + # no canonicalize method + # @runtime_checkable Protocol checks attribute presence at + # isinstance-time; an object missing the method is rejected. + assert not isinstance(_Incomplete(), PiStar) + + +# --- registry_key --------------------------------------------------- + + +def test_registry_key_format(): + """``registry_key`` returns ``name@version`` exactly.""" + s = _IdentityStub(name="my-kernel", version="v3") + assert registry_key(s) == "my-kernel@v3" + + +def test_registry_key_distinct_versions_distinct_keys(): + a = _IdentityStub(name="kern", version="v1") + b = _IdentityStub(name="kern", version="v2") + assert registry_key(a) != registry_key(b) + + +# --- equivalence_class_id ------------------------------------------ + + +def test_equivalence_class_id_sha256_over_canonical_bytes(): + """``equivalence_class_id`` is sha256 over canonical bytes.""" + s = _IdentityStub() + raw = b"hello world" + expected = hashlib.sha256(raw).hexdigest() + assert equivalence_class_id(s, raw) == expected + + +def test_equivalence_class_id_invariant_under_pre_canonical_form(): + """Two raw inputs that canonicalize to the same bytes → same + equivalence_class_id. Demonstrates the canonicalization + fundamental property.""" + s = _StripWhitespaceStub() + a = b" hello " + b = b"hello" + # Both canonicalize to b"hello". + assert equivalence_class_id(s, a) == equivalence_class_id(s, b) + + +def test_equivalence_class_id_distinguishes_different_canonicals(): + s = _IdentityStub() + assert equivalence_class_id(s, b"a") != equivalence_class_id(s, b"b") + + +# --- assert_round_trip ---------------------------------------------- + + +def test_assert_round_trip_passes_on_idempotent(): + """Idempotent π* survives the round-trip check.""" + assert_round_trip(_IdentityStub(), b"any input") + assert_round_trip(_StripWhitespaceStub(), b" spaced ") + + +def test_assert_round_trip_raises_on_non_idempotent(): + """Non-idempotent π* → AssertionError naming the kernel.""" + with pytest.raises(AssertionError, match="double-stub@v1"): + assert_round_trip(_NonIdempotentStub(), b"doubles") + + +def test_assert_round_trip_propagates_pi_star_error(): + """If `canonicalize` itself raises PiStarError on the input, + that propagates (round-trip can't be checked on out-of-domain + input — caller's responsibility to gate).""" + with pytest.raises(PiStarError, match="out-of-domain"): + assert_round_trip(_DomainGuardStub(), b"no prefix") + + +# --- domain-guard contract ------------------------------------------ + + +def test_canonicalize_raises_pi_star_error_on_undefined_input(): + """Per the protocol docstring: 'canonicalize raises PiStarError + when raw is outside the projection's domain.'""" + s = _DomainGuardStub() + with pytest.raises(PiStarError): + s.canonicalize(b"missing prefix") + + +def test_canonicalize_succeeds_within_domain(): + s = _DomainGuardStub() + out = s.canonicalize(b"X:in-domain") + assert out == b"in-domain" + + +# --- registry — register / get / list_keys / domains ---------------- + + +@pytest.fixture +def isolated_registry(monkeypatch): + """Replace REGISTRY with an empty dict for the duration of a + test, restoring on teardown. Avoids polluting the global + registry (which has no public unregister per the module + docstring discipline).""" + monkeypatch.setattr(reg, "REGISTRY", {}) + + +def test_register_inserts_and_get_retrieves(isolated_registry): + s = _IdentityStub(name="reg-test-kernel", version="v1") + reg.register(s) + assert reg.get("reg-test-kernel@v1") is s + + +def test_register_idempotent_for_same_instance(isolated_registry): + """Re-registering the SAME instance is a no-op (no error).""" + s = _IdentityStub(name="idem-kernel", version="v1") + reg.register(s) + reg.register(s) # must not raise + assert reg.get("idem-kernel@v1") is s + + +def test_register_rejects_different_instance_at_same_key(isolated_registry): + """Two different instances claiming the same name@version → + PiStarError (cache_key invariant: name@version is content-pinned).""" + a = _IdentityStub(name="dup-kernel", version="v1") + b = _IdentityStub(name="dup-kernel", version="v1") # different instance + reg.register(a) + with pytest.raises(PiStarError, match="already bound"): + reg.register(b) + + +def test_register_allows_different_versions_same_name(isolated_registry): + """Same name, different version → distinct keys, both register OK. + Lets a v2 kernel coexist with v1 (cache_key invariant unaffected).""" + a = _IdentityStub(name="multi-version-kernel", version="v1") + b = _IdentityStub(name="multi-version-kernel", version="v2") + reg.register(a) + reg.register(b) + assert reg.get("multi-version-kernel@v1") is a + assert reg.get("multi-version-kernel@v2") is b + + +def test_get_raises_keyerror_on_unknown_key(isolated_registry): + with pytest.raises(KeyError, match="unknown"): + reg.get("never-registered@v1") + + +def test_list_keys_returns_sorted_keys(isolated_registry): + """Sorted return order is part of the contract — bench harnesses + iterate the list expecting deterministic order.""" + reg.register(_IdentityStub(name="zeta", version="v1")) + reg.register(_IdentityStub(name="alpha", version="v1")) + reg.register(_IdentityStub(name="mu", version="v1")) + keys = reg.list_keys() + assert keys == sorted(keys) + assert keys == ["alpha@v1", "mu@v1", "zeta@v1"] + + +def test_domains_groups_by_domain(isolated_registry): + reg.register(_IdentityStub(name="text-kern-a", version="v1", domain="text")) + reg.register(_IdentityStub(name="text-kern-b", version="v1", domain="text")) + reg.register(_IdentityStub(name="logic-kern", version="v1", domain="logic")) + domains = reg.domains() + assert set(domains.keys()) == {"text", "logic"} + assert domains["text"] == ["text-kern-a@v1", "text-kern-b@v1"] + assert domains["logic"] == ["logic-kern@v1"] + + +def test_domains_per_domain_keys_sorted(isolated_registry): + """Each domain's key list is sorted (deterministic for cache_key + derivation downstream).""" + reg.register(_IdentityStub(name="z-kern", version="v1", domain="text")) + reg.register(_IdentityStub(name="a-kern", version="v1", domain="text")) + reg.register(_IdentityStub(name="m-kern", version="v1", domain="text")) + domains = reg.domains() + assert domains["text"] == sorted(domains["text"]) + + +def test_domains_empty_when_registry_empty(isolated_registry): + assert reg.domains() == {}