""" The Runs page's reading of `Logs\\events_.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("") _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())