"""Tests for the SQLite review cache (stdlib sqlite3, temp DB per test).""" from __future__ import annotations import json import sqlite3 import pytest from app.store import ReviewStore # The schema as it shipped before pr_created_at / pr_node_id were added. _OLD_SCHEMA = """ CREATE TABLE reviews ( owner TEXT NOT NULL, repo TEXT NOT NULL, number INTEGER NOT NULL, status TEXT NOT NULL, diff_hash TEXT, review_json TEXT, error TEXT, attempts INTEGER NOT NULL DEFAULT 0, pr_title TEXT, pr_author TEXT, pr_url TEXT, pr_updated_at TEXT, cached_at REAL NOT NULL, closed_at REAL, PRIMARY KEY (owner, repo, number) ); """ @pytest.fixture def store(tmp_path) -> ReviewStore: return ReviewStore(str(tmp_path / "cache.db")) def test_upsert_insert_then_get(store: ReviewStore) -> None: store.upsert( "o", "r", 1, status="ready", diff_hash="abc", review_json=json.dumps({"summary": "hi"}), pr_title="Title", pr_author="octocat", pr_url="https://x/1", pr_updated_at="2026-01-01", ) row = store.get("o", "r", 1) assert row is not None assert row["status"] == "ready" assert row["diff_hash"] == "abc" assert row["pr_title"] == "Title" assert json.loads(row["review_json"])["summary"] == "hi" assert row["cached_at"] > 0 def test_get_missing_returns_none(store: ReviewStore) -> None: assert store.get("o", "r", 999) is None def test_update_preserves_unspecified_columns(store: ReviewStore) -> None: store.upsert("o", "r", 1, status="ready", diff_hash="h1", review_json="{}") # Update only status; diff_hash and review_json must survive. store.upsert("o", "r", 1, status="reviewing") row = store.get("o", "r", 1) assert row["status"] == "reviewing" assert row["diff_hash"] == "h1" assert row["review_json"] == "{}" def test_unknown_column_raises(store: ReviewStore) -> None: with pytest.raises(ValueError, match="unknown review columns"): store.upsert("o", "r", 1, status="ready", bogus="x") def test_list_orders_and_filters_closed(store: ReviewStore) -> None: store.upsert("o", "r", 1, status="ready", pr_updated_at="2026-01-01") store.upsert( "o", "r", 2, status="closed", pr_updated_at="2026-02-02", closed_at=1.0 ) all_rows = store.list() assert [r["number"] for r in all_rows] == [2, 1] # updated_at desc open_rows = store.list(include_closed=False) assert [r["number"] for r in open_rows] == [1] def test_reset_stale_reviewing(store: ReviewStore) -> None: store.upsert("o", "r", 1, status="reviewing") store.upsert("o", "r", 2, status="ready") n = store.reset_stale_reviewing() assert n == 1 assert store.get("o", "r", 1)["status"] == "error" assert store.get("o", "r", 1)["error"] == "interrupted" assert store.get("o", "r", 2)["status"] == "ready" def test_migration_adds_columns_to_old_schema(tmp_path) -> None: db = str(tmp_path / "old.db") con = sqlite3.connect(db) con.executescript(_OLD_SCHEMA) con.execute( "INSERT INTO reviews (owner, repo, number, status, cached_at) " "VALUES ('o', 'r', 1, 'ready', 1.0)" ) con.commit() con.close() store = ReviewStore(db) # __init__ migrates con = sqlite3.connect(db) cols = {row[1] for row in con.execute("PRAGMA table_info(reviews)")} con.close() assert "pr_created_at" in cols assert "pr_node_id" in cols # Existing row survived, and the new columns are writable. assert store.get("o", "r", 1)["status"] == "ready" store.upsert( "o", "r", 1, status="ready", pr_created_at="2026-06-01", pr_node_id="PR_x" ) row = store.get("o", "r", 1) assert row["pr_created_at"] == "2026-06-01" assert row["pr_node_id"] == "PR_x" def test_migration_is_idempotent(tmp_path) -> None: db = str(tmp_path / "c.db") ReviewStore(db) ReviewStore(db) # second open must not fail on already-present columns con = sqlite3.connect(db) cols = {row[1] for row in con.execute("PRAGMA table_info(reviews)")} con.close() assert {"pr_created_at", "pr_node_id"} <= cols def test_mark_missing_closed_and_purge(store: ReviewStore) -> None: store.upsert("o", "r", 1, status="ready") store.upsert("o", "r", 2, status="ready") # Only PR 1 is still live; PR 2 should be closed. store.mark_missing_closed({("o", "r", 1)}) assert store.get("o", "r", 1)["status"] == "ready" closed = store.get("o", "r", 2) assert closed["status"] == "closed" assert closed["closed_at"] is not None # A long grace keeps it; a zero grace purges it. assert store.purge_closed(older_than_seconds=10_000) == 0 assert store.get("o", "r", 2) is not None assert store.purge_closed(older_than_seconds=0) == 1 assert store.get("o", "r", 2) is None