r""" flows/recovery.py — the run gets itself out of a stuck screen (operator, 2026-10-30: "you can close or refresh and re-login and continue the agent"). Live 2026-10-29: a Dental claim's sliding fee check left Patient Information open; its overlay took every click, and the next 18 claims were each marked "Bot could not search the claim" without being looked at. Now a claim the bot could not search or read sets off a recovery: 1. the login page on screen = the session ended → PageStuck (re-login); 2. Escape, close Patient Information, clear the screen → search ready? 3. reopen the claim search page → search ready? 4. still blocked → PageStuck. After a recovery the same claim is tried once more. Two claims in a row that still fail after their recovery → PageStuck, so the supervisor restarts Chrome and logs in again. python tests\test_recovery.py No browser: the page steps are stand-ins. """ import sys import pathlib sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src")) from ecw_bot.flows import recovery # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn class Steps: """Stand-ins for the page steps; `blocked` = what search_blocker says, in order, one per look.""" def __init__(self, blocked, logged_out=False): self.blocked = list(blocked) self.logged_out = logged_out self.done = [] def install(self): recovery._logged_out = lambda page: self.logged_out recovery._escape_dialogs = lambda page: self.done.append("escape") recovery._clear = lambda page, cid, keep: self.done.append("clear") recovery._blocker = lambda page: self.blocked.pop(0) def reopen(self, page): self.done.append("reopen") @case def a_dialog_cleared_by_escape_needs_no_reopen(): s = Steps([None]) s.install() assert recovery.recover(None, "1", (), s.reopen) == "cleared" assert s.done == ["escape", "clear"], s.done @case def a_page_still_blocked_is_reopened(): s = Steps(["covered by div.modalOverlay", None]) s.install() assert recovery.recover(None, "1", (), s.reopen) == "reopened" assert s.done == ["escape", "clear", "reopen"], s.done @case def a_page_blocked_after_reopen_is_stuck(): s = Steps(["covered", "covered"]) s.install() try: recovery.recover(None, "1", (), s.reopen) except recovery.PageStuck as exc: assert "covered" in str(exc), exc else: raise AssertionError("no PageStuck") @case def the_login_page_means_the_session_ended(): s = Steps([], logged_out=True) s.install() try: recovery.recover(None, "1", (), s.reopen) except recovery.PageStuck as exc: assert "login page" in str(exc), exc else: raise AssertionError("no PageStuck") assert s.done == [], s.done def _attempts(*outcomes): """attempt() returning each outcome in turn: 'ok' or 'bot'.""" seq = list(outcomes) def attempt(): o = seq.pop(0) return ({"search_error": "x"}, {}) if o == "bot" else ({}, {"cpt": []}) return attempt @case def a_claim_that_fails_is_recovered_and_tried_again(): tried = [] state = {} r, data = recovery.run_claim(_attempts("bot", "ok"), lambda: tried.append(1), state) assert tried == [1] and not r.get("search_error"), (tried, r) assert state["streak"] == 0 @case def a_claim_that_fails_again_is_kept_as_review(): state = {} r, _ = recovery.run_claim(_attempts("bot", "bot"), lambda: None, state) assert r.get("search_error") and state["streak"] == 1 @case def two_claims_in_a_row_failing_after_recovery_stop_the_run(): state = {} recovery.run_claim(_attempts("bot", "bot"), lambda: None, state) try: recovery.run_claim(_attempts("bot", "bot"), lambda: None, state) except recovery.PageStuck: pass else: raise AssertionError("no PageStuck after two in a row") @case def a_good_claim_resets_the_streak(): state = {} recovery.run_claim(_attempts("bot", "bot"), lambda: None, state) recovery.run_claim(_attempts("ok"), lambda: None, state) assert state["streak"] == 0 def main() -> int: failures = 0 real = {k: getattr(recovery, k, None) for k in ("_logged_out", "_escape_dialogs", "_clear", "_blocker")} 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 finally: for k, v in real.items(): setattr(recovery, k, v) print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())