Initial release: iai-mcp v0.1.0
Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
This commit is contained in:
commit
f6b876fbe7
332 changed files with 97258 additions and 0 deletions
343
tests/test_s5_kernel.py
Normal file
343
tests/test_s5_kernel.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
"""Tests for iai_mcp.s5 -- identity kernel (MEM-09, D-22).
|
||||
|
||||
D-22 constitutional:
|
||||
- ρ_identity = 0.99 (stricter than write-path ρ=0.95 and S4 ρ=0.97).
|
||||
- M-of-N = 3-of-5: a proposal becomes an invariant update only after 3
|
||||
vigilance-passing proposals within the consensus window.
|
||||
- 48h cooldown on recently-updated invariants.
|
||||
- trust threshold = 0.9: any record with s5_trust_score >= 0.9 is an
|
||||
"invariant-tier" record that cannot be written directly.
|
||||
- Every commit writes `s5_invariant_update` event with full provenance.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from iai_mcp.types import EMBED_DIM, MemoryRecord
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- helpers
|
||||
|
||||
def _anchor(
|
||||
*,
|
||||
text: str = "User is Alice",
|
||||
vec: list[float] | None = None,
|
||||
s5_trust_score: float = 0.9,
|
||||
tier: str = "semantic",
|
||||
tags: list[str] | None = None,
|
||||
language: str = "en",
|
||||
) -> MemoryRecord:
|
||||
if vec is None:
|
||||
# Normalised primary-axis vector so cosine against a near-identical
|
||||
# proposal is close to 1.
|
||||
vec = [1.0] + [0.0] * (EMBED_DIM - 1)
|
||||
now = datetime.now(timezone.utc)
|
||||
return MemoryRecord(
|
||||
id=uuid4(),
|
||||
tier=tier,
|
||||
literal_surface=text,
|
||||
aaak_index="",
|
||||
embedding=vec,
|
||||
community_id=None,
|
||||
centrality=0.0,
|
||||
detail_level=5,
|
||||
pinned=True,
|
||||
stability=0.5,
|
||||
difficulty=0.3,
|
||||
last_reviewed=now,
|
||||
never_decay=True,
|
||||
never_merge=True,
|
||||
provenance=[],
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
tags=list(tags or ["identity"]),
|
||||
language=language,
|
||||
s5_trust_score=s5_trust_score,
|
||||
)
|
||||
|
||||
|
||||
class _FakeEmbedder:
|
||||
"""Deterministic embedder that returns a vector aligned with the anchor's
|
||||
primary axis. Used to guarantee high cosine without hitting bge-m3."""
|
||||
|
||||
DIM = EMBED_DIM
|
||||
|
||||
def embed(self, text: str) -> list[float]:
|
||||
return [1.0] + [0.0] * (EMBED_DIM - 1)
|
||||
|
||||
def embed_batch(self, texts):
|
||||
return [self.embed(t) for t in texts]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_embedder(monkeypatch):
|
||||
"""Monkeypatch Embedder inside s5.py so propose_invariant_update doesn't
|
||||
try to load bge-m3 when encoding the proposed fact."""
|
||||
# We patch at the Embedder class level so any `from iai_mcp.embed import Embedder`
|
||||
# import inside s5 gets our fake.
|
||||
from iai_mcp import embed as embed_mod
|
||||
|
||||
monkeypatch.setattr(embed_mod, "Embedder", _FakeEmbedder)
|
||||
yield
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- constants
|
||||
|
||||
def test_s5_constants():
|
||||
from iai_mcp import s5
|
||||
|
||||
assert s5.IDENTITY_VIGILANCE_RHO == 0.99
|
||||
assert s5.S5_CONSENSUS_M == 3
|
||||
assert s5.S5_CONSENSUS_N == 5
|
||||
assert s5.COOLDOWN_HOURS == 48
|
||||
assert s5.TRUST_THRESHOLD_IDENTITY == 0.9
|
||||
|
||||
|
||||
def test_s5_exports_propose_invariant_update():
|
||||
from iai_mcp import s5
|
||||
|
||||
assert callable(getattr(s5, "propose_invariant_update", None))
|
||||
|
||||
|
||||
def test_s5_exports_check_identity_anchor_on_write():
|
||||
from iai_mcp import s5
|
||||
|
||||
assert callable(getattr(s5, "check_identity_anchor_on_write", None))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- propose_invariant_update
|
||||
|
||||
|
||||
def test_propose_invariant_update_first_proposal_stages(tmp_path):
|
||||
"""First call on an anchor returns ("staged", proposal_id)."""
|
||||
from iai_mcp.s5 import propose_invariant_update
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
anchor = _anchor()
|
||||
store.insert(anchor)
|
||||
|
||||
verdict, pid = propose_invariant_update(
|
||||
store, anchor.id, "new identity fact", session_id="s1"
|
||||
)
|
||||
assert verdict == "staged"
|
||||
assert isinstance(pid, UUID)
|
||||
|
||||
|
||||
def test_propose_invariant_update_consensus_commits(tmp_path):
|
||||
"""3 distinct-session proposals agreeing -> 3rd returns ("committed", ...)."""
|
||||
from iai_mcp.s5 import propose_invariant_update
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
anchor = _anchor()
|
||||
store.insert(anchor)
|
||||
|
||||
r1 = propose_invariant_update(store, anchor.id, "fact", "s1")
|
||||
r2 = propose_invariant_update(store, anchor.id, "fact", "s2")
|
||||
r3 = propose_invariant_update(store, anchor.id, "fact", "s3")
|
||||
assert r1[0] == "staged"
|
||||
assert r2[0] == "staged"
|
||||
assert r3[0] == "committed"
|
||||
|
||||
|
||||
def test_propose_invariant_update_insufficient_consensus_rejected(tmp_path, monkeypatch):
|
||||
"""5 proposals with only 2 vigilance-passing -> ("rejected", None) at N=5."""
|
||||
# For this test we need proposals that DON'T align with the anchor.
|
||||
# Patch the embedder to return orthogonal vectors for every 2nd proposal.
|
||||
from iai_mcp import embed as embed_mod
|
||||
from iai_mcp.s5 import propose_invariant_update
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
# Cycle: pass, fail, fail, fail, fail -> 1 vigilance pass total (NOT 3).
|
||||
call_count = {"n": 0}
|
||||
|
||||
class _AlternatingEmbedder:
|
||||
DIM = EMBED_DIM
|
||||
|
||||
def embed(self, text):
|
||||
call_count["n"] += 1
|
||||
if call_count["n"] == 1:
|
||||
# First proposal matches anchor exactly (cosine=1.0 passes ρ=0.99).
|
||||
return [1.0] + [0.0] * (EMBED_DIM - 1)
|
||||
# All subsequent proposals are orthogonal (cosine=0 < 0.99).
|
||||
vec = [0.0] * EMBED_DIM
|
||||
vec[call_count["n"] % EMBED_DIM] = 1.0
|
||||
return vec
|
||||
|
||||
def embed_batch(self, texts):
|
||||
return [self.embed(t) for t in texts]
|
||||
|
||||
monkeypatch.setattr(embed_mod, "Embedder", _AlternatingEmbedder)
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
anchor = _anchor()
|
||||
store.insert(anchor)
|
||||
|
||||
verdicts = []
|
||||
for i in range(5):
|
||||
v, _ = propose_invariant_update(store, anchor.id, f"fact {i}", f"s{i}")
|
||||
verdicts.append(v)
|
||||
# 1 pass + 4 fails != 3-of-5 consensus -> final Nth should be "rejected"
|
||||
assert verdicts[-1] == "rejected"
|
||||
|
||||
|
||||
def test_propose_invariant_update_cooldown(tmp_path):
|
||||
"""After a successful update, subsequent proposals return ("cooldown", None)."""
|
||||
from iai_mcp.s5 import propose_invariant_update
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
anchor = _anchor()
|
||||
store.insert(anchor)
|
||||
|
||||
# Push through consensus
|
||||
propose_invariant_update(store, anchor.id, "fact", "s1")
|
||||
propose_invariant_update(store, anchor.id, "fact", "s2")
|
||||
verdict_commit, _ = propose_invariant_update(store, anchor.id, "fact", "s3")
|
||||
assert verdict_commit == "committed"
|
||||
|
||||
# Next proposal hits cooldown
|
||||
verdict_next, pid = propose_invariant_update(
|
||||
store, anchor.id, "another fact", "s4"
|
||||
)
|
||||
assert verdict_next == "cooldown"
|
||||
assert pid is None
|
||||
|
||||
|
||||
def test_propose_invariant_update_writes_event(tmp_path):
|
||||
"""On commit, events table has kind=s5_invariant_update with provenance."""
|
||||
from iai_mcp.events import query_events
|
||||
from iai_mcp.s5 import propose_invariant_update
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
anchor = _anchor()
|
||||
store.insert(anchor)
|
||||
|
||||
propose_invariant_update(store, anchor.id, "fact", "s1")
|
||||
propose_invariant_update(store, anchor.id, "fact", "s2")
|
||||
propose_invariant_update(store, anchor.id, "fact", "s3")
|
||||
|
||||
events = query_events(store, kind="s5_invariant_update")
|
||||
assert len(events) == 1
|
||||
ev = events[0]
|
||||
assert ev["data"]["anchor_id"] == str(anchor.id)
|
||||
assert "new_record_id" in ev["data"]
|
||||
assert "session_ids" in ev["data"]
|
||||
assert "agree_count" in ev["data"]
|
||||
|
||||
|
||||
def test_propose_invariant_update_vigilance_099(tmp_path, monkeypatch):
|
||||
"""Proposals with cosine < 0.99 (even if textually similar) don't count as consensus votes."""
|
||||
from iai_mcp import embed as embed_mod
|
||||
from iai_mcp.s5 import propose_invariant_update
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
# Every proposal is orthogonal to the anchor -> none pass vigilance.
|
||||
class _LowCosineEmbedder:
|
||||
DIM = EMBED_DIM
|
||||
_n = 0
|
||||
|
||||
def embed(self, text):
|
||||
# Return a mostly-orthogonal vector; cosine with anchor [1,0,...,0]
|
||||
# will be near zero.
|
||||
vec = [0.0] * EMBED_DIM
|
||||
vec[1] = 1.0
|
||||
return vec
|
||||
|
||||
def embed_batch(self, texts):
|
||||
return [self.embed(t) for t in texts]
|
||||
|
||||
monkeypatch.setattr(embed_mod, "Embedder", _LowCosineEmbedder)
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
anchor = _anchor()
|
||||
store.insert(anchor)
|
||||
|
||||
# 5 proposals, none passing vigilance -> reject at Nth
|
||||
verdicts = []
|
||||
for i in range(5):
|
||||
v, _ = propose_invariant_update(store, anchor.id, f"fact {i}", f"s{i}")
|
||||
verdicts.append(v)
|
||||
# None committed
|
||||
assert "committed" not in verdicts
|
||||
# Final verdict is "rejected" once total=N=5
|
||||
assert verdicts[-1] == "rejected"
|
||||
|
||||
|
||||
def test_propose_invariant_update_unknown_anchor_rejected(tmp_path):
|
||||
from iai_mcp.s5 import propose_invariant_update
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
ghost = uuid4()
|
||||
verdict, pid = propose_invariant_update(store, ghost, "fact", "s")
|
||||
assert verdict == "rejected"
|
||||
assert pid is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- check_identity_anchor_on_write
|
||||
|
||||
|
||||
def test_check_identity_anchor_on_write_blocks_direct(tmp_path):
|
||||
"""Identity-tier record (s5_trust_score>=0.9) without s5_consensus tag: blocked."""
|
||||
from iai_mcp.s5 import check_identity_anchor_on_write
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
identity_rec = _anchor(s5_trust_score=0.95, tags=["identity"])
|
||||
# No "s5_consensus" marker
|
||||
ok, reason = check_identity_anchor_on_write(store, identity_rec, {})
|
||||
assert ok is False
|
||||
assert "identity-tier" in reason.lower() or "propose" in reason.lower()
|
||||
|
||||
|
||||
def test_check_identity_anchor_on_write_allows_low_trust(tmp_path):
|
||||
"""s5_trust_score < 0.9 -> always allowed."""
|
||||
from iai_mcp.s5 import check_identity_anchor_on_write
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
rec = _anchor(s5_trust_score=0.5)
|
||||
ok, reason = check_identity_anchor_on_write(store, rec, {})
|
||||
assert ok is True
|
||||
|
||||
|
||||
def test_check_identity_anchor_on_write_allows_with_consensus_marker(tmp_path):
|
||||
"""Identity-tier record carrying s5_consensus tag -> allowed (coming from propose)."""
|
||||
from iai_mcp.s5 import check_identity_anchor_on_write
|
||||
from iai_mcp.store import MemoryStore
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
rec = _anchor(s5_trust_score=0.95, tags=["identity", "s5_consensus"])
|
||||
ok, reason = check_identity_anchor_on_write(store, rec, {})
|
||||
assert ok is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- guarded_insert
|
||||
|
||||
def test_guarded_insert_blocks_direct_identity_write(tmp_path):
|
||||
"""write.guarded_insert rejects direct identity-tier writes; caller
|
||||
should route via propose_invariant_update."""
|
||||
from iai_mcp.store import MemoryStore
|
||||
from iai_mcp.write import guarded_insert
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
rec = _anchor(s5_trust_score=0.95)
|
||||
ok, reason = guarded_insert(store, rec, {})
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_guarded_insert_allows_low_trust_write(tmp_path):
|
||||
"""Non-identity write passes guarded_insert cleanly."""
|
||||
from iai_mcp.store import MemoryStore
|
||||
from iai_mcp.write import guarded_insert
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
rec = _anchor(s5_trust_score=0.5)
|
||||
ok, reason = guarded_insert(store, rec, {})
|
||||
assert ok is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue