140 lines
5.8 KiB
Python
140 lines
5.8 KiB
Python
"""SQLite store: paper lifecycle (upsert/freeze/rollover), digest idempotency, deliveries."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
|
|
from _fakes import WEEK, make_paper, make_payload, make_summary
|
|
|
|
from sebrief.models import DigestResult
|
|
from sebrief.store import Store
|
|
|
|
|
|
def make_store(tmp_path) -> Store:
|
|
return Store(tmp_path / "data" / "test.sqlite3")
|
|
|
|
|
|
def make_result(papers=None, degraded=False) -> DigestResult:
|
|
payload = make_payload(papers=papers)
|
|
return DigestResult(
|
|
payload=payload, digest=None, markdown="# md", model_used="m", degraded=degraded
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------------------------------- papers
|
|
def test_upsert_inserts_and_counts_new(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
p1, p2 = make_paper("10.1/a"), make_paper("10.1/b")
|
|
assert store.upsert_papers([p1, p2]) == 2
|
|
assert store.upsert_papers([p1]) == 0 # re-collection is not new
|
|
assert store.paper_count() == 2
|
|
|
|
|
|
def test_upsert_refreshes_undigested_metadata(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
store.upsert_papers([make_paper("10.1/a", abstract="")])
|
|
store.upsert_papers([make_paper("10.1/a", abstract="now with abstract")])
|
|
[paper] = store.pending_papers(WEEK)
|
|
assert paper.abstract == "now with abstract"
|
|
|
|
|
|
def test_digested_papers_are_frozen_and_skipped(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
store.upsert_papers([make_paper("10.1/a", title="original")])
|
|
store.mark_digested(["10.1/a"], WEEK)
|
|
store.upsert_papers([make_paper("10.1/a", title="rewritten")])
|
|
# not pending for a later week, and metadata unchanged
|
|
assert store.pending_papers("2026-W37") == []
|
|
[paper] = store.pending_papers(WEEK) # --force re-run of the same week still sees it
|
|
assert paper.title == "original"
|
|
|
|
|
|
def test_pending_includes_undigested_leftovers_and_orders_by_date(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
old = make_paper("10.1/old", published_date=date(2026, 8, 20)) # last week's late arrival
|
|
new = make_paper("10.1/new", published_date=date(2026, 9, 2))
|
|
undated = make_paper("10.1/undated", published_date=None)
|
|
store.upsert_papers([new, undated, old])
|
|
dois = [p.doi for p in store.pending_papers(WEEK)]
|
|
assert dois == ["10.1/old", "10.1/new", "10.1/undated"] # dated first (ascending), undated last
|
|
|
|
|
|
def test_paper_roundtrip_preserves_fields(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
original = make_paper("10.1/a", authors=["김철수", "Bob"], topics=["Testing", "LLM"])
|
|
store.upsert_papers([original])
|
|
[loaded] = store.pending_papers(WEEK)
|
|
assert loaded == original
|
|
|
|
|
|
# ---------------------------------------------------------------------------- paper summaries
|
|
def test_paper_summaries_roundtrip_and_survive_metadata_refresh(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
store.upsert_papers([make_paper("10.1/a")])
|
|
summary = make_summary(tldr="한 줄.")
|
|
store.save_paper_summaries({"10.1/a": summary})
|
|
[paper] = store.pending_papers(WEEK)
|
|
assert paper.summary == summary
|
|
# a later re-collection refreshes metadata but must not clear the summary
|
|
store.upsert_papers([make_paper("10.1/a", abstract="updated")])
|
|
[paper] = store.pending_papers(WEEK)
|
|
assert paper.abstract == "updated" and paper.summary == summary
|
|
|
|
|
|
def test_paper_summaries_allowed_on_frozen_papers(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
store.upsert_papers([make_paper("10.1/a")])
|
|
store.mark_digested(["10.1/a"], WEEK)
|
|
store.save_paper_summaries({"10.1/a": make_summary()})
|
|
[paper] = store.pending_papers(WEEK) # digest_week = WEEK: visible on --force re-run
|
|
assert paper.summary is not None
|
|
|
|
|
|
def test_migration_adds_summary_column_to_old_db(tmp_path):
|
|
import sqlite3
|
|
|
|
db = tmp_path / "data" / "old.sqlite3"
|
|
db.parent.mkdir(parents=True)
|
|
conn = sqlite3.connect(db)
|
|
conn.executescript(
|
|
"""
|
|
CREATE TABLE papers (
|
|
doi TEXT PRIMARY KEY, title TEXT NOT NULL, abstract TEXT NOT NULL DEFAULT '',
|
|
authors TEXT NOT NULL DEFAULT '[]', venue TEXT NOT NULL DEFAULT '',
|
|
published_date TEXT, oa_url TEXT, pdf_url TEXT, topics TEXT NOT NULL DEFAULT '[]',
|
|
source TEXT NOT NULL DEFAULT '', collected_at TEXT, digest_week TEXT
|
|
);
|
|
INSERT INTO papers (doi, title) VALUES ('10.1/legacy', 'Legacy Paper');
|
|
"""
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
with Store(db) as store:
|
|
[paper] = store.pending_papers(WEEK)
|
|
assert paper.doi == "10.1/legacy" and paper.summary is None
|
|
store.save_paper_summaries({"10.1/legacy": make_summary()})
|
|
[paper] = store.pending_papers(WEEK)
|
|
assert paper.summary is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------------- digests
|
|
def test_digest_idempotency_and_upsert(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
assert store.has_digest(WEEK) is False
|
|
store.save_digest(make_result())
|
|
assert store.has_digest(WEEK) is True
|
|
store.save_digest(make_result(degraded=True)) # --force resend overwrites
|
|
row = store._conn.execute("SELECT degraded FROM digests WHERE week = ?", (WEEK,)).fetchone()
|
|
assert row["degraded"] == 1
|
|
|
|
|
|
def test_deliveries(tmp_path):
|
|
with make_store(tmp_path) as store:
|
|
store.save_digest(make_result())
|
|
store.mark_sent(WEEK, "file", "/tmp/2026-W36.md")
|
|
store.mark_sent(WEEK, "file", "/tmp/2026-W36.md (resend)")
|
|
rows = store.deliveries_for(WEEK)
|
|
assert [r["channel"] for r in rows] == ["file", "file"]
|
|
assert rows[1]["receipt"].endswith("(resend)")
|