Files
ManikandanandClaude Opus 5.5 ba15a574ab Initial import: eCW Charge Validation bot and results Portal
Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one
commit with no earlier history: the tracked files were de-identified
first (synthetic claim ids and patient data only; see Context.md
section 7). Credentials, run logs, screenshots, claim reports and client
workbooks are gitignored and not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:38:17 +05:30

343 lines
12 KiB
Python

r"""
web/run_status.py — the bot's run log read into the page's live status.
python tests\test_run_status.py
"""
import sys
import pathlib
import tempfile
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
from web import run_status as rs # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
def _status(**over):
"""The runner's status (web/bot_runner.py): no run yet, unless `over`
says otherwise."""
st = {"running": False, "starting": False, "log_path": None,
"out_path": None, "exit_code": None, "stopped_by_operator": False,
"started_at": None, "ended_at": None}
st.update(over)
return st
LOG = """\
======================================================================
2026-10-14 10:00:00 — run start
[CONFIG] user=x otp=imap
[LOGIN] Opening the login page
[OTP] Waiting for the approval mail
[OK] Logged in in 95s
[CLAIMS] --- claim 1/3: 9000001 ---
[RESULT] claim 1/3 9000001: Pass
[CLAIMS] --- claim 2/3: 9000002 ---
[CLAIMS] --- claim 2/3 done, moving to next ---
[RESULT] claim 2/3 9000002: Fail
[CLAIMS] --- claim 3/3: 9000003 ---
"""
@case
def phase_follows_the_last_tagged_line():
assert rs.parse("[LOGIN] x")["phase"] == "Logging in"
assert rs.parse("[LOGIN] x\n[2FA] y")["phase"] == \
"Waiting for the 2FA approval mail"
assert rs.parse("[OTP] y\n[OK] Logged in in 3s")["phase"] == "Logged in"
assert rs.parse(LOG)["phase"] == "Checking claims"
assert rs.parse("")["phase"] == "Starting"
@case
def progress_and_current_claim_come_from_the_claim_header():
p = rs.parse(LOG)
assert p["progress"] == [3, 3] and p["current"] == "9000003", p
@case
def a_done_line_is_not_a_claim_header():
p = rs.parse("[CLAIMS] --- claim 2/3 done, moving to next ---")
assert p["progress"] is None, p
@case
def counts_come_from_result_lines():
c = rs.parse(LOG + "[RESULT] claim 3/3 9000003: Review\n")["counts"]
assert c == {"Pass": 1, "Fail": 1, "Review": 1, "Not found": 0}, c
@case
def tail_is_the_last_lines():
text = "\n".join(f"line {i}" for i in range(100))
tail = rs.parse(text)["tail"]
assert len(tail) == rs.TAIL_LINES and tail[-1] == "line 99", tail
@case
def guard_line_is_kept():
p = rs.parse("[GUARD] Refused: next attempt allowed at 10:40")
assert p["guard"] == "[GUARD] Refused: next attempt allowed at 10:40"
@case
def exit_codes_read_as_plain_words():
assert rs.exit_text(0) == "Finished"
assert "setting" in rs.exit_text(2)
assert "guard" in rs.exit_text(3)
assert "claims step failed" in rs.exit_text(5)
assert "2FA" in rs.exit_text(6)
assert "restricted" in rs.exit_text(4) and "do not retry" in rs.exit_text(4)
assert rs.exit_text(1) == "The run failed (see the log)"
assert rs.exit_text(9) == "Ended with code 9"
assert rs.exit_text(None, stopped=True) == "Stopped by operator"
assert "no exit code" in rs.exit_text(None)
@case
def read_tail_reads_the_end_of_a_file_and_missing_is_empty():
with tempfile.TemporaryDirectory() as tmp:
p = pathlib.Path(tmp) / "run.log"
p.write_text("a\n" * 1000 + "LAST\n", encoding="utf-8")
assert rs.read_tail(p, max_bytes=100).endswith("LAST\n")
assert rs.read_tail(pathlib.Path(tmp) / "none.log") == ""
assert rs.read_tail(None) == ""
@case
def summarize_running_uses_the_phase_as_the_message():
with tempfile.TemporaryDirectory() as tmp:
p = pathlib.Path(tmp) / "run.log"
p.write_text(LOG, encoding="utf-8")
s = rs.summarize(_status(running=True, log_path=str(p),
started_at="2026-10-14T10:00:00", pid=123,
source="web"))
assert s["message"] == "Checking claims", s
assert s["progress"] == [3, 3], s
for k in ("log_path", "out_path", "pid"):
assert k not in s, (k, s)
assert set(s) == {"running", "starting", "started_at", "ended_at",
"exit_code", "stopped_by_operator", "source", "phase",
"progress", "current", "counts", "tail", "message",
"elapsed_s", "claims", "results_run_at",
"started_by"}, set(s)
@case
def summarize_ended_says_how_and_how_long():
with tempfile.TemporaryDirectory() as tmp:
p = pathlib.Path(tmp) / "run.log"
p.write_text("[GUARD] Refused: next attempt allowed at 10:40\n",
encoding="utf-8")
s = rs.summarize(_status(log_path=str(p), exit_code=3,
started_at="2026-10-14T10:00:00",
ended_at="2026-10-14T10:04:12"))
assert s["message"].startswith("Login guard refused"), s
assert "next attempt allowed at 10:40" in s["message"], s
assert "[GUARD]" not in s["message"], s
assert s["elapsed_s"] == 252, s
QUEUE_LOG = """\
[CLAIMS] Queue: 9000001, 9000002, 9000003, 9000004
[CLAIMS] --- claim 1/4: 9000001 ---
[RESULT] claim 1/4 9000001: Pass
[CLAIMS] --- claim 2/4: 9000002 ---
[RESULT] claim 2/4 9000002: Fail
[CLAIMS] --- claim 3/4: 9000003 ---
"""
@case
def claims_lists_every_queued_claim_with_its_status():
"""The Claim status panel (operator, 2026-10-15): every claim of the
run — done with its result, the one being checked, the ones waiting."""
claims = rs.parse(QUEUE_LOG)["claims"]
assert claims == [
{"n": 1, "id": "9000001", "status": "Pass", "done": True},
{"n": 2, "id": "9000002", "status": "Fail", "done": True},
{"n": 3, "id": "9000003", "status": "Checking", "done": False},
{"n": 4, "id": "9000004", "status": "Waiting", "done": False},
], claims
@case
def claims_without_a_queue_line_come_from_the_claim_lines():
"""An older log, written before the Queue line existed."""
claims = rs.parse(LOG)["claims"]
assert [c["id"] for c in claims] == ["9000001", "9000002", "9000003"]
assert [c["status"] for c in claims] == ["Pass", "Fail", "Checking"]
@case
def a_claim_left_half_way_is_not_finished_once_the_run_ended():
def summary(running):
with tempfile.TemporaryDirectory() as tmp:
p = pathlib.Path(tmp) / "run.log"
p.write_text(QUEUE_LOG, encoding="utf-8")
return rs.summarize(_status(
running=running, log_path=str(p),
stopped_by_operator=not running,
started_at="2026-10-15T10:00:00",
ended_at=None if running else "2026-10-15T10:05:00"))
live = summary(True)["claims"]
assert live[2]["status"] == "Checking" and live[3]["status"] == "Waiting"
ended = summary(False)["claims"]
assert ended[2]["status"] == "Not finished", ended
assert ended[3]["status"] == "Not checked", ended
assert [c["done"] for c in ended] == [True, True, False, False], ended
@case
def a_not_found_claim_is_done_with_no_result_category():
"""Not found ends the claim (it counts as done) — the panel shows it
done, but it has no results row to open."""
claims = rs.parse("[CLAIMS] Queue: 9000001, 9000002\n"
"[CLAIMS] --- claim 1/2: 9000001 ---\n"
"[RESULT] claim 1/2 9000001: Not found\n")["claims"]
assert claims[0] == {"n": 1, "id": "9000001", "status": "Not found",
"done": True}, claims
assert claims[1]["done"] is False, claims
@case
def the_results_run_at_line_names_the_run_the_claims_are_saved_in():
"""Review 2026-10-19: the Claim status panel opened a claim in the run
the table showed, not the one it was saved in. The bot names that run
right after the Queue line (only with RESULTS_DB=1)."""
text = ("[CLAIMS] Queue: 9000001, 9000002\n"
"[CLAIMS] Results run_at: 2026-10-16T19:08:37\n"
"[CLAIMS] --- claim 1/2: 9000001 ---\n")
assert rs.parse(text)["results_run_at"] == "2026-10-16T19:08:37"
with tempfile.TemporaryDirectory() as tmp:
p = pathlib.Path(tmp) / "run.log"
p.write_text(text, encoding="utf-8")
s = rs.summarize(_status(running=True, log_path=str(p)))
assert s["results_run_at"] == "2026-10-16T19:08:37", s
@case
def no_results_run_at_line_is_none():
"""RESULTS_DB off (or an older log): no line, no saved run."""
assert rs.parse(QUEUE_LOG)["results_run_at"] is None
assert rs.parse("")["results_run_at"] is None
assert rs.summarize(_status())["results_run_at"] is None
@case
def a_later_run_in_the_same_log_drops_the_earlier_runs_time():
"""A new Queue line starts a new run: a run_at from before it is not
this run's."""
text = ("[CLAIMS] Queue: 9000001\n"
"[CLAIMS] Results run_at: 2026-10-16T19:08:37\n"
"[CLAIMS] Queue: 9000002\n")
assert rs.parse(text)["results_run_at"] is None
@case
def a_results_run_at_that_is_not_a_time_is_ignored():
"""It goes into the page's link: only the bot's own ISO form counts."""
for bad in ("[CLAIMS] Results run_at: 2026-10-16T19:08:37&x=1",
"[CLAIMS] Results run_at: soon",
"[CLAIMS] Results run_at: "):
assert rs.parse(bad)["results_run_at"] is None, bad
@case
def a_run_that_ended_with_no_end_time_has_no_duration():
"""Review 2026-10-14: a run killed outside the page (or crashed) never
wrote ended_at; measured to now(), its duration grew on every poll —
"Ended ... in 87m 3s". No end time, no duration. A running run still
counts up to now."""
ended = rs.summarize(_status(started_at="2026-10-14T10:00:00"))
assert ended["elapsed_s"] is None, ended
running = rs.summarize(_status(running=True,
started_at="2026-10-14T10:00:00"))
assert isinstance(running["elapsed_s"], int), running
@case
def summarize_starting_reads_the_child_output_file():
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp) / "web_run.out"
out.write_text("Traceback: boom\n", encoding="utf-8")
s = rs.summarize(_status(running=True, starting=True,
out_path=str(out)))
assert s["message"] == "Starting" and s["tail"] == ["Traceback: boom"], s
@case
def a_done_line_with_extra_words_is_not_a_claim_header():
p = rs.parse("[CLAIMS] --- claim 2/3 done (shown in its review box), "
"moving to next ---")
assert p["progress"] is None and p["current"] is None, p
@case
def a_crlf_log_parses_the_same():
a, b = rs.parse(LOG), rs.parse(LOG.replace("\n", "\r\n"))
for k in ("phase", "progress", "current", "counts", "guard"):
assert a[k] == b[k], (k, a[k], b[k])
assert b["tail"] == a["tail"], b["tail"]
@case
def summarize_counts_the_whole_log_not_just_its_tail():
with tempfile.TemporaryDirectory() as tmp:
p = pathlib.Path(tmp) / "run.log"
body = ("[RESULT] claim 1/9 111: Pass\n"
"[RESULT] claim 2/9 222: Fail\n"
+ "[CLAIMS] noise line to push the results out of the tail\n"
* 10_000
+ "[CLAIMS] --- claim 9/9: 999 ---\n")
p.write_text(body, encoding="utf-8")
assert p.stat().st_size > 256_000
s = rs.summarize(_status(running=True, log_path=str(p)))
assert s["counts"]["Pass"] == 1 and s["counts"]["Fail"] == 1, s["counts"]
assert s["progress"] == [9, 9] and len(s["tail"]) == rs.TAIL_LINES, s
@case
def the_tail_hides_the_otp_code_and_the_username():
text = ("[CONFIG] user=jsmith otp=imap proxy=-:- headless=False\n"
"[OTP] Code 482913 in 'Your code 482913' (sent 10:01:02)\n"
"[OTP] Approval link in 'Sign-in' (sent 10:01:02)\n")
p = rs.parse(text)
assert p["tail"] == [
"[CONFIG] user=(hidden) otp=imap proxy=-:- headless=False",
"[OTP] Code (hidden)",
"[OTP] Approval link in 'Sign-in' (sent 10:01:02)"], p["tail"]
joined = "\n".join(p["tail"])
assert "482913" not in joined and "jsmith" not in joined, joined
assert p["phase"] == "Waiting for the 2FA approval mail", p
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())