Self-healing agent (supervisor, recovery, login fixes, operator mail) and Portal sage redesign
Snapshot of D:\Ecw Process a11c9ff (2026-10-30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fafa5285eb
commit
d500f750da
26 files changed
+1704
-357
No files matched your search
@@ -0,0 +1,168 @@
|
||||
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())
|
||||
Reference in new issue
Block a user