import json from datetime import UTC, datetime, timedelta from pathlib import Path import app.store as store_module from app.scanner import scan_repository from app.store import ScanStore def test_scan_history_comparison_and_trend(tmp_path: Path) -> None: repository = tmp_path / "repo" repository.mkdir() (repository / "README.md").write_text("# Demo\n", encoding="utf-8") (repository / ".gitignore").write_text("*.db\n", encoding="utf-8") store = ScanStore(str(tmp_path / "repopulse.db")) first = store.save(scan_repository(str(repository))) (repository / "app.py").write_text("print('hello')\n", encoding="utf-8") (repository / "tests").mkdir() (repository / "tests" / "test_app.py").write_text("def test_ok():\n assert True\n", encoding="utf-8") second = store.save(scan_repository(str(repository))) assert store.get(second.id) == second assert store.recent(1) == [second] assert store.previous_for_repository(second.repository_path, second.id) == first comparison = store.compare(second) assert comparison.previous_scan_id == first.id assert comparison.health_score_delta == second.health_score - first.health_score assert comparison.metric_deltas["source_files"] == 2 trend = store.trend(second.repository_path) assert [item.scan_id for item in trend] == [first.id, second.id] assert trend[-1].health_score == second.health_score def test_comparison_uses_the_scan_before_the_selected_scan(tmp_path: Path) -> None: repository = tmp_path / "repo" repository.mkdir() (repository / "README.md").write_text("# Demo\n", encoding="utf-8") (repository / ".gitignore").write_text("*.db\n", encoding="utf-8") store = ScanStore(str(tmp_path / "repopulse.db")) first = store.save(scan_repository(str(repository))) second = store.save(scan_repository(str(repository))) third = store.save(scan_repository(str(repository))) assert store.previous_for_repository(second.repository_path, second.id) == first assert store.previous_for_repository(third.repository_path, third.id) == second def test_repository_summary_exposes_compact_remediation_context(tmp_path: Path) -> None: repository = tmp_path / "repo" repository.mkdir() (repository / "README.md").write_text("# Demo\n", encoding="utf-8") (repository / ".gitignore").write_text(".env\n", encoding="utf-8") (repository / ".env").write_text("TOKEN=actual-secret-value-123456\n", encoding="utf-8") store = ScanStore(str(tmp_path / "repopulse.db")) store.save(scan_repository(str(repository))) (repository / ".env").unlink() (repository / "app.py").write_text("api_key = 'actual-secret-value-123456'\n", encoding="utf-8") store.save(scan_repository(str(repository))) summary = store.list_repositories()[0] assert summary.last_scan_blockers == 1 assert summary.last_scan_primary_signal is not None assert summary.last_scan_primary_signal.code == "security.secret-assignment" assert summary.last_scan_primary_signal.count == 1 assert summary.last_scan_added_findings >= 1 assert summary.last_scan_resolved_findings == 1 def test_corrupt_database_is_quarantined_and_recreated(tmp_path: Path) -> None: database = tmp_path / "repopulse.db" database.write_bytes(b"not a sqlite database") store = ScanStore(str(database)) assert store.recent() == [] assert list(tmp_path.glob("repopulse.db.corrupt-*")) assert store.database_recovery_status()["status"] == "degraded_recovered" def test_schema_migration_records_the_durable_backup_artifact(tmp_path: Path) -> None: database = tmp_path / "repopulse.db" initial_store = ScanStore(str(database)) with initial_store._connection() as db: db.execute("PRAGMA user_version = 3") migrated_store = ScanStore(str(database)) recorded = migrated_store.get_state("last_schema_migration_backup") assert recorded is not None metadata = json.loads(recorded) assert metadata["from_version"] == 3 assert metadata["to_version"] == store_module.SCHEMA_VERSION assert (tmp_path / metadata["backup_file"]).is_file() assert migrated_store.storage_stats()["last_schema_migration_backup"] == recorded def test_history_is_bounded_per_repository(tmp_path: Path, monkeypatch) -> None: repository = tmp_path / "repo" repository.mkdir() (repository / "README.md").write_text("# Demo\n", encoding="utf-8") monkeypatch.setattr(store_module, "MAX_HISTORY_PER_REPOSITORY", 2) store = ScanStore(str(tmp_path / "repopulse.db")) first = store.save(scan_repository(str(repository))) second = store.save(scan_repository(str(repository))) third = store.save(scan_repository(str(repository))) assert [item.id for item in store.recent_for_repository(third.repository_path)] == [third.id, second.id] assert store.get(first.id) is None def test_retention_compacts_old_detailed_payloads_and_expires_old_snapshots(tmp_path: Path, monkeypatch) -> None: repository = tmp_path / "repo" repository.mkdir() (repository / "README.md").write_text("# Demo\n", encoding="utf-8") (repository / ".gitignore").write_text("*.db\n", encoding="utf-8") monkeypatch.setenv("REPOPULSE_FULL_REPORTS_PER_REPOSITORY", "1") monkeypatch.setenv("REPOPULSE_REPORT_RETENTION_DAYS", "30") store = ScanStore(str(tmp_path / "repopulse.db")) first = store.save(scan_repository(str(repository))) second = store.save(scan_repository(str(repository))) compacted = store.get(first.id) assert compacted is not None and compacted.compacted is True assert compacted.findings == [] assert store.get(second.id) is not None expired = second.model_copy( update={"id": "expired-scan", "scanned_at": datetime.now(UTC) - timedelta(days=31)} ) store.save(expired) assert store.get("expired-scan") is None def test_catalog_summary_uses_denormalized_fields_without_report_json_parsing(tmp_path: Path, monkeypatch) -> None: repository = tmp_path / "repo" repository.mkdir() (repository / "README.md").write_text("# Demo\n", encoding="utf-8") (repository / ".gitignore").write_text("*.db\n", encoding="utf-8") store = ScanStore(str(tmp_path / "repopulse.db")) store.save(scan_repository(str(repository))) def fail_if_called(_row): raise AssertionError("catalog list must not parse report_json") monkeypatch.setattr(store, "_report_from_row", fail_if_called) summary = store.list_repositories()[0] assert summary.last_scan_id storage = store.storage_stats() assert storage["scan_count"] == 1 assert storage["mirror_count"] >= 0 assert storage["worktree_count"] >= 0