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>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-23 18:38:17 +05:30
commit ba15a574ab
205 files changed
+56772

No files matched your search

+243
View File
@@ -0,0 +1,243 @@
"""
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())