atlas-iac/testing/tests/test_hermes_soteria_kanban_recovery.py

102 lines
4.8 KiB
Python

"""Fail-closed installation tests for the reviewed Soteria recovery artifact."""
from __future__ import annotations
import importlib.util
import shutil
import sqlite3
from pathlib import Path
import pytest
ROOT = Path(__file__).parents[2]
SPEC = importlib.util.spec_from_file_location(
"recover_soteria_kanban", ROOT / "services/hermes/scripts/recover_soteria_kanban.py"
)
assert SPEC and SPEC.loader
recovery = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(recovery)
def _candidate(path: Path) -> Path:
"""Build the reviewed-shape candidate without relying on production data."""
with sqlite3.connect(path) as connection:
connection.executescript("""
CREATE TABLE tasks (id TEXT); CREATE TABLE task_events (id INTEGER);
CREATE TABLE task_comments (id INTEGER); CREATE TABLE task_runs (id INTEGER);
CREATE TABLE task_links (id INTEGER); CREATE TABLE task_attachments (id INTEGER);
CREATE TABLE kanban_notify_subs (id INTEGER); CREATE TABLE supervisor_roots (root_task_id TEXT);
CREATE TABLE supervisor_children (id TEXT);
""")
connection.executemany("INSERT INTO tasks VALUES(?)", [(str(value),) for value in range(4)])
connection.executemany("INSERT INTO task_events VALUES(?)", [(value,) for value in range(159)])
connection.executemany("INSERT INTO task_comments VALUES(?)", [(value,) for value in range(8)])
connection.executemany("INSERT INTO task_runs VALUES(?)", [(value,) for value in range(4)])
connection.executemany("INSERT INTO supervisor_roots VALUES(?)", [(value,) for value in recovery.ROOTS])
return path
def test_recovery_replaces_only_verified_source_and_is_restart_safe(tmp_path, monkeypatch):
candidate = _candidate(tmp_path / "candidate.db")
database = tmp_path / "kanban.db"
shutil.copyfile(candidate, database)
with sqlite3.connect(database) as connection:
connection.execute("CREATE TABLE forensic_source (value TEXT)")
monkeypatch.setattr(recovery, "CANDIDATE_SHA", recovery._sha(candidate))
monkeypatch.setattr(recovery, "SOURCE_SHA", recovery._sha(database))
result = recovery.recover(database, candidate)
assert result["state"] == "recovered"
assert recovery._integrity(database)
assert list(tmp_path.glob("kanban.db.corrupt.recovery-*.bak"))
candidate.unlink()
assert recovery.recover(database, candidate)["state"] == "already-recovered"
def test_recovery_refuses_an_unexpected_live_database(tmp_path, monkeypatch):
candidate = _candidate(tmp_path / "candidate.db")
database = _candidate(tmp_path / "kanban.db")
with sqlite3.connect(database) as connection:
connection.execute("CREATE TABLE unexpected_live_state (value TEXT)")
monkeypatch.setattr(recovery, "CANDIDATE_SHA", recovery._sha(candidate))
monkeypatch.setattr(recovery, "SOURCE_SHA", "0" * 64)
with pytest.raises(recovery.RecoveryError, match="quarantined source"):
recovery.recover(database, candidate)
assert not list(tmp_path.glob("*.bak"))
def test_recovery_rejects_unreviewed_wal_before_replacement(tmp_path, monkeypatch):
candidate = _candidate(tmp_path / "candidate.db")
database = tmp_path / "kanban.db"
shutil.copyfile(candidate, database)
with sqlite3.connect(database) as connection:
connection.execute("CREATE TABLE forensic_source (value TEXT)")
monkeypatch.setattr(recovery, "CANDIDATE_SHA", recovery._sha(candidate))
monkeypatch.setattr(recovery, "SOURCE_SHA", recovery._sha(database))
database.with_name("kanban.db-wal").write_bytes(b"unreviewed writes")
with pytest.raises(recovery.RecoveryError, match="unreviewed journal"):
recovery.recover(database, candidate)
assert not list(tmp_path.glob("*.bak"))
def test_recovery_finalizes_an_interrupted_atomic_replacement(tmp_path, monkeypatch):
candidate = _candidate(tmp_path / "candidate.db")
database = tmp_path / "kanban.db"
shutil.copyfile(candidate, database)
with sqlite3.connect(database) as connection:
connection.execute("CREATE TABLE forensic_source (value TEXT)")
monkeypatch.setattr(recovery, "CANDIDATE_SHA", recovery._sha(candidate))
monkeypatch.setattr(recovery, "SOURCE_SHA", recovery._sha(database))
original_marker = recovery._write_marker
monkeypatch.setattr(recovery, "_write_marker", lambda _database: (_ for _ in ()).throw(OSError("crash")))
with pytest.raises(OSError, match="crash"):
recovery.recover(database, candidate)
assert recovery._sha(database) == recovery.CANDIDATE_SHA
assert list(tmp_path.glob("kanban.db.corrupt.recovery-*.bak"))
monkeypatch.setattr(recovery, "_write_marker", original_marker)
assert recovery.recover(database, candidate)["state"] == "recovery-finalized"
assert recovery.recover(database, candidate)["state"] == "already-recovered"