Files
stack/tests/prisma/test_search_due.py
kert fe99b6bbf2 fix: correct the part 1 review findings
Export reads the extraction JSON instead of a tag fragment and writes the claim. A logged search stores its project so the nightly check can clear. Claims keep the effect and the subject, a later strand tags an existing DOI, the dedup command prints borderline pairs, and due dates compare the calendar day.
2026-10-31 21:31:11 -04:00

358 lines
10 KiB
Python

"""Search is due nightly, and also when a trigger lands. No search is run."""
from __future__ import annotations
import pytest
from typer.testing import CliRunner
from cli.prisma import app
from lead.ledger.store import append, open_ledger
from prisma.search.due import due
runner = CliRunner()
PROJECT = "lead-precursors"
NOW = "2026-10-31"
YESTERDAY = "2026-10-30"
def _ledger(tmp_path):
return open_ledger(str(tmp_path / "ledger.duckdb"))
def _search(
con, *, searched_on: str, project: str = PROJECT, suffix: str = "s"
) -> None:
assert append(
con,
"search",
{
"project": project,
"database": "pubmed",
"interface": "eutils",
"searched_on": searched_on,
"query": "lead",
"filters": "",
"hit_count": 1,
"export_sha256": suffix,
"actor": "tester",
"content_hash": f"search-{suffix}",
},
)
def _claim(con, *, certainty: str, suffix: str, target_id: str = "Q.ACR") -> None:
assert append(
con,
"claim",
{
"id": suffix,
"project": PROJECT,
"kind": "precursor",
"target_id": target_id,
"certainty": certainty,
"actor": "tester",
"content_hash": f"claim-{suffix}",
},
)
def _finding(con, *, claim_id: str, suffix: str, created_at: str | None = None) -> None:
row: dict[str, object] = {
"claim_id": claim_id,
"relation": "contradicts",
"grain": "beneficiary",
"actor": "tester",
"content_hash": f"finding-{suffix}",
}
if created_at is not None:
row["created_at"] = created_at
assert append(con, "finding", row)
def test_missing_or_stale_search_is_nightly(tmp_path):
con = _ledger(tmp_path)
try:
assert due(con, project=PROJECT, now=NOW) == ("nightly",)
_search(con, searched_on=NOW, suffix="today")
assert due(con, project=PROJECT, now=NOW) == ()
_search(con, searched_on=YESTERDAY, suffix="yesterday", project="lead-history")
assert due(con, project=PROJECT, now=NOW) == ()
finally:
con.close()
con = open_ledger(str(tmp_path / "stale.duckdb"))
try:
_search(con, searched_on=YESTERDAY, suffix="old")
assert due(con, project=PROJECT, now=NOW) == ("nightly",)
finally:
con.close()
def test_another_project_does_not_clear_nightly(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW, project="lead-history", suffix="other")
assert due(con, project=PROJECT, now=NOW) == ("nightly",)
finally:
con.close()
@pytest.mark.parametrize(
("certainty", "expected"),
[("low", ()), ("moderate", ("contradiction",)), ("high", ("contradiction",))],
)
def test_contradiction_respects_certainty(tmp_path, certainty, expected):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
_claim(con, certainty=certainty, suffix=certainty)
_finding(con, claim_id=certainty, suffix=certainty, created_at="2026-11-01")
assert due(con, project=PROJECT, now=NOW) == expected
finally:
con.close()
def test_contradiction_on_the_search_day_does_not_refire(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
_claim(con, certainty="moderate", suffix="same-day")
_finding(
con,
claim_id="same-day",
suffix="same-day",
created_at="2026-10-31T10:00:00Z",
)
assert due(con, project=PROJECT, now=NOW) == ()
finally:
con.close()
def test_contradiction_without_a_search_stays_nightly(tmp_path):
con = _ledger(tmp_path)
try:
_claim(con, certainty="high", suffix="nosearch")
_finding(con, claim_id="nosearch", suffix="nosearch", created_at="2026-11-01")
assert due(con, project=PROJECT, now=NOW) == ("nightly",)
finally:
con.close()
def test_exogenous_event_on_a_claimed_target(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
assert append(
con,
"exogenous",
{
"project": PROJECT,
"target_id": "Q.ACR",
"series_or_event": "cms guidance",
"event_on": "2026-11-01",
"citation": "cms",
"actor": "tester",
"content_hash": "exo-1",
},
)
assert due(con, project=PROJECT, now=NOW) == ()
_claim(con, certainty="low", suffix="qacr")
assert due(con, project=PROJECT, now=NOW) == ("exogenous",)
finally:
con.close()
def test_exogenous_on_the_search_day_does_not_refire(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
_claim(con, certainty="low", suffix="same-day")
assert append(
con,
"exogenous",
{
"project": PROJECT,
"target_id": "Q.ACR",
"series_or_event": "cms guidance",
"event_on": NOW,
"citation": "cms",
"actor": "tester",
"content_hash": "exo-same-day",
},
)
assert due(con, project=PROJECT, now=NOW) == ()
finally:
con.close()
def test_exogenous_without_a_search_stays_nightly(tmp_path):
con = _ledger(tmp_path)
try:
_claim(con, certainty="low", suffix="nosearch")
assert append(
con,
"exogenous",
{
"target_id": "Q.ACR",
"series_or_event": "cms guidance",
"event_on": "2026-11-01",
"actor": "tester",
"content_hash": "exo-nosearch",
},
)
assert due(con, project=PROJECT, now=NOW) == ("nightly",)
finally:
con.close()
def test_cms_source_newer_than_the_search(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
assert append(
con,
"source",
{
"project": PROJECT,
"database": "cms",
"zotero_key": "ABCD2345",
"created_at": NOW,
"actor": "tester",
"content_hash": "src-same-day",
},
)
assert due(con, project=PROJECT, now=NOW) == ()
assert append(
con,
"source",
{
"project": PROJECT,
"database": "cms",
"zotero_key": "EFGH5678",
"created_at": "2026-11-01",
"actor": "tester",
"content_hash": "src-later",
},
)
assert due(con, project=PROJECT, now=NOW) == ("cms",)
finally:
con.close()
def test_cms_iso_timestamp_on_the_search_day_does_not_fire(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
assert append(
con,
"source",
{
"project": PROJECT,
"database": "cms",
"zotero_key": "ABCD2345",
"created_at": "2026-10-31T10:00:00Z",
"actor": "tester",
"content_hash": "src-iso-same",
},
)
assert due(con, project=PROJECT, now=NOW) == ()
finally:
con.close()
def test_joined_cms_on_the_search_day_does_not_fire(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
assert append(
con,
"source",
{
"project": PROJECT,
"database": "citation,cms",
"zotero_key": "IJKM2345",
"created_at": "2026-10-31T10:00:00Z",
"actor": "tester",
"content_hash": "src-joined-same",
},
)
assert due(con, project=PROJECT, now=NOW) == ()
finally:
con.close()
def test_cms_token_in_a_joined_database_fires_after_the_search_day(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=NOW)
assert append(
con,
"source",
{
"project": PROJECT,
"database": "citation,cms",
"zotero_key": "EFGH5678",
"created_at": "2026-11-01T00:00:00Z",
"actor": "tester",
"content_hash": "src-iso-later",
},
)
assert due(con, project=PROJECT, now=NOW) == ("cms",)
finally:
con.close()
def test_reasons_stay_in_order(tmp_path):
con = _ledger(tmp_path)
try:
_search(con, searched_on=YESTERDAY, suffix="old")
_claim(con, certainty="high", suffix="high")
_finding(con, claim_id="high", suffix="high", created_at="2026-11-01")
assert append(
con,
"exogenous",
{
"target_id": "Q.ACR",
"series_or_event": "cms guidance",
"event_on": NOW,
"actor": "tester",
"content_hash": "exo-order",
},
)
assert append(
con,
"source",
{
"project": PROJECT,
"database": "cms",
"created_at": "2026-11-01",
"actor": "tester",
"content_hash": "src-order",
},
)
assert due(con, project=PROJECT, now=NOW) == (
"nightly",
"contradiction",
"exogenous",
"cms",
)
finally:
con.close()
def test_due_command_is_registered_and_quiet_when_current(tmp_path):
result = runner.invoke(app, ["search", "due", "--help"])
assert result.exit_code == 0
path = tmp_path / "ledger.duckdb"
con = open_ledger(str(path))
try:
_search(con, searched_on=NOW, suffix="cli")
finally:
con.close()
result = runner.invoke(
app,
["search", "due", "--db", str(path), "--project", PROJECT, "--now", NOW],
)
assert result.exit_code == 0
assert result.stdout.strip() == ""