"""SelfModel — schema, snapshot, falsification (ticket #000014). Covers: - canonical-JSON round-trip stability - selfmodel_root invariance under capability-claim ordering - snapshot determinism given fixed store state - store_snapshot idempotency on the same root - falsify and mark_stale audit events - chain-check stays clean after SelfModel landing """ from __future__ import annotations import time import pytest from arborist.selfmodel import ( CapabilityClaim, SelfModel, canonical_body, claim_hash, falsify, latest, load, mark_stale, selfmodel_root, snapshot, store_snapshot, ) from arborist.selfmodel.canonical import SCHEMA_VERSION, with_claims from arborist.selfmodel.store import claims_for from arborist.store import connect, latest_event_hash, transaction # --- canonical-JSON --------------------------------------------------- def _bare_model() -> SelfModel: return SelfModel( schema_version=SCHEMA_VERSION, parent_selfmodel_root=None, model_profile_hash="model-profile-aaaa", verifier_method_root="verifier-root-bbbb", governance_policy_hash="gov-cccc", canonicalization_version="norm-v1", chunking_version="tok-512-v1", accepted_patch_root=None, rejected_patch_root=None, memory_root=None, ) def test_canonical_body_stable_across_field_reorder(): a = _bare_model() # Build "different" object with same logical content via dict round-trip. b = SelfModel( chunking_version="tok-512-v1", accepted_patch_root=None, canonicalization_version="norm-v1", governance_policy_hash="gov-cccc", memory_root=None, model_profile_hash="model-profile-aaaa", parent_selfmodel_root=None, rejected_patch_root=None, schema_version=SCHEMA_VERSION, verifier_method_root="verifier-root-bbbb", ) assert canonical_body(a) == canonical_body(b) assert selfmodel_root(a) == selfmodel_root(b) def test_selfmodel_root_invariant_under_claim_order(): base = _bare_model() c1 = CapabilityClaim( metric="strict_rate", threshold=0.50, eval_digest="d1" ) c2 = CapabilityClaim( metric="directive_coverage", threshold=0.99, eval_digest="d2" ) a = with_claims(base, [c1, c2]) b = with_claims(base, [c2, c1]) assert selfmodel_root(a) == selfmodel_root(b) def test_canonical_body_changes_when_field_changes(): a = _bare_model() b = SelfModel(**{**a.__dict__, "model_profile_hash": "different"}) assert selfmodel_root(a) != selfmodel_root(b) # --- snapshot --------------------------------------------------------- def test_snapshot_returns_selfmodel_with_defaults_on_empty_db(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) assert sm.schema_version == SCHEMA_VERSION assert sm.governance_policy_hash == "unset" assert sm.model_profile_hash == "unset" assert sm.canonicalization_version == "norm-v1" assert sm.chunking_version == "tok-512-v1" assert sm.parent_selfmodel_root is None # verifier_method_root is SHA-256 over an empty list; always 64 hex. assert len(sm.verifier_method_root) == 64 finally: conn.close() def test_snapshot_deterministic_on_same_state(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): a = snapshot(conn) b = snapshot(conn) assert selfmodel_root(a) == selfmodel_root(b) finally: conn.close() # --- store_snapshot --------------------------------------------------- def test_store_snapshot_persists_and_emits_audit(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) root = store_snapshot(conn, sm) loaded = load(conn, root) assert loaded is not None assert loaded["selfmodel_root"] == root assert loaded["state"] == "live" assert loaded["audit_event_hash"] is not None # An audit event of type selfmodel_snapshot_landed exists. row = conn.execute( "SELECT event_type FROM audit_events " "WHERE event_type='selfmodel_snapshot_landed' " "AND subject_root = ?", (root,), ).fetchone() assert row is not None finally: conn.close() def test_store_snapshot_idempotent(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) r1 = store_snapshot(conn, sm) r2 = store_snapshot(conn, sm) assert r1 == r2 # Only one selfmodel_snapshot_landed event. n = conn.execute( "SELECT COUNT(*) FROM audit_events " "WHERE event_type='selfmodel_snapshot_landed' " "AND subject_root = ?", (r1,), ).fetchone()[0] assert n == 1 finally: conn.close() def test_store_snapshot_with_claims(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: c = CapabilityClaim( metric="strict_rate", threshold=0.50, eval_digest="bench-fixture-aabb", measured_value=0.54, measured_at=1700000000, ) with transaction(conn): sm = with_claims(snapshot(conn), [c]) root = store_snapshot(conn, sm, claims=[c]) rows = claims_for(conn, root) assert len(rows) == 1 assert rows[0]["metric"] == "strict_rate" assert rows[0]["threshold"] == pytest.approx(0.50) # Audit event for claim emission. ev = conn.execute( "SELECT event_type FROM audit_events " "WHERE event_type='selfmodel_capability_claim_added' " "AND subject_root = ?", (root,), ).fetchone() assert ev is not None finally: conn.close() def test_latest_returns_most_recent_live(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm1 = snapshot(conn) r1 = store_snapshot(conn, sm1, ts=1700000000) # Force a different model-profile-hash on the second snapshot. sm2_dict = {**sm1.__dict__, "model_profile_hash": "shifted"} sm2 = SelfModel(**sm2_dict) r2 = store_snapshot(conn, sm2, ts=1700000100) live = latest(conn) assert live is not None assert live["selfmodel_root"] == r2 assert r1 != r2 finally: conn.close() # --- falsify / mark_stale --------------------------------------------- def test_falsify_flips_state_and_emits_event(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) root = store_snapshot(conn, sm) event_hash = falsify( conn, root, reason="strict_rate dropped to 0.40 under bench", triggering_claim_hash="claim-aaaa", ) loaded = load(conn, root) assert loaded["state"] == "falsified" assert loaded["falsified_reason"].startswith("strict_rate") assert event_hash != "" ev = conn.execute( "SELECT body FROM audit_events WHERE event_hash = ?", (event_hash,), ).fetchone() assert ev is not None finally: conn.close() def test_falsify_idempotent(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) root = store_snapshot(conn, sm) falsify(conn, root, reason="r1") second = falsify(conn, root, reason="r2") assert second == "" # no-op on already-falsified finally: conn.close() def test_mark_stale_flips_state_and_emits_event(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) root = store_snapshot(conn, sm) event_hash = mark_stale(conn, root, reason="verifier method shifted") loaded = load(conn, root) assert loaded["state"] == "stale" assert event_hash != "" finally: conn.close() def test_mark_stale_does_not_overwrite_falsified(tmp_path): db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) root = store_snapshot(conn, sm) falsify(conn, root, reason="hard regression") second = mark_stale(conn, root, reason="just a soft drift") assert second == "" # no-op, falsified is terminal loaded = load(conn, root) assert loaded["state"] == "falsified" finally: conn.close() # --- audit chain integrity -------------------------------------------- def test_audit_chain_stays_clean_after_selfmodel_ops(tmp_path): """All SelfModel ops use append_audit, which chains via prev_event_hash. Recompute the chain from scratch and assert no breaks. """ import hashlib db = tmp_path / "shard.db" conn = connect(db) try: with transaction(conn): sm = snapshot(conn) root = store_snapshot(conn, sm) falsify(conn, root, reason="r") rows = conn.execute( "SELECT event_hash, prev_event_hash, body, ts " "FROM audit_events ORDER BY seq" ).fetchall() prev = None for row in rows: h = hashlib.sha256() if prev is not None: h.update(bytes.fromhex(prev)) h.update(row["body"].encode("utf-8", errors="surrogatepass")) assert h.hexdigest() == row["event_hash"], ( "audit chain break detected" ) assert row["prev_event_hash"] == prev prev = row["event_hash"] assert prev == latest_event_hash(conn) finally: conn.close()