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:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,458 @@
|
||||
r"""
|
||||
The Condition Framework's runner — exception isolation, not condition logic.
|
||||
|
||||
python tests\test_conditions_runner.py
|
||||
|
||||
See docs\superpowers\specs\2026-09-16-condition-framework-design.md. This
|
||||
file tests `conditions.base.run_all` in isolation, against small fake
|
||||
condition modules built for each case — never the real telehealth logic
|
||||
(that stays in test_conditions_telehealth.py and test_telehealth_fix.py).
|
||||
The one thing this file exists to prove: **one condition's bug can never
|
||||
block or corrupt another's result on the same claim.**
|
||||
|
||||
No Playwright needed for most cases — `apply()` on a fake condition never
|
||||
touches `page`, so `page` is passed through untouched (`None` is fine).
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import pathlib
|
||||
import types
|
||||
import contextlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from ecw_bot.conditions.base import ConditionResult, run_all # noqa: E402
|
||||
import ecw_bot.conditions as conditions_pkg # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
def _fake_condition(name, *, detects=True, changed=True, raises_in=None):
|
||||
"""
|
||||
Builds a minimal condition module (matching the real detect/apply/
|
||||
describe contract) as a throwaway `types.ModuleType`, so these tests
|
||||
never need real files on disk for scenarios that only exist to prove
|
||||
the runner's exception handling.
|
||||
"""
|
||||
mod = types.ModuleType(name)
|
||||
mod.NAME = name
|
||||
|
||||
def detect(claim_id, data):
|
||||
if raises_in == "detect":
|
||||
raise RuntimeError(f"{name} detect() blew up")
|
||||
return detects
|
||||
|
||||
def apply(page, claim_id, data):
|
||||
if raises_in == "apply":
|
||||
raise RuntimeError(f"{name} apply() blew up")
|
||||
return ConditionResult(
|
||||
condition_name=name,
|
||||
changed=changed,
|
||||
changes=[{"row": 0, "code": "X", "before": {}, "after": {}}]
|
||||
if changed else [],
|
||||
)
|
||||
|
||||
def describe(result):
|
||||
return f"{name}: {len(result.changes)} change(s)"
|
||||
|
||||
mod.detect, mod.apply, mod.describe = detect, apply, describe
|
||||
return mod
|
||||
|
||||
|
||||
@case
|
||||
def a_condition_that_does_not_detect_is_never_applied():
|
||||
calls = []
|
||||
|
||||
def detect(claim_id, data):
|
||||
return False
|
||||
|
||||
def apply(page, claim_id, data):
|
||||
calls.append("apply")
|
||||
return ConditionResult.none("never")
|
||||
|
||||
mod = types.ModuleType("never")
|
||||
mod.NAME = "never"
|
||||
mod.detect, mod.apply, mod.describe = detect, apply, lambda r: ""
|
||||
|
||||
results = run_all(None, "C1", {}, conditions=[mod])
|
||||
assert results == [], results
|
||||
assert calls == [], "apply() must not run when detect() returned False"
|
||||
|
||||
|
||||
@case
|
||||
def results_is_empty_when_every_condition_detects_false():
|
||||
a = _fake_condition("a", detects=False)
|
||||
b = _fake_condition("b", detects=False)
|
||||
results = run_all(None, "C1", {}, conditions=[a, b])
|
||||
assert results == [], results
|
||||
|
||||
|
||||
@case
|
||||
def a_raising_condition_is_skipped_and_does_not_block_the_next_one():
|
||||
broken = _fake_condition("broken", raises_in="apply")
|
||||
healthy = _fake_condition("healthy", changed=True)
|
||||
results = run_all(None, "C1", {}, conditions=[broken, healthy])
|
||||
assert len(results) == 1, results
|
||||
assert results[0].condition_name == "healthy", results
|
||||
|
||||
|
||||
@case
|
||||
def a_raising_detect_is_also_skipped_without_blocking_others():
|
||||
broken = _fake_condition("broken_detect", raises_in="detect")
|
||||
healthy = _fake_condition("healthy2", changed=True)
|
||||
results = run_all(None, "C1", {}, conditions=[broken, healthy])
|
||||
assert len(results) == 1, results
|
||||
assert results[0].condition_name == "healthy2", results
|
||||
|
||||
|
||||
@case
|
||||
def a_condition_that_detects_but_changes_nothing_contributes_no_result():
|
||||
mod = _fake_condition("no_op", detects=True, changed=False)
|
||||
results = run_all(None, "C1", {}, conditions=[mod])
|
||||
assert results == [], results
|
||||
|
||||
|
||||
@case
|
||||
def the_package_exposes_run_all_the_way_export_medcor_ready_calls_it():
|
||||
"""
|
||||
Regression test for a real bug (run_20260916_162654.log, claim
|
||||
9130000): `conditions/__init__.py` never re-exported `run_all` from
|
||||
`base.py`, so `conditions.run_all(...)` — exactly how
|
||||
export_medcor_ready.py calls it (`from .. import conditions` then
|
||||
`conditions.run_all(...)`) — raised AttributeError on every single
|
||||
claim. Every other test here imported `run_all` straight from
|
||||
`conditions.base`, so none of them caught it. This one imports the
|
||||
package itself, the same way the real caller does, so it cannot pass
|
||||
by accident the way the others did.
|
||||
"""
|
||||
assert hasattr(conditions_pkg, "run_all"), (
|
||||
"conditions.run_all is missing — export_medcor_ready.py's "
|
||||
"conditions.run_all(...) call would raise AttributeError on "
|
||||
"every claim")
|
||||
assert conditions_pkg.run_all is run_all
|
||||
|
||||
|
||||
@case
|
||||
def multiple_fired_conditions_all_appear_in_order():
|
||||
a = _fake_condition("a", changed=True)
|
||||
b = _fake_condition("b", changed=True)
|
||||
results = run_all(None, "C1", {}, conditions=[a, b])
|
||||
assert [r.condition_name for r in results] == ["a", "b"], results
|
||||
|
||||
|
||||
@case
|
||||
def before_apply_receives_only_fired_conditions_and_runs_before_any_apply():
|
||||
"""
|
||||
The before/after message-box pair the operator sees per claim needs a
|
||||
checkpoint between "everything has detect()ed" and "the first apply()
|
||||
touches the DOM" — this proves `before_apply` lands exactly there:
|
||||
called once, with only the conditions that actually detected True
|
||||
(never the one that didn't), and before either fired condition's
|
||||
apply() has run.
|
||||
"""
|
||||
calls = []
|
||||
|
||||
def tracking_apply(name):
|
||||
def apply(page, claim_id, data):
|
||||
calls.append(f"apply:{name}")
|
||||
return ConditionResult(condition_name=name, changed=True,
|
||||
changes=[{"row": 0}])
|
||||
return apply
|
||||
|
||||
a = _fake_condition("a", detects=True)
|
||||
a.apply = tracking_apply("a")
|
||||
b = _fake_condition("b", detects=False)
|
||||
b.apply = tracking_apply("b")
|
||||
c = _fake_condition("c", detects=True)
|
||||
c.apply = tracking_apply("c")
|
||||
|
||||
def before_apply(fired):
|
||||
calls.append("before_apply:" + ",".join(m.NAME for m in fired))
|
||||
|
||||
results = run_all(None, "C1", {}, conditions=[a, b, c],
|
||||
before_apply=before_apply)
|
||||
|
||||
assert calls == ["before_apply:a,c", "apply:a", "apply:c"], calls
|
||||
assert [r.condition_name for r in results] == ["a", "c"], results
|
||||
|
||||
|
||||
@case
|
||||
def before_apply_is_not_called_when_nothing_fires():
|
||||
calls = []
|
||||
a = _fake_condition("a", detects=False)
|
||||
run_all(None, "C1", {}, conditions=[a],
|
||||
before_apply=lambda fired: calls.append(fired))
|
||||
assert calls == [], calls
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# report= — what really happened, not only what changed (review items D2/P2)
|
||||
# =============================================================================
|
||||
# Before 2026-10-05 run_all returned the changed results and nothing else, so
|
||||
# a condition that raised, or matched and declined to act, left no trace and
|
||||
# the Charge Summary called the claim "Clean / Pass".
|
||||
|
||||
def _quiet(fn, *args, **kwargs):
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
|
||||
def _hooked(name, *, detects, needs_review=None, summary_comment=None,
|
||||
result=None):
|
||||
"""A fake condition with the optional hooks, and a fixed apply() result."""
|
||||
mod = _fake_condition(name, detects=detects)
|
||||
if result is not None:
|
||||
mod.apply = lambda page, claim_id, data: result
|
||||
if needs_review is not None:
|
||||
mod.needs_review = needs_review
|
||||
if summary_comment is not None:
|
||||
mod.summary_comment = summary_comment
|
||||
return mod
|
||||
|
||||
|
||||
@case
|
||||
def report_records_a_detect_that_raised_as_failed():
|
||||
report = {}
|
||||
broken = _fake_condition("broken_detect", raises_in="detect")
|
||||
healthy = _fake_condition("healthy")
|
||||
results = _quiet(run_all, None, "C1", {}, conditions=[broken, healthy],
|
||||
report=report)
|
||||
assert [r.condition_name for r in results] == ["healthy"], results
|
||||
assert report["failed"] == [{"name": "broken_detect", "stage": "detect",
|
||||
"error": "broken_detect detect() blew up"}], \
|
||||
report
|
||||
assert report["fired"] == ["healthy"], report
|
||||
assert report["changed"] == ["healthy"], report
|
||||
|
||||
|
||||
@case
|
||||
def report_records_an_apply_that_raised_as_failed_but_still_fired():
|
||||
report = {}
|
||||
broken = _fake_condition("broken", raises_in="apply")
|
||||
healthy = _fake_condition("healthy")
|
||||
results = _quiet(run_all, None, "C1", {}, conditions=[broken, healthy],
|
||||
report=report)
|
||||
assert [r.condition_name for r in results] == ["healthy"], results
|
||||
assert report["fired"] == ["broken", "healthy"], report
|
||||
assert report["changed"] == ["healthy"], report
|
||||
assert report["failed"] == [{"name": "broken", "stage": "apply",
|
||||
"error": "broken apply() blew up"}], report
|
||||
|
||||
|
||||
@case
|
||||
def report_keeps_only_the_first_line_of_a_long_error():
|
||||
report = {}
|
||||
mod = _fake_condition("long")
|
||||
|
||||
def apply(page, claim_id, data):
|
||||
raise RuntimeError("Timeout 30000ms exceeded.\nCall log:\n - waiting")
|
||||
mod.apply = apply
|
||||
_quiet(run_all, None, "C1", {}, conditions=[mod], report=report)
|
||||
assert report["failed"][0]["error"] == "Timeout 30000ms exceeded.", report
|
||||
|
||||
|
||||
@case
|
||||
def a_declined_result_is_review_not_changed():
|
||||
report = {}
|
||||
mod = _hooked("decliner", detects=True,
|
||||
result=ConditionResult.declined("decliner", "tab gone"))
|
||||
results = run_all(None, "C1", {}, conditions=[mod], report=report)
|
||||
assert results == [], results
|
||||
assert report["fired"] == ["decliner"] and report["changed"] == [], report
|
||||
assert report["review"] == [{"name": "decliner", "reason": "tab gone"}], \
|
||||
report
|
||||
assert report["failed"] == [], report
|
||||
|
||||
|
||||
@case
|
||||
def declined_is_not_changed_and_none_is_unchanged():
|
||||
d = ConditionResult.declined("x", "why")
|
||||
assert (d.changed, d.changes, d.review) == (False, [], "why"), d
|
||||
n = ConditionResult.none("x")
|
||||
assert (n.changed, n.changes, n.review) == (False, [], ""), n
|
||||
# The new field defaults, so every pre-existing construction still works.
|
||||
assert ConditionResult("x", True, [1]).review == ""
|
||||
|
||||
|
||||
@case
|
||||
def a_changed_result_with_a_review_reason_is_both_changed_and_review():
|
||||
report = {}
|
||||
res = ConditionResult(condition_name="half", changed=True,
|
||||
changes=[{"row": 0}], review="check by hand")
|
||||
mod = _hooked("half", detects=True, result=res)
|
||||
results = run_all(None, "C1", {}, conditions=[mod], report=report)
|
||||
assert results == [res], results
|
||||
assert report["changed"] == ["half"], report
|
||||
assert report["review"] == [{"name": "half",
|
||||
"reason": "check by hand"}], report
|
||||
|
||||
|
||||
@case
|
||||
def needs_review_hook_is_asked_only_when_detect_said_no():
|
||||
asked = []
|
||||
|
||||
def hook(claim_id, data):
|
||||
asked.append(claim_id)
|
||||
return "payer not covered"
|
||||
|
||||
report = {}
|
||||
quiet = _hooked("quiet", detects=False, needs_review=hook)
|
||||
loud = _hooked("loud", detects=True, needs_review=hook)
|
||||
run_all(None, "C1", {}, conditions=[quiet, loud], report=report)
|
||||
assert asked == ["C1"], "asked once — for the one that did not fire"
|
||||
assert report["review"] == [{"name": "quiet",
|
||||
"reason": "payer not covered"}], report
|
||||
|
||||
|
||||
@case
|
||||
def needs_review_returning_nothing_adds_no_entry():
|
||||
report = {}
|
||||
mod = _hooked("fine", detects=False, needs_review=lambda c, d: None)
|
||||
run_all(None, "C1", {}, conditions=[mod], report=report)
|
||||
assert report["review"] == [] and report["failed"] == [], report
|
||||
|
||||
|
||||
@case
|
||||
def a_raising_needs_review_hook_is_failed_and_blocks_nothing_else():
|
||||
def hook(claim_id, data):
|
||||
raise ValueError("bad mapping")
|
||||
|
||||
report = {}
|
||||
broken = _hooked("hooked", detects=False, needs_review=hook)
|
||||
healthy = _fake_condition("healthy")
|
||||
results = _quiet(run_all, None, "C1", {}, conditions=[broken, healthy],
|
||||
report=report)
|
||||
assert [r.condition_name for r in results] == ["healthy"], results
|
||||
assert report["failed"] == [{"name": "hooked", "stage": "needs_review",
|
||||
"error": "bad mapping"}], report
|
||||
|
||||
|
||||
@case
|
||||
def summary_comment_hook_words_the_comment_of_a_changed_result():
|
||||
report = {}
|
||||
mod = _hooked("worded", detects=True,
|
||||
summary_comment=lambda r: f"{len(r.changes)} line(s) fixed")
|
||||
plain = _fake_condition("plain")
|
||||
run_all(None, "C1", {}, conditions=[mod, plain], report=report)
|
||||
assert report["comments"] == {"worded": "1 line(s) fixed"}, report
|
||||
|
||||
|
||||
@case
|
||||
def summary_comment_is_not_asked_for_an_unchanged_result():
|
||||
asked = []
|
||||
mod = _hooked("same", detects=True,
|
||||
result=ConditionResult.none("same"),
|
||||
summary_comment=lambda r: asked.append(r) or "x")
|
||||
report = {}
|
||||
run_all(None, "C1", {}, conditions=[mod], report=report)
|
||||
assert asked == [] and report["comments"] == {}, report
|
||||
|
||||
|
||||
@case
|
||||
def a_raising_summary_comment_falls_back_without_tainting_the_claim():
|
||||
def hook(result):
|
||||
raise KeyError("missing")
|
||||
|
||||
report = {}
|
||||
mod = _hooked("worded", detects=True, summary_comment=hook)
|
||||
results = _quiet(run_all, None, "C1", {}, conditions=[mod], report=report)
|
||||
assert [r.condition_name for r in results] == ["worded"], results
|
||||
assert report["comments"] == {} and report["failed"] == [], report
|
||||
assert report["review"] == [], report
|
||||
|
||||
|
||||
@case
|
||||
def return_value_is_unchanged_without_a_report():
|
||||
"""Old callers (no report=) see exactly the list they always did."""
|
||||
a = _fake_condition("a")
|
||||
b = _hooked("b", detects=True,
|
||||
result=ConditionResult.declined("b", "why"))
|
||||
c = _fake_condition("c", raises_in="apply")
|
||||
with_report = _quiet(run_all, None, "C1", {}, conditions=[a, b, c],
|
||||
report={})
|
||||
without = _quiet(run_all, None, "C1", {}, conditions=[a, b, c])
|
||||
assert [r.condition_name for r in without] == ["a"], without
|
||||
assert without == with_report, (without, with_report)
|
||||
|
||||
|
||||
@case
|
||||
def a_report_passed_twice_is_reset_not_appended_to():
|
||||
report = {}
|
||||
broken = _fake_condition("broken", raises_in="apply")
|
||||
_quiet(run_all, None, "C1", {}, conditions=[broken], report=report)
|
||||
_quiet(run_all, None, "C2", {}, conditions=[broken], report=report)
|
||||
assert len(report["failed"]) == 1, report
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Every real condition: a CPT/HCPCS tab that will not open is declined
|
||||
# =============================================================================
|
||||
|
||||
@case
|
||||
def every_registered_condition_declines_when_the_cpt_tab_cannot_open():
|
||||
"""
|
||||
Every registered `apply()` re-clicks CPT/HCPCS first and used to return
|
||||
`none()` when that failed — "nothing to do", reported Clean, on a claim
|
||||
that was never checked (review item D2). The branch runs before any
|
||||
page access, so `page=None` is enough.
|
||||
"""
|
||||
from ecw_bot.conditions import CONDITIONS
|
||||
from ecw_bot.conditions.base import CPT_TAB_NOT_CONFIRMED
|
||||
from ecw_bot.pages import claim_form
|
||||
|
||||
def tab_that_raises(page):
|
||||
raise RuntimeError("No CPT/HCPCS tab matched (simulated)")
|
||||
|
||||
real = claim_form.open_cpt_hcpcs_tab
|
||||
claim_form.open_cpt_hcpcs_tab = tab_that_raises
|
||||
try:
|
||||
for cond in CONDITIONS:
|
||||
result = _quiet(cond.apply, None, "C1", {})
|
||||
assert result == ConditionResult.declined(
|
||||
cond.NAME, CPT_TAB_NOT_CONFIRMED), (cond.NAME, result)
|
||||
finally:
|
||||
claim_form.open_cpt_hcpcs_tab = real
|
||||
# The whole registry: the common rule's two halves bracket it, and no
|
||||
# condition is listed twice.
|
||||
names = [c.NAME for c in CONDITIONS]
|
||||
assert len(set(names)) == len(names), names
|
||||
assert names[0] == "zero_fee_uncheck", names
|
||||
assert names[-1] == "priced_line_check", names
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures = 0
|
||||
# run_all screenshots a condition that raised (base.shot), and shot()
|
||||
# creates the operator's dated Screenshots\ folder before it fails on a
|
||||
# page of None. A no-op here keeps the suite out of that folder (final
|
||||
# review, 2026-10-05), as test_save_corrections_gate.py already does.
|
||||
from ecw_bot.conditions import base
|
||||
orig_shot = base.shot
|
||||
base.shot = lambda page, name: None
|
||||
try:
|
||||
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
|
||||
finally:
|
||||
base.shot = orig_shot
|
||||
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user