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

+193
View File
@@ -0,0 +1,193 @@
"""
Run events — `core/run_events.py` (spec 2026-10-22 Stage 1, EEWA-31).
python tests\\test_run_events.py
Every Run writes its events, each with its time, to
`Logs\\events_<run>.jsonl` beside `run_<run>.log`. What has to hold:
* **The file sits beside the log, named after it**, so the Portal finds a
Run's events from the log path `bot_run.json` already holds.
* **One JSON object per line, flushed at once** — a killed Run still
leaves every event it wrote.
* **An event never stops a Run**: no file, a bad value, a closed handle —
the event is dropped and the Run goes on.
* **eCW Login Attempts are numbered** and every step carries its attempt.
* **A Run Error carries the traceback** of the exception that caused it.
* **No secret reaches the file**: the run_start fields are a fixed list.
"""
import os
import sys
import json
import pathlib
import tempfile
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from ecw_bot.core import run_events # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
def _fresh():
run_events._reset()
d = pathlib.Path(tempfile.mkdtemp())
log = d / "run_20261022_141530.log"
log.write_text("")
return log
def _read(log):
path = run_events.path_for(log)
return [json.loads(l) for l in path.read_text(encoding="utf-8").splitlines()]
@case
def the_events_file_is_named_after_the_log():
log = pathlib.Path(r"D:\x\Logs\run_20261022_141530.log")
assert run_events.path_for(log).name == "events_20261022_141530.jsonl"
assert run_events.path_for(log).parent == log.parent
assert run_events.run_id_of(log) == "20261022_141530"
@case
def start_writes_run_start_with_source_and_who():
log = _fresh()
os.environ["ECW_STARTED_BY"] = "asha"
os.environ["ECW_RUN_SOURCE"] = "web"
try:
run_events.start(log, mode="VERIFY", claim_limit=20,
test_sheet=r"D:\Ecw Process\Input\sheet.xlsx")
finally:
del os.environ["ECW_STARTED_BY"], os.environ["ECW_RUN_SOURCE"]
ev = _read(log)
assert len(ev) == 1 and ev[0]["kind"] == "run_start", ev
e = ev[0]
assert e["started_by"] == "asha" and e["source"] == "portal", e
assert e["mode"] == "VERIFY" and e["claim_limit"] == 20, e
assert e["test_sheet"] == "sheet.xlsx", e # file name only
assert e["pid"] == os.getpid() and "t" in e, e
assert len(e["t"]) == 23, e["t"] # ms: 2026-10-22T14:15:30.123
@case
def a_terminal_run_is_started_by_the_windows_user():
log = _fresh()
os.environ.pop("ECW_STARTED_BY", None)
os.environ.pop("ECW_RUN_SOURCE", None)
run_events.start(log, mode="VERIFY")
e = _read(log)[0]
assert e["source"] == "terminal", e
assert e["started_by"] and e["started_by"] == run_events._windows_user(), e
@case
def emit_appends_one_line_per_event():
log = _fresh()
run_events.start(log, mode="VERIFY")
run_events.emit("queue", claims=["1", "2"])
run_events.warning("not_found", "not in eCW", claim="2")
ev = _read(log)
assert [e["kind"] for e in ev] == ["run_start", "queue", "warning"], ev
assert ev[2]["code"] == "not_found" and ev[2]["claim"] == "2", ev[2]
@case
def login_attempts_are_numbered_and_steps_carry_the_attempt():
log = _fresh()
run_events.start(log, mode="VERIFY")
run_events.login_attempt_start()
run_events.login_step("page_open")
run_events.login_attempt_end("twofa_timeout", "no mail")
run_events.login_attempt_start()
run_events.login_step("home")
run_events.login_attempt_end("ok")
ev = [e for e in _read(log) if e["kind"] != "run_start"]
assert [(e["kind"], e["attempt"]) for e in ev] == [
("login_attempt_start", 1), ("login_step", 1),
("login_attempt_end", 1), ("login_attempt_start", 2),
("login_step", 2), ("login_attempt_end", 2)], ev
assert ev[2]["result"] == "twofa_timeout" and ev[2]["message"] == "no mail"
assert ev[1]["step"] == "page_open"
@case
def a_run_error_carries_the_traceback():
log = _fresh()
run_events.start(log, mode="VERIFY")
try:
raise ValueError("boom")
except ValueError as exc:
run_events.run_error("claims", str(exc), 5, exc=exc)
e = _read(log)[-1]
assert e["kind"] == "run_error" and e["exit_code"] == 5, e
assert e["step"] == "claims" and e["message"] == "boom", e
assert "ValueError: boom" in e["traceback"], e["traceback"]
@case
def a_run_error_without_an_exception_has_no_traceback():
log = _fresh()
run_events.start(log, mode="VERIFY")
run_events.run_error("config", "ECW_URL is not set", 2)
assert _read(log)[-1]["traceback"] == ""
@case
def before_start_and_after_a_failure_nothing_raises():
run_events._reset()
run_events.emit("queue", claims=[]) # not started: dropped
run_events.warning("x", "y")
log = _fresh()
run_events.start(log, mode="VERIFY")
run_events.emit("odd", value=object()) # not JSON: str() it
assert _read(log)[-1]["value"].startswith("<object"), _read(log)[-1]
run_events._handle.close() # handle gone: dropped
run_events.emit("queue", claims=[])
@case
def start_into_a_missing_folder_does_not_raise():
run_events._reset()
run_events.start(pathlib.Path(r"Z:\no\such\run_20261022_000000.log"),
mode="VERIFY")
run_events.emit("queue", claims=[])
@case
def append_to_writes_into_an_ended_runs_file():
log = _fresh()
run_events.start(log, mode="VERIFY")
run_events._reset()
run_events.append_to(log, "stopped", stopped_by="asha")
ev = _read(log)
assert ev[-1]["kind"] == "stopped" and ev[-1]["stopped_by"] == "asha", ev
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
run_events._reset()
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())