Snapshot of D:\Ecw Process a11c9ff (2026-10-30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
4.0 KiB
Python
141 lines
4.0 KiB
Python
r"""
|
|
flows/supervisor.py — the agent loop around the bot (operator, 2026-10-30):
|
|
run the day's queue to the end without a person, restarting on technical
|
|
failures and mailing what happened.
|
|
|
|
Rules (operator's choices that day):
|
|
* login failures (exit 1, 6): retry after 10, 20, then 30 minutes; the
|
|
3rd failure in a row stops the agent and mails it — the client's eCW
|
|
account must not be restricted;
|
|
* restricted (4), eCW screen changed (7), config (2): stop at once — no
|
|
retry can fix them;
|
|
* a stuck page (9), a stall (8), a claims error (5): restart with a fresh
|
|
Chrome and login for the claims still pending;
|
|
* the attempt guard said wait (3): wait, it is not a failure;
|
|
* nothing pending: done. A clean run that left the same claims pending
|
|
as before: stop, those are for a person.
|
|
|
|
python tests\test_supervisor.py
|
|
|
|
No bot, no browser, no mail, no database: all stand-ins.
|
|
"""
|
|
|
|
import sys
|
|
import pathlib
|
|
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
|
|
|
|
from ecw_bot.flows import supervisor as sv # noqa: E402
|
|
|
|
CASES = []
|
|
|
|
|
|
def case(fn):
|
|
CASES.append(fn)
|
|
return fn
|
|
|
|
|
|
def _decide(code, pending, state=None):
|
|
state = state if state is not None else sv.State()
|
|
return sv.next_action(code, pending, state), state
|
|
|
|
|
|
@case
|
|
def nothing_pending_is_done():
|
|
(action, _, _), _ = _decide(0, [])
|
|
assert action == "done"
|
|
|
|
|
|
@case
|
|
def login_failures_back_off_10_20_then_stop_on_the_third():
|
|
st = sv.State()
|
|
waits = []
|
|
for _ in range(2):
|
|
(action, wait, _), st = _decide(1, ["a"], st)
|
|
assert action == "retry"
|
|
waits.append(wait)
|
|
assert waits == [600, 1200], waits
|
|
(action, _, why), st = _decide(6, ["a"], st)
|
|
assert action == "stop" and "3 login" in why, why
|
|
|
|
|
|
@case
|
|
def a_good_login_resets_the_login_failures():
|
|
st = sv.State()
|
|
_decide(1, ["a"], st)
|
|
_decide(9, ["a"], st)
|
|
assert st.login_fails == 0
|
|
|
|
|
|
@case
|
|
def restricted_screen_changed_and_config_stop_at_once():
|
|
for code in (2, 4, 7):
|
|
(action, _, _), _ = _decide(code, ["a"])
|
|
assert action == "stop", code
|
|
|
|
|
|
@case
|
|
def a_stuck_page_restarts_soon():
|
|
(action, wait, _), _ = _decide(9, ["a", "b"])
|
|
assert action == "retry" and wait <= 120
|
|
|
|
|
|
@case
|
|
def the_guard_saying_wait_is_not_a_failure():
|
|
(action, wait, _), st = _decide(3, ["a"])
|
|
assert action == "retry" and wait >= 300 and st.login_fails == 0
|
|
|
|
|
|
@case
|
|
def a_clean_run_that_left_the_same_claims_stops():
|
|
st = sv.State()
|
|
(action, _, _), st = _decide(0, ["a", "b"], st)
|
|
assert action == "retry"
|
|
(action, _, why), st = _decide(0, ["a", "b"], st)
|
|
assert action == "stop" and "person" in why, why
|
|
|
|
|
|
@case
|
|
def pending_is_the_queue_minus_claims_done_since_the_agent_started():
|
|
queue = ["1", "2", "3", "4"]
|
|
rows = [("1", "Clean claims"), ("2", "Bot could not search the claim"),
|
|
("3", "Telehealth (POS 02 on T1017)")]
|
|
assert sv.pending(queue, rows) == ["2", "4"]
|
|
|
|
|
|
@case
|
|
def the_loop_restarts_mails_and_ends_when_nothing_is_left():
|
|
codes = [6, 9, 0]
|
|
left = [["1", "2"], ["2"], []]
|
|
runs, mails, slept = [], [], []
|
|
st = sv.run_loop(
|
|
run_bot=lambda resume: runs.append(resume) or codes.pop(0),
|
|
pending_now=lambda: left.pop(0),
|
|
mail=lambda kind, *a: mails.append(kind),
|
|
sleep=slept.append)
|
|
assert runs == [None, ["1", "2"], ["2"]], runs
|
|
assert slept == [600, 60], slept
|
|
assert mails == ["incident", "incident", "summary"], mails
|
|
assert st.outcome == "done"
|
|
|
|
|
|
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())
|