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())