Snapshot of D:\Ecw Process a11c9ff (2026-10-30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
4.9 KiB
Python
169 lines
4.9 KiB
Python
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())
|