"""Tests for the invariant_anchor edge type (MEM-09, D-22). invariant_anchor edges are the structural marker of S5 identity commitments: - created when propose_invariant_update reaches 3-of-5 consensus - src = original anchor record; dst = new consensus record - NEVER decayed by the FSRS sweep (sleep._decay_edges filters hebbian only) - At most 1 edge per 48h cooldown window (D-22 prevents rapid poisoning) """ from __future__ import annotations from datetime import datetime, timezone from uuid import UUID, uuid4 import pytest from iai_mcp.types import EMBED_DIM, MemoryRecord def _anchor(s5_trust_score: float = 0.9) -> MemoryRecord: now = datetime.now(timezone.utc) return MemoryRecord( id=uuid4(), tier="semantic", literal_surface="User identity: Alice", aaak_index="", embedding=[1.0] + [0.0] * (EMBED_DIM - 1), 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=["identity"], language="en", s5_trust_score=s5_trust_score, ) class _FakeEmbedder: DIM = EMBED_DIM def embed(self, text): 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): from iai_mcp import embed as embed_mod monkeypatch.setattr(embed_mod, "Embedder", _FakeEmbedder) yield def _reach_consensus(store, anchor_id): """Helper: run 3 proposals so we land on a commit.""" from iai_mcp.s5 import propose_invariant_update propose_invariant_update(store, anchor_id, "fact", "s1") propose_invariant_update(store, anchor_id, "fact", "s2") return propose_invariant_update(store, anchor_id, "fact", "s3") def test_invariant_anchor_edge_on_s5_promotion(tmp_path): """After consensus commit, an invariant_anchor edge exists from anchor to the new consensus record.""" from iai_mcp.store import EDGES_TABLE, MemoryStore store = MemoryStore(path=tmp_path) anchor = _anchor() store.insert(anchor) verdict, new_id = _reach_consensus(store, anchor.id) assert verdict == "committed" assert new_id is not None df = store.db.open_table(EDGES_TABLE).to_pandas() ia = df[df["edge_type"] == "invariant_anchor"] assert len(ia) >= 1 ids = {str(anchor.id), str(new_id)} # boost_edges canonicalises (src,dst) as sorted, so src/dst may be either. found = any( {str(row["src"]), str(row["dst"])} == ids for _, row in ia.iterrows() ) assert found def test_invariant_anchor_edge_never_decays(tmp_path): """invariant_anchor edges survive FSRS decay sweep indefinitely.""" from iai_mcp.sleep import _decay_edges from iai_mcp.store import EDGES_TABLE, MemoryStore store = MemoryStore(path=tmp_path) anchor = _anchor() store.insert(anchor) _reach_consensus(store, anchor.id) # Artificially age the invariant_anchor edge to 500 days old with tiny weight. edges_tbl = store.db.open_table(EDGES_TABLE) df = edges_tbl.to_pandas() ia_rows = df[df["edge_type"] == "invariant_anchor"] assert not ia_rows.empty first = ia_rows.iloc[0] from datetime import timedelta as _td old_ts = datetime.now(timezone.utc) - _td(days=500) edges_tbl.update( where=( f"src = '{first['src']}' AND dst = '{first['dst']}' " f"AND edge_type = 'invariant_anchor'" ), values={"weight": 0.001, "updated_at": old_ts}, ) # Run decay sweep _decay_edges(store) # invariant_anchor row still present df2 = store.db.open_table(EDGES_TABLE).to_pandas() survivors = df2[df2["edge_type"] == "invariant_anchor"] assert not survivors.empty def test_invariant_anchor_edge_no_duplicate_within_cooldown(tmp_path): """Second consensus attempt within 48h returns cooldown -> no new edge.""" from iai_mcp.store import EDGES_TABLE, MemoryStore store = MemoryStore(path=tmp_path) anchor = _anchor() store.insert(anchor) _reach_consensus(store, anchor.id) df_after_first = store.db.open_table(EDGES_TABLE).to_pandas() ia_first = df_after_first[df_after_first["edge_type"] == "invariant_anchor"] count_first = len(ia_first) # Try for a second consensus -- all should be blocked by cooldown from iai_mcp.s5 import propose_invariant_update verdict, _ = propose_invariant_update(store, anchor.id, "another", "s4") assert verdict == "cooldown" df_after_second = store.db.open_table(EDGES_TABLE).to_pandas() ia_second = df_after_second[df_after_second["edge_type"] == "invariant_anchor"] assert len(ia_second) == count_first