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.
280 lines
9.4 KiB
Python
280 lines
9.4 KiB
Python
"""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() == {}
|