claimflow-api / tests /test_specialist_flow.py
Minifigures's picture
feat: ClaimFlow API demo backend
ceea9e1 verified
Raw
History Blame Contribute Delete
13.6 kB
"""Specialist workflow end-to-end through the API: forward -> stage-2 note ->
recommendation queue -> send-to-insurer -> stage-3 summary, plus the return /
request-further-testing / regenerate paths. TestClient runs BackgroundTasks
synchronously, so each action's inference completes before the next assertion."""
import hashlib
import json
import shutil
from pathlib import Path
from fastapi.testclient import TestClient
from PIL import Image
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.claimguard import audit
from app.models import (
AdjudicationSummary,
ArtifactStatus,
Claim,
ClaimState,
DiagnosticReport,
Document,
DocumentKind,
Modality,
Notification,
Recommendation,
RecommendationNote,
User,
)
from tests.conftest import login
SEED_TAMPERED = Path(__file__).resolve().parents[1] / "seed-assets" / "tampered_xray.dcm"
CLAIM_BODY = {
"claim_type": "imaging",
"description": "Wrist X-ray after a fall on ice",
"procedure_code": "IMG-2-0042",
"diagnosis_code": "S62.10",
"incident_date": "2026-05-20",
"amount_claimed": 420.5,
}
def _attach_imaging_doc(
session: Session,
tmp_path: Path,
claim_id: int,
uploader_id: int,
filename: str,
source: Path | None = None,
) -> Document:
image_path = tmp_path / filename
if source is not None:
shutil.copyfile(source, image_path)
else:
Image.new("L", (64, 64), color=128).save(image_path, format="PNG")
data = image_path.read_bytes()
doc = Document(
claim_id=claim_id,
uploader_id=uploader_id,
kind=DocumentKind.IMAGING,
modality=Modality.XRAY,
filename=filename,
mime="image/png",
size_bytes=len(data),
sha256=hashlib.sha256(data).hexdigest(),
storage_path=str(image_path),
)
session.add(doc)
session.commit()
return doc
def _claim_in_imaging_review(
client: TestClient,
session: Session,
users: dict[str, User],
tmp_path: Path,
*,
filename: str = "wrist_xray.png",
source: Path | None = None,
) -> dict:
"""Claim + imaging doc + COMPLETE stage-1 report, parked at IMAGING_REVIEW."""
login(client, "claimant@demo.ca")
resp = client.post("/api/claims", json=CLAIM_BODY)
assert resp.status_code == 201, resp.text
claim = resp.json()
_attach_imaging_doc(
session, tmp_path, claim["id"], users["claimant"].id, filename, source
)
assert client.post(f"/api/claims/{claim['id']}/analyze").status_code == 200
session.expire_all()
row = session.get(Claim, claim["id"])
assert row is not None and row.state is ClaimState.IMAGING_REVIEW
return claim
def _forward(client: TestClient, claim_id: int) -> dict:
login(client, "imaging@demo.ca")
resp = client.post(f"/api/specialist/cases/{claim_id}/forward")
assert resp.status_code == 200, resp.text
return resp.json()
def test_forward_to_adjudication_end_to_end(
client: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _claim_in_imaging_review(client, session, users, tmp_path)
forwarded = _forward(client, claim["id"])
assert forwarded["state"] == "SPECIALIST_REVIEW"
assert forwarded["note_status"] == "pending" # status at response time; ran after
session.expire_all()
note = session.get(RecommendationNote, forwarded["note_id"])
assert note is not None
assert note.status is ArtifactStatus.COMPLETE
assert note.error is None
assert note.completed_at is not None
assert note.recommendation is Recommendation.SUPPORTS_CLAIM # authentic + xray matches IMG-2
assert note.generated_by == "fallback_template"
assert note.fallback_reason == "no_api_key"
assert note.payload_json is not None
payload = json.loads(note.payload_json)
assert payload["recommendation"] == "SUPPORTS_CLAIM"
assert "advisory_notice" in payload
# double-forward: claim already left IMAGING_REVIEW
assert client.post(f"/api/specialist/cases/{claim['id']}/forward").status_code == 409
# recommendation queue is medical-specialist only
resp = client.get("/api/specialist/queue", params={"stage": "recommendation"})
assert resp.status_code == 403 # still logged in as imaging specialist
login(client, "specialist@demo.ca")
resp = client.get("/api/specialist/queue", params={"stage": "recommendation"})
assert resp.status_code == 200, resp.text
items = resp.json()
assert len(items) == 1
item = items[0]
assert item["claim_id"] == claim["id"]
assert item["state"] == "SPECIALIST_REVIEW"
assert item["note_status"] == "complete"
assert item["recommendation"] == "SUPPORTS_CLAIM"
assert item["requires_mandatory_review"] is True # keyless fallback confidence 0.0
# full case detail carries all artifact versions with parsed payloads
detail = client.get(f"/api/specialist/cases/{claim['id']}").json()
assert detail["claimant"] == "Casey Claimant"
assert detail["claim_ref"] == claim["claim_ref"]
assert len(detail["documents"]) == 1
assert len(detail["diagnostic_reports"]) == 1
assert detail["diagnostic_reports"][0]["status"] == "complete"
assert detail["diagnostic_reports"][0]["payload"]["classifier"]["modality"] == "xray"
assert detail["recommendation_notes"][0]["payload"]["recommendation"] == "SUPPORTS_CLAIM"
resp = client.post(f"/api/specialist/cases/{claim['id']}/send-to-insurer")
assert resp.status_code == 200, resp.text
sent = resp.json()
assert sent["state"] == "ADJUDICATION"
session.expire_all()
summary = session.get(AdjudicationSummary, sent["summary_id"])
assert summary is not None
assert summary.status is ArtifactStatus.COMPLETE
assert summary.error is None
assert summary.recommendation_lean == "LEAN_APPROVE" # SUPPORTS_CLAIM + authentic
assert summary.generated_by == "fallback_template"
assert summary.fallback_reason == "no_api_key"
assert summary.payload_json is not None
payload = json.loads(summary.payload_json)
assert isinstance(payload["similar_case_outcomes"], list) # system-copied; may be empty
assert payload["recommendation_lean"] == "LEAN_APPROVE"
assert "advisory_notice" in payload
session.expire_all()
valid, _ = audit.verify_chain(session)
assert valid is True
def test_return_path_notifies_claimant_and_records_timeline_note(
client: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _claim_in_imaging_review(client, session, users, tmp_path)
login(client, "imaging@demo.ca")
resp = client.post(
f"/api/specialist/cases/{claim['id']}/return",
json={"note": "image too blurry; please rescan"},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["state"] == "RETURNED_TO_CLAIMANT"
session.expire_all()
notification = session.get(Notification, body["notification_id"])
assert notification is not None
assert notification.claim_id == claim["id"]
assert notification.recipient_id == users["claimant"].id
assert claim["claim_ref"] in notification.subject
assert "image too blurry; please rescan" in notification.body_text
assert "Casey" in notification.body_text # first name only
login(client, "claimant@demo.ca")
timeline = client.get(f"/api/claims/{claim['id']}/timeline").json()
returned = [e for e in timeline if e["action"] == "return_to_claimant"]
assert len(returned) == 1
assert returned[0]["note"] == "image too blurry; please rescan"
assert returned[0]["to_state"] == "RETURNED_TO_CLAIMANT"
valid, _ = audit.verify_chain(session)
assert valid is True
def test_request_further_testing_path(
client: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _claim_in_imaging_review(client, session, users, tmp_path)
_forward(client, claim["id"])
login(client, "specialist@demo.ca")
resp = client.post(
f"/api/specialist/cases/{claim['id']}/request-further-testing",
json={"note": "please provide a follow-up CT study"},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["state"] == "PENDING_FURTHER_TESTING"
session.expire_all()
notification = session.get(Notification, body["notification_id"])
assert notification is not None
assert notification.recipient_id == users["claimant"].id
assert "please provide a follow-up CT study" in notification.body_text
login(client, "claimant@demo.ca")
timeline = client.get(f"/api/claims/{claim['id']}/timeline").json()
requested = [e for e in timeline if e["action"] == "request_further_testing"]
assert len(requested) == 1
assert requested[0]["note"] == "please provide a follow-up CT study"
def test_regenerate_failed_recommendation_note(
client: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _claim_in_imaging_review(client, session, users, tmp_path)
forwarded = _forward(client, claim["id"])
# regenerate refuses while the latest note is COMPLETE
login(client, "specialist@demo.ca")
resp = client.post(
f"/api/specialist/cases/{claim['id']}/regenerate",
json={"stage": "recommendation"},
)
assert resp.status_code == 409
note = session.get(RecommendationNote, forwarded["note_id"])
assert note is not None
note.status = ArtifactStatus.FAILED
note.error = "boom (forced by test)"
session.commit()
# wrong role for the stage
login(client, "imaging@demo.ca")
assert (
client.post(
f"/api/specialist/cases/{claim['id']}/regenerate",
json={"stage": "recommendation"},
).status_code
== 403
)
login(client, "specialist@demo.ca")
resp = client.post(
f"/api/specialist/cases/{claim['id']}/regenerate",
json={"stage": "recommendation"},
)
assert resp.status_code == 200, resp.text
assert resp.json() == {
"claim_id": claim["id"],
"stage": "recommendation",
"artifact_id": forwarded["note_id"],
"status": "pending",
}
session.expire_all()
note = session.get(RecommendationNote, forwarded["note_id"])
assert note is not None
assert note.status is ArtifactStatus.COMPLETE
assert note.error is None
assert note.recommendation is not None
def test_tampered_image_forces_further_testing_and_mandatory_review(
client: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
assert SEED_TAMPERED.is_file()
claim = _claim_in_imaging_review(
client, session, users, tmp_path, filename="tampered_xray.dcm", source=SEED_TAMPERED
)
report = session.scalar(
select(DiagnosticReport).where(DiagnosticReport.claim_id == claim["id"])
)
assert report is not None
assert report.authenticity_verdict == "likely_fraudulent"
assert report.requires_mandatory_review is True
forwarded = _forward(client, claim["id"])
session.expire_all()
note = session.get(RecommendationNote, forwarded["note_id"])
assert note is not None
assert note.status is ArtifactStatus.COMPLETE
assert note.recommendation is Recommendation.REQUIRES_FURTHER_TESTING # fallback rule 1
assert note.requires_mandatory_review is True
payload = json.loads(note.payload_json or "{}")
assert payload["recommendation"] == "REQUIRES_FURTHER_TESTING"
def test_wrong_roles_get_403_on_every_action(
client: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _claim_in_imaging_review(client, session, users, tmp_path)
cases = f"/api/specialist/cases/{claim['id']}"
login(client, "specialist@demo.ca") # medical specialist cannot act on imaging stage
assert client.post(f"{cases}/forward").status_code == 403
assert client.post(f"{cases}/return", json={"note": "x"}).status_code == 403
assert client.get("/api/specialist/queue", params={"stage": "imaging"}).status_code == 403
login(client, "imaging@demo.ca") # imaging specialist cannot act on specialist stage
assert client.post(f"{cases}/send-to-insurer").status_code == 403
assert client.post(f"{cases}/request-further-testing", json={"note": "x"}).status_code == 403
assert (
client.post(f"{cases}/regenerate", json={"stage": "adjudication"}).status_code == 403
)
login(client, "claimant@demo.ca") # claimants never reach specialist endpoints
assert client.get("/api/specialist/queue").status_code == 403
assert client.get(cases).status_code == 403
assert client.post(f"{cases}/forward").status_code == 403
assert client.post(f"{cases}/return", json={"note": "x"}).status_code == 403
assert client.post(f"{cases}/regenerate", json={"stage": "imaging"}).status_code == 403
login(client, "agent@demo.ca") # agents only regenerate adjudication artifacts
assert client.get(cases).status_code == 403
assert client.post(f"{cases}/forward").status_code == 403
assert client.post(f"{cases}/regenerate", json={"stage": "imaging"}).status_code == 403
# adjudication regenerate is allowed for agents, but there is no failed summary yet
assert (
client.post(f"{cases}/regenerate", json={"stage": "adjudication"}).status_code == 409
)
login(client, "imaging@demo.ca")
assert client.get("/api/specialist/cases/999999").status_code == 404