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>
279 lines
9.1 KiB
Python
279 lines
9.1 KiB
Python
r"""
|
|
Stage 3 (EEWA-33, spec 2026-10-22): the Dashboard's banner for a Run that
|
|
ended badly, and Started By in `dbo.CV_ChargeSummary`.
|
|
|
|
python tests\test_run_alerts.py
|
|
|
|
* **Banner**: the latest Run ended in a Run Error, or without a record
|
|
(killed) → a red banner naming when, who, the step and a link to the
|
|
Run. Done, Stopped and Running → no banner.
|
|
* **started_by** is written only once its column exists — a table not yet
|
|
updated must never cost a Run its rows — and the Portal shows it in a
|
|
claim's side panel; a Portal reading a table without it says how to fix
|
|
that.
|
|
|
|
Claim numbers here are made up.
|
|
"""
|
|
|
|
import io
|
|
import os
|
|
import sys
|
|
import json
|
|
import pathlib
|
|
import tempfile
|
|
import contextlib
|
|
|
|
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 ecw_bot import settings as cfg # noqa: E402
|
|
from ecw_bot.core import results_db as rdb # noqa: E402
|
|
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
|
|
from web import runs as run_view # noqa: E402
|
|
from web.repo import SqlResultsRepo # noqa: E402
|
|
from test_web_app import (FakeRepo, FakeRunner, signed_in, _row, # noqa: E402
|
|
_quiet, DAY)
|
|
from test_results_db import _row as _sheet_row, _sheet # noqa: E402
|
|
|
|
CASES = []
|
|
|
|
|
|
class DbError(Exception):
|
|
"""A pyodbc error: SQLSTATE first, as the driver gives it."""
|
|
|
|
|
|
def case(fn):
|
|
CASES.append(fn)
|
|
return fn
|
|
|
|
|
|
def ev(t, kind, **f):
|
|
return {"t": f"2026-10-22T{t}.000", "kind": kind, **f}
|
|
|
|
|
|
START = ev("14:00:00", "run_start", source="portal", started_by="olu",
|
|
mode="VERIFY", claim_limit=2, test_sheet="", pid=1, log="x")
|
|
|
|
|
|
def _logs(*runs):
|
|
d = pathlib.Path(tempfile.mkdtemp())
|
|
for run_id, events in runs:
|
|
(d / f"events_{run_id}.jsonl").write_text(
|
|
"".join(json.dumps(e) + "\n" for e in events), encoding="utf-8")
|
|
return d
|
|
|
|
|
|
ERRORED = [START, ev("14:00:05", "queue", claims=["9001", "9002"]),
|
|
ev("14:00:06", "claim_start", n=1, total=2, claim="9001"),
|
|
ev("14:01:00", "run_error", step="claims", exit_code=5,
|
|
message="Target closed", traceback=""),
|
|
ev("14:01:01", "run_end", exit_code=5)]
|
|
DONE = [{**START, "t": "2026-10-22T15:00:00.000"},
|
|
{"t": "2026-10-22T15:01:00.000", "kind": "run_end", "exit_code": 0}]
|
|
|
|
|
|
# ── Banner ────────────────────────────────────────────────────────────────────
|
|
|
|
def _dashboard(logs, runner=None):
|
|
client, _ = signed_in(FakeRepo(), runner or FakeRunner(), logs_dir=logs)
|
|
return client.get("/").text
|
|
|
|
|
|
@case
|
|
def a_run_error_last_shows_the_banner_with_who_what_and_a_link():
|
|
page = _dashboard(_logs(("20261022_140000", ERRORED)))
|
|
assert 'id="last-run-alert"' in page, page[:500]
|
|
assert "ended in a Run Error" in page and "started by olu" in page
|
|
assert "claims (exit 5): Target closed" in page
|
|
assert "1 claim(s) not reached" in page
|
|
assert 'href="/runs/20261022_140000"' in page
|
|
|
|
|
|
@case
|
|
def a_finished_run_after_the_error_clears_the_banner():
|
|
page = _dashboard(_logs(("20261022_140000", ERRORED),
|
|
("20261022_150000", DONE)))
|
|
assert 'id="last-run-alert"' not in page
|
|
|
|
|
|
@case
|
|
def a_killed_run_shows_the_banner_a_stopped_or_running_one_does_not():
|
|
killed = ERRORED[:3]
|
|
page = _dashboard(_logs(("20261022_140000", killed)))
|
|
assert "ended without a record" in page
|
|
stopped = killed + [ev("14:02:00", "stopped", stopped_by="asha")]
|
|
assert 'id="last-run-alert"' not in _dashboard(
|
|
_logs(("20261022_140000", stopped)))
|
|
runner = FakeRunner(running=True)
|
|
st = runner._st
|
|
runner._st = lambda: {**st(),
|
|
"log_path": r"D:\x\Logs\run_20261022_140000.log"}
|
|
assert 'id="last-run-alert"' not in _dashboard(
|
|
_logs(("20261022_140000", killed)), runner)
|
|
|
|
|
|
@case
|
|
def no_runs_no_banner_and_the_latest_is_read_alone():
|
|
assert 'id="last-run-alert"' not in _dashboard(_logs())
|
|
d = _logs(("20261022_140000", ERRORED), ("20261022_150000", DONE))
|
|
assert run_view.latest_run(d)["run_id"] == "20261022_150000"
|
|
assert run_view.latest_run(_logs()) is None
|
|
|
|
|
|
# ── started_by in the results table ───────────────────────────────────────────
|
|
|
|
class ColCursor:
|
|
def __init__(self, log, has):
|
|
self.log, self.has = log, has
|
|
|
|
def execute(self, sql, params):
|
|
self.log.append(("execute", sql, list(params)))
|
|
if self.has == "raise":
|
|
raise RuntimeError("no permission")
|
|
return self
|
|
|
|
def fetchone(self):
|
|
return (100,) if self.has else (None,)
|
|
|
|
def executemany(self, sql, params):
|
|
self.log.append(("executemany", sql, list(params)))
|
|
|
|
|
|
class ColConn:
|
|
def __init__(self, has):
|
|
self.log, self.has = [], has
|
|
|
|
def cursor(self):
|
|
return ColCursor(self.log, self.has)
|
|
|
|
def commit(self):
|
|
self.log.append(("commit",))
|
|
|
|
def close(self):
|
|
self.log.append(("close",))
|
|
|
|
|
|
def _records():
|
|
return [rdb.to_record(dict(zip(_sheet(_sheet_row())[0],
|
|
_sheet(_sheet_row())[1])),
|
|
run_at=None, run_mode="VERIFY", details=None)]
|
|
|
|
|
|
def _insert(conn):
|
|
return next(e for e in conn.log if e[0] == "executemany")
|
|
|
|
|
|
@case
|
|
def started_by_is_written_when_the_column_exists():
|
|
conn = ColConn(has=True)
|
|
assert rdb.insert_rows(conn, _records(), started_by="olu") == 1
|
|
_, sql, params = _insert(conn)
|
|
assert sql.split("(")[1].split(")")[0].endswith("details_json, "
|
|
"started_by"), sql
|
|
assert params[0][-1] == "olu" and len(params[0]) == len(rdb.DB_COLUMNS) + 1
|
|
check = conn.log[0]
|
|
assert check[1] == "SELECT COL_LENGTH(?, ?)", check
|
|
assert check[2] == [rdb.TABLE, "started_by"], "parameterized"
|
|
|
|
|
|
@case
|
|
def a_table_without_the_column_still_gets_its_rows():
|
|
for has in (False, "raise"):
|
|
conn = ColConn(has=has)
|
|
assert rdb.insert_rows(conn, _records(), started_by="olu") == 1
|
|
_, sql, params = _insert(conn)
|
|
assert "started_by" not in sql and len(params[0]) == \
|
|
len(rdb.DB_COLUMNS), has
|
|
|
|
|
|
@case
|
|
def no_started_by_no_check():
|
|
conn = ColConn(has=True)
|
|
rdb.insert_rows(conn, _records())
|
|
assert [e[0] for e in conn.log] == ["executemany", "commit"], conn.log
|
|
|
|
|
|
@case
|
|
def the_flow_saves_started_by_with_the_rows():
|
|
seen = {}
|
|
saved = (rdb.save_summary, cfg.RESULTS_DB, flow.charge_summary.build)
|
|
os.environ["ECW_STARTED_BY"] = "olu"
|
|
rdb.save_summary = lambda *a, **k: seen.update(k)
|
|
flow.charge_summary.build = lambda *a: [["CLAIMS#"], ["9001"]]
|
|
cfg.RESULTS_DB = True
|
|
try:
|
|
info = {"ids": ["9001"], "file": None}
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
flow._save_results_db(info, {"9001": {"Category": "Pass"}},
|
|
{"9001": {}})
|
|
finally:
|
|
rdb.save_summary, cfg.RESULTS_DB, flow.charge_summary.build = saved
|
|
del os.environ["ECW_STARTED_BY"]
|
|
assert seen.get("started_by") == "olu", seen
|
|
|
|
|
|
@case
|
|
def the_portal_reads_and_shows_started_by():
|
|
class Cur:
|
|
def __init__(self, log):
|
|
self.log, self.description = log, [("claim_no",)]
|
|
|
|
def execute(self, sql, params):
|
|
self.log.append(sql)
|
|
return self
|
|
|
|
def fetchall(self):
|
|
return []
|
|
|
|
class Conn:
|
|
def __init__(self):
|
|
self.log = []
|
|
|
|
def cursor(self):
|
|
return Cur(self.log)
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
conn = Conn()
|
|
SqlResultsRepo("DSN=x", connect=lambda cs, timeout: conn).day_results(DAY)
|
|
assert "started_by" in conn.log[0], conn.log[0]
|
|
row = _row("9000001", "Pass", 1)
|
|
row["started_by"] = "olu"
|
|
client, _ = signed_in(FakeRepo(rows=[row]), FakeRunner())
|
|
page = client.get(f"/day/{DAY.isoformat()}").text
|
|
assert '["Started By", "olu"]' in page, "a field of the side panel"
|
|
|
|
|
|
@case
|
|
def a_table_without_the_column_says_run_the_script_again():
|
|
client, _ = signed_in(FakeRepo(fail=DbError("42S22", "Invalid column "
|
|
"name 'started_by'")),
|
|
FakeRunner())
|
|
r = _quiet(client.get, "/days")
|
|
assert r.status_code == 503 and "needs an update" in r.text, r.text
|
|
assert "create_tables.sql" in r.text
|
|
|
|
|
|
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())
|