arborist/tests/test_session_migrations.py
russell@unturf.com fea761c577
tests: unit + integration + functional coverage for this session's surface
51 new tests across the three layers (unit / integration / functional)
for tickets #000014/#000015/#000017/#000020/#000021/#000023/#000024/
#000025/#000019.

NEW FILES

tests/test_cli_session.py (17 tests) — functional CLI coverage:
  arborist selfmodel snapshot|show|show --root|falsify|falsify-idempotent|list
  arborist memory     snapshot|show|branches|falsify
  arborist capital    summary|summary --op-type|op-cost|top|top --rejects-unknown-form
  + audit chain stays clean across all three CLI families

tests/test_session_migrations.py (7 tests) — schema migration semantics:
  fresh-db has all five new tables
  re-connect is idempotent
  explicit migration helpers re-apply without error
  PRAGMA table_info confirms expected columns
  CHECK constraints reject invalid state values
  audit chain re-verifies after writes from all three modules
  capital_ledger writes do not chain into audit_events (sibling invariant)

tests/test_session_integration.py (11 tests) — cross-module flows:
  ingest emits one capital_ledger row per batch tied to last event hash
  SelfModel.snapshot folds memory_root from memory_records when present
  SelfModel.snapshot returns memory_root=None on empty memory table
  π* registry rejects conflicting registration (name@version pinned)
  π* registry tolerates same-instance re-registration
  pi_star.get raises KeyError on unknown
  Battery runtime_digest fingerprint shifts when registry changes
  Full Dav1DPrometheus suite via runner --all returns 0; 312 fixtures
  _DEFAULT_FIXTURES sums to 312 deterministic tasks
  Phase 1a fixture digests stay byte-stable
  Full state-space round-trip: ingest → SelfModel + Memory + Capital

EXTENDED FILES

tests/test_pi_star.py (+6 tests):
  assert_round_trip passes on idempotent / raises on non-idempotent π*
  equivalence_class_id determinism + input sensitivity
  registry_key format
  domains() partitioning invariant

tests/test_bench_batteries.py (+10 tests):
  _eval_propositional parens nesting
  _eval_propositional rejects unknown variable + malformed
  _eval_propositional XOR/IMPL/IFF truth-table coverage
  _walk_relation_path: self-loop, cycles without infinite-loop, unreachable
  _walk_relation_path rejects non-whitelisted relation
  _content_tokens strips punctuation, handles unicode
  _capital_cost_delta handles missing/empty budget

Full suite: 1161 passed, 36 skipped. Up from 1110.
2026-05-07 21:15:14 -04:00

232 lines
7.8 KiB
Python

"""Schema migration idempotency for tickets #000014/#000017/#000020.
Verifies the three new ``_migrate_*`` functions in
``arborist.store.connect()``:
- ``_migrate_selfmodel_tables`` (selfmodel_records + selfmodel_capability_claims)
- ``_migrate_capital_ledger`` (capital_ledger)
- ``_migrate_memory_root`` (memory_records + memory_branch_summaries)
Each migration must be idempotent (re-running produces no errors) and
purely additive (existing tables/data unchanged).
"""
from __future__ import annotations
import hashlib
import json
import pytest
from arborist.store import (
_migrate_capital_ledger,
_migrate_memory_root,
_migrate_selfmodel_tables,
append_audit,
connect,
transaction,
)
_NEW_SESSION_TABLES = (
"selfmodel_records",
"selfmodel_capability_claims",
"capital_ledger",
"memory_records",
"memory_branch_summaries",
)
def _list_tables(conn) -> set[str]:
rows = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
).fetchall()
return {r["name"] for r in rows}
def test_fresh_db_has_all_session_tables(tmp_path):
db = tmp_path / "shard.db"
conn = connect(db)
try:
names = _list_tables(conn)
for t in _NEW_SESSION_TABLES:
assert t in names, f"missing table on fresh shard: {t}"
finally:
conn.close()
def test_re_connect_is_idempotent(tmp_path):
"""Opening the same shard twice must not raise."""
db = tmp_path / "shard.db"
conn1 = connect(db)
conn1.close()
conn2 = connect(db)
try:
names = _list_tables(conn2)
for t in _NEW_SESSION_TABLES:
assert t in names
finally:
conn2.close()
def test_explicit_migration_reapply_no_error(tmp_path):
"""Calling each ``_migrate_*`` helper twice on the same conn is a no-op."""
db = tmp_path / "shard.db"
conn = connect(db)
try:
_migrate_selfmodel_tables(conn)
_migrate_selfmodel_tables(conn)
_migrate_capital_ledger(conn)
_migrate_capital_ledger(conn)
_migrate_memory_root(conn)
_migrate_memory_root(conn)
# No exception → idempotency confirmed.
names = _list_tables(conn)
for t in _NEW_SESSION_TABLES:
assert t in names
finally:
conn.close()
def test_session_tables_have_expected_schema(tmp_path):
db = tmp_path / "shard.db"
conn = connect(db)
try:
# Use PRAGMA table_info to check critical columns.
sm_cols = {r["name"] for r in conn.execute(
"PRAGMA table_info(selfmodel_records)"
).fetchall()}
for col in (
"selfmodel_root", "schema_version", "model_profile_hash",
"verifier_method_root", "governance_policy_hash",
"memory_root", "state", "body_blob", "audit_event_hash",
):
assert col in sm_cols, f"selfmodel_records missing col {col}"
cap_cols = {r["name"] for r in conn.execute(
"PRAGMA table_info(capital_ledger)"
).fetchall()}
for col in (
"ledger_id", "audit_event_hash", "op_type",
"living", "material", "financial", "intellectual",
"experiential", "social", "cultural", "spiritual",
"estimator_version", "estimator_inputs_blob", "recorded_at",
):
assert col in cap_cols, f"capital_ledger missing col {col}"
mem_cols = {r["name"] for r in conn.execute(
"PRAGMA table_info(memory_records)"
).fetchall()}
for col in (
"memory_root", "schema_version", "audit_events_high_water",
"branch_summaries_blob", "state", "audit_event_hash",
):
assert col in mem_cols, f"memory_records missing col {col}"
finally:
conn.close()
def test_check_constraints_enforce_state_values(tmp_path):
"""``state`` columns reject anything outside live/stale/falsified."""
import sqlite3
db = tmp_path / "shard.db"
conn = connect(db)
try:
with pytest.raises(sqlite3.IntegrityError):
conn.execute(
"INSERT INTO selfmodel_records ("
" selfmodel_root, schema_version, model_profile_hash,"
" verifier_method_root, governance_policy_hash,"
" canonicalization_version, chunking_version,"
" state, body_blob, audit_event_hash, created_at"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
"deadbeef" * 8, "selfmodel-v1", "mph", "vmr", "gph",
"norm-v1", "tok-512-v1",
"INVALID_STATE", # rejected by CHECK constraint
b"{}", "ev", 0,
),
)
finally:
conn.close()
def test_audit_chain_intact_after_session_writes(tmp_path):
"""All three new modules append events via ``store.append_audit``;
the chain must re-verify byte-for-byte."""
from arborist.capital import CapitalProfile, record as capital_record
from arborist.memory import snapshot as memory_snapshot, store_snapshot as memory_store
from arborist.selfmodel import snapshot as sm_snapshot, store_snapshot as sm_store
db = tmp_path / "shard.db"
conn = connect(db)
try:
with transaction(conn):
# SelfModel snapshot.
sm = sm_snapshot(conn)
sm_store(conn, sm)
# Memory snapshot.
ms = memory_snapshot(conn)
memory_store(conn, ms)
# Capital ledger row tied to a fresh audit event.
ev = append_audit(
conn, event_type="ingest", subject_root=None,
body={"docs": 1},
)
capital_record(
conn, audit_event_hash=ev, op_type="ingest",
profile=CapitalProfile(material=0.5),
)
rows = conn.execute(
"SELECT event_hash, prev_event_hash, body 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"]
assert row["prev_event_hash"] == prev
prev = row["event_hash"]
finally:
conn.close()
def test_capital_writes_do_not_chain_into_audit_events(tmp_path):
"""Sibling-table invariant: capital_ledger rows must NOT participate
in the audit chain. Recompute the chain ignoring capital rows;
chain still verifies."""
import sqlite3
from arborist.capital import CapitalProfile, record as capital_record
db = tmp_path / "shard.db"
conn = connect(db)
try:
with transaction(conn):
ev1 = append_audit(conn, event_type="ingest",
subject_root=None, body={"x": 1})
capital_record(conn, audit_event_hash=ev1, op_type="ingest",
profile=CapitalProfile(material=1.0))
ev2 = append_audit(conn, event_type="qa",
subject_root=None, body={"y": 2})
# Multiple capital rows pointing at different events.
capital_record(conn, audit_event_hash=ev2, op_type="qa",
profile=CapitalProfile(financial=0.05))
capital_record(conn, audit_event_hash=ev2, op_type="qa",
profile=CapitalProfile(experiential=0.1))
# capital_ledger has 3 rows.
n_capital = conn.execute(
"SELECT COUNT(*) FROM capital_ledger"
).fetchone()[0]
assert n_capital == 3
# audit_events has 2 rows.
n_audit = conn.execute(
"SELECT COUNT(*) FROM audit_events"
).fetchone()[0]
assert n_audit == 2
finally:
conn.close()