"""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"