Minor from the final fix wave: _codes_for's "~20 min of SQL before any model call" warning describes elements' per-code LLM classification pass. lineage --all-payable also calls _codes_for but its own --all-payable path (lineage_all) is one inverted pass over fr_anchors — seconds, not minutes — so the warning there was misleading. New warn_slow keyword (default True, elements' two call sites unchanged; lineage --all-payable passes warn_slow=False).
933 lines
33 KiB
Python
933 lines
33 KiB
Python
"""stack pfs — elements / lineage / families / guidance / reaction / review."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import duckdb
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
import cli.pfs as pfs_cli
|
|
from cli import app
|
|
from pfs.codetables import (
|
|
ElementRow,
|
|
EventRow,
|
|
GuidanceRow,
|
|
ReactionRow,
|
|
ensure_tables,
|
|
read_elements,
|
|
read_events,
|
|
read_families,
|
|
read_guidance,
|
|
read_reaction,
|
|
write_cpt_edition,
|
|
write_elements,
|
|
write_events,
|
|
write_guidance,
|
|
)
|
|
from pfs.cpt_model import CptCode, CptEdition, CptSection
|
|
from pfs.extract import Extraction
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
@pytest.fixture
|
|
def restore_families():
|
|
"""Snapshot ``pfs.families.FAMILIES`` and restore it in teardown, even
|
|
if the test body raises — a test that reaches ``refresh_from`` must
|
|
not leave the live registry clobbered for the rest of the session."""
|
|
from pfs import families as mod
|
|
|
|
before = dict(mod.FAMILIES)
|
|
try:
|
|
yield mod
|
|
finally:
|
|
mod.FAMILIES.clear()
|
|
mod.FAMILIES.update(before)
|
|
|
|
|
|
@pytest.fixture
|
|
def con(monkeypatch, restore_families):
|
|
c = duckdb.connect(":memory:")
|
|
ensure_tables(c)
|
|
c.execute(
|
|
"CREATE TABLE pfs.rvu (hcpcs VARCHAR, mod VARCHAR, description VARCHAR, status_code VARCHAR, non_fac_total DOUBLE, year INTEGER)"
|
|
)
|
|
c.executemany(
|
|
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
|
|
[
|
|
("99490", None, "Chrnc care mgmt staff 1st 20", "A", 1.0, 2026),
|
|
("G2058", None, "Ccm add 20min", "A", 1.0, 2020),
|
|
("99999", None, "bundled thing", "B", 0.0, 2026),
|
|
],
|
|
)
|
|
|
|
class _Batch:
|
|
def __enter__(self):
|
|
return c
|
|
|
|
def __exit__(self, *a):
|
|
return False
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", lambda: _Batch())
|
|
# A DuckDB cursor, not `c` itself: `stack pfs reaction` (unlike the
|
|
# other commands here) calls `_read()` and then, on `--write`,
|
|
# `_batch()` in the same invocation — closing a `.close()`d `c`
|
|
# would break the batch. A cursor shares `c`'s catalog/data and
|
|
# closes independently of it, matching how `_read()`/`_batch()` are
|
|
# genuinely separate connections in production.
|
|
monkeypatch.setattr(pfs_cli, "_read", lambda: c.cursor())
|
|
monkeypatch.setattr(pfs_cli, "_store", lambda: object())
|
|
published = []
|
|
monkeypatch.setattr(pfs_cli, "_publish", lambda: published.append(True))
|
|
|
|
class _ConProxy:
|
|
"""``DuckDBPyConnection`` has no ``__dict__`` (C extension type), so
|
|
a plain ``con.published = []`` fails; wrap it to attach the
|
|
publish-tracking list the tests assert on while forwarding
|
|
everything else (execute, executemany, ...) to the real connection."""
|
|
|
|
def __init__(self, real, published):
|
|
object.__setattr__(self, "_real", real)
|
|
object.__setattr__(self, "published", published)
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(object.__getattribute__(self, "_real"), name)
|
|
|
|
try:
|
|
yield _ConProxy(c, published)
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
class TestElements:
|
|
def test_writes_rows_and_publishes(self, con, monkeypatch):
|
|
def fake_extract(store, con_, code, *, classify=None):
|
|
return Extraction(
|
|
code,
|
|
(
|
|
ElementRow(
|
|
code,
|
|
2026,
|
|
"activity",
|
|
"consent",
|
|
"",
|
|
"Consent;",
|
|
"K",
|
|
1,
|
|
2,
|
|
"fr",
|
|
),
|
|
),
|
|
(),
|
|
)
|
|
|
|
monkeypatch.setattr(pfs_cli, "extract_code", fake_extract)
|
|
res = runner.invoke(app, ["pfs", "elements", "--code", "g0556", "--no-llm"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "G0556: 1 elements, 0 for review" in res.output
|
|
assert [r.value for r in read_elements(con, "G0556")] == ["consent"]
|
|
assert con.published == [True]
|
|
|
|
def test_dry_run_does_not_write(self, con, monkeypatch):
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"extract_code",
|
|
lambda s, c, code, *, classify=None: Extraction(code, (), ()),
|
|
)
|
|
|
|
def fail_batch():
|
|
raise AssertionError("--dry-run must not open a RW duckdb_batch connection")
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
|
|
read_calls = []
|
|
orig_read = pfs_cli._read
|
|
|
|
def spy_read():
|
|
read_calls.append(True)
|
|
return orig_read()
|
|
|
|
monkeypatch.setattr(pfs_cli, "_read", spy_read)
|
|
|
|
res = runner.invoke(
|
|
app, ["pfs", "elements", "--code", "G0556", "--no-llm", "--dry-run"]
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert con.published == []
|
|
assert read_calls == [True]
|
|
|
|
def test_all_payable_selects_art_status_in_newest_year(self, con, monkeypatch):
|
|
seen = []
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"extract_code",
|
|
lambda s, c, code, *, classify=None: (
|
|
seen.append(code) or Extraction(code, (), ())
|
|
),
|
|
)
|
|
res = runner.invoke(
|
|
app, ["pfs", "elements", "--all-payable", "--no-llm", "--dry-run"]
|
|
)
|
|
assert res.exit_code == 0 and seen == ["99490"]
|
|
|
|
def test_repeated_family_option_expands_every_family(self, con, monkeypatch):
|
|
# #684 regression: --family used to be a single str, so a second
|
|
# --family silently discarded the first. It must now be repeatable
|
|
# and expand every family named.
|
|
seen = []
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"extract_code",
|
|
lambda s, c, code, *, classify=None: (
|
|
seen.append(code) or Extraction(code, (), ())
|
|
),
|
|
)
|
|
res = runner.invoke(
|
|
app,
|
|
[
|
|
"pfs",
|
|
"elements",
|
|
"--family",
|
|
"CCM",
|
|
"--family",
|
|
"APCM",
|
|
"--no-llm",
|
|
"--dry-run",
|
|
],
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
from pfs.families import HAND_FAMILIES
|
|
|
|
expected = set(HAND_FAMILIES["CCM"].codes) | set(HAND_FAMILIES["APCM"].codes)
|
|
assert set(seen) == expected
|
|
|
|
|
|
class TestCodesForWarnSlow:
|
|
"""``_codes_for``'s "~20 min of SQL" warning describes the
|
|
per-code LLM classification pass ``elements`` runs — it does not
|
|
apply to ``lineage --all-payable`` (one inverted pass, seconds)."""
|
|
|
|
def test_warns_by_default(self, con, caplog):
|
|
pfs_cli._codes_for(con, [], [], True)
|
|
assert "20 min" in caplog.text
|
|
|
|
def test_silent_when_warn_slow_is_false(self, con, caplog):
|
|
pfs_cli._codes_for(con, [], [], True, warn_slow=False)
|
|
assert "20 min" not in caplog.text
|
|
|
|
|
|
class TestLineage:
|
|
def test_prints_and_writes(self, con, monkeypatch):
|
|
ev = EventRow(
|
|
"G2058",
|
|
2021,
|
|
"replaced_by",
|
|
"",
|
|
"99439",
|
|
"YBM4IZUS",
|
|
1578,
|
|
84639,
|
|
"fr",
|
|
True,
|
|
"",
|
|
)
|
|
monkeypatch.setattr(pfs_cli, "lineage", lambda c, s, code: [ev])
|
|
res = runner.invoke(app, ["pfs", "lineage", "--code", "G2058", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "2021 replaced_by" in res.output and "YBM4IZUS ¶1578" in res.output
|
|
assert read_events(con, "G2058")[0].to_codes == "99439"
|
|
assert con.published == [True]
|
|
|
|
def test_prints_cpt_event_with_cpt_anchor_text(self, con, monkeypatch):
|
|
ev = EventRow(
|
|
"99490",
|
|
2015,
|
|
"cpt_changed",
|
|
"",
|
|
"",
|
|
"GQGTPGYV",
|
|
0,
|
|
0,
|
|
"cpt",
|
|
True,
|
|
"CPT Changes 2015",
|
|
)
|
|
monkeypatch.setattr(pfs_cli, "lineage", lambda c, s, code: [ev])
|
|
res = runner.invoke(app, ["pfs", "lineage", "--code", "99490", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "2015 cpt_changed" in res.output
|
|
assert "cpt 2015 GQGTPGYV" in res.output
|
|
|
|
def test_without_write_reads_and_never_takes_the_write_lock(self, con, monkeypatch):
|
|
# I4: `stack pfs lineage` without --write must read the replica,
|
|
# not open a RW duckdb_batch connection (the DuckDB single-writer
|
|
# lock a notebook may be holding, #508-#514).
|
|
def fail_batch():
|
|
raise AssertionError(
|
|
"lineage without --write must not open a RW duckdb_batch connection"
|
|
)
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
monkeypatch.setattr(pfs_cli, "lineage", lambda c, s, code: [])
|
|
|
|
read_calls = []
|
|
orig_read = pfs_cli._read
|
|
|
|
def spy_read():
|
|
read_calls.append(True)
|
|
return orig_read()
|
|
|
|
monkeypatch.setattr(pfs_cli, "_read", spy_read)
|
|
|
|
res = runner.invoke(app, ["pfs", "lineage", "--code", "G2058"])
|
|
assert res.exit_code == 0, res.output
|
|
assert read_calls == [True]
|
|
assert con.published == []
|
|
|
|
def test_requires_code_or_all_payable(self, con):
|
|
res = runner.invoke(app, ["pfs", "lineage"])
|
|
assert res.exit_code != 0
|
|
|
|
def test_all_payable_limit_write_calls_writer_once_per_code_after_build(
|
|
self, con, monkeypatch
|
|
):
|
|
# Extra A/R/T, newest-year codes so --limit has something to
|
|
# trim: sorted, the newest-year A/R/T targets are
|
|
# 99490 < A0001 < B0002 ('9' sorts before letters); 99999 is
|
|
# status B (excluded) and G2058 is a stale year (excluded).
|
|
con.execute(
|
|
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
|
|
["A0001", None, "extra a", "A", 1.0, 2026],
|
|
)
|
|
con.execute(
|
|
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
|
|
["B0002", None, "extra b", "R", 1.0, 2026],
|
|
)
|
|
order: list[tuple] = []
|
|
|
|
def fake_lineage_all(con_, store_, codes):
|
|
order.append(("lineage_all", tuple(codes)))
|
|
return {c: [] for c in codes}
|
|
|
|
monkeypatch.setattr(pfs_cli, "lineage_all", fake_lineage_all)
|
|
|
|
written: list[str] = []
|
|
|
|
def fake_write_events(con_, code, rows):
|
|
order.append(("write", code))
|
|
written.append(code)
|
|
return 0
|
|
|
|
monkeypatch.setattr(pfs_cli, "write_events", fake_write_events)
|
|
|
|
real_batch = pfs_cli._batch
|
|
|
|
def recording_batch():
|
|
order.append(("batch",))
|
|
return real_batch()
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", recording_batch)
|
|
|
|
res = runner.invoke(
|
|
app, ["pfs", "lineage", "--all-payable", "--limit", "2", "--write"]
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert order[0] == ("lineage_all", ("99490", "A0001"))
|
|
assert order[1] == ("batch",)
|
|
assert order[2:] == [("write", "99490"), ("write", "A0001")]
|
|
assert written == ["99490", "A0001"]
|
|
assert con.published == [True]
|
|
|
|
def test_all_payable_without_write_never_opens_the_batch(self, con, monkeypatch):
|
|
monkeypatch.setattr(
|
|
pfs_cli, "lineage_all", lambda c, s, codes: {c: [] for c in codes}
|
|
)
|
|
|
|
def fail_batch():
|
|
raise AssertionError(
|
|
"lineage --all-payable without --write must not open a RW "
|
|
"duckdb_batch connection"
|
|
)
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
res = runner.invoke(app, ["pfs", "lineage", "--all-payable"])
|
|
assert res.exit_code == 0, res.output
|
|
assert con.published == []
|
|
assert "lineage: 1 codes targeted" in res.output
|
|
|
|
|
|
class TestFamilies:
|
|
def test_derives_from_tables_and_writes(self, con):
|
|
write_elements(
|
|
con,
|
|
"99439",
|
|
[
|
|
ElementRow(
|
|
"99439", 2021, "relation", "addon-of", "99490", "", "K", 1, 1, "fr"
|
|
)
|
|
],
|
|
[],
|
|
)
|
|
write_events(
|
|
con,
|
|
"G2058",
|
|
[
|
|
EventRow(
|
|
"G2058", 2021, "replaced_by", "", "99439", "K", 2, 1, "fr", True, ""
|
|
)
|
|
],
|
|
)
|
|
res = runner.invoke(app, ["pfs", "families", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
fams = read_families(con)
|
|
assert {r.code for r in fams if r.key == "CCM"} >= {"99490", "99439", "G2058"}
|
|
# Minor: --write prints the summary line by default; the
|
|
# per-family dump ("CCM" among them) is --verbose only.
|
|
assert "CCM" not in res.output
|
|
# No pfs.cpt_* rows seeded in this test — the summary line must
|
|
# still print, with cpt-named at 0 (I4: read-only-tolerant path
|
|
# also applies to the write path when the CPT tables are empty).
|
|
assert "families: total 2, multi-code 1, cpt-named 0, hand 1, other 1" in (
|
|
res.output
|
|
)
|
|
|
|
def test_verbose_prints_the_per_family_dump(self, con):
|
|
write_elements(
|
|
con,
|
|
"99439",
|
|
[
|
|
ElementRow(
|
|
"99439", 2021, "relation", "addon-of", "99490", "", "K", 1, 1, "fr"
|
|
)
|
|
],
|
|
[],
|
|
)
|
|
res = runner.invoke(app, ["pfs", "families", "--write", "--verbose"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "CCM" in res.output
|
|
assert "families: total" in res.output
|
|
|
|
def _ccm_cpt_edition(self, year=2024):
|
|
section = CptSection(
|
|
sec_id="sec_ccm",
|
|
level=3,
|
|
title="Chronic Care Management Services",
|
|
path=(
|
|
"Evaluation and Management",
|
|
"Care Management Services",
|
|
"Chronic Care Management Services",
|
|
),
|
|
code_lo="99490",
|
|
code_hi="99439",
|
|
guideline="",
|
|
)
|
|
codes = (
|
|
CptCode(
|
|
code="99490",
|
|
sec_id="sec_ccm",
|
|
descriptor="Chronic care management services, first 20 minutes.",
|
|
stem="Chronic care management services",
|
|
elements=(),
|
|
tail="first 20 minutes.",
|
|
addon=False,
|
|
resequenced=False,
|
|
new=False,
|
|
revised=False,
|
|
telemedicine=False,
|
|
parent="",
|
|
mod51_exempt=False,
|
|
audio_only=False,
|
|
fda_pending=False,
|
|
pla=False,
|
|
category="I",
|
|
),
|
|
CptCode(
|
|
code="99439",
|
|
sec_id="sec_ccm",
|
|
descriptor="each additional 20 minutes.",
|
|
stem="Chronic care management services",
|
|
elements=(),
|
|
tail="each additional 20 minutes.",
|
|
addon=True,
|
|
resequenced=False,
|
|
new=False,
|
|
revised=False,
|
|
telemedicine=False,
|
|
parent="99490",
|
|
mod51_exempt=False,
|
|
audio_only=False,
|
|
fda_pending=False,
|
|
pla=False,
|
|
category="I",
|
|
),
|
|
)
|
|
return CptEdition(
|
|
year=year,
|
|
sections=(section,),
|
|
codes=codes,
|
|
instructions=(),
|
|
references=(),
|
|
crosswalks=(),
|
|
lists=(),
|
|
)
|
|
|
|
def test_feeds_cpt_inputs_when_present_and_prints_summary_line(self, con):
|
|
write_cpt_edition(con, self._ccm_cpt_edition(), "ITEM0001")
|
|
res = runner.invoke(app, ["pfs", "families", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
fams = read_families(con)
|
|
ccm = {r.code: r for r in fams if r.key == "CCM"}
|
|
assert {"99490", "99439"} <= set(ccm)
|
|
assert ccm["99490"].note == (
|
|
"Evaluation and Management > Care Management Services > "
|
|
"Chronic Care Management Services"
|
|
)
|
|
assert "families: total" in res.output
|
|
assert "cpt-named" in res.output
|
|
|
|
def test_since_spans_all_ingested_editions_not_just_the_newest(self, con):
|
|
# Ruling C12: _cpt_inputs' cpt_presence comes from a full
|
|
# SELECT code, edition_year FROM pfs.cpt_code (every edition
|
|
# write_cpt_edition has ever written), so `since` is the
|
|
# earliest edition, not clamped to whichever one is newest.
|
|
write_cpt_edition(con, self._ccm_cpt_edition(year=2019), "ITEM_2019")
|
|
write_cpt_edition(con, self._ccm_cpt_edition(year=2024), "ITEM_2024")
|
|
res = runner.invoke(app, ["pfs", "families", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
fams = read_families(con)
|
|
ccm = {r.code: r for r in fams if r.key == "CCM"}
|
|
assert ccm["99490"].since == 2019
|
|
assert ccm["99490"].until is None
|
|
|
|
def test_write_tolerates_null_rvu_description(self, con):
|
|
# #687 regression: a real pfs.rvu row can carry a NULL description
|
|
# (a corpus parsing artifact) at the current max year with a
|
|
# payable status — that row lands in the derivation scope and used
|
|
# to crash `families --write` with an AttributeError.
|
|
con.execute(
|
|
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
|
|
("Z9999", None, None, "A", 1.0, 2026),
|
|
)
|
|
res = runner.invoke(app, ["pfs", "families", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
|
|
def test_without_write_reads_and_never_takes_the_write_lock(self, con, monkeypatch):
|
|
# I4: `stack pfs families` without --write must read the replica,
|
|
# not open a RW duckdb_batch connection.
|
|
def fail_batch():
|
|
raise AssertionError(
|
|
"families without --write must not open a RW duckdb_batch connection"
|
|
)
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
|
|
read_calls = []
|
|
orig_read = pfs_cli._read
|
|
|
|
def spy_read():
|
|
read_calls.append(True)
|
|
return orig_read()
|
|
|
|
monkeypatch.setattr(pfs_cli, "_read", spy_read)
|
|
|
|
res = runner.invoke(app, ["pfs", "families"])
|
|
assert res.exit_code == 0, res.output
|
|
assert read_calls == [True]
|
|
assert con.published == []
|
|
|
|
def test_no_derived_tables_yet_is_a_friendly_no_op(self, monkeypatch):
|
|
# I4: a fresh replica with no pfs.code_element/code_event/code_family
|
|
# (nothing has run --write yet) is not a bug — print a friendly
|
|
# message and exit 0, without ever taking the write lock.
|
|
bare = duckdb.connect(":memory:")
|
|
monkeypatch.setattr(pfs_cli, "_read", lambda: bare)
|
|
|
|
def fail_batch():
|
|
raise AssertionError("must not open a RW duckdb_batch connection")
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
try:
|
|
res = runner.invoke(app, ["pfs", "families"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "no derived tables yet" in res.output
|
|
finally:
|
|
bare.close()
|
|
|
|
def test_unknown_family_error_lists_hand_families_only(self, con, monkeypatch):
|
|
# M3: FAMILIES can hold ~15k derived keys once `refresh_from` has
|
|
# run — the error text must stay short and human-scannable.
|
|
pfs_cli.FAMILIES["ZZZZ-99999"] = pfs_cli.FAMILIES["CCM"]
|
|
res = runner.invoke(
|
|
app, ["pfs", "elements", "--family", "NOPE", "--no-llm", "--dry-run"]
|
|
)
|
|
assert res.exit_code != 0
|
|
assert "ZZZZ-99999" not in res.output
|
|
for key in pfs_cli.HAND_FAMILIES:
|
|
assert key in res.output
|
|
|
|
|
|
class TestCptIngest:
|
|
def test_dry_run_never_opens_batch(self, con, monkeypatch):
|
|
seen = {}
|
|
|
|
def fake_ingest(store, con_, *, years=None, dry_run=False):
|
|
seen["store"] = store
|
|
seen["con"] = con_
|
|
seen["years"] = years
|
|
seen["dry_run"] = dry_run
|
|
return {2024: {"sections": 3, "codes": 5}}
|
|
|
|
monkeypatch.setattr(pfs_cli, "cpt_ingest", fake_ingest)
|
|
|
|
def fail_batch():
|
|
raise AssertionError("--dry-run must not open a RW duckdb_batch connection")
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
|
|
res = runner.invoke(
|
|
app, ["pfs", "cpt-ingest", "--edition", "2024", "--dry-run"]
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert seen["dry_run"] is True
|
|
assert seen["con"] is None
|
|
assert seen["years"] == [2024]
|
|
assert "2024: sections=3, codes=5" in res.output
|
|
assert con.published == []
|
|
|
|
def test_all_flag_passes_years_none(self, con, monkeypatch):
|
|
seen = {}
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"cpt_ingest",
|
|
lambda store, con_, *, years=None, dry_run=False: (
|
|
seen.update(years=years) or {}
|
|
),
|
|
)
|
|
res = runner.invoke(app, ["pfs", "cpt-ingest", "--all", "--dry-run"])
|
|
assert res.exit_code == 0, res.output
|
|
assert seen["years"] is None
|
|
|
|
def test_requires_edition_or_all(self, con):
|
|
res = runner.invoke(app, ["pfs", "cpt-ingest"])
|
|
assert res.exit_code != 0
|
|
|
|
def test_write_path_opens_batch_ensures_tables_and_publishes(
|
|
self, con, monkeypatch
|
|
):
|
|
seen = {}
|
|
|
|
def fake_ingest(store, con_, *, years=None, dry_run=False):
|
|
seen["con"] = con_
|
|
seen["dry_run"] = dry_run
|
|
return {2024: {"sections": 1, "codes": 2}}
|
|
|
|
monkeypatch.setattr(pfs_cli, "cpt_ingest", fake_ingest)
|
|
res = runner.invoke(app, ["pfs", "cpt-ingest", "--edition", "2024"])
|
|
assert res.exit_code == 0, res.output
|
|
assert seen["dry_run"] is False
|
|
assert seen["con"] is not None
|
|
assert "2024: sections=1, codes=2" in res.output
|
|
assert con.published == [True]
|
|
|
|
|
|
class TestGuidance:
|
|
def _row(self, family="CCM", code="99490"):
|
|
return GuidanceRow(
|
|
family, code, "cfr", "42 CFR 410.78(a)(3)", "SEC", "K", 398, 32389
|
|
)
|
|
|
|
def test_write_builds_writes_and_publishes(self, con, monkeypatch):
|
|
seen = {}
|
|
|
|
def fake_build(con_, store_, codes, *, family):
|
|
seen["con"] = con_
|
|
seen["store"] = store_
|
|
seen["codes"] = codes
|
|
seen["family"] = family
|
|
return [self._row(family=family)]
|
|
|
|
monkeypatch.setattr(pfs_cli, "build_guidance", fake_build)
|
|
res = runner.invoke(app, ["pfs", "guidance", "--family", "ccm", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
assert seen["family"] == "CCM"
|
|
assert seen["con"] is not None
|
|
assert seen["store"] is not None
|
|
assert set(seen["codes"]) == set(pfs_cli.HAND_FAMILIES["CCM"].codes)
|
|
got = read_guidance(con, "CCM")
|
|
assert [r.code for r in got] == ["99490"]
|
|
assert con.published == [True]
|
|
assert "CCM cfr 42 CFR 410.78(a)(3) SEC ← K ¶398" in res.output
|
|
|
|
def test_write_scopes_per_family(self, con, monkeypatch):
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"build_guidance",
|
|
lambda con_, store_, codes, *, family: [self._row(family=family)],
|
|
)
|
|
res = runner.invoke(
|
|
app, ["pfs", "guidance", "--family", "CCM", "--family", "APCM", "--write"]
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert [r.family for r in read_guidance(con, "CCM")] == ["CCM"]
|
|
assert [r.family for r in read_guidance(con, "APCM")] == ["APCM"]
|
|
|
|
def test_without_write_reads_and_never_takes_the_write_lock(self, con, monkeypatch):
|
|
write_guidance(con, "CCM", [self._row()])
|
|
|
|
def fail_batch():
|
|
raise AssertionError("must not open a RW duckdb_batch connection")
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
res = runner.invoke(app, ["pfs", "guidance", "--family", "CCM"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "CCM cfr 42 CFR 410.78(a)(3) SEC ← K ¶398" in res.output
|
|
assert con.published == []
|
|
|
|
def test_no_derived_guidance_yet_is_a_friendly_no_op(self, monkeypatch):
|
|
# I4: a fresh replica with no pfs.code_guidance (guidance --write
|
|
# hasn't run yet) is not a bug — print a friendly message per
|
|
# family and exit 0, without ever taking the write lock.
|
|
bare = duckdb.connect(":memory:")
|
|
monkeypatch.setattr(pfs_cli, "_read", lambda: bare)
|
|
|
|
def fail_batch():
|
|
raise AssertionError("must not open a RW duckdb_batch connection")
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
try:
|
|
res = runner.invoke(app, ["pfs", "guidance", "--family", "CCM"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "no derived guidance yet" in res.output
|
|
finally:
|
|
bare.close()
|
|
|
|
def test_unknown_family_error_lists_hand_families_only(self, con, monkeypatch):
|
|
pfs_cli.FAMILIES["ZZZZ-99999"] = pfs_cli.FAMILIES["CCM"]
|
|
res = runner.invoke(app, ["pfs", "guidance", "--family", "NOPE", "--write"])
|
|
assert res.exit_code != 0
|
|
assert "ZZZZ-99999" not in res.output
|
|
for key in pfs_cli.HAND_FAMILIES:
|
|
assert key in res.output
|
|
|
|
def test_unknown_key_among_several_errors_before_any_batch(self, con, monkeypatch):
|
|
# Every --family key must be resolved and validated up front — an
|
|
# unknown key later in the list must fail before build_guidance
|
|
# runs for any family and before the write lock is ever opened.
|
|
built: list[str] = []
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"build_guidance",
|
|
lambda con_, store_, codes, *, family: built.append(family) or [],
|
|
)
|
|
|
|
def fail_batch():
|
|
raise AssertionError("must not open a RW duckdb_batch connection")
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
res = runner.invoke(
|
|
app, ["pfs", "guidance", "--family", "CCM", "--family", "NOPE", "--write"]
|
|
)
|
|
assert res.exit_code != 0
|
|
assert built == []
|
|
|
|
def test_write_builds_every_family_before_opening_the_batch(self, con, monkeypatch):
|
|
order: list[str] = []
|
|
|
|
def fake_build(con_, store_, codes, *, family):
|
|
order.append("build")
|
|
return [self._row(family=family)]
|
|
|
|
real_batch = pfs_cli._batch
|
|
|
|
def recording_batch():
|
|
order.append("batch")
|
|
return real_batch()
|
|
|
|
monkeypatch.setattr(pfs_cli, "build_guidance", fake_build)
|
|
monkeypatch.setattr(pfs_cli, "_batch", recording_batch)
|
|
res = runner.invoke(
|
|
app, ["pfs", "guidance", "--family", "CCM", "--family", "APCM", "--write"]
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert order == ["build", "build", "batch"]
|
|
|
|
def test_requires_family(self, con):
|
|
res = runner.invoke(app, ["pfs", "guidance"])
|
|
assert res.exit_code != 0
|
|
|
|
|
|
class TestReaction:
|
|
def _row(
|
|
self, family="CCM", period="CMS-2023-0121", period_kind="docket", year=2023
|
|
):
|
|
return ReactionRow(family, period, period_kind, year, 5, 20, 1, 0, 0, 1, "[]")
|
|
|
|
def test_write_builds_writes_and_publishes(self, con, monkeypatch):
|
|
seen = {}
|
|
|
|
def fake_series(
|
|
engine,
|
|
store_,
|
|
family,
|
|
codes,
|
|
*,
|
|
classify=None,
|
|
stance_sample_n=0,
|
|
name=None,
|
|
):
|
|
seen["engine"] = engine
|
|
seen["store"] = store_
|
|
seen["family"] = family
|
|
seen["codes"] = codes
|
|
seen["classify"] = classify
|
|
seen["n"] = stance_sample_n
|
|
seen["name"] = name
|
|
return [self._row(family=family)]
|
|
|
|
monkeypatch.setattr(pfs_cli, "reaction_series", fake_series)
|
|
monkeypatch.setattr(pfs_cli, "_engine", lambda: "ENGINE")
|
|
res = runner.invoke(app, ["pfs", "reaction", "--family", "ccm", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
assert seen["family"] == "CCM"
|
|
assert seen["engine"] == "ENGINE"
|
|
assert seen["classify"] is None # no --stance-sample: no LLM at all
|
|
assert seen["n"] == 0
|
|
assert seen["name"] == pfs_cli.HAND_FAMILIES["CCM"].name
|
|
assert set(seen["codes"]) == set(pfs_cli.HAND_FAMILIES["CCM"].codes)
|
|
got = read_reaction(con, "CCM")
|
|
assert [r.period for r in got] == ["CMS-2023-0121"]
|
|
assert con.published == [True]
|
|
assert "CCM CMS-2023-0121 (2023) 5/20 commenters" in res.output
|
|
|
|
def test_code_makes_a_single_code_pseudo_family_with_no_display_name(
|
|
self, con, monkeypatch
|
|
):
|
|
seen = {}
|
|
|
|
def fake_series(
|
|
engine,
|
|
store_,
|
|
family,
|
|
codes,
|
|
*,
|
|
classify=None,
|
|
stance_sample_n=0,
|
|
name=None,
|
|
):
|
|
seen["family"] = family
|
|
seen["codes"] = codes
|
|
seen["name"] = name
|
|
return [self._row(family=family)]
|
|
|
|
monkeypatch.setattr(pfs_cli, "reaction_series", fake_series)
|
|
monkeypatch.setattr(pfs_cli, "_engine", lambda: "ENGINE")
|
|
res = runner.invoke(app, ["pfs", "reaction", "--code", "g2211", "--write"])
|
|
assert res.exit_code == 0, res.output
|
|
assert seen["family"] == "G2211"
|
|
assert seen["codes"] == ("G2211",)
|
|
assert seen["name"] is None
|
|
assert [r.family for r in read_reaction(con, "G2211")] == ["G2211"]
|
|
|
|
def test_stance_sample_builds_the_classifier(self, con, monkeypatch):
|
|
seen = {}
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"reaction_series",
|
|
lambda engine, store_, family, codes, *, classify=None, stance_sample_n=0, name=None: ( # noqa: E501
|
|
seen.update(classify=classify, n=stance_sample_n) or []
|
|
),
|
|
)
|
|
monkeypatch.setattr(pfs_cli, "_engine", lambda: "ENGINE")
|
|
monkeypatch.setattr(pfs_cli, "_classifier", lambda: "CLASSIFIER")
|
|
res = runner.invoke(
|
|
app, ["pfs", "reaction", "--family", "CCM", "--stance-sample", "5"]
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert seen["classify"] == "CLASSIFIER"
|
|
assert seen["n"] == 5
|
|
|
|
def test_write_computes_classifier_and_series_before_opening_the_batch(
|
|
self, con, monkeypatch
|
|
):
|
|
# Ruling A13: the classifier and every family's series must be
|
|
# fully computed BEFORE the DuckDB write lock is taken — the
|
|
# batch block only writes and publishes.
|
|
order: list[str] = []
|
|
|
|
def fake_classifier():
|
|
order.append("classifier")
|
|
return "CLASSIFIER"
|
|
|
|
def fake_series(
|
|
engine,
|
|
store_,
|
|
family,
|
|
codes,
|
|
*,
|
|
classify=None,
|
|
stance_sample_n=0,
|
|
name=None,
|
|
):
|
|
order.append("series")
|
|
assert classify == "CLASSIFIER" # already built by the time series runs
|
|
return [self._row(family=family)]
|
|
|
|
real_batch = pfs_cli._batch
|
|
|
|
def recording_batch():
|
|
order.append("batch")
|
|
return real_batch()
|
|
|
|
monkeypatch.setattr(pfs_cli, "_classifier", fake_classifier)
|
|
monkeypatch.setattr(pfs_cli, "reaction_series", fake_series)
|
|
monkeypatch.setattr(pfs_cli, "_engine", lambda: "ENGINE")
|
|
monkeypatch.setattr(pfs_cli, "_batch", recording_batch)
|
|
res = runner.invoke(
|
|
app,
|
|
["pfs", "reaction", "--family", "CCM", "--stance-sample", "5", "--write"],
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert order == ["classifier", "series", "batch"]
|
|
|
|
def test_without_write_never_opens_a_batch_connection(self, con, monkeypatch):
|
|
monkeypatch.setattr(
|
|
pfs_cli,
|
|
"reaction_series",
|
|
lambda engine, store_, family, codes, *, classify=None, stance_sample_n=0, name=None: [ # noqa: E501
|
|
self._row(family=family)
|
|
],
|
|
)
|
|
monkeypatch.setattr(pfs_cli, "_engine", lambda: "ENGINE")
|
|
|
|
def fail_batch():
|
|
raise AssertionError("must not open a RW duckdb_batch connection")
|
|
|
|
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
|
|
res = runner.invoke(app, ["pfs", "reaction", "--family", "CCM"])
|
|
assert res.exit_code == 0, res.output
|
|
assert "CCM CMS-2023-0121 (2023) 5/20 commenters" in res.output
|
|
assert con.published == [] # nothing persisted, batch never opened
|
|
|
|
def test_unknown_family_error_lists_hand_families_only(self, con, monkeypatch):
|
|
pfs_cli.FAMILIES["ZZZZ-99999"] = pfs_cli.FAMILIES["CCM"]
|
|
res = runner.invoke(app, ["pfs", "reaction", "--family", "NOPE", "--write"])
|
|
assert res.exit_code != 0
|
|
assert "ZZZZ-99999" not in res.output
|
|
for key in pfs_cli.HAND_FAMILIES:
|
|
assert key in res.output
|
|
|
|
def test_requires_family_or_code(self, con):
|
|
res = runner.invoke(app, ["pfs", "reaction"])
|
|
assert res.exit_code != 0
|
|
|
|
|
|
class TestReview:
|
|
def test_lists_queue(self, con):
|
|
from pfs.codetables import ReviewRow
|
|
|
|
write_elements(
|
|
con, "G0556", [], [ReviewRow("G0556", "Odd line;", "", "", "K", 9)]
|
|
)
|
|
res = runner.invoke(app, ["pfs", "review"])
|
|
assert res.exit_code == 0 and "G0556" in res.output and "Odd line" in res.output
|