claimflow-api / tests /test_seed.py
Minifigures's picture
feat: ClaimFlow API demo backend
ceea9e1 verified
Raw
History Blame Contribute Delete
10.5 kB
from collections.abc import Iterator
from pathlib import Path
import pytest
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app import db
from app.claimguard.audit import verify_chain
from app.config import Settings
from app.main import create_app
from app.models import (
AuditEvent,
Claim,
ClaimAction,
ClaimHistory,
ClaimState,
Decision,
DiagnosticReport,
Document,
Notification,
RecommendationNote,
Role,
User,
)
from app.rag.store import get_adjudicated_cases_collection, get_client
from scripts.seed import (
FIXTURE_STATES,
REF_ADJUDICATION_FR,
REF_APPROVED,
REF_IMAGING_TAMPERED,
REF_SPECIALIST_REVIEW,
SeedSummary,
seed_database,
)
ALL_ROLES = {
Role.CLAIMANT,
Role.IMAGING_SPECIALIST,
Role.MEDICAL_SPECIALIST,
Role.INSURANCE_AGENT,
}
def _user_count(session: Session) -> int:
return session.scalar(select(func.count()).select_from(User)) or 0
def _history_count(session: Session, member_id: str | None = None) -> int:
stmt = select(func.count()).select_from(ClaimHistory)
if member_id is not None:
stmt = stmt.where(ClaimHistory.member_id == member_id)
return session.scalar(stmt) or 0
def _history_tuples(session: Session) -> list[tuple]:
return [
tuple(row)
for row in session.execute(
select(
ClaimHistory.member_id,
ClaimHistory.claim_type,
ClaimHistory.procedure_code,
ClaimHistory.diagnosis_code,
ClaimHistory.modality,
ClaimHistory.billed_amount,
ClaimHistory.outcome,
ClaimHistory.date_of_service,
ClaimHistory.decided_at,
ClaimHistory.notes,
).order_by(ClaimHistory.id)
).all()
]
def _fresh_session(db_path: Path) -> Session:
engine = db.make_engine(f"sqlite:///{db_path}")
db.Base.metadata.create_all(engine)
return Session(engine)
def test_seed_is_idempotent(session: Session) -> None:
first = seed_database(session, fixtures=False)
assert len(first.users_created) == 5
assert first.history_inserted == 40
users_after_first = _user_count(session)
history_after_first = _history_count(session)
second = seed_database(session, fixtures=False)
assert second.users_created == []
assert len(second.users_skipped) == 5
assert second.history_inserted == 0
assert _user_count(session) == users_after_first
assert _history_count(session) == history_after_first
def test_seed_creates_all_roles_and_history(session: Session) -> None:
seed_database(session, fixtures=False)
roles = set(session.scalars(select(User.role)).all())
assert ALL_ROLES <= roles
assert _history_count(session, "MBR-1001") == 25
assert _history_count(session, "MBR-1002") == 15
def test_seed_history_is_deterministic(tmp_path: Path) -> None:
one = _fresh_session(tmp_path / "one.sqlite")
two = _fresh_session(tmp_path / "two.sqlite")
try:
seed_database(one, fixtures=False)
seed_database(two, fixtures=False)
rows_one = _history_tuples(one)
rows_two = _history_tuples(two)
assert len(rows_one) == 40
assert rows_one == rows_two
finally:
one.close()
two.close()
def test_no_fixtures_leaves_zero_claims(session: Session, settings: Settings) -> None:
seed_database(session, settings, fixtures=False)
assert (session.scalar(select(func.count()).select_from(Claim)) or 0) == 0
# ------------------------------------------------------------------ demo fixture claims
#
# The full fixture seed walks seven claims through the real state machine and the
# stage-1/2/3 runners, so it is seeded once per module and shared by the assertions.
@pytest.fixture(scope="module")
def seeded(
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[tuple[Session, Settings, SeedSummary]]:
tmp = tmp_path_factory.mktemp("seed-fixtures")
settings = Settings(
database_url=f"sqlite:///{tmp}/seed.sqlite",
upload_dir=tmp / "uploads",
jwt_secret="test-secret-0123456789abcdef-0123456789",
cookie_secure=False,
email_provider="console",
model_backend="stub",
anthropic_api_key="",
chroma_dir=tmp / "chroma",
)
create_app(settings)
factory = db.get_session_factory()
session = factory()
try:
summary = seed_database(session, settings)
yield session, settings, summary
finally:
session.close()
def _claim_by_ref(session: Session, claim_ref: str) -> Claim:
claim = session.scalar(select(Claim).where(Claim.claim_ref == claim_ref))
assert claim is not None, f"fixture claim {claim_ref} missing"
return claim
def test_fixtures_create_exactly_seven_claims_in_the_right_states(
seeded: tuple[Session, Settings, SeedSummary],
) -> None:
session, _, summary = seeded
assert summary.fixture_claims == list(FIXTURE_STATES)
assert (session.scalar(select(func.count()).select_from(Claim)) or 0) == 7
actual = {
ref: state for ref, state in session.execute(select(Claim.claim_ref, Claim.state)).all()
}
assert actual == FIXTURE_STATES
def test_tampered_claim_report_is_flagged(
seeded: tuple[Session, Settings, SeedSummary],
) -> None:
"""Non-authentic either way: the stub's filename hook says likely_fraudulent,
the real fusion lands suspicious via CNN + metadata hard-override."""
session, _, _ = seeded
claim = _claim_by_ref(session, REF_IMAGING_TAMPERED)
report = session.scalar(
select(DiagnosticReport)
.where(DiagnosticReport.claim_id == claim.id)
.order_by(DiagnosticReport.id.desc())
.limit(1)
)
assert report is not None
assert report.authenticity_verdict in ("suspicious", "likely_fraudulent")
assert report.requires_mandatory_review is True
document = session.scalar(select(Document).where(Document.claim_id == claim.id))
assert document is not None and "tampered" in document.filename
assert document.mime == "application/dicom"
assert document.dicom_meta_json is not None # metadata signal feeds on this
def test_specialist_and_adjudication_artifacts_are_complete(
seeded: tuple[Session, Settings, SeedSummary],
) -> None:
session, _, _ = seeded
claim_c = _claim_by_ref(session, REF_SPECIALIST_REVIEW)
note = session.scalar(
select(RecommendationNote).where(RecommendationNote.claim_id == claim_c.id)
)
assert note is not None and note.status.value == "complete"
claim_d = _claim_by_ref(session, REF_ADJUDICATION_FR)
assert claim_d.claimant.preferred_language == "fr"
assert claim_d.claimant.member_id == "MBR-1002"
summary_row = session.execute(
select(func.count()).select_from(Decision).where(Decision.claim_id == claim_d.id)
).scalar_one()
assert summary_row >= 3 # submit, system imaging_complete, forward, send_to_insurer
def test_approved_claim_has_notification_and_agent_decision(
seeded: tuple[Session, Settings, SeedSummary],
) -> None:
session, _, _ = seeded
claim = _claim_by_ref(session, REF_APPROVED)
assert claim.state is ClaimState.APPROVED
notifications = session.scalars(
select(Notification).where(Notification.claim_id == claim.id)
).all()
assert len(notifications) >= 1
assert any(claim.claim_ref in n.subject for n in notifications)
assert any("approved" in n.body_text.lower() for n in notifications)
approve = session.scalar(
select(Decision).where(
Decision.claim_id == claim.id, Decision.action == ClaimAction.APPROVE
)
)
assert approve is not None
assert approve.actor_role == Role.INSURANCE_AGENT.value
def test_audit_chain_verifies_after_full_seed(
seeded: tuple[Session, Settings, SeedSummary],
) -> None:
session, _, _ = seeded
valid, checked = verify_chain(session)
assert valid is True
assert checked > 0
def test_precedent_collection_holds_at_least_twelve_cases(
seeded: tuple[Session, Settings, SeedSummary],
) -> None:
_, settings, summary = seeded
assert summary.precedents_indexed == 12
collection = get_adjudicated_cases_collection(get_client(settings))
assert collection.count() >= 12
def test_second_seed_run_adds_nothing(
seeded: tuple[Session, Settings, SeedSummary],
) -> None:
session, settings, _ = seeded
before = {
model: session.scalar(select(func.count()).select_from(model)) or 0
for model in (Claim, Document, Decision, Notification, AuditEvent)
}
second = seed_database(session, settings)
assert second.fixture_claims == []
assert second.precedents_indexed == 0
after = {
model: session.scalar(select(func.count()).select_from(model)) or 0
for model in (Claim, Document, Decision, Notification, AuditEvent)
}
assert after == before
def test_partial_fixture_seed_self_heals(tmp_path: Path) -> None:
"""A half-seeded demo (some fixture refs present, not all) wipes and reseeds."""
from app.auth.passwords import hash_password
from scripts.seed import _heal_partial_fixtures
settings = Settings(
database_url=f"sqlite:///{tmp_path}/heal.sqlite",
upload_dir=tmp_path / "uploads",
chroma_dir=tmp_path / "chroma",
model_backend="stub",
anthropic_api_key="",
)
create_app(settings)
factory = db.get_session_factory()
with factory() as session:
claimant = User(
email="partial@demo.ca",
password_hash=hash_password("demo1234"),
role=Role.CLAIMANT,
full_name="Partial Seed",
member_id="MBR-0000",
)
session.add(claimant)
session.flush()
session.add(
Claim(
claim_ref=REF_IMAGING_TAMPERED,
claimant_id=claimant.id,
claim_type="imaging",
state=ClaimState.SUBMITTED,
)
)
session.commit()
assert _heal_partial_fixtures(session, settings) is True
assert (session.scalar(select(func.count()).select_from(Claim)) or 0) == 0
assert (session.scalar(select(func.count()).select_from(User)) or 0) == 0
# And a complete set (or empty DB) must NOT trigger the wipe.
assert _heal_partial_fixtures(session, settings) is False