Files
ecw-charge-validation/tests/test_runs_view.py
T
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

244 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
The Runs page's reading of `Logs\\events_<run>.jsonl` — `web/runs.py`
(spec 2026-10-22 Stage 1, EEWA-31).
python tests\\test_runs_view.py
What has to hold:
* **A Run's result**: Done (exit 0), Run Error (any other exit, with the
step and message of its Run Error), Stopped (the Portal's `stopped`
event), Running (no end yet and its process is the live one), Ended
without a record (no end, not live — killed).
* **Each eCW Login Attempt**: start, end, duration, result, every step's
time, and the 2FA wait = mail received − 2FA requested.
* **Counts**: Pass / Fail / Review / Not found, Claim Errors, Warnings.
* **Not Reached**: queued claims never started — only once the Run is
over and did not finish.
* **A half-written last line** (a killed Run) is skipped, not fatal.
* **Only real Run ids and only files inside Screenshots\\** are served.
* Old runs with no events file are not listed.
"""
import sys
import json
import pathlib
import tempfile
from datetime import datetime
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
from web import runs # noqa: E402
CASES = []
NOW = datetime(2026, 10, 22, 15, 0, 0)
def case(fn):
CASES.append(fn)
return fn
def ev(t, kind, **f):
return {"t": f"2026-10-22T{t}.000", "kind": kind, **f}
def _write(d, run_id, events, tail=""):
p = pathlib.Path(d) / f"events_{run_id}.jsonl"
p.write_text("".join(json.dumps(e) + "\n" for e in events) + tail,
encoding="utf-8")
return p
START = ev("14:00:00", "run_start", source="portal", started_by="asha",
mode="VERIFY", claim_limit=3, test_sheet="s.xlsx", pid=1, log="x")
LOGIN = [
ev("14:00:01", "login_attempt_start", attempt=1),
ev("14:00:05", "login_step", attempt=1, step="page_open"),
ev("14:00:09", "login_step", attempt=1, step="password"),
ev("14:00:10", "login_step", attempt=1, step="twofa_requested"),
ev("14:09:10", "login_attempt_end", attempt=1, result="twofa_timeout",
message="no mail"),
ev("14:09:20", "login_attempt_start", attempt=2),
ev("14:09:25", "login_step", attempt=2, step="page_open"),
ev("14:09:30", "login_step", attempt=2, step="twofa_requested"),
ev("14:10:12", "login_step", attempt=2, step="mail_received",
mail_kind="link"),
ev("14:10:15", "login_step", attempt=2, step="approval_clicked"),
ev("14:10:20", "login_step", attempt=2, step="home"),
ev("14:10:20", "login_attempt_end", attempt=2, result="ok", message=""),
]
CLAIMS = [
ev("14:10:30", "queue", claims=["A1", "A2", "A3"]),
ev("14:10:31", "claim_start", n=1, total=3, claim="A1"),
ev("14:10:50", "claim_result", n=1, claim="A1", category="Pass"),
ev("14:10:51", "claim_start", n=2, total=3, claim="A2"),
ev("14:11:00", "claim_error", claim="A2", step="form", message="bad"),
ev("14:11:01", "claim_result", n=2, claim="A2", category="Review"),
ev("14:11:02", "warning", code="fallback", message="Lookup alt"),
]
@case
def a_finished_run_is_done_with_its_counts_and_login_attempts():
d = tempfile.mkdtemp()
_write(d, "20261022_140000", [START, *LOGIN, *CLAIMS,
ev("14:11:03", "claim_start", n=3, total=3, claim="A3"),
ev("14:11:20", "claim_result", n=3, claim="A3", category="Fail"),
ev("14:11:30", "run_end", exit_code=0)])
r = runs.load_run(d, "20261022_140000", live_run_id=None, now=NOW)
assert r["status"] == "Done" and r["exit_code"] == 0, r["status"]
assert r["started_by"] == "asha" and r["source"] == "portal", r
assert r["duration_s"] == 690, r["duration_s"]
assert (r["pass"], r["fail"], r["review"], r["not_found"]) == (1, 1, 1, 0)
assert len(r["claim_errors"]) == 1 and len(r["warnings"]) == 1, r
assert r["not_reached"] == [], r["not_reached"]
a1, a2 = r["login_attempts"]
assert a1["result"] == "twofa_timeout" and a1["duration_s"] == 549, a1
assert a1["twofa_wait_s"] is None, a1 # no mail ever came
assert a2["result"] == "ok" and a2["duration_s"] == 60, a2
assert a2["twofa_wait_s"] == 42, a2
assert a2["steps"]["home"].endswith("14:10:20.000"), a2["steps"]
assert r["login_total_s"] == 609, r["login_total_s"]
@case
def a_run_error_names_its_step_and_lists_the_claims_not_reached():
d = tempfile.mkdtemp()
_write(d, "20261022_140000", [START, *LOGIN, *CLAIMS,
ev("14:11:05", "run_error", step="claims", exit_code=5,
message="Target closed", traceback="Traceback ...\nError"),
ev("14:11:06", "run_end", exit_code=5)])
r = runs.load_run(d, "20261022_140000", live_run_id=None, now=NOW)
assert r["status"] == "Run Error" and r["exit_code"] == 5, r["status"]
assert r["error_step"] == "claims" and r["error_message"] == "Target closed"
assert r["run_errors"][0]["traceback"].startswith("Traceback"), r
assert r["not_reached"] == ["A3"], r["not_reached"]
@case
def a_running_run_has_no_not_reached_and_counts_up_to_now():
d = tempfile.mkdtemp()
_write(d, "20261022_140000", [START, *LOGIN, *CLAIMS])
r = runs.load_run(d, "20261022_140000", live_run_id="20261022_140000",
now=NOW)
assert r["status"] == "Running", r["status"]
assert r["duration_s"] == 3600, r["duration_s"]
assert r["not_reached"] == [], r["not_reached"]
@case
def a_killed_run_and_a_stopped_run():
d = tempfile.mkdtemp()
_write(d, "20261022_140000", [START, *LOGIN, *CLAIMS])
r = runs.load_run(d, "20261022_140000", live_run_id=None, now=NOW)
assert r["status"] == "Ended without a record", r["status"]
assert r["not_reached"] == ["A3"], r["not_reached"]
_write(d, "20261022_140000", [START, *LOGIN, *CLAIMS,
ev("14:12:00", "stopped", stopped_by="asha")])
r = runs.load_run(d, "20261022_140000", live_run_id=None, now=NOW)
assert r["status"] == "Stopped" and r["stopped_by"] == "asha", r
assert r["duration_s"] == 720, r["duration_s"]
@case
def a_half_written_last_line_is_skipped():
d = tempfile.mkdtemp()
_write(d, "20261022_140000", [START], tail='{"t": "2026-10-22T14:0')
r = runs.load_run(d, "20261022_140000", live_run_id=None, now=NOW)
assert r["status"] == "Ended without a record", r
@case
def the_list_is_newest_first_and_filters_by_date_and_result():
d = tempfile.mkdtemp()
_write(d, "20261021_090000", [
{**START, "t": "2026-10-21T09:00:00.000"},
{"t": "2026-10-21T09:05:00.000", "kind": "run_error",
"step": "twofa_timeout", "exit_code": 6, "message": "x",
"traceback": ""},
{"t": "2026-10-21T09:05:00.000", "kind": "run_end", "exit_code": 6}])
_write(d, "20261022_140000", [START, ev("14:01:00", "run_end",
exit_code=0)])
(pathlib.Path(d) / "run_20261020_080000.log").write_text("old")
(pathlib.Path(d) / "events_bad.jsonl").write_text("{}")
rows = runs.list_runs(d, live_run_id=None, now=NOW)
assert [r["run_id"] for r in rows] == ["20261022_140000",
"20261021_090000"], rows
rows = runs.list_runs(d, live_run_id=None, now=NOW,
day_from="2026-10-22")
assert [r["run_id"] for r in rows] == ["20261022_140000"], rows
rows = runs.list_runs(d, live_run_id=None, now=NOW,
day_to="2026-10-21", status="Run Error")
assert [r["run_id"] for r in rows] == ["20261021_090000"], rows
@case
def the_timeline_has_a_line_for_every_event_in_order():
d = tempfile.mkdtemp()
_write(d, "20261022_140000", [START, *LOGIN[:3]])
r = runs.load_run(d, "20261022_140000", live_run_id=None, now=NOW)
assert [e["kind"] for e in r["timeline"]] == [
"run_start", "login_attempt_start", "login_step", "login_step"], r
assert all(e["text"] for e in r["timeline"]), r["timeline"]
@case
def a_bad_run_id_or_missing_run_is_none():
d = tempfile.mkdtemp()
assert runs.load_run(d, "..\\..\\x", live_run_id=None, now=NOW) is None
assert runs.load_run(d, "20261022_140001", live_run_id=None,
now=NOW) is None
assert not runs.valid_run_id("20261022_14000a")
@case
def a_screenshot_is_served_only_from_inside_the_screenshots_folder():
d = pathlib.Path(tempfile.mkdtemp())
shots = d / "Screenshots" / "10-22-2026"
shots.mkdir(parents=True)
png = shots / "140500_failed.png"
png.write_bytes(b"png")
outside = d / "secret.png"
outside.write_bytes(b"no")
_write(d, "20261022_140000", [START,
ev("14:05:00", "screenshot", path=str(png), tag="failed"),
ev("14:05:01", "screenshot", path=str(outside), tag="failed"),
ev("14:05:02", "screenshot", path=str(shots / "gone.png"),
tag="failed")])
root = d / "Screenshots"
assert runs.screenshot_path(d, "20261022_140000", 0, root) == png
assert runs.screenshot_path(d, "20261022_140000", 1, root) is None
assert runs.screenshot_path(d, "20261022_140000", 2, root) is None
assert runs.screenshot_path(d, "20261022_140000", 9, root) is None
page = shots / "140500_failed.html" # eCW's own scripts
page.write_text("<script></script>")
_write(d, "20261022_140001", [START,
ev("14:05:00", "screenshot", path=str(page), tag="failed")])
assert runs.screenshot_path(d, "20261022_140001", 0, root) is None
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())