r""" export_medcor_ready - the SAVE_CORRECTIONS gate and the review box under it. python tests\test_save_corrections_gate.py Real incident this guards against: claim 9103000 got saved for real during a live test run (Logs\run_20260917_172622.log: "[COND 9103000] Saved: depression_dx_pointer") because someone answered the confirmation box while conditions were still being verified against live claims, not because the code did anything wrong. SAVE_CORRECTIONS=0 makes that impossible while a condition is still under test: the confirmation box must never even appear, and save_claim() must never be called, regardless of what a human would have answered. No Playwright page needed - SAVE_CORRECTIONS=0 must return before the function ever touches one. """ import io import sys import pathlib 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.pages import claim_form # noqa: E402 from ecw_bot.conditions.base import ConditionResult # noqa: E402 from ecw_bot.flows import export_medcor_ready as flow # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn _RESULT = [ConditionResult( condition_name="depression_dx_pointer", changed=True, changes=[{"row": 0, "code": "G8510", "before": {"icd": ["1", "", "", ""]}, "after": {"icd": ["2", "", "", ""]}}])] 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 while SAVE_CORRECTIONS is off") @case def save_corrections_off_never_shows_the_box_or_saves(): orig_save, orig_flag = cfg.SAVE_CORRECTIONS, cfg.SAVE_CORRECTIONS orig_ok_cancel = msgbox.ok_cancel orig_save_claim = claim_form.save_claim cfg.SAVE_CORRECTIONS = False msgbox.ok_cancel = _forbidden claim_form.save_claim = _forbidden try: saved = flow._confirm_and_save(None, "9103000", _RESULT) assert saved is False, saved finally: cfg.SAVE_CORRECTIONS = orig_flag msgbox.ok_cancel = orig_ok_cancel claim_form.save_claim = orig_save_claim @case def save_corrections_on_and_operator_approves_still_saves(): """ The pre-existing path, unchanged: proves the gate only ever REMOVES the save, never adds a save that was not already going to happen. """ orig_flag = cfg.SAVE_CORRECTIONS orig_ok_cancel = msgbox.ok_cancel orig_save_claim = claim_form.save_claim cfg.SAVE_CORRECTIONS = True msgbox.ok_cancel = lambda *a, **k: True claim_form.save_claim = lambda page: True try: saved = flow._confirm_and_save(None, "9103000", _RESULT) assert saved is True, saved finally: cfg.SAVE_CORRECTIONS = orig_flag msgbox.ok_cancel = orig_ok_cancel claim_form.save_claim = orig_save_claim @case def save_box_makes_cancel_the_default_button(): """ Review item P1: OK on this box writes the claim to live eCW, so Enter must answer Cancel. Proven at the call site; test_msgbox.py proves that default_cancel=True really adds MB_DEFBUTTON2. """ calls = [] orig_flag = cfg.SAVE_CORRECTIONS orig_ok_cancel = msgbox.ok_cancel orig_save_claim = claim_form.save_claim cfg.SAVE_CORRECTIONS = True msgbox.ok_cancel = lambda *a, **k: calls.append(k) or False claim_form.save_claim = _forbidden try: flow._confirm_and_save(None, "9103000", _RESULT) finally: cfg.SAVE_CORRECTIONS = orig_flag msgbox.ok_cancel = orig_ok_cancel claim_form.save_claim = orig_save_claim assert len(calls) == 1, calls assert calls[0].get("default_cancel") is True, calls @case def save_corrections_on_and_operator_declines_does_not_save(): orig_flag = cfg.SAVE_CORRECTIONS orig_ok_cancel = msgbox.ok_cancel orig_save_claim = claim_form.save_claim cfg.SAVE_CORRECTIONS = True msgbox.ok_cancel = lambda *a, **k: False claim_form.save_claim = _forbidden try: saved = flow._confirm_and_save(None, "9103000", _RESULT) assert saved is False, saved finally: cfg.SAVE_CORRECTIONS = orig_flag msgbox.ok_cancel = orig_ok_cancel claim_form.save_claim = orig_save_claim @case def no_results_never_saves_regardless_of_the_flag(): saved = flow._confirm_and_save(None, "9103000", []) assert saved is False, saved # ============================================================================= # _review_before_close - the pause the gate was missing # ============================================================================= # SAVE_CORRECTIONS=0 filled the form in and clicked Cancel in the same breath, # so "applied for review" was only ever reviewable in the log and the # screenshot. These cases pin the pause to exactly the path that lacked one, # and - the point of the whole gate - prove it still cannot write. _SHOWN = [] def _record_info_box(title, msg): _SHOWN.append((title, msg)) def _with(save_corrections, review_before_close, results, info_box): """Run _review_before_close under a given config, restoring it after.""" orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, msgbox.info_box, claim_form.save_claim) _SHOWN.clear() cfg.SAVE_CORRECTIONS = save_corrections cfg.REVIEW_BEFORE_CLOSE = review_before_close msgbox.info_box = info_box # Nothing on this path may write, whatever the operator does with the box. claim_form.save_claim = _forbidden try: return flow._review_before_close("9103000", results) finally: (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, msgbox.info_box, claim_form.save_claim) = orig @case def review_box_appears_when_the_save_gate_is_off(): """The gap: gate off, a condition fired, so the form must be held open.""" shown = _with(False, True, _RESULT, _record_info_box) assert shown is True, shown assert len(_SHOWN) == 1, _SHOWN title, msg = _SHOWN[0] assert "9103000" in title, title # The operator has to be told what to look for, not just that something # happened - so the condition's own describe() output has to be in there. assert "G8510" in msg, msg @case def review_box_does_not_appear_when_the_save_gate_is_on(): """ With the gate on, _confirm_and_save's own box is already the pause before closing. A second one would re-ask a decision that was just made. """ shown = _with(True, True, _RESULT, _forbidden) assert shown is False, shown @case def review_box_does_not_appear_when_no_condition_fired(): shown = _with(False, True, [], _forbidden) assert shown is False, shown @case def review_box_can_be_switched_off_for_unattended_runs(): shown = _with(False, False, _RESULT, _forbidden) assert shown is False, shown @case def review_box_is_shown_before_the_form_is_closed(): """ The ordering is the whole feature: a box after close_claim() would be describing a form that is no longer on screen. Recorded rather than assumed, because nothing else in _read_claim_form's `finally` enforces it. `conditions.run_all` is stubbed wholesale here, so it never actually calls the `before_apply` callback `_read_claim_form` passes it — that wiring (and the BEFORE box's own content) is proven separately below, against the real `run_all`. This case only needs the stub to accept the new keyword without blowing up. """ calls = [] orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab, claim_form.wait_claim_loaded, claim_form.extract, claim_form.save, claim_form.close_claim, claim_form.save_claim, flow.conditions.run_all) cfg.SAVE_CORRECTIONS = False cfg.REVIEW_BEFORE_CLOSE = True cfg.APPLY_CORRECTIONS = True cfg.OPEN_CLAIM_FORM = True cfg.OPEN_PROGRESS_NOTES = False msgbox.info_box = lambda t, m: calls.append("review_box") claim_form.open_claim = lambda page, cid: True # Stubbed too (review item D7): on page=None the real one would # poll its full 20s before giving up. claim_form.wait_claim_loaded = lambda page, cid: None claim_form.open_cpt_hcpcs_tab = lambda page: None claim_form.extract = lambda page: {"cpt": [], "icd": [], "revenue": []} claim_form.save = lambda data: pathlib.Path("unused.json") claim_form.close_claim = lambda page: calls.append("close_claim") claim_form.save_claim = _forbidden flow.conditions.run_all = (lambda page, cid, data, before_apply=None, report=None: _RESULT) try: flow._read_claim_form(None, "9103000") finally: (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab, claim_form.wait_claim_loaded, claim_form.extract, claim_form.save, claim_form.close_claim, claim_form.save_claim, flow.conditions.run_all) = orig assert calls == ["review_box", "close_claim"], calls # ============================================================================= # _log_before_apply - the BEFORE half, now a log line only (review item D9) # ============================================================================= # _review_before_close shows what changed, once apply() has already written # to the DOM. In front of it there used to be a second box, "N condition(s) # matched - about to apply". Since D9 (2026-10-05) that is a log line only: # a corrected claim gets ONE box, and these cases pin that the old box is # gone whatever REVIEW_BEFORE_CLOSE says, while its log line stays. # (Updated: these two cases used to assert the box appeared with REVIEW on.) _FIRED = [types.SimpleNamespace(NAME="depression_dx_pointer"), types.SimpleNamespace(NAME="j2930_j2001_update")] def _log_before_apply_with(review_before_close): orig = (cfg.REVIEW_BEFORE_CLOSE, msgbox.info_box) cfg.REVIEW_BEFORE_CLOSE = review_before_close msgbox.info_box = _forbidden out = io.StringIO() try: with contextlib.redirect_stdout(out): flow._log_before_apply("9103000", _FIRED) finally: cfg.REVIEW_BEFORE_CLOSE, msgbox.info_box = orig return out.getvalue() @case def before_apply_logs_the_matched_conditions_and_shows_no_box(): out = _log_before_apply_with(True) assert ("[COND 9103000] 2 condition(s) matched — about to apply: " "depression_dx_pointer, j2930_j2001_update") in out, out @case def before_apply_still_logs_with_review_before_close_off(): out = _log_before_apply_with(False) assert "2 condition(s) matched — about to apply" in out, out @case def read_claim_form_wires_before_apply_through_to_run_all(): """ Proves the actual wiring in `_read_claim_form` — not a stubbed `run_all`, but the real one, fed fake condition modules so this stays independent of whatever the real registry currently contains. If `_read_claim_form` stopped passing `before_apply=...`, or passed it with the wrong `claim_id`, the BEFORE log line would never be written on a real run and this would catch it. Updated for review item D9: the BEFORE half used to be a box, first in `calls`; it is now only the log line, so the one box is the review box. """ calls = [] orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab, claim_form.wait_claim_loaded, claim_form.extract, claim_form.save, claim_form.close_claim, claim_form.save_claim, flow.conditions.CONDITIONS) fake = types.ModuleType("fake_cond") fake.NAME = "fake_cond" fake.detect = lambda claim_id, data: True fake.apply = lambda page, claim_id, data: ( calls.append("apply:fake_cond") or _RESULT[0]) cfg.SAVE_CORRECTIONS = False cfg.REVIEW_BEFORE_CLOSE = True cfg.APPLY_CORRECTIONS = True cfg.OPEN_CLAIM_FORM = True cfg.OPEN_PROGRESS_NOTES = False msgbox.info_box = lambda t, m: calls.append(f"box:{t}") claim_form.open_claim = lambda page, cid: True # Stubbed too (review item D7): on page=None the real one would # poll its full 20s before giving up. claim_form.wait_claim_loaded = lambda page, cid: None claim_form.open_cpt_hcpcs_tab = lambda page: None claim_form.extract = lambda page: {"cpt": [], "icd": [], "revenue": []} claim_form.save = lambda data: pathlib.Path("unused.json") claim_form.close_claim = lambda page: calls.append("close_claim") claim_form.save_claim = _forbidden flow.conditions.CONDITIONS = [fake] out = io.StringIO() try: with contextlib.redirect_stdout(out): flow._read_claim_form(None, "9103000") finally: (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab, claim_form.wait_claim_loaded, claim_form.extract, claim_form.save, claim_form.close_claim, claim_form.save_claim, flow.conditions.CONDITIONS) = orig # BEFORE log line (no box), the fix lands, the ONE box, then close. assert ("[COND 9103000] 1 condition(s) matched — about to apply: " "fake_cond") in out.getvalue(), out.getvalue() assert calls == ["apply:fake_cond", "box:eCW Bot — Claim 9103000: review before close", "close_claim"], calls @case def read_claim_form_names_the_snapshot_by_the_searched_claim_id(): """ The HCFA screen has no UB claim-number field, so extract() returns no claim_no there and every HCFA claim used to overwrite one shared Output\\claims\\unknown.json — losing 9109000's progress note. """ saved = [] orig = (cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, claim_form.open_claim, claim_form.is_dental_claim_form, claim_form.wait_claim_loaded, claim_form.open_cpt_hcpcs_tab, claim_form.extract, claim_form.save, claim_form.close_claim) cfg.APPLY_CORRECTIONS = False cfg.OPEN_CLAIM_FORM = True cfg.OPEN_PROGRESS_NOTES = False claim_form.open_claim = lambda page, cid: True # Stubbed too (review item D7): on page=None the real one would # poll its full 20s before giving up. claim_form.wait_claim_loaded = lambda page, cid: None claim_form.is_dental_claim_form = lambda page: False claim_form.open_cpt_hcpcs_tab = lambda page: None claim_form.extract = lambda page: {"cpt": [], "icd": [], "revenue": []} claim_form.save = lambda data: (saved.append(data.get("claim_no")) or pathlib.Path("unused.json")) claim_form.close_claim = lambda page: None try: flow._read_claim_form(None, "9109000") finally: (cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, claim_form.open_claim, claim_form.is_dental_claim_form, claim_form.wait_claim_loaded, claim_form.open_cpt_hcpcs_tab, claim_form.extract, claim_form.save, claim_form.close_claim) = orig assert saved == ["9109000"], saved # ============================================================================= # A tainted claim is never offered for save (review item P2, unit D) # ============================================================================= # A condition that raised half-way through apply() leaves its edits on the # form with no result to describe them, and Save writes the whole form. So a # run_all report with any `failed` or `review` entry keeps the box away even # with SAVE_CORRECTIONS=1 — the box is never shown, save_claim never called. def _gate_on(report): orig = (cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim) cfg.SAVE_CORRECTIONS = True msgbox.ok_cancel = _forbidden claim_form.save_claim = _forbidden out = io.StringIO() try: with contextlib.redirect_stdout(out): saved = flow._confirm_and_save(None, "9103000", _RESULT, report) finally: cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim = orig return saved, out.getvalue() @case def a_failed_condition_blocks_the_save_box_even_with_save_on(): saved, out = _gate_on({"failed": [{"name": "telehealth", "stage": "apply", "error": "Timeout 30000ms"}], "review": []}) assert saved is False, saved assert ("[COND 9103000] Not offering Save: telehealth failed (apply): " "Timeout 30000ms — closing with Cancel") in out, out @case def a_review_entry_blocks_the_save_box_even_with_save_on(): saved, out = _gate_on({"failed": [], "review": [ {"name": "vaccine_admin_codes", "reason": "90471 could not be added cleanly — check the CPT grid " "by hand"}]}) assert saved is False, saved assert "Not offering Save: vaccine_admin_codes: 90471 could not be " \ "added cleanly" in out, out @case def a_clean_report_still_reaches_the_save_box(): """The control: an empty report changes nothing about the old path.""" orig = (cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim) cfg.SAVE_CORRECTIONS = True asked = [] msgbox.ok_cancel = lambda *a, **k: asked.append(1) or False claim_form.save_claim = _forbidden try: with contextlib.redirect_stdout(io.StringIO()): flow._confirm_and_save(None, "9103000", _RESULT, {"failed": [], "review": [], "comments": {}}) finally: cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim = orig assert asked == [1], asked def _read_with(conds, save_corrections=True, open_claim=None, note=None, ask=None, closed=None, any_open=None, save_claim=None): """ The real _read_claim_form and the real run_all, fed fake condition modules; every page step stubbed and save_claim / the save box forbidden. Returns (data, printed text). `note`: the progress note open_progress_notes returns (OPEN_PROGRESS_NOTES on); None = notes off. `ask`: a stand-in for the save box (default: forbidden). `closed`: a list close_claim appends to. `any_open`: a stand-in for claim_form.any_claim_form_open. `save_claim`: a stand-in for claim_form.save_claim (default: forbidden). """ orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, msgbox.info_box, msgbox.ok_cancel, claim_form.open_claim, claim_form.is_dental_claim_form, claim_form.open_cpt_hcpcs_tab, claim_form.wait_claim_loaded, claim_form.extract, claim_form.save, claim_form.close_claim, claim_form.save_claim, flow.conditions.CONDITIONS, claim_form.open_progress_notes, claim_form.any_claim_form_open) cfg.SAVE_CORRECTIONS = save_corrections cfg.REVIEW_BEFORE_CLOSE = False cfg.APPLY_CORRECTIONS = True cfg.OPEN_CLAIM_FORM = True cfg.OPEN_PROGRESS_NOTES = note is not None msgbox.info_box = lambda t, m: None msgbox.ok_cancel = ask or _forbidden claim_form.open_claim = open_claim or (lambda page, cid: True) claim_form.is_dental_claim_form = lambda page: False claim_form.wait_claim_loaded = lambda page, cid: None claim_form.open_cpt_hcpcs_tab = lambda page: None claim_form.extract = lambda page: {"cpt": [{"code": "99213"}], "icd": [{"#": "1", "code": "R69"}], "revenue": []} claim_form.save = lambda data: pathlib.Path("unused.json") claim_form.close_claim = ((lambda page: closed.append(True)) if closed is not None else (lambda page: None)) claim_form.save_claim = save_claim or _forbidden claim_form.open_progress_notes = lambda page: dict(note or {}) if any_open is not None: claim_form.any_claim_form_open = any_open flow.conditions.CONDITIONS = conds out = io.StringIO() try: with contextlib.redirect_stdout(out): data = flow._read_claim_form(None, "9103000") finally: (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, msgbox.info_box, msgbox.ok_cancel, claim_form.open_claim, claim_form.is_dental_claim_form, claim_form.open_cpt_hcpcs_tab, claim_form.wait_claim_loaded, claim_form.extract, claim_form.save, claim_form.close_claim, claim_form.save_claim, flow.conditions.CONDITIONS, claim_form.open_progress_notes, claim_form.any_claim_form_open) = orig return data, out.getvalue() def _fake(name, apply): mod = types.ModuleType(name) mod.NAME = name mod.detect = lambda claim_id, data: True mod.apply = apply mod.describe = lambda result: f"{name} changed" return mod @case def read_claim_form_never_offers_save_after_a_condition_raised_in_apply(): """ End to end, SAVE_CORRECTIONS=1: one condition changes something, the next raises mid-apply(). The box would list only the first while the form also carries whatever the second did — so it is never shown. """ def boom(page, claim_id, data): raise RuntimeError("fill failed\ncall log ...") good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0]) bad = _fake("telehealth", boom) orig_shot = flow.conditions.base.shot flow.conditions.base.shot = lambda page, name: None try: data, out = _read_with([good, bad]) finally: flow.conditions.base.shot = orig_shot cond = data["_conditions"] assert cond["fired"] == ["depression_dx_pointer"], cond assert cond["saved"] is False, cond assert cond["failed"] == [{"name": "telehealth", "stage": "apply", "error": "fill failed"}], cond assert cond["review"] == [] and cond["comments"] == {}, cond assert "Not offering Save" in out, out @case def read_claim_form_stores_a_declined_conditions_review_reason(): from ecw_bot.conditions.base import CPT_TAB_NOT_CONFIRMED good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0]) declines = _fake("dummy_codes", lambda p, c, d: ConditionResult.declined( "dummy_codes", CPT_TAB_NOT_CONFIRMED)) data, out = _read_with([good, declines]) assert data["_conditions"]["review"] == [ {"name": "dummy_codes", "reason": CPT_TAB_NOT_CONFIRMED}], data assert "Not offering Save" in out, out # ============================================================================= # Claim-level review reasons block the save box too (final review, 2026-10-05) # ============================================================================= # The gate used to read only run_all's report. A claim the Charge Summary # itself sends to Review — a partial note, an unread note, no CPT rows — # could still be offered for Save: a partial note can hide "Audio Only" and # turn 93 into 95, and one OK writes that to live eCW. _PARTIAL_NOTE = {"opened": "frame", "sections": {"TELEHEALTH": "x"}, "partial": True, "partial_sections": 1} _READ_NOTE = {"opened": "frame", "sections": {"Procedure Codes": "x"}} @case def a_partial_note_blocks_the_save_box_even_with_save_on(): good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0]) # ok_cancel and save_claim are both forbidden in _read_with. data, out = _read_with([good], note=_PARTIAL_NOTE) assert data["_conditions"]["fired"] == ["depression_dx_pointer"], data assert data["_conditions"]["saved"] is False, data assert ("[COND 9103000] Not offering Save: Progress note may be partial " "(1 section(s))") in out, out @case def an_unread_note_blocks_the_save_box_even_with_save_on(): good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0]) data, out = _read_with([good], note={"opened": "nothing"}) assert "Not offering Save: Progress note not read (nothing)" in out, out @case def a_fully_read_claim_still_reaches_the_save_box(): """The control: a claim with no review reason is offered as before.""" asked = [] good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0]) data, out = _read_with([good], note=_READ_NOTE, ask=lambda *a, **k: asked.append(1) or False) assert asked == [1], out assert "Not offering Save" not in out, out @case def the_conditions_account_survives_a_raise_after_run_all(): """ Final review, 2026-10-05: `_conditions` is stored right after run_all, so a raise inside _confirm_and_save (here a describe() that meets an unexpected change shape) no longer loses what the conditions changed. """ good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0]) def bad_describe(result): raise KeyError("after") orig_by_name = flow.conditions.BY_NAME flow.conditions.BY_NAME = dict( orig_by_name, depression_dx_pointer=types.SimpleNamespace(describe=bad_describe)) try: data, out = _read_with([good], save_corrections=False) finally: flow.conditions.BY_NAME = orig_by_name assert data["_error"] == "'after'", data assert data["_conditions"]["fired"] == ["depression_dx_pointer"], data from ecw_bot.core import charge_summary as cs o = cs.claim_outcome(True, data) assert o["Category"] == "Review", o assert "Bot failed after the conditions ran ('after')" in o["Comments"], o assert "could not read" not in o["Comments"], o @case def an_approved_save_eCW_did_not_confirm_is_review_not_not_saved(): """ Final review, 2026-10-05: save_claim() answers False when OK WAS clicked and the form just did not hide in time — eCW may have written the claim. The row used to read "Corrections pending (not saved)", a guess stated as fact; it is now Review, saying the save is unconfirmed. """ good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0]) closed = [] data, out = _read_with([good], note=_READ_NOTE, ask=lambda *a, **k: True, save_claim=lambda page: False, closed=closed) cond = data["_conditions"] assert cond["saved"] is False and cond.get("save_unconfirmed") is True, cond assert closed == [True], "the form was not Cancelled after the failed save" from ecw_bot.core import charge_summary as cs o = cs.claim_outcome(True, data) assert o["Category"] == "Review", o assert o["Current Status"] == "Needs manual review", o assert cs.SAVE_UNCONFIRMED in o["Comments"], o assert "not saved" not in o["Current Status"], o # ============================================================================= # A form left on screen by a raise is still Cancelled (final review) # ============================================================================= def _raises_with_form_open(page, cid): raise RuntimeError("Opened the form for claim '5000001' after clicking " "'9103000'") @case def a_form_open_when_open_claim_raises_is_closed_with_cancel(): """ open_claim's UB wrong-claim check raises AFTER the form opened. That form used to be left open (the flow's `opened` was never set), and the next claim's click fallback would accept it as its own form. """ closed = [] data, _ = _read_with([], open_claim=_raises_with_form_open, closed=closed, any_open=lambda page: True) assert data["_error"].startswith("Opened the form for claim"), data assert closed == [True], "the form open on screen was not Cancelled" @case def nothing_is_cancelled_when_open_claim_raises_with_no_form_open(): closed = [] _read_with([], open_claim=_cannot_open, closed=closed, any_open=lambda page: False) assert closed == [], closed # ============================================================================= # A form that could not be read is not "not found" (review item D2, unit D) # ============================================================================= def _cannot_open(page, cid): raise RuntimeError("Timeout 30000ms exceeded.\nCall log:\n - waiting") @case def read_claim_form_returns_the_error_instead_of_an_empty_dict(): data, out = _read_with([], open_claim=_cannot_open) assert data == {"claim_no": "9103000", "_error": "Timeout 30000ms exceeded."}, data assert "[FORM 9103000] Could not read the form" in out, out @case def process_claims_counts_an_unreadable_form_as_not_read_and_reviews_it(): """ `_process_claims` with search and every page step stubbed: the form raises on open. It must count as found-but-not-read, the SHOW_EACH_CLAIM box must not run claim_form.summary() on the error dict, and the charge summary row must be Review, not blank. """ from ecw_bot.core import charge_summary as cs written, boxes = [], [] orig = (cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT, cfg.OPEN_CLAIM_FORM, msgbox.info_box, claim_form.open_claim, claim_form.close_claim, claim_form.summary, flow.claim_results.search_claim, flow.claim_filters.clear_claim_no, flow._write_charge_summary, cfg.RESULTS_DB) cfg.SHOW_EACH_CLAIM = True cfg.SHOW_CLAIM_IDS = False cfg.CLAIM_PROCESS_LIMIT = 0 cfg.OPEN_CLAIM_FORM = True msgbox.info_box = lambda t, m: boxes.append((t, m)) claim_form.open_claim = _cannot_open claim_form.close_claim = lambda page: None claim_form.summary = _forbidden flow.claim_results.search_claim = lambda page, cid: { "id": cid, "count": 1, "found": True, "seconds": 1.0} flow.claim_filters.clear_claim_no = lambda page: None flow._write_charge_summary = lambda info, outcomes: written.append(outcomes) # Never the real results database, whatever this machine's .env says. cfg.RESULTS_DB = False out = io.StringIO() try: with contextlib.redirect_stdout(out): flow._process_claims(None, {"ids": ["9103000"], "file": None}) finally: (cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT, cfg.OPEN_CLAIM_FORM, msgbox.info_box, claim_form.open_claim, claim_form.close_claim, claim_form.summary, flow.claim_results.search_claim, flow.claim_filters.clear_claim_no, flow._write_charge_summary, cfg.RESULTS_DB) = orig text = out.getvalue() assert ("1 found, 0 not found, 0 form(s) read, 1 could not be read" in text), text assert len(boxes) == 1 and \ "Could not read this claim: Timeout 30000ms exceeded." in boxes[0][1], \ boxes outcome = written[0]["9103000"] assert outcome == { "Comments": "Bot could not read the claim (Timeout 30000ms " "exceeded.)", "Current Status": "Needs manual review", "Category": "Review"}, outcome assert outcome != cs.claim_outcome(True, {}), "must not be blank" 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())