"""Unit tests for src/cache.py.""" import json import os import tempfile from pathlib import Path import pytest from src.cache import Cache, CacheError, MANIFEST_VERSION @pytest.fixture def tmp_cache(tmp_path): return Cache(tmp_path) class TestIsCached: def test_miss_when_no_entry(self, tmp_cache): assert tmp_cache.is_cached("claude", "conv-abc", "2024-01-01T00:00:00Z") is False def test_hit_after_mark_exported(self, tmp_cache): tmp_cache.mark_exported("claude", "conv-abc", {"updated_at": "2024-01-01T00:00:00Z"}) assert tmp_cache.is_cached("claude", "conv-abc", "2024-01-01T00:00:00Z") is True def test_stale_when_provider_has_newer_date(self, tmp_cache): tmp_cache.mark_exported("claude", "conv-abc", {"updated_at": "2024-01-01T00:00:00Z"}) assert tmp_cache.is_cached("claude", "conv-abc", "2024-06-01T00:00:00Z") is False def test_hit_when_provider_has_same_date(self, tmp_cache): tmp_cache.mark_exported("chatgpt", "conv-xyz", {"updated_at": "2024-06-01T00:00:00Z"}) assert tmp_cache.is_cached("chatgpt", "conv-xyz", "2024-06-01T00:00:00Z") is True def test_miss_for_different_provider(self, tmp_cache): tmp_cache.mark_exported("claude", "conv-abc", {"updated_at": "2024-01-01T00:00:00Z"}) assert tmp_cache.is_cached("chatgpt", "conv-abc", "2024-01-01T00:00:00Z") is False class TestAtomicWrite: def test_manifest_has_600_permissions(self, tmp_path): c = Cache(tmp_path) c.mark_exported("claude", "x", {"updated_at": "2024-01-01"}) manifest = tmp_path / "manifest.json" mode = oct(os.stat(manifest).st_mode)[-3:] assert mode == "600" def test_no_tmp_file_left_after_write(self, tmp_path): c = Cache(tmp_path) c.mark_exported("claude", "x", {"updated_at": "2024-01-01"}) tmp_files = list(tmp_path.glob("*.tmp")) assert tmp_files == [] def test_manifest_is_valid_json(self, tmp_path): c = Cache(tmp_path) c.mark_exported("claude", "x", {}) manifest = tmp_path / "manifest.json" data = json.loads(manifest.read_text()) assert isinstance(data, dict) assert "claude" in data class TestStats: def test_empty_stats(self, tmp_cache): stats = tmp_cache.stats() assert stats["chatgpt"] == 0 assert stats["claude"] == 0 def test_stats_after_exports(self, tmp_cache): tmp_cache.mark_exported("claude", "c1", {}) tmp_cache.mark_exported("claude", "c2", {}) tmp_cache.mark_exported("chatgpt", "g1", {}) stats = tmp_cache.stats() assert stats["claude"] == 2 assert stats["chatgpt"] == 1 class TestClear: def test_clear_single_provider(self, tmp_cache): tmp_cache.mark_exported("claude", "c1", {}) tmp_cache.mark_exported("chatgpt", "g1", {}) tmp_cache.clear("claude") assert tmp_cache.stats()["claude"] == 0 assert tmp_cache.stats()["chatgpt"] == 1 def test_clear_all(self, tmp_cache): tmp_cache.mark_exported("claude", "c1", {}) tmp_cache.mark_exported("chatgpt", "g1", {}) tmp_cache.clear() assert tmp_cache.stats()["claude"] == 0 assert tmp_cache.stats()["chatgpt"] == 0 class TestCorruptManifestRecovery: def test_recovers_from_invalid_json(self, tmp_path): manifest = tmp_path / "manifest.json" manifest.write_text("{invalid json!!!", encoding="utf-8") # Should not raise, should start fresh c = Cache(tmp_path) assert c.stats()["claude"] == 0 # Backup should exist backup = tmp_path / "manifest.json.bak" assert backup.exists() assert backup.read_text() == "{invalid json!!!" def test_raises_on_future_version(self, tmp_path): manifest = tmp_path / "manifest.json" manifest.write_text( json.dumps({"version": MANIFEST_VERSION + 99, "chatgpt": {}, "claude": {}}), encoding="utf-8", ) with pytest.raises(CacheError, match="Unsupported manifest version"): Cache(tmp_path) class TestTosAcknowledgement: def test_not_acknowledged_by_default(self, tmp_cache): assert tmp_cache.is_tos_acknowledged() is False def test_acknowledged_after_call(self, tmp_cache): tmp_cache.acknowledge_tos() assert tmp_cache.is_tos_acknowledged() is True def test_acknowledgement_persists_across_instances(self, tmp_path): c1 = Cache(tmp_path) c1.acknowledge_tos() c2 = Cache(tmp_path) assert c2.is_tos_acknowledged() is True class TestGetNewOrUpdated: def test_returns_all_when_cache_empty(self, tmp_cache): convs = [ {"id": "a", "updated_at": "2024-01-01T00:00:00Z"}, {"id": "b", "updated_at": "2024-01-02T00:00:00Z"}, ] result = tmp_cache.get_new_or_updated("claude", convs) assert len(result) == 2 def test_skips_cached_unchanged(self, tmp_cache): tmp_cache.mark_exported("claude", "a", {"updated_at": "2024-01-01T00:00:00Z"}) convs = [ {"id": "a", "updated_at": "2024-01-01T00:00:00Z"}, {"id": "b", "updated_at": "2024-01-02T00:00:00Z"}, ] result = tmp_cache.get_new_or_updated("claude", convs) assert len(result) == 1 assert result[0]["id"] == "b" def test_includes_stale_conversations(self, tmp_cache): tmp_cache.mark_exported("claude", "a", {"updated_at": "2024-01-01T00:00:00Z"}) convs = [{"id": "a", "updated_at": "2024-06-01T00:00:00Z"}] result = tmp_cache.get_new_or_updated("claude", convs) assert len(result) == 1