File size: 7,444 Bytes
f824c7f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8adedd
f824c7f
 
 
 
f8adedd
f824c7f
 
f8adedd
f824c7f
 
 
 
 
 
 
f8adedd
f824c7f
 
 
 
 
 
 
 
f8adedd
f824c7f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8adedd
f824c7f
 
 
 
 
 
 
 
97aee42
 
f824c7f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97aee42
f824c7f
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""Integration tests: verify FE components correctly consume BE models."""

from __future__ import annotations

import json

from app.components.gap_card import render_gap_card
from app.components.profile_card import render_profile_card
from app.components.progress_tracker import render_progress_tracker
from app.components.trial_card import render_trial_card
from app.services.mock_data import (
    MOCK_ELIGIBILITY_LEDGERS,
    MOCK_PATIENT_PROFILE,
    MOCK_TRIAL_CANDIDATES,
)
from app.services.state_manager import JOURNEY_STATES
from trialpath.models import (
    EligibilityLedger,
    PatientProfile,
    SearchAnchors,
    TrialCandidate,
)


class TestMockDataIntegrity:
    """Verify mock data uses real BE models correctly."""

    def test_mock_profile_is_patient_profile(self):
        assert isinstance(MOCK_PATIENT_PROFILE, PatientProfile)

    def test_mock_trials_are_trial_candidates(self):
        for t in MOCK_TRIAL_CANDIDATES:
            assert isinstance(t, TrialCandidate)

    def test_mock_ledgers_are_eligibility_ledgers(self):
        for lg in MOCK_ELIGIBILITY_LEDGERS:
            assert isinstance(lg, EligibilityLedger)

    def test_mock_ledger_nct_ids_match_trials(self):
        trial_ids = {t.nct_id for t in MOCK_TRIAL_CANDIDATES}
        ledger_ids = {lg.nct_id for lg in MOCK_ELIGIBILITY_LEDGERS}
        assert ledger_ids == trial_ids

    def test_mock_profile_has_minimum_prescreen_data(self):
        assert MOCK_PATIENT_PROFILE.has_minimum_prescreen_data()

    def test_mock_profile_serializes_to_json(self):
        data = MOCK_PATIENT_PROFILE.model_dump_json()
        restored = PatientProfile.model_validate_json(data)
        assert restored.patient_id == MOCK_PATIENT_PROFILE.patient_id

    def test_mock_trials_serialize_to_json(self):
        for t in MOCK_TRIAL_CANDIDATES:
            data = t.model_dump_json()
            restored = TrialCandidate.model_validate_json(data)
            assert restored.nct_id == t.nct_id

    def test_mock_ledgers_serialize_to_json(self):
        for lg in MOCK_ELIGIBILITY_LEDGERS:
            data = lg.model_dump_json()
            restored = EligibilityLedger.model_validate_json(data)
            assert restored.nct_id == lg.nct_id


class TestComponentModelIntegration:
    """Verify FE components produce correct output from BE models."""

    def test_profile_card_renders_mock_profile(self):
        spec = render_profile_card(MOCK_PATIENT_PROFILE)
        assert spec["patient_id"] == "MOCK-P001"
        assert spec["has_minimum_prescreen_data"] is True
        assert len(spec["biomarkers"]) == 3

    def test_trial_card_renders_green_trial(self):
        # MOCK-NCT-FLAURA2 is LIKELY_ELIGIBLE -> green
        trial = MOCK_TRIAL_CANDIDATES[1]
        ledger = MOCK_ELIGIBILITY_LEDGERS[1]
        spec = render_trial_card(trial, ledger)
        assert spec["traffic_light"] == "green"
        assert spec["nct_id"] == "MOCK-NCT-FLAURA2"

    def test_trial_card_renders_yellow_trial(self):
        # MOCK-NCT-KEYNOTE999 is UNCERTAIN -> yellow
        trial = MOCK_TRIAL_CANDIDATES[0]
        ledger = MOCK_ELIGIBILITY_LEDGERS[0]
        spec = render_trial_card(trial, ledger)
        assert spec["traffic_light"] == "yellow"
        assert len(spec["gaps"]) == 1

    def test_trial_card_renders_red_trial(self):
        # MOCK-NCT-CM817 is LIKELY_INELIGIBLE -> red
        trial = MOCK_TRIAL_CANDIDATES[2]
        ledger = MOCK_ELIGIBILITY_LEDGERS[2]
        spec = render_trial_card(trial, ledger)
        assert spec["traffic_light"] == "red"

    def test_gap_card_renders_from_ledger_gap(self):
        ledger = MOCK_ELIGIBILITY_LEDGERS[0]
        gap = ledger.gaps[0]
        spec = render_gap_card(gap, affected_trials=["MOCK-NCT-KEYNOTE999"])
        assert "Brain MRI" in spec["description"]
        assert spec["importance_color"] == "red"  # high importance

    def test_progress_tracker_all_states(self):
        for state in JOURNEY_STATES:
            spec = render_progress_tracker(state)
            assert len(spec["steps"]) == 5
            current_steps = [s for s in spec["steps"] if s["status"] == "current"]
            assert len(current_steps) == 1


class TestDoctorPacketGeneration:
    """Verify doctor packet export generates valid JSON/Markdown."""

    def test_json_packet_structure(self):
        profile = MOCK_PATIENT_PROFILE
        ledgers = MOCK_ELIGIBILITY_LEDGERS
        eligible = sum(1 for lg in ledgers if lg.traffic_light == "green")
        uncertain = sum(1 for lg in ledgers if lg.traffic_light == "yellow")
        ineligible = sum(1 for lg in ledgers if lg.traffic_light == "red")
        total_gaps = sum(len(lg.gaps) for lg in ledgers)

        packet = {
            "patient_id": profile.patient_id,
            "summary": {
                "eligible_count": eligible,
                "uncertain_count": uncertain,
                "ineligible_count": ineligible,
                "total_gaps": total_gaps,
            },
            "trials": [
                {
                    "nct_id": lg.nct_id,
                    "overall_assessment": lg.overall_assessment.value,
                    "met": lg.met_count,
                    "not_met": lg.not_met_count,
                    "unknown": lg.unknown_count,
                    "gaps": [g.description for g in lg.gaps],
                }
                for lg in ledgers
            ],
        }

        serialized = json.dumps(packet, indent=2)
        restored = json.loads(serialized)
        assert restored["patient_id"] == "MOCK-P001"
        assert restored["summary"]["eligible_count"] == 1
        assert restored["summary"]["uncertain_count"] == 1
        assert restored["summary"]["ineligible_count"] == 1
        assert restored["summary"]["total_gaps"] == 2
        assert len(restored["trials"]) == 3

    def test_all_trial_nct_ids_in_packet(self):
        ledgers = MOCK_ELIGIBILITY_LEDGERS
        packet_ids = [lg.nct_id for lg in ledgers]
        expected_ids = ["MOCK-NCT-KEYNOTE999", "MOCK-NCT-FLAURA2", "MOCK-NCT-CM817"]
        assert packet_ids == expected_ids


class TestSearchAnchorsFromProfile:
    """Verify BE model can generate SearchAnchors from PatientProfile."""

    def test_profile_to_search_anchors(self):
        profile = MOCK_PATIENT_PROFILE
        assert profile.diagnosis is not None
        assert profile.performance_status is not None
        anchors = SearchAnchors(
            condition=profile.diagnosis.primary_condition,
            subtype=profile.diagnosis.histology,
            biomarkers=[b.name for b in profile.biomarkers],
            stage=profile.diagnosis.stage,
            age=profile.demographics.age,
            performance_status_max=profile.performance_status.value,
        )
        assert anchors.condition == "Non-Small Cell Lung Cancer"
        assert "EGFR" in anchors.biomarkers
        assert anchors.stage == "IIIB"
        assert anchors.age == 62

    def test_search_anchors_serializes(self):
        profile = MOCK_PATIENT_PROFILE
        assert profile.diagnosis is not None
        anchors = SearchAnchors(
            condition=profile.diagnosis.primary_condition,
            biomarkers=[b.name for b in profile.biomarkers],
            stage=profile.diagnosis.stage,
        )
        data = anchors.model_dump_json()
        restored = SearchAnchors.model_validate_json(data)
        assert restored.condition == anchors.condition