pr-reviewer/tests/test_store.py

145 lines
4.8 KiB
Python
Raw Normal View History

"""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