Files
ecw-charge-validation/tests/test_mode_banner.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

342 lines
12 KiB
Python

r"""
export_medcor_ready - the [MODE] banner and the LIVE-WRITE start box.
python tests\test_mode_banner.py
Review item P1 (2026-10-01): nothing at the start of a run said whether it
could write to live eCW. The only way to tell VERIFY from LIVE-WRITE was to
open .env, and the .env reader itself could get it wrong silently. So:
* `mode_banner()` prints, after the [CONFIG] lines, which mode this is, the
claim limit, and whether the ids come from a test sheet.
* With SAVE_CORRECTIONS=1, `main()` asks for an OK BEFORE login, with
Cancel as the default button. Anything but an explicit OK stops the run
with exit 2 - nothing spent: no login attempt, no 2FA mail.
`main()` is driven with everything past the config check stubbed: the run
log, the browser and the login all raise if reached, so these cases prove
where `main()` stops, never touching eCW, the Logs\ folder or the real .env.
"""
import io
import sys
import pathlib
import tempfile
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.flows import export_medcor_ready as flow # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
_CFG_KEYS = ("SAVE_CORRECTIONS", "APPLY_CORRECTIONS", "CLAIM_PROCESS_LIMIT",
"TEST_CLAIM_IDS_FILE", "CLAIM_STATUS_LABEL", "ECW_USERNAME",
"PROXY_IP", "PROXY_PORT", "HEADLESS", "validate",
"resolved_otp_mode", "WRITE_CHARGE_SUMMARY", "BROWSER_CDP_URL")
def _with_cfg(fn, **values):
"""Run fn() with cfg attributes set, restoring every one after."""
saved = {k: getattr(cfg, k) for k in _CFG_KEYS}
saved_bad = list(cfg._BAD_VALUES)
try:
for k, v in values.items():
assert k in _CFG_KEYS, k
setattr(cfg, k, v)
return fn()
finally:
for k, v in saved.items():
setattr(cfg, k, v)
cfg._BAD_VALUES[:] = saved_bad
_VERIFY = dict(SAVE_CORRECTIONS=False, APPLY_CORRECTIONS=True,
CLAIM_PROCESS_LIMIT=1, TEST_CLAIM_IDS_FILE="",
CLAIM_STATUS_LABEL="Medcor Ready")
# =============================================================================
# mode_banner()
# =============================================================================
@case
def verify_mode_banner():
lines = _with_cfg(flow.mode_banner, **_VERIFY)
assert lines == [
"[MODE] VERIFY — corrections are filled in on screen for review "
"and NEVER saved (SAVE_CORRECTIONS=0)",
"[MODE] CLAIM_PROCESS_LIMIT=1 (0 = all)",
], lines
@case
def live_write_mode_banner():
lines = _with_cfg(flow.mode_banner,
**{**_VERIFY, "SAVE_CORRECTIONS": True})
assert lines[0] == ("[MODE] LIVE-WRITE — corrections are SAVED to eCW "
"after the per-claim OK; box ticks and added CPT "
"rows save at once, OK or not "
"(SAVE_CORRECTIONS=1)"), lines
assert not any("VERIFY" in l for l in lines), lines
@case
def apply_off_says_conditions_are_not_run():
lines = _with_cfg(flow.mode_banner,
**{**_VERIFY, "APPLY_CORRECTIONS": False})
assert "[MODE] conditions are NOT run (APPLY_CORRECTIONS=0)" in lines, \
lines
@case
def apply_on_says_nothing_about_conditions():
lines = _with_cfg(flow.mode_banner, **_VERIFY)
assert not any("APPLY_CORRECTIONS" in l for l in lines), lines
@case
def claim_limit_zero_is_shown_as_all():
lines = _with_cfg(flow.mode_banner,
**{**_VERIFY, "CLAIM_PROCESS_LIMIT": 0})
assert "[MODE] CLAIM_PROCESS_LIMIT=0 (0 = all)" in lines, lines
@case
def claim_ids_from_a_test_sheet_are_named_by_file_name():
sheet = str(pathlib.Path("C:/somewhere/Standard Claims.xlsx"))
lines = _with_cfg(flow.mode_banner,
**{**_VERIFY, "TEST_CLAIM_IDS_FILE": sheet})
assert lines[-1] == ("[MODE] TEST MODE — claim ids come from Standard "
"Claims.xlsx, not the live Medcor Ready queue"), lines
@case
def no_test_mode_line_without_a_test_file():
lines = _with_cfg(flow.mode_banner, **_VERIFY)
assert not any("TEST MODE" in l for l in lines), lines
# =============================================================================
# main() - where it stops
# =============================================================================
class _ReachedLogin(Exception):
"""The browser/login was reached."""
class _BoxShown(Exception):
"""A box that must not appear in this mode appeared."""
def _reached_login(*a, **k):
raise _ReachedLogin()
def _run_main(save, box_answer=None, validate=None):
"""
main() with the config check passed (unless `validate` is given), the
browser and login stubbed to raise _ReachedLogin, and the run log not
started. `box_answer`: what the LIVE-WRITE box answers, or None to make
any box an error. Returns (exit code or _ReachedLogin, printed text,
the boxes shown as (title, body, kwargs)).
"""
boxes = []
def _box(title, msg, **kw):
boxes.append((title, msg, kw))
if box_answer is None:
raise _BoxShown(title)
return box_answer
orig = (flow.runlog.start, flow.sync_playwright, flow._login_with_retry,
msgbox.ok_cancel, flow.run_lock.acquire, flow.run_lock.finish)
# In a temp folder: main() writes the Run's events beside the log.
unused_log = pathlib.Path(tempfile.mkdtemp()) / "unused.log"
flow.runlog.start = lambda: unused_log
flow.sync_playwright = _reached_login
flow._login_with_retry = _reached_login
msgbox.ok_cancel = _box
flow.run_lock.acquire = lambda log_path, source=None: {}
flow.run_lock.finish = lambda code: None
out = io.StringIO()
def _go():
with contextlib.redirect_stdout(out):
try:
return flow.main()
except _ReachedLogin as exc:
return exc
values = {**_VERIFY, "SAVE_CORRECTIONS": save,
"ECW_USERNAME": "test-user", "PROXY_IP": "", "PROXY_PORT": "",
# Never the operator's Chrome: with BROWSER_CDP_URL from .env
# main() would open a real one, here with no proxy — and a
# Portal run then attached to it (2026-10-22).
"BROWSER_CDP_URL": "",
"HEADLESS": False, "resolved_otp_mode": lambda: "manual",
"validate": validate or (lambda: [])}
try:
result = _with_cfg(_go, **values)
finally:
(flow.runlog.start, flow.sync_playwright, flow._login_with_retry,
msgbox.ok_cancel, flow.run_lock.acquire, flow.run_lock.finish) = orig
return result, out.getvalue(), boxes
@case
def live_write_not_confirmed_stops_before_login_with_exit_2():
result, out, boxes = _run_main(save=True, box_answer=False)
assert result == 2, (result, out)
assert ("[MODE] Operator did not confirm LIVE-WRITE mode — stopping "
"before login") in out, out
assert len(boxes) == 1, boxes
title, body, kw = boxes[0]
assert title == "eCW Bot — LIVE-WRITE mode", title
# Enter must answer Cancel here too: OK starts a run that writes.
assert kw.get("default_cancel") is True, kw
assert "live eCW" in body, body
# The banner is printed first, so the log says why the box appeared.
assert out.index("[MODE] LIVE-WRITE") < out.index("did not confirm"), out
@case
def live_write_confirmed_goes_on_to_login():
result, out, boxes = _run_main(save=True, box_answer=True)
assert isinstance(result, _ReachedLogin), (result, out)
assert len(boxes) == 1, boxes
assert "did not confirm" not in out, out
@case
def verify_mode_asks_nothing_and_goes_on_to_login():
result, out, boxes = _run_main(save=False, box_answer=None)
assert isinstance(result, _ReachedLogin), (result, out)
assert boxes == [], boxes
assert "[MODE] VERIFY" in out, out
@case
def mode_banner_follows_the_config_lines():
_, out, _ = _run_main(save=False)
assert out.index("[CONFIG] log=") < out.index("[MODE] VERIFY"), out
@case
def unreadable_setting_exits_2_before_banner_and_login():
"""
P1's strict parsing, end to end: a bad yes/no value recorded by the
settings parser is a validate() problem, so main() stops at the config
check - before the banner, the box and the login.
"""
def _validate():
cfg._BAD_VALUES.append(("SAVE_CORRECTIONS", "O",
"a valid yes/no value"))
return orig_validate()
orig_validate = cfg.validate
result, out, boxes = _run_main(save=True, box_answer=None,
validate=_validate)
assert result == 2, (result, out)
assert "SAVE_CORRECTIONS='O' is not a valid yes/no value" in out, out
assert "[MODE]" not in out, out
assert boxes == [], boxes
# ── tools/check_claims_from_excel.py (final review, 2026-10-05) ──────────────
# A second entry point that logs in and reaches the flow's real
# _process_claims (so it can save). It only printed a WARNING line and went
# straight to login with SAVE_CORRECTIONS=1; it now goes through the same
# start_mode_gate() as main().
def _tool():
import importlib.util
path = ROOT / "tools" / "check_claims_from_excel.py"
spec = importlib.util.spec_from_file_location("_check_tool", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _run_tool(save, box_answer=None):
tool = _tool()
boxes = []
def _box(title, msg, **kw):
boxes.append((title, msg, kw))
if box_answer is None:
raise _BoxShown(title)
return box_answer
orig = (tool.runlog.start, msgbox.ok_cancel)
tool.runlog.start = lambda: pathlib.Path("unused.log")
tool.sync_playwright = _reached_login
tool.login = _reached_login
tool.load_claims_from_excel = lambda path: [("9103000", "")]
msgbox.ok_cancel = _box
out = io.StringIO()
def _go():
with contextlib.redirect_stdout(out):
try:
return tool.main(pathlib.Path("unused.xlsx"))
except _ReachedLogin as exc:
return exc
values = {**_VERIFY, "SAVE_CORRECTIONS": save,
"ECW_USERNAME": "test-user", "WRITE_CHARGE_SUMMARY": True,
"validate": lambda: []}
try:
result = _with_cfg(_go, **values)
finally:
tool.runlog.start, msgbox.ok_cancel = orig
return result, out.getvalue(), boxes
@case
def the_check_tool_stops_before_login_when_live_write_is_not_confirmed():
result, out, boxes = _run_tool(save=True, box_answer=False)
assert result == 2, (result, out)
assert "[MODE] LIVE-WRITE" in out, out
assert "did not confirm LIVE-WRITE" in out, out
assert len(boxes) == 1 and boxes[0][2].get("default_cancel") is True, \
boxes
@case
def the_check_tool_in_verify_mode_asks_nothing_and_goes_on_to_login():
result, out, boxes = _run_tool(save=False)
assert isinstance(result, _ReachedLogin), (result, out)
assert boxes == [], boxes
assert "[MODE] VERIFY" in out, out
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())