# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc from __future__ import annotations import json from pathlib import Path import pytest from solstone.think.backup import state def _config_path(journal: Path) -> Path: return journal / "config" / "journal.json" def _write_config(journal: Path, payload: dict) -> None: config_path = _config_path(journal) config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") def _read_config(journal: Path) -> dict: return json.loads(_config_path(journal).read_text(encoding="utf-8")) def test_missing_offload_key_reads_pinned_defaults( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) _write_config(tmp_path, {"backup": {"enabled": True}}) assert state.get_backup_config()["offload"] == { "enabled": False, "budget_bytes": None, "floor_bytes": None, } def test_set_offload_valid_write_round_trips( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) _write_config(tmp_path, {}) offload = { "enabled": True, "budget_bytes": 500_000_000_000, "floor_bytes": None, } state.set_offload(offload) assert _read_config(tmp_path)["backup"]["offload"] == offload assert state.get_backup_config()["offload"] == offload @pytest.mark.parametrize( "offload", [ {"enabled": 1, "budget_bytes": None, "floor_bytes": None}, {"enabled": "true", "budget_bytes": None, "floor_bytes": None}, {"enabled": True, "budget_bytes": True, "floor_bytes": None}, {"enabled": True, "budget_bytes": False, "floor_bytes": None}, {"enabled": True, "budget_bytes": 0, "floor_bytes": None}, {"enabled": True, "budget_bytes": -1, "floor_bytes": None}, {"enabled": True, "budget_bytes": "1", "floor_bytes": None}, {"enabled": True, "budget_bytes": None, "floor_bytes": 0}, {"enabled": True, "budget_bytes": None, "floor_bytes": -1}, {"enabled": True, "budget_bytes": None, "floor_bytes": "1"}, {"enabled": True, "budget_bytes": None}, { "enabled": True, "budget_bytes": None, "floor_bytes": None, "extra": 1, }, ], ) def test_set_offload_rejects_invalid_shapes_without_writing( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, offload: dict ) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) original = {"backup": {"enabled": False}} _write_config(tmp_path, original) before = _read_config(tmp_path) with pytest.raises(ValueError): state.set_offload(offload) assert _read_config(tmp_path) == before def test_set_offload_preserves_existing_backup_section_without_materializing_defaults( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) backup = { "enabled": False, "mode": "operated", "retention": {"hourly": 2, "daily": 3, "weekly": 4, "monthly": 5}, "custom_marker": {"nested": ["keep", 7]}, } _write_config(tmp_path, {"backup": backup}) before_backup = _read_config(tmp_path)["backup"] state.set_offload( { "enabled": True, "budget_bytes": 10, "floor_bytes": 5, } ) after_backup = _read_config(tmp_path)["backup"] assert set(after_backup) == {*before_backup, "offload"} for key, value in before_backup.items(): assert json.dumps(after_backup[key], sort_keys=True) == json.dumps( value, sort_keys=True ) assert "destination" not in after_backup assert "last_prune" not in after_backup def test_set_offload_has_no_cross_field_backup_enabled_rule( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) _write_config(tmp_path, {"backup": {"enabled": False}}) state.set_offload( { "enabled": True, "budget_bytes": 10, "floor_bytes": 20, } ) assert _read_config(tmp_path)["backup"]["enabled"] is False assert _read_config(tmp_path)["backup"]["offload"] == { "enabled": True, "budget_bytes": 10, "floor_bytes": 20, } def test_offload_state_records_and_status_view( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) _write_config(tmp_path, {"backup": {}}) config = state.get_backup_config() assert config["last_offload"] == { "time": None, "status": None, "reason": None, "last_ok_time": None, "files_offloaded": 0, "bytes_offloaded": 0, "ran_out_of_media": False, } assert config["last_verification"] == { "time": None, "status": None, "reason": None, "last_ok_time": None, "checked_subset": None, } assert config["last_restore"] == { "time": None, "status": None, "reason": None, "scope": None, "day": None, "segments_selected": 0, "segments_restored": 0, "files_expected": 0, "files_restored": 0, "bytes_expected": 0, "bytes_restored": 0, } state.record_offload_result( status="ok", time=100, reason=None, files_offloaded=2, bytes_offloaded=50, ran_out_of_media=False, ) state.record_offload_result( status="stalled", time=123, reason="backup_not_ready", files_offloaded=1, bytes_offloaded=25, ran_out_of_media=False, ) state.record_verification_result(status="skipped", time=None, reason="disabled") state.record_restore_result( status="ok", time=200, reason=None, scope="day", day="20260101", segments_selected=2, segments_restored=2, files_expected=5, files_restored=5, bytes_expected=500, bytes_restored=500, ) state.record_restore_result( status="error", time=210, reason="verification_failed", scope="day", day="20260102", segments_selected=1, segments_restored=0, files_expected=2, files_restored=0, bytes_expected=200, bytes_restored=0, ) backup = _read_config(tmp_path)["backup"] assert backup["last_offload"] == { "time": 123, "status": "stalled", "reason": "backup_not_ready", "last_ok_time": 100, "files_offloaded": 1, "bytes_offloaded": 25, "ran_out_of_media": False, } assert backup["last_verification"] == { "time": None, "status": "skipped", "reason": "disabled", "last_ok_time": None, "checked_subset": None, } assert backup["last_restore"] == { "time": 210, "status": "error", "reason": "verification_failed", "scope": "day", "day": "20260102", "segments_selected": 1, "segments_restored": 0, "files_expected": 2, "files_restored": 0, "bytes_expected": 200, "bytes_restored": 0, } with pytest.raises(ValueError): state.record_offload_result( status="degraded", time=1, files_offloaded=0, bytes_offloaded=0, ran_out_of_media=False, ) with pytest.raises(ValueError): state.record_verification_result(status="stalled", time=1) with pytest.raises(ValueError): state.record_restore_result( status="skipped", time=1, reason=None, scope="day", day="20260101", segments_selected=0, segments_restored=0, files_expected=0, files_restored=0, bytes_expected=0, bytes_restored=0, ) view = state.status_view() assert view["offload"] == { "enabled": False, "budget_bytes": None, "floor_bytes": None, } assert view["last_offload"] == backup["last_offload"] assert view["last_verification"] == backup["last_verification"] assert view["last_restore"] == backup["last_restore"] def test_verification_record_preserves_last_ok_and_clears_failed_subset( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) _write_config(tmp_path, {"backup": {}}) state.record_verification_result( status="ok", time=100, reason=None, checked_subset="7/52", ) assert _read_config(tmp_path)["backup"]["last_verification"] == { "time": 100, "status": "ok", "reason": None, "last_ok_time": 100, "checked_subset": "7/52", } state.record_verification_result( status="error", time=200, reason="locked", checked_subset="8/52", ) assert _read_config(tmp_path)["backup"]["last_verification"] == { "time": 200, "status": "error", "reason": "locked", "last_ok_time": 100, "checked_subset": None, }