Files
ecw-charge-validation/tests/test_test_claim_ids_file.py
T
ManikandanandClaude Opus 5.5 ba15a574ab Initial import: eCW Charge Validation bot and results Portal
Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one
commit with no earlier history: the tracked files were de-identified
first (synthetic claim ids and patient data only; see Context.md
section 7). Credentials, run logs, screenshots, claim reports and client
workbooks are gitignored and not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:38:17 +05:30

355 lines
14 KiB
Python

r"""
export_medcor_ready._claims_phase - the TEST_CLAIM_IDS_FILE override.
python tests\test_test_claim_ids_file.py
TEMPORARY testing feature: a hand-maintained "standard" sheet lists one
known claim per implemented condition, so a run against it exercises every
condition against a real claim without waiting on the full live-queue bulk
search and export. `TEST_CLAIM_IDS_FILE` blank is the permanent, default
behaviour and must be provably untouched; set, it must skip exactly the
bulk-lookup/export/categorize steps and nothing else — the claim-status/
date/balance filter setup and the per-claim search/open/apply pipeline stay
identical either way, since only the *source of the id list* changes.
Everything `_claims_phase` touches is a real Playwright page action or a
live network call, so every dependency is stubbed here; this proves the
branching and wiring, not any one page module's own behaviour (those have
their own test files).
Review item D1 (2026-10-05) made a test run say so and fail loudly: a
`[TEST MODE]` line names the sheet and the `_TEST_` summary file, a path that
does not exist is a `validate()` problem (exit 2 before login), and a sheet
that cannot be read raises, which `main()` turns into exit 5 — it used to
return quietly and exit 0.
"""
import io
import sys
import pathlib
import tempfile
import types
import contextlib
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from ecw_bot import settings as cfg # noqa: E402
from ecw_bot.core import msgbox # noqa: E402
from ecw_bot import claim_file # noqa: E402
from ecw_bot.pages import leftnav, claim_filters, claim_results, claim_report # noqa: E402
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
class _Called(Exception):
"""Raised by a stub to prove it was reached, without needing a mock lib."""
def _forbidden(*a, **k):
raise _Called("must not be called")
# Every attribute any case below replaces, restored after each case. The
# real .env is loaded at import, so a case that depends on a setting sets it
# itself rather than trusting what the operator's .env happens to say.
_PATCHED = [
(cfg, "TEST_CLAIM_IDS_FILE"), (cfg, "SHOW_CLAIM_IDS"),
(cfg, "WRITE_CHARGE_SUMMARY"), (cfg, "CLAIM_STATUS_LABEL"),
(cfg, "SAVE_CORRECTIONS"), (cfg, "ECW_USERNAME"), (cfg, "validate"),
(cfg, "PROXY_IP"), (cfg, "PROXY_PORT"), (cfg, "BROWSER_CDP_URL"),
(leftnav, "open_claims"), (claim_filters, "set_claim_status"),
(claim_filters, "set_date_type"), (claim_filters, "set_date_range"),
(claim_filters, "clear_balance"), (claim_results, "run_lookup"),
(claim_report, "export_claims_report"), (claim_file, "claim_ids"),
(msgbox, "info_box"), (flow, "_process_claims"),
(flow, "sync_playwright"), (flow, "_login_with_retry"),
(flow, "KEEP_OPEN"), (flow.runlog, "start"),
(flow.run_lock, "acquire"), (flow.run_lock, "finish"),
(flow.guard, "record_success"),
]
_ORIG = [(obj, name, getattr(obj, name)) for obj, name in _PATCHED]
def _restore():
for obj, name, value in _ORIG:
setattr(obj, name, value)
def _stub_filters(calls):
"""Every filter-setup step _claims_phase always runs, live path or not."""
leftnav.open_claims = lambda page: calls.append("open_claims") or True
claim_filters.set_claim_status = \
lambda page, *a, **k: calls.append("set_claim_status") or True
claim_filters.set_date_type = \
lambda page, *a, **k: calls.append("set_date_type") or True
claim_filters.set_date_range = \
lambda page, *a, **k: calls.append("set_date_range") or True
claim_filters.clear_balance = \
lambda page: calls.append("clear_balance") or True
@case
def blank_file_setting_leaves_the_live_path_untouched():
"""
The regression check that matters most: TEST_CLAIM_IDS_FILE unset (the
default, "" from settings.py) must still run the live bulk search and
export exactly as before this feature existed.
"""
calls = []
try:
cfg.TEST_CLAIM_IDS_FILE = ""
_stub_filters(calls)
claim_results.run_lookup = \
lambda page: calls.append("run_lookup") or True
claim_report.export_claims_report = \
lambda page: calls.append("export_claims_report") or None
claim_file.claim_ids = _forbidden
flow._process_claims = lambda page, info: calls.append("process_claims")
# _read_back_claim_ids(None) short-circuits (saved is None) and
# _categorize_by_payer(None) does too — both real, both no-ops here.
flow._claims_phase(types.SimpleNamespace(url='fake://claims'))
finally:
_restore()
assert calls == ["open_claims", "set_claim_status", "set_date_type",
"set_date_range", "clear_balance",
"run_lookup", "export_claims_report"], calls
@case
def set_file_setting_skips_the_live_bulk_search_and_export():
calls = []
fake_info = {"file": pathlib.Path("standard.xlsx"), "header": "CLAIMS#",
"column": 1, "ids": ["9103000"], "duplicates": [],
"blank_rows": 0, "total_rows": 1}
try:
cfg.TEST_CLAIM_IDS_FILE = str(ROOT / "standard.xlsx")
cfg.SHOW_CLAIM_IDS = False
_stub_filters(calls)
claim_results.run_lookup = _forbidden
claim_report.export_claims_report = _forbidden
claim_file.claim_ids = \
lambda path: calls.append(f"claim_ids:{path}") or fake_info
flow._process_claims = \
lambda page, info: calls.append(f"process_claims:{info['ids']}")
flow._claims_phase(types.SimpleNamespace(url='fake://claims'))
finally:
_restore()
assert calls == ["open_claims", "set_claim_status", "set_date_type",
"set_date_range", "clear_balance",
f"claim_ids:{ROOT / 'standard.xlsx'}",
"process_claims:['9103000']"], calls
@case
def set_file_setting_still_shows_the_summary_box_when_configured_to():
calls = []
fake_info = {"file": pathlib.Path("standard.xlsx"), "header": "CLAIMS#",
"column": 1, "ids": ["9103000"], "duplicates": [],
"blank_rows": 0, "total_rows": 1}
try:
cfg.TEST_CLAIM_IDS_FILE = str(ROOT / "standard.xlsx")
cfg.SHOW_CLAIM_IDS = True
_stub_filters(calls)
claim_results.run_lookup = _forbidden
claim_report.export_claims_report = _forbidden
claim_file.claim_ids = lambda path: fake_info
msgbox.info_box = lambda t, m: calls.append(f"box:{t}")
flow._process_claims = lambda page, info: calls.append("process_claims")
flow._claims_phase(types.SimpleNamespace(url='fake://claims'))
finally:
_restore()
assert "box:eCW Bot — Test claim ids" in calls, calls
assert "process_claims" in calls, calls
@case
def a_bad_file_shows_an_error_box_then_raises_and_never_calls_process_claims():
"""
`claim_file.claim_ids` is left real here (not stubbed) so this proves
the actual FileNotFoundError a missing path raises is caught and
reported, not just that some exception would be.
Changed 2026-10-05 (review item D1): this used to pin a quiet return,
which made `main()` exit 0 after processing nothing. The box is still
shown first; then it raises, so `main()` exits 5.
"""
calls = []
path = ROOT / "does-not-exist.xlsx"
raised = None
try:
cfg.TEST_CLAIM_IDS_FILE = str(path)
_stub_filters(calls)
claim_results.run_lookup = _forbidden
claim_report.export_claims_report = _forbidden
msgbox.info_box = lambda t, m: calls.append(f"box:{t}")
flow._process_claims = _forbidden
with contextlib.redirect_stdout(io.StringIO()):
flow._claims_phase(types.SimpleNamespace(url='fake://claims'))
except RuntimeError as exc:
raised = exc
finally:
_restore()
assert raised is not None, "an unreadable test sheet must raise"
assert str(raised).startswith(
f"Could not read the test claim sheet {path}: "), raised
assert isinstance(raised.__cause__, FileNotFoundError), raised.__cause__
assert "box:eCW Bot — Could not read the test claim sheet" in calls, calls
assert "process_claims" not in calls, calls
@case
def an_unreadable_test_sheet_makes_main_exit_5():
"""
End to end through `main()`: login stubbed to succeed, the claims phase
real down to `claim_file.claim_ids`, which fails on a sheet that is
there but is not a spreadsheet — so `validate()`'s existence check
passes and the read failure is what stops the run.
"""
printed = io.StringIO()
with tempfile.TemporaryDirectory() as tmp:
sheet = pathlib.Path(tmp) / "Standard Claims.xlsx"
sheet.write_bytes(b"\xd0\xcf\x11\xe0 legacy OLE2, unreadable here")
try:
cfg.TEST_CLAIM_IDS_FILE = str(sheet)
cfg.SAVE_CORRECTIONS = False # VERIFY: no start box
# Nothing from the operator's .env in the captured output.
cfg.ECW_USERNAME = "test-user"
cfg.PROXY_IP, cfg.PROXY_PORT = "", ""
# Never the operator's Chrome: with BROWSER_CDP_URL from .env the
# run would open a real one — here with no proxy (2026-10-22).
cfg.BROWSER_CDP_URL = ""
cfg.validate = lambda: []
# In tmp: main() writes the Run's events beside the log.
flow.runlog.start = lambda: pathlib.Path(tmp) / "unused.log"
flow.run_lock.acquire = lambda log_path, source=None: {}
flow.run_lock.finish = lambda code: None
flow.sync_playwright = lambda: contextlib.nullcontext(None)
page = types.SimpleNamespace(url="fake://landing")
flow._login_with_retry = lambda pw: (None, None, page)
flow.guard.record_success = lambda: None
flow.KEEP_OPEN = False
_stub_filters([])
claim_results.run_lookup = _forbidden
claim_report.export_claims_report = _forbidden
msgbox.info_box = lambda t, m: None
flow._process_claims = _forbidden
with contextlib.redirect_stdout(printed):
code = flow.main()
finally:
_restore()
out = printed.getvalue()
assert code == 5, (code, out)
assert "[CLAIMS] Failed: Could not read the test claim sheet" in out, out
_FAKE_INFO = {"file": pathlib.Path("standard.xlsx"), "header": "CLAIMS#",
"column": 1, "ids": ["9103000"], "duplicates": [],
"blank_rows": 0, "total_rows": 1}
@case
def test_mode_line_names_the_sheet_and_the_TEST_summary_file():
printed = io.StringIO()
try:
cfg.TEST_CLAIM_IDS_FILE = str(ROOT / "standard.xlsx")
cfg.SHOW_CLAIM_IDS = False
cfg.WRITE_CHARGE_SUMMARY = True
cfg.CLAIM_STATUS_LABEL = "Medcor Ready"
_stub_filters([])
claim_results.run_lookup = _forbidden
claim_report.export_claims_report = _forbidden
claim_file.claim_ids = lambda path: _FAKE_INFO
flow._process_claims = lambda page, info: None
with contextlib.redirect_stdout(printed):
flow._claims_phase(types.SimpleNamespace(url='fake://claims'))
finally:
_restore()
want = (f"[TEST MODE] Claim ids come from the test sheet standard.xlsx, "
f"not the live Medcor Ready queue — the charge summary goes to "
f"Charge Summary_TEST_{cfg.TODAY}.xlsx, not the live "
f"Charge Summary_{cfg.TODAY}.xlsx")
assert want in printed.getvalue(), printed.getvalue()
@case
def test_mode_line_says_when_no_summary_is_written():
printed = io.StringIO()
try:
cfg.TEST_CLAIM_IDS_FILE = str(ROOT / "standard.xlsx")
cfg.SHOW_CLAIM_IDS = False
cfg.WRITE_CHARGE_SUMMARY = False
_stub_filters([])
claim_file.claim_ids = lambda path: _FAKE_INFO
flow._process_claims = lambda page, info: None
with contextlib.redirect_stdout(printed):
flow._claims_phase(types.SimpleNamespace(url='fake://claims'))
finally:
_restore()
out = printed.getvalue()
assert "[TEST MODE] Claim ids come from the test sheet standard.xlsx" \
in out, out
assert "no charge summary is written (WRITE_CHARGE_SUMMARY=0)" in out, out
assert "Charge Summary_TEST_" not in out, out
@case
def validate_reports_a_test_claim_ids_file_that_does_not_exist():
missing = str(ROOT / "does-not-exist.xlsx")
try:
cfg.TEST_CLAIM_IDS_FILE = missing
problems = cfg.validate()
finally:
_restore()
assert f"TEST_CLAIM_IDS_FILE={missing} does not exist" in problems, \
problems
@case
def validate_accepts_a_test_claim_ids_file_that_exists_or_is_blank():
with tempfile.TemporaryDirectory() as tmp:
sheet = pathlib.Path(tmp) / "Standard Claims.xlsx"
sheet.write_bytes(b"present")
for value in (str(sheet), ""):
try:
cfg.TEST_CLAIM_IDS_FILE = value
problems = cfg.validate()
finally:
_restore()
assert not any("TEST_CLAIM_IDS_FILE" in p for p in problems), \
(value, problems)
def main() -> int:
failures = 0
for fn in CASES:
name = fn.__name__.replace("_", " ")
try:
fn()
print(f" ok {name}")
except AssertionError as exc:
print(f" FAIL {name}: {exc}")
failures += 1
except Exception as exc:
print(f" ERROR {name}: {type(exc).__name__}: {exc}")
failures += 1
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())