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>
169 lines
5.5 KiB
Python
169 lines
5.5 KiB
Python
r"""
|
|
The Portal's Runs pages — `/runs`, `/runs/<id>`, `/runs/<id>/shot/<n>`
|
|
(spec 2026-10-22 Stage 1, EEWA-31), against events files in a temp folder
|
|
and the fake repository / runner of test_web_app.py.
|
|
|
|
python tests\test_runs_page.py
|
|
|
|
Claim numbers here are made up.
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import pathlib
|
|
import tempfile
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
sys.path.insert(0, str(ROOT / "tests"))
|
|
|
|
from test_web_app import FakeRepo, FakeRunner, signed_in # noqa: E402
|
|
|
|
CASES = []
|
|
RUN = "20261022_140000"
|
|
|
|
|
|
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 _setup(events, runner=None, run_id=RUN):
|
|
d = pathlib.Path(tempfile.mkdtemp())
|
|
(d / f"events_{run_id}.jsonl").write_text(
|
|
"".join(json.dumps(e) + "\n" for e in events), encoding="utf-8")
|
|
shots = d / "Screenshots"
|
|
shots.mkdir()
|
|
client, _ = signed_in(FakeRepo(), runner or FakeRunner(), logs_dir=d,
|
|
screenshots_root=shots)
|
|
return client, d, shots
|
|
|
|
|
|
START = ev("14:00:00", "run_start", source="portal", started_by="asha",
|
|
mode="VERIFY", claim_limit=2, test_sheet="", pid=1, log="x")
|
|
ERROR_RUN = [
|
|
START,
|
|
ev("14:00:01", "login_attempt_start", attempt=1),
|
|
ev("14:00:10", "login_step", attempt=1, step="twofa_requested"),
|
|
ev("14:00:52", "login_step", attempt=1, step="mail_received",
|
|
mail_kind="link"),
|
|
ev("14:01:00", "login_attempt_end", attempt=1, result="ok", message=""),
|
|
ev("14:01:10", "queue", claims=["9001", "9002"]),
|
|
ev("14:01:11", "claim_start", n=1, total=2, claim="9001"),
|
|
ev("14:01:20", "claim_error", claim="9001", step="search",
|
|
message="<b>timeout</b>"),
|
|
ev("14:01:21", "claim_result", n=1, claim="9001", category="Review"),
|
|
ev("14:01:22", "run_error", step="claims", exit_code=5,
|
|
message="Target closed", traceback="Traceback (most recent)\nX"),
|
|
ev("14:01:23", "run_end", exit_code=5),
|
|
]
|
|
|
|
|
|
@case
|
|
def the_runs_list_shows_a_run_with_its_result_and_counts():
|
|
client, _, _ = _setup(ERROR_RUN)
|
|
page = client.get("/runs").text
|
|
assert f'href="/runs/{RUN}"' in page, page
|
|
assert "asha" in page and "Run Error" in page and "exit 5" in page
|
|
assert "1 attempt · 59s" in page, "login count + time"
|
|
|
|
|
|
@case
|
|
def the_runs_list_filters_and_rejects_a_bad_filter():
|
|
client, _, _ = _setup(ERROR_RUN)
|
|
assert RUN not in client.get("/runs?status=Done").text
|
|
assert RUN in client.get("/runs?status=Run+Error").text
|
|
assert RUN not in client.get("/runs?day_from=2026-10-23").text
|
|
assert client.get("/runs?day_from=yesterday").status_code == 400
|
|
assert client.get("/runs?status=Bogus").status_code == 400
|
|
|
|
|
|
@case
|
|
def a_runs_page_has_login_errors_not_reached_and_timeline():
|
|
client, _, _ = _setup(ERROR_RUN)
|
|
page = client.get(f"/runs/{RUN}").text
|
|
assert "eCW Login Attempts" in page and "42s" in page, "2FA wait"
|
|
assert "Run Error" in page and "Traceback (most recent)" in page
|
|
assert "Claim Errors (1)" in page
|
|
assert "Not Reached (1)" in page and "9002" in page
|
|
assert "Timeline" in page and "Run ended, exit 5" in page
|
|
assert 'http-equiv="refresh"' not in page, "an ended run does not poll"
|
|
|
|
|
|
@case
|
|
def event_text_reaches_the_page_escaped():
|
|
client, _, _ = _setup(ERROR_RUN)
|
|
page = client.get(f"/runs/{RUN}").text
|
|
assert "<b>timeout</b>" not in page
|
|
assert "<b>timeout</b>" in page
|
|
|
|
|
|
@case
|
|
def a_running_run_refreshes_every_five_seconds():
|
|
runner = FakeRunner(running=True)
|
|
st = runner._st
|
|
runner._st = lambda: {**st(), "log_path": rf"D:\x\Logs\run_{RUN}.log"}
|
|
client, _, _ = _setup(ERROR_RUN[:7], runner=runner)
|
|
page = client.get(f"/runs/{RUN}").text
|
|
assert "Running" in page, page
|
|
assert '<meta http-equiv="refresh" content="5">' in page
|
|
|
|
|
|
@case
|
|
def an_unknown_or_bad_run_is_404():
|
|
client, _, _ = _setup(ERROR_RUN)
|
|
assert client.get("/runs/20261022_140001").status_code == 404
|
|
assert client.get("/runs/..%5C..%5Cx").status_code in (404, 400)
|
|
|
|
|
|
@case
|
|
def a_screenshot_is_served_and_nothing_else():
|
|
d = pathlib.Path(tempfile.mkdtemp())
|
|
client, d, shots = _setup([START])
|
|
png = shots / "140100_failed.png"
|
|
png.write_bytes(b"\x89PNG")
|
|
(d / f"events_{RUN}.jsonl").write_text(
|
|
json.dumps(START) + "\n" + json.dumps(
|
|
ev("14:01:00", "screenshot", path=str(png), tag="failed")) + "\n",
|
|
encoding="utf-8")
|
|
r = client.get(f"/runs/{RUN}/shot/0")
|
|
assert r.status_code == 200 and r.content == b"\x89PNG", r.status_code
|
|
assert client.get(f"/runs/{RUN}/shot/1").status_code == 404
|
|
page = client.get(f"/runs/{RUN}").text
|
|
assert f'/runs/{RUN}/shot/0' in page
|
|
|
|
|
|
@case
|
|
def every_page_links_the_runs_page_and_carries_no_inline_style():
|
|
client, _, _ = _setup(ERROR_RUN)
|
|
for path in ("/", "/runs", f"/runs/{RUN}"):
|
|
page = client.get(path).text
|
|
assert 'href="/runs"' in page, path
|
|
assert " style=" not in page, path
|
|
|
|
|
|
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())
|