Files
rune/tests/test_governance_quartet.py
T
Nick d39655b4be feat(governance): QUARTET CLOSED — mutation guards, execution trace, checkpoint/replay, experience encoding (contract BUILDER-2026-10-08T10:34:14Z-c5be53)
- #156: mutation_guards.py — ALLOW/CONFIRM/DENY by kind/op/scope
  severity; unattributed identity = DENY; unbacked destructive = DENY
  with snapshot recovery; 4 live tests
- #155: execution_trace.py — durable per-task receipts from the
  task.outcome journal (memories/rules/capability/model/actions/
  outcome); 3 live tests incl. journal round-trip
- #154: state_checkpoint.py — snapshot/restore/diff of the entries DB;
  restore snapshots live state first (rollback of rollback); journal
  never touched; 3 live tests incl. round-trip
- #153: experience_encoding.py — the ONE admission rule (IGNORE/
  EVIDENCE/EPISODE via lexicon discipline + grounding, provenance
  attached); 5 live tests incl. oracle-confirmed framing-only text

15/15 green. Ratchet at baseline (new handlers annotated).
2026-10-08 11:41:12 +01:00

146 lines
5.8 KiB
Python

"""test_governance_quartet.py — #156/#155/#154/#153: the governance
quartet's programmatic cores, each with live-behavior assertions."""
import json
import os
import sqlite3
import sys
import tempfile
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import experience_encoding as ee # noqa: E402
import execution_trace as et # noqa: E402
import mutation_guards as mg # noqa: E402
import state_checkpoint as sc # noqa: E402
class TestMutationGuards:
def test_small_fact_append_allows(self):
v = mg.guard_mutation({"target_kind": "fact", "operation": "append"})
assert v["verdict"] == "ALLOW"
def test_unattributed_identity_denies(self):
v = mg.guard_mutation({"target_kind": "identity", "operation": "overwrite",
"attributed": False})
assert v["verdict"] == "DENY"
assert "attribution" in v["reason"]
def test_unbacked_constitution_delete_denies(self):
v = mg.guard_mutation({"target_kind": "constitution", "operation": "delete",
"scope": "whole", "attributed": True, "backed_up": False})
assert v["verdict"] == "DENY"
assert "snapshot" in v["recovery"]
def test_backed_large_change_confirms(self):
v = mg.guard_mutation({"target_kind": "skill", "operation": "overwrite",
"scope": "whole", "attributed": True, "backed_up": True})
assert v["verdict"] == "CONFIRM"
class TestExecutionTrace:
def test_receipt_shape(self):
task = {"task_id": "t1", "description": "do x", "status": "completed",
"assigned_model": "m", "memories_used": ["a"], "rules_used": ["r"],
"capability": "c", "actions": ["a1"], "completed_at": "now"}
r = et.build_receipt(task, {"status": "completed", "model": "m2"})
assert r["task_id"] == "t1"
assert r["outcome"] == "completed"
assert r["memories_used"] == ["a"]
def test_trace_none_without_outcome(self, tmp_path, monkeypatch):
monkeypatch.setattr(et, "_journal_path", lambda: str(tmp_path / "missing.jsonl"))
assert et.trace_task("ghost", None) is None
def test_trace_from_journal(self, tmp_path, monkeypatch):
journal = tmp_path / "events.jsonl"
journal.write_text(json.dumps({"event_type": "task.outcome",
"payload": {"task_id": "t9", "status": "completed"}}) + "\n")
monkeypatch.setattr(et, "_journal_path", lambda: str(journal))
r = et.trace_task("t9", {"task_id": "t9", "description": "d"})
assert r is not None and r["outcome"] == "completed"
class TestStateCheckpoint:
def _db(self, tmp_path):
db = str(tmp_path / "store.db")
conn = sqlite3.connect(db)
conn.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, status TEXT)")
conn.executemany("INSERT INTO entries VALUES (?,?)", [(1, "active"), (2, "active")])
conn.commit()
conn.close()
return db
def test_snapshot_restore_roundtrip(self, tmp_path):
db = self._db(tmp_path)
ledger = str(tmp_path / "checkpoints.json")
rec = sc.snapshot("test", store_db=db, ledger_path=ledger)
assert rec["entries"] == 2
# mutate the live store
conn = sqlite3.connect(db)
conn.execute("DELETE FROM entries WHERE id=1")
conn.commit()
conn.close()
# restore
r = sc.restore(rec["id"], ledger_path=ledger, store_db=db)
assert r["restored"] is True
assert "safety_snapshot" in r # rollback of the rollback
conn = sqlite3.connect(db)
assert conn.execute("SELECT COUNT(*) FROM entries").fetchone()[0] == 2
conn.close()
def test_restore_unknown_fails(self, tmp_path):
r = sc.restore("nope", ledger_path=str(tmp_path / "c.json"),
store_db=str(tmp_path / "s.db"))
assert r["restored"] is False
def test_diff(self, tmp_path):
db = self._db(tmp_path)
ledger = str(tmp_path / "c.json")
a = sc.snapshot("a", store_db=db, ledger_path=ledger)
conn = sqlite3.connect(db)
conn.execute("INSERT INTO entries VALUES (3, 'active')")
conn.commit()
conn.close()
b = sc.snapshot("b", store_db=db, ledger_path=ledger)
d = sc.diff(a["id"], b["id"], ledger_path=ledger)
assert d["delta_entries"] == 1
class TestExperienceEncoding:
def test_trivial_ignored(self):
v = ee.evaluate({"text": "hi", "source": "user"})
assert v["verdict"] == "IGNORE"
def test_duplicate_ignored(self):
v = ee.evaluate({"text": "Nick prefers dark coffee", "source": "user"},
existing_episodes=[{"text": "nick prefers DARK coffee"}])
assert v["verdict"] == "IGNORE"
assert "duplicate" in v["reason"]
def test_framing_only_ignored(self):
# oracle-confirmed framing-only: 'things'/'ways' (plural) carry
# signal; the singular framing forms do not
v = ee.evaluate({"text": "work thing and way always", "source": "user"})
assert v["verdict"] == "IGNORE"
def test_grounded_novel_becomes_episode(self):
v = ee.evaluate(
{"text": "nick migrated the modules to the hybrid process",
"source": "session"},
existing_episodes=[{"text": "nick writes code daily"}],
corpus_texts=("nick migrated the modules yesterday",
"the hybrid process runs go and rust"))
assert v["verdict"] == "EPISODE"
def test_provenance_attached(self):
v = ee.evaluate({"text": "x" * 20, "source": "curator",
"session_id": "s1"})
assert v["provenance"]["source"] == "curator"
assert v["provenance"]["session"] == "s1"