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>
244 lines
9.8 KiB
Python
244 lines
9.8 KiB
Python
"""
|
||
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())
|