--dry-run's help text now describes what it computes (the merged item→codes mapping) instead of a stale "report per-collection item/tag counts". The CLI's total line now prints capped_wide= for the per-item --max-codes-per-item cap count (the stats dict key stays skipped_wide); docs regenerated via docs/scripts/extract_cli.py.
457 lines
15 KiB
Python
457 lines
15 KiB
Python
"""Deep exercising tests for cli/bib.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
from cli.bib import app
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
class TestSync:
|
|
@patch("bib.meta.apply_column_comments", return_value=["s1"])
|
|
@patch("bib.meta.collect_column_comments", return_value={"r": "d"})
|
|
@patch("bib.connect", return_value=MagicMock())
|
|
@patch("conf.path", return_value="/tmp/test.duckdb")
|
|
@patch("duckdb.connect")
|
|
def test_sync(self, mc_duck, mc_path, mc_connect, mc_cc, mc_ac):
|
|
result = runner.invoke(app, ["sync"])
|
|
assert result.exit_code == 0
|
|
|
|
@patch("bib.meta.collect_column_comments", return_value={"r": "d"})
|
|
@patch("bib.connect", return_value=MagicMock())
|
|
def test_sync_dry(self, mc_connect, mc_cc):
|
|
result = runner.invoke(app, ["sync", "--dry-run"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
class TestTag:
|
|
@patch("bib.connect")
|
|
def test_tag(self, mc):
|
|
store = MagicMock()
|
|
store.list_tags.return_value = [{"name": "t1", "count": 5}]
|
|
mc.return_value = store
|
|
result = runner.invoke(app, ["tag"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
class TestQuery:
|
|
@patch("bib.connect")
|
|
def test_query(self, mc):
|
|
store = MagicMock()
|
|
store.list_items.return_value = []
|
|
mc.return_value = store
|
|
result = runner.invoke(app, ["query", "test"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
class TestDiscoverPfsRules:
|
|
@patch("bib.federalregister.pfs_rules", return_value=[])
|
|
def test_dry_run(self, mc):
|
|
result = runner.invoke(app, ["discover-pfs-rules", "--dry-run"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
class TestImportZotero:
|
|
@patch("bib.zotero_import.import_books")
|
|
@patch("bib.zotero_import.list_collection")
|
|
@patch("conf.connect.zotero", return_value=MagicMock())
|
|
@patch("bib.connect", return_value=MagicMock())
|
|
def test_dry_run_prints_would_create_and_writes_nothing(
|
|
self, mc_connect, mc_zotero, mc_list, mc_import
|
|
):
|
|
from bib.zotero_import import ZoteroBook
|
|
|
|
book = ZoteroBook(
|
|
key="ZKEY1",
|
|
title="CPT 2018",
|
|
url="https://x",
|
|
year="2017",
|
|
publisher="AMA",
|
|
attachments=(),
|
|
)
|
|
mc_list.return_value = [book]
|
|
mc_import.return_value = {
|
|
"created": 1,
|
|
"updated": 0,
|
|
"unchanged": 0,
|
|
"attached": 0,
|
|
"missing": 0,
|
|
"items": [
|
|
{
|
|
"key": "",
|
|
"zotero_key": "ZKEY1",
|
|
"title": "CPT 2018",
|
|
"status": "created",
|
|
"attached": 0,
|
|
"missing": 0,
|
|
"missing_paths": [],
|
|
}
|
|
],
|
|
}
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"import-zotero",
|
|
"--collection",
|
|
"AMA Coding Publications",
|
|
"--dry-run",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "would-created" in result.stdout
|
|
assert "CPT 2018" in result.stdout
|
|
# dry_run=True is forwarded to import_books, which is the sole
|
|
# place writes happen — the CLI itself performs none.
|
|
_, kwargs = mc_import.call_args
|
|
assert kwargs["dry_run"] is True
|
|
assert kwargs["tags"] == []
|
|
|
|
@patch("bib.zotero_import.import_books")
|
|
@patch("bib.zotero_import.list_collection")
|
|
@patch("conf.connect.zotero", return_value=MagicMock())
|
|
@patch("bib.connect", return_value=MagicMock())
|
|
def test_only_filters_by_title_substring(
|
|
self, mc_connect, mc_zotero, mc_list, mc_import
|
|
):
|
|
from bib.zotero_import import ZoteroBook
|
|
|
|
cpt = ZoteroBook(
|
|
key="A", title="CPT 2018", url="", year="2017", publisher="", attachments=()
|
|
)
|
|
icd = ZoteroBook(
|
|
key="B",
|
|
title="ICD-10-CM 2018",
|
|
url="",
|
|
year="2017",
|
|
publisher="",
|
|
attachments=(),
|
|
)
|
|
mc_list.return_value = [cpt, icd]
|
|
mc_import.return_value = {
|
|
"created": 0,
|
|
"updated": 0,
|
|
"unchanged": 0,
|
|
"attached": 0,
|
|
"missing": 0,
|
|
"items": [],
|
|
}
|
|
|
|
runner.invoke(
|
|
app,
|
|
[
|
|
"import-zotero",
|
|
"--collection",
|
|
"X",
|
|
"--only",
|
|
"cpt",
|
|
"--dry-run",
|
|
],
|
|
)
|
|
|
|
passed_books = mc_import.call_args[0][1]
|
|
assert [b.key for b in passed_books] == ["A"]
|
|
|
|
@patch("bib.zotero_import.import_books")
|
|
@patch("bib.zotero_import.list_collection", return_value=[])
|
|
@patch("conf.connect.zotero", return_value=MagicMock())
|
|
@patch("bib.connect", return_value=MagicMock())
|
|
def test_tags_passed_through(self, mc_connect, mc_zotero, mc_list, mc_import):
|
|
mc_import.return_value = {
|
|
"created": 0,
|
|
"updated": 0,
|
|
"unchanged": 0,
|
|
"attached": 0,
|
|
"missing": 0,
|
|
"items": [],
|
|
}
|
|
|
|
runner.invoke(
|
|
app,
|
|
[
|
|
"import-zotero",
|
|
"--collection",
|
|
"X",
|
|
"--tag",
|
|
"source:ama",
|
|
"--tag",
|
|
"module:coding",
|
|
],
|
|
)
|
|
|
|
_, kwargs = mc_import.call_args
|
|
assert kwargs["tags"] == ["source:ama", "module:coding"]
|
|
|
|
@patch("bib.zotero_import.import_books")
|
|
@patch("bib.zotero_import.list_collection")
|
|
@patch("conf.connect.zotero", return_value=MagicMock())
|
|
@patch("bib.connect", return_value=MagicMock())
|
|
def test_missing_file_printed_to_stderr(
|
|
self, mc_connect, mc_zotero, mc_list, mc_import
|
|
):
|
|
from bib.zotero_import import ZoteroBook
|
|
|
|
book = ZoteroBook(
|
|
key="G1", title="Ghost", url="", year="2020", publisher="", attachments=()
|
|
)
|
|
mc_list.return_value = [book]
|
|
mc_import.return_value = {
|
|
"created": 1,
|
|
"updated": 0,
|
|
"unchanged": 0,
|
|
"attached": 0,
|
|
"missing": 1,
|
|
"items": [
|
|
{
|
|
"key": "GKEY",
|
|
"zotero_key": "G1",
|
|
"title": "Ghost",
|
|
"status": "created",
|
|
"attached": 0,
|
|
"missing": 1,
|
|
"missing_paths": ["/data/storage/XX/gone.pdf"],
|
|
}
|
|
],
|
|
}
|
|
|
|
result = runner.invoke(app, ["import-zotero", "--collection", "X"])
|
|
|
|
assert "! missing on disk: /data/storage/XX/gone.pdf" in result.output
|
|
assert "missing=1" in result.output
|
|
|
|
|
|
class TestAllHelp:
|
|
"""Exercise every registered command's --help (exercises Typer paths)."""
|
|
|
|
def test_fetch_docket(self):
|
|
assert runner.invoke(app, ["fetch-docket-comments", "--help"]).exit_code == 0
|
|
|
|
def test_fetch_pfs(self):
|
|
assert runner.invoke(app, ["fetch-pfs-comments", "--help"]).exit_code == 0
|
|
|
|
def test_ingest_iom(self):
|
|
assert runner.invoke(app, ["ingest-iom", "--help"]).exit_code == 0
|
|
|
|
def test_attach_iom(self):
|
|
assert runner.invoke(app, ["attach-iom", "--help"]).exit_code == 0
|
|
|
|
def test_ingest_oig(self):
|
|
assert runner.invoke(app, ["ingest-oig", "--help"]).exit_code == 0
|
|
|
|
def test_attach_oig(self):
|
|
assert runner.invoke(app, ["attach-oig", "--help"]).exit_code == 0
|
|
|
|
def test_sync_zotero(self):
|
|
assert runner.invoke(app, ["sync-zotero", "--help"]).exit_code == 0
|
|
|
|
def test_import_zotero(self):
|
|
assert runner.invoke(app, ["import-zotero", "--help"]).exit_code == 0
|
|
|
|
def test_backfill(self):
|
|
assert runner.invoke(app, ["backfill-comments", "--help"]).exit_code == 0
|
|
|
|
def test_ingest_mail(self):
|
|
assert runner.invoke(app, ["ingest-mail", "--help"]).exit_code == 0
|
|
|
|
def test_refresh_iom(self):
|
|
assert runner.invoke(app, ["refresh-iom", "--help"]).exit_code == 0
|
|
|
|
def test_refresh_oig(self):
|
|
assert runner.invoke(app, ["refresh-oig", "--help"]).exit_code == 0
|
|
|
|
|
|
class TestCodeTags:
|
|
@patch("bib.codetags.apply_code_tags")
|
|
@patch("bib.codetags.item_codes_from_chunks")
|
|
@patch("bib.codetags.item_codes_from_anchors")
|
|
@patch("pfs.families.refresh_from")
|
|
@patch("pfs.anchors.code_family_index", return_value={})
|
|
@patch("os.path.exists", return_value=False)
|
|
@patch("llm.index._engine")
|
|
@patch("llm.config.load")
|
|
@patch("bib.connect")
|
|
def test_all_collections_merge_into_one_apply_call(
|
|
self,
|
|
mc_connect,
|
|
mc_cfg,
|
|
mc_engine,
|
|
mc_exists,
|
|
mc_code_index,
|
|
mc_refresh,
|
|
mc_anchors,
|
|
mc_chunks,
|
|
mc_apply,
|
|
):
|
|
mc_anchors.return_value = {"R1": ({"99490"}, {"CCM"})}
|
|
mc_chunks.return_value = {"C1": ({"99497"}, set())}
|
|
mc_apply.return_value = {"items": 2, "tags_added": 2, "skipped_wide": 0}
|
|
|
|
result = runner.invoke(app, ["code-tags", "--collection", "all"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
mc_anchors.assert_called_once()
|
|
# rules(anchors) + rules(chunks) + comments(chunks) + corpus(chunks)
|
|
# all feed ONE merged mapping — exactly one apply_code_tags call
|
|
# (Ruling A7: the cap must see an item's combined code set).
|
|
mc_apply.assert_called_once()
|
|
mapping = mc_apply.call_args.args[1]
|
|
assert mapping["R1"] == ({"99490"}, {"CCM"})
|
|
assert mapping["C1"] == ({"99497"}, set())
|
|
assert mc_apply.call_args.kwargs["dry_run"] is False
|
|
|
|
@patch("bib.codetags.apply_code_tags")
|
|
@patch("bib.codetags.item_codes_from_chunks")
|
|
@patch("bib.codetags.item_codes_from_anchors")
|
|
@patch("pfs.families.refresh_from")
|
|
@patch("pfs.anchors.code_family_index", return_value={})
|
|
@patch("os.path.exists", return_value=False)
|
|
@patch("llm.index._engine")
|
|
@patch("llm.config.load")
|
|
@patch("bib.connect")
|
|
def test_straddling_item_is_merged_before_apply(
|
|
self,
|
|
mc_connect,
|
|
mc_cfg,
|
|
mc_engine,
|
|
mc_exists,
|
|
mc_code_index,
|
|
mc_refresh,
|
|
mc_anchors,
|
|
mc_chunks,
|
|
mc_apply,
|
|
):
|
|
# Same item_key surfaces from rules-anchors and again from
|
|
# corpus-chunks (a rule item whose PDF also landed in corpus) —
|
|
# the codes must union into one mapping entry, not two capped
|
|
# separately.
|
|
mc_anchors.return_value = {"X1": ({"99490"}, {"CCM"})}
|
|
mc_chunks.return_value = {"X1": ({"99497"}, {"TCM"})}
|
|
mc_apply.return_value = {"items": 1, "tags_added": 3, "skipped_wide": 0}
|
|
|
|
result = runner.invoke(app, ["code-tags", "--collection", "all"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
mc_apply.assert_called_once()
|
|
mapping = mc_apply.call_args.args[1]
|
|
assert mapping["X1"] == ({"99490", "99497"}, {"CCM", "TCM"})
|
|
|
|
@patch("bib.codetags.apply_code_tags")
|
|
@patch("bib.codetags.item_codes_from_chunks")
|
|
@patch("bib.codetags.item_codes_from_anchors", return_value={})
|
|
@patch("pfs.families.refresh_from")
|
|
@patch("pfs.anchors.code_family_index", return_value={})
|
|
@patch("os.path.exists", return_value=False)
|
|
@patch("llm.index._engine")
|
|
@patch("llm.config.load")
|
|
@patch("bib.connect")
|
|
def test_dry_run_passes_through_and_writes_nothing(
|
|
self,
|
|
mc_connect,
|
|
mc_cfg,
|
|
mc_engine,
|
|
mc_exists,
|
|
mc_code_index,
|
|
mc_refresh,
|
|
mc_anchors,
|
|
mc_chunks,
|
|
mc_apply,
|
|
):
|
|
mc_chunks.return_value = {}
|
|
mc_apply.return_value = {"items": 0, "tags_added": 0, "skipped_wide": 0}
|
|
store = MagicMock()
|
|
mc_connect.return_value = store
|
|
|
|
result = runner.invoke(
|
|
app, ["code-tags", "--collection", "comments", "--dry-run"]
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert mc_apply.call_args.kwargs["dry_run"] is True
|
|
store.add_tag.assert_not_called()
|
|
store.add_tags.assert_not_called()
|
|
|
|
@patch("bib.codetags.apply_code_tags")
|
|
@patch("bib.codetags.item_codes_from_chunks")
|
|
@patch("bib.codetags.item_codes_from_anchors", return_value={})
|
|
@patch("pfs.families.refresh_from")
|
|
@patch("pfs.anchors.code_family_index", return_value={})
|
|
@patch("os.path.exists", return_value=False)
|
|
@patch("llm.index._engine")
|
|
@patch("llm.config.load")
|
|
@patch("bib.connect")
|
|
def test_anchors_only_skips_pgvector_entirely(
|
|
self,
|
|
mc_connect,
|
|
mc_cfg,
|
|
mc_engine,
|
|
mc_exists,
|
|
mc_code_index,
|
|
mc_refresh,
|
|
mc_anchors,
|
|
mc_chunks,
|
|
mc_apply,
|
|
):
|
|
mc_apply.return_value = {"items": 0, "tags_added": 0, "skipped_wide": 0}
|
|
|
|
result = runner.invoke(
|
|
app, ["code-tags", "--collection", "all", "--anchors-only"]
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
mc_anchors.assert_called_once()
|
|
mc_chunks.assert_not_called()
|
|
mc_engine.assert_not_called()
|
|
# only the rules mapping gets applied — comments/corpus skipped
|
|
assert mc_apply.call_count == 1
|
|
|
|
@patch("bib.codetags.apply_code_tags")
|
|
@patch("bib.codetags.item_codes_from_chunks")
|
|
@patch("bib.codetags.item_codes_from_anchors")
|
|
@patch("pfs.families.refresh_from")
|
|
@patch("pfs.anchors.code_family_index", return_value={})
|
|
@patch("os.path.exists", return_value=False)
|
|
@patch("llm.index._engine")
|
|
@patch("llm.config.load")
|
|
@patch("bib.connect")
|
|
def test_anchors_only_with_non_rules_collection_skips_everything(
|
|
self,
|
|
mc_connect,
|
|
mc_cfg,
|
|
mc_engine,
|
|
mc_exists,
|
|
mc_code_index,
|
|
mc_refresh,
|
|
mc_anchors,
|
|
mc_chunks,
|
|
mc_apply,
|
|
):
|
|
store = MagicMock()
|
|
mc_connect.return_value = store
|
|
|
|
result = runner.invoke(
|
|
app, ["code-tags", "--collection", "comments", "--anchors-only"]
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert "comments: skipped (--anchors-only)" in result.output
|
|
assert "total: items=0 tags_added=0 capped_wide=0" in result.output
|
|
mc_anchors.assert_not_called()
|
|
mc_chunks.assert_not_called()
|
|
mc_apply.assert_not_called()
|
|
mc_engine.assert_not_called()
|
|
store.add_tag.assert_not_called()
|
|
store.add_tags.assert_not_called()
|
|
|
|
def test_help(self):
|
|
assert runner.invoke(app, ["code-tags", "--help"]).exit_code == 0
|
|
|
|
@patch("bib.connect")
|
|
def test_bad_collection_raises_bad_parameter(self, mc_connect):
|
|
result = runner.invoke(app, ["code-tags", "--collection", "nope"])
|
|
assert result.exit_code != 0
|