- #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).
146 lines
5.8 KiB
Python
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"
|