# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc from __future__ import annotations import hashlib import json import os import shlex import shutil import socket from pathlib import Path from typing import Any import pytest from solstone.think.backup import restore, rotation, teardown from solstone.think.backup.destination import Destination, validate_destination from solstone.think.backup.engine import BACKUP_EXCLUDES from solstone.think.backup.install import ensure_restic from solstone.think.backup.readiness import ( RESTIC_SCHEMA_VERSION, RESTIC_TOOL, RESTIC_VERSION, _binary_path, _platform_info, _sentinel_path, _tool_dir, ) from solstone.think.backup.repo import init_repository from solstone.think.backup.runner import run_restic from solstone.think.backup.state import BACKUP_DEFAULTS, get_destination from solstone.think.indexer.journal import ScanReport from tests.helpers.journal_config import seed_journal_config RESTIC_BIN = shutil.which("restic") def _config_path(journal: Path) -> Path: return journal / "config" / "journal.json" def _write_json(path: Path, payload: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") def _read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def _destination(repo: Path) -> Destination: return Destination( repository=f"local:{repo}", backend="s3", credentials={ "access_key_id": "test-access-key", "secret_access_key": "test-secret-key", }, ) def _seed_backup_config( journal: Path, *, destination: Destination, daily_key: str, recovery_key: str, ) -> None: seed_journal_config( { "backup": { "enabled": True, "destination": { "repository": destination.repository, "backend": destination.backend, "credentials": destination.credentials, }, "daily_key": daily_key, "recovery_key": recovery_key, "confirmed_recovery_key": True, } }, journal, ) def _install_restic_wrapper(restic_bin: str) -> Path: os_name, arch = _platform_info() tool_dir = _tool_dir(os_name) tool_dir.mkdir(parents=True, exist_ok=True) binary_path = _binary_path(tool_dir) binary_path.write_text( "#!/usr/bin/env sh\n" 'if [ "$1" = "version" ]; then\n' f" echo 'restic {RESTIC_VERSION} test wrapper'\n" " exit 0\n" "fi\n" f'exec {shlex.quote(restic_bin)} "$@"\n', encoding="utf-8", ) binary_path.chmod(0o755) digest = hashlib.sha256(binary_path.read_bytes()).hexdigest() _write_json( _sentinel_path(tool_dir), { "schema_version": RESTIC_SCHEMA_VERSION, "tool": RESTIC_TOOL, "version": RESTIC_VERSION, "sha256": digest, "platform": {"os": os_name, "arch": arch}, "binary_path": str(binary_path), }, ) return binary_path def _backup_journal( *, journal: Path, destination: Destination, daily_key: str, recovery_key: str, restic_path: Path, ) -> None: init_repository( destination, daily_key=daily_key, recovery_key=recovery_key, restic_path=restic_path, timeout=30, ) args = ["backup", str(journal)] for pattern in BACKUP_EXCLUDES: args.extend(["--exclude", pattern]) result = run_restic( args, repository=destination.repository, password=daily_key, restic_path=restic_path, timeout=60, ) assert result.returncode == 0, result.stderr @pytest.mark.skipif(RESTIC_BIN is None, reason="restic is not installed") @pytest.mark.integration @pytest.mark.timeout(120) def test_backup_restore_rotation_teardown_real_local_round_trip( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: home = tmp_path / "home" home.mkdir() monkeypatch.setenv("HOME", str(home)) restic_path = _install_restic_wrapper(RESTIC_BIN or "") repo = tmp_path / "repo" source_journal = tmp_path / "source-journal" restored_journal = tmp_path / "restored-journal" source_journal.mkdir() restored_journal.mkdir() destination = _destination(repo) daily_key = "daily-password" recovery_key = ("0" * 32) + ("1" * 32) entered_recovery_key = ("O" * 32) + ("I" * 32) _seed_backup_config( source_journal, destination=destination, daily_key=daily_key, recovery_key=recovery_key, ) (source_journal / "chronicle" / "20260612").mkdir(parents=True) (source_journal / "chronicle" / "20260612" / "note.txt").write_text( "journal content\n", encoding="utf-8", ) (source_journal / "health" / "pruning-runs").mkdir(parents=True) (source_journal / "health" / "retention.log").write_text( "retention audit\n", encoding="utf-8", ) (source_journal / "health" / "pruning-runs" / "20260701.jsonl").write_text( "pruning audit\n", encoding="utf-8", ) (source_journal / "health" / "supervisor.pid").write_text( "12345\n", encoding="utf-8", ) (source_journal / "health" / "supervisor.ready").write_text( "ready\n", encoding="utf-8", ) (source_journal / "health" / "convey.port").write_text( "5015\n", encoding="utf-8", ) (source_journal / "health" / ".steward.lock").write_text( "locked\n", encoding="utf-8", ) (source_journal / "health" / ".steward_inflight.tmp").write_text( "partial\n", encoding="utf-8", ) (source_journal / "chronicle" / "20260701" / "health" / "talent-provenance").mkdir( parents=True ) ( source_journal / "chronicle" / "20260701" / "health" / "talent-provenance" / "run.json" ).write_text('{"run":"audit"}\n', encoding="utf-8") callosum_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) cwd = os.getcwd() try: os.chdir(source_journal / "health") callosum_socket.bind("callosum.sock") finally: os.chdir(cwd) monkeypatch.setenv("SOLSTONE_JOURNAL", str(source_journal)) assert ensure_restic() == restic_path try: _backup_journal( journal=source_journal, destination=destination, daily_key=daily_key, recovery_key=recovery_key, restic_path=restic_path, ) finally: callosum_socket.close() scan_calls: list[tuple[str, dict[str, Any]]] = [] def fake_scan_journal(journal: str, **kwargs: Any) -> ScanReport: scan_calls.append((journal, kwargs)) return ScanReport(changed=True, edge_rows_inserted=0) monkeypatch.setattr(restore, "scan_journal", fake_scan_journal) monkeypatch.setenv("SOLSTONE_JOURNAL", str(restored_journal)) restore_result = restore.restore_journal(destination, entered_recovery_key) assert restore_result.status == "ok" assert restore_result.integrity_ok is True assert restore_result.resumable is True assert isinstance(restore_result.bytes_restored, int) assert restore_result.bytes_restored > 0 assert (restored_journal / "chronicle" / "20260612" / "note.txt").read_text( encoding="utf-8" ) == "journal content\n" assert (restored_journal / "health" / "retention.log").read_text( encoding="utf-8" ) == "retention audit\n" assert (restored_journal / "health" / "pruning-runs" / "20260701.jsonl").read_text( encoding="utf-8" ) == "pruning audit\n" assert ( restored_journal / "chronicle" / "20260701" / "health" / "talent-provenance" / "run.json" ).read_text(encoding="utf-8") == '{"run":"audit"}\n' assert not (restored_journal / "health" / "callosum.sock").exists() assert not (restored_journal / "health" / "supervisor.pid").exists() assert not (restored_journal / "health" / "supervisor.ready").exists() assert not (restored_journal / "health" / "convey.port").exists() assert not (restored_journal / "health" / ".steward.lock").exists() assert not (restored_journal / "health" / ".steward_inflight.tmp").exists() restored_config = _read_json(_config_path(restored_journal)) assert restored_config["backup"]["daily_key"] == daily_key assert restored_config["backup"]["destination"] == { "repository": destination.repository, "backend": destination.backend, "credentials": destination.credentials, } assert restored_config["backup"]["recovery_key"] == recovery_key assert restored_config["backup"]["confirmed_recovery_key"] is True assert scan_calls == [(str(restored_journal), {"full": True})] rotation_result = rotation.rotate_recovery_key() assert rotation_result.status == "ok" assert rotation_result.recovery_key is not None assert rotation_result.recovery_key != recovery_key active_config = _read_json(_config_path(restored_journal)) assert active_config["backup"]["daily_key"] == daily_key assert active_config["backup"]["recovery_key"] == rotation_result.recovery_key assert active_config["backup"]["confirmed_recovery_key"] is False active_destination = get_destination() assert active_destination == destination new_status = validate_destination( active_destination, rotation_result.recovery_key, restic_path=restic_path, timeout=30, ) old_status = validate_destination( active_destination, recovery_key, restic_path=restic_path, timeout=30, ) assert new_status.reason_code == "repo_exists" assert old_status.reason_code == "auth_failed" teardown_result = teardown.teardown_backup() assert teardown_result.status == "ok" snapshots = run_restic( ["snapshots"], repository=destination.repository, password=daily_key, restic_path=restic_path, json=True, timeout=30, ) assert snapshots.returncode == 0, snapshots.stderr assert snapshots.json == [] assert _read_json(_config_path(restored_journal))["backup"] == BACKUP_DEFAULTS