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())