"""Tests for nightly DB backups (T32). The backup service is intentionally simple: a flat ``data/backups/`` dir containing timestamped copies of ``chat.db``, with retention of the most recent 14. The scheduling decision (``should_take_backup``) is a pure function of clock + filesystem state so it can be unit-tested without spinning up the BackgroundWorker tick loop. """ from __future__ import annotations from datetime import datetime from unittest.mock import patch from chat.services.backup import ( prune_backups, should_take_backup, take_backup, ) def test_take_backup_creates_timestamped_copy(tmp_path): db = tmp_path / "chat.db" db.write_text("fake db contents") backup_path = take_backup(db_path=db, data_dir=tmp_path / "data") assert backup_path.exists() assert backup_path.name.startswith("chat-") assert backup_path.name.endswith(".db") # Contents copied assert backup_path.read_text() == "fake db contents" # Located in data/backups/ assert backup_path.parent == tmp_path / "data" / "backups" def test_prune_keeps_last_14(tmp_path): backup_dir = tmp_path / "data" / "backups" backup_dir.mkdir(parents=True) # Create 17 dummy backup files spanning days 1..17 of Jan 2026. # Filenames sort lexicographically by the embedded timestamp, so # prune_backups should drop the three oldest. for i in range(1, 18): (backup_dir / f"chat-202601{i:02d}T000000Z.db").write_text( f"backup {i}" ) removed = prune_backups(tmp_path / "data", keep=14) assert removed == 3 remaining = sorted(backup_dir.glob("chat-*.db")) assert len(remaining) == 14 # Days 1, 2, 3 removed; day 4 is now the oldest retained backup. assert remaining[0].name == "chat-20260104T000000Z.db" def test_should_take_backup_when_no_prior_and_target_hour_matches(tmp_path): from chat.services import backup as backup_mod class FakeDateTime(datetime): @classmethod def now(cls, tz=None): return datetime(2026, 4, 26, 3, 0, 0) with patch.object(backup_mod, "datetime", FakeDateTime): assert should_take_backup(tmp_path / "data") is True def test_should_not_take_backup_outside_target_hour(tmp_path): from chat.services import backup as backup_mod class FakeDateTime(datetime): @classmethod def now(cls, tz=None): return datetime(2026, 4, 26, 14, 0, 0) with patch.object(backup_mod, "datetime", FakeDateTime): assert should_take_backup(tmp_path / "data") is False def test_should_not_take_backup_when_recent_backup_exists(tmp_path): backup_dir = tmp_path / "data" / "backups" backup_dir.mkdir(parents=True) recent = backup_dir / "chat-recent.db" recent.write_text("x") # mtime defaults to "now" — within the 23h freshness window so # should_take_backup must return False even at the target hour. from chat.services import backup as backup_mod class FakeDateTime(datetime): @classmethod def now(cls, tz=None): return datetime(2026, 4, 26, 3, 0, 0) with patch.object(backup_mod, "datetime", FakeDateTime): assert should_take_backup(tmp_path / "data") is False