Create integration_v2.py
Browse files- tests/integration_v2.py +103 -0
tests/integration_v2.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tests/integration_v2.py
|
| 2 |
+
"""
|
| 3 |
+
Integration sanity check for AI E-Consult V2 core layers.
|
| 4 |
+
Run with: python -m tests.integration_v2
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Dict, Any
|
| 11 |
+
|
| 12 |
+
# Ensure repo root on path
|
| 13 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
if str(REPO_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 16 |
+
|
| 17 |
+
from src import config, billing, modal_templates, store # noqa: E402
|
| 18 |
+
|
| 19 |
+
RESULTS = {"passed": 0, "failed": 0}
|
| 20 |
+
|
| 21 |
+
def _ok(name: str):
|
| 22 |
+
RESULTS["passed"] += 1
|
| 23 |
+
print(f"[PASS] {name}")
|
| 24 |
+
|
| 25 |
+
def _fail(name: str, e: Exception):
|
| 26 |
+
RESULTS["failed"] += 1
|
| 27 |
+
print(f"[FAIL] {name}: {e}")
|
| 28 |
+
|
| 29 |
+
def check(name: str):
|
| 30 |
+
def deco(fn):
|
| 31 |
+
def wrapper():
|
| 32 |
+
try:
|
| 33 |
+
fn()
|
| 34 |
+
_ok(name)
|
| 35 |
+
except Exception as e:
|
| 36 |
+
_fail(name, e)
|
| 37 |
+
return wrapper
|
| 38 |
+
return deco
|
| 39 |
+
|
| 40 |
+
@check("seed_cases_and_v2_schema")
|
| 41 |
+
def test_seed_cases_and_v2_schema():
|
| 42 |
+
created = store.seed_cases(reset=True)
|
| 43 |
+
assert len(created) == 4
|
| 44 |
+
for cid in created:
|
| 45 |
+
case = store.read_case(cid)
|
| 46 |
+
assert case and case["schema_version"] == config.SCHEMA_VERSION
|
| 47 |
+
for k in ["status", "created_at", "updated_at", "billing", "explainability"]:
|
| 48 |
+
assert k in case
|
| 49 |
+
|
| 50 |
+
@check("load_sample_case")
|
| 51 |
+
def test_load_sample_case():
|
| 52 |
+
items = store.list_cases()
|
| 53 |
+
assert items, "No cases after seeding"
|
| 54 |
+
case = store.read_case(items[0]["case_id"])
|
| 55 |
+
assert case is not None
|
| 56 |
+
print(" case_id:", case["case_id"], "status:", case["status"])
|
| 57 |
+
|
| 58 |
+
@check("billing_autosuggest_representative")
|
| 59 |
+
def test_billing_autosuggest_representative():
|
| 60 |
+
combos = [(5, False), (12, True), (35, True)]
|
| 61 |
+
for minutes, spoke in combos:
|
| 62 |
+
sugg = billing.autosuggest_cpt(minutes=minutes, spoke=spoke)
|
| 63 |
+
assert isinstance(sugg, list) and len(sugg) >= 1
|
| 64 |
+
codes = [s.code for s in sugg]
|
| 65 |
+
print(f" minutes={minutes} spoke={spoke} -> {codes} (eligible={[s.code for s in sugg if s.eligible]})")
|
| 66 |
+
|
| 67 |
+
@check("modal_previews_safe_without_streamlit")
|
| 68 |
+
def test_modal_previews_safe_without_streamlit():
|
| 69 |
+
# Build a simple note and claim, then render via modal helpers.
|
| 70 |
+
cases = store.list_cases()
|
| 71 |
+
case = store.read_case(cases[0]["case_id"])
|
| 72 |
+
assert case
|
| 73 |
+
note_md = f"# Consult Note for {case['patient']['name']}\n\nGenerated for integration preview."
|
| 74 |
+
pick = billing.autosuggest_cpt(minutes=12, spoke=True)
|
| 75 |
+
code = next((s.code for s in pick if s.eligible), "99447")
|
| 76 |
+
rate = next((s.rate for s in pick if s.code == code), 55.0)
|
| 77 |
+
claim = billing.build_837_claim(case, code=code, rate=rate, minutes=12, spoke=True, attested=True)
|
| 78 |
+
# These calls are safe even if Streamlit is missing—helpers no-op or use fallbacks
|
| 79 |
+
modal_templates.show_consult_note_preview(note_md)
|
| 80 |
+
modal_templates.show_837_claim_preview(claim)
|
| 81 |
+
|
| 82 |
+
@check("export_filename_determinism")
|
| 83 |
+
def test_export_filename_determinism():
|
| 84 |
+
p1 = config.make_export_path("INTEG", "consult_note.md")
|
| 85 |
+
p2 = config.make_export_path("INTEG", "claim_837.json")
|
| 86 |
+
assert p1.parent.exists() and p2.parent.exists()
|
| 87 |
+
assert p1.name.startswith("EC-INTEG_") and p1.name.endswith("_consult_note.md")
|
| 88 |
+
assert p2.name.startswith("EC-INTEG_") and p2.name.endswith("_claim_837.json")
|
| 89 |
+
|
| 90 |
+
def main():
|
| 91 |
+
test_seed_cases_and_v2_schema()
|
| 92 |
+
test_load_sample_case()
|
| 93 |
+
test_billing_autosuggest_representative()
|
| 94 |
+
test_modal_previews_safe_without_streamlit()
|
| 95 |
+
test_export_filename_determinism()
|
| 96 |
+
|
| 97 |
+
total = RESULTS["passed"] + RESULTS["failed"]
|
| 98 |
+
print(f"\n=== Integration Summary: {RESULTS['passed']} passed, {RESULTS['failed']} failed, {total} total ===")
|
| 99 |
+
if RESULTS["failed"]:
|
| 100 |
+
raise SystemExit(1)
|
| 101 |
+
|
| 102 |
+
if __name__ == "__main__":
|
| 103 |
+
main()
|