yakilee Claude Opus 4.6 commited on
Commit
9b26c81
·
1 Parent(s): 9a1cb2e

test: add tests for SearchAnchors new fields and enhanced MCP search

Browse files

- Model tests: interventions/eligibility_keywords serialization,
default empty lists, JSON roundtrip with new fields
- MCP tests: query.cond/query.intr params, eligibility keywords,
backward compatibility, multi-variant intervention variants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

trialpath/tests/test_mcp.py CHANGED
@@ -137,6 +137,116 @@ class TestMCPClient:
137
  assert args["conditions"] == ["NSCLC"]
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  class TestNormalizeTrial:
141
  """Test normalize_trial conversion."""
142
 
 
137
  assert args["conditions"] == ["NSCLC"]
138
 
139
 
140
+ class TestSearchDirectEnhanced:
141
+ """Test enhanced search_direct with intervention and eligibility dimensions."""
142
+
143
+ @pytest.fixture
144
+ def client(self):
145
+ return ClinicalTrialsMCPClient(mcp_url="http://localhost:3000")
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_search_direct_uses_query_cond_and_intr(self, client):
149
+ """search_direct should use query.cond for condition and query.intr for intervention."""
150
+ anchors = SearchAnchors(
151
+ condition="Non-Small Cell Lung Cancer",
152
+ subtype="adenocarcinoma",
153
+ interventions=["osimertinib", "erlotinib"],
154
+ )
155
+
156
+ with patch("trialpath.services.mcp_client.requests") as mock_requests:
157
+ mock_resp = MagicMock()
158
+ mock_resp.json.return_value = {"studies": []}
159
+ mock_resp.raise_for_status = MagicMock()
160
+ mock_requests.get.return_value = mock_resp
161
+
162
+ await client.search_direct(anchors)
163
+
164
+ call_args = mock_requests.get.call_args
165
+ params = call_args.kwargs.get("params", call_args[1].get("params", {}))
166
+ assert params["query.cond"] == "Non-Small Cell Lung Cancer adenocarcinoma"
167
+ assert params["query.intr"] == "osimertinib"
168
+
169
+ @pytest.mark.asyncio
170
+ async def test_search_direct_uses_eligibility_keywords(self, client):
171
+ """search_direct should use query.term for eligibility keywords."""
172
+ anchors = SearchAnchors(
173
+ condition="NSCLC",
174
+ eligibility_keywords=["ECOG 0-1", "stage IV"],
175
+ )
176
+
177
+ with patch("trialpath.services.mcp_client.requests") as mock_requests:
178
+ mock_resp = MagicMock()
179
+ mock_resp.json.return_value = {"studies": []}
180
+ mock_resp.raise_for_status = MagicMock()
181
+ mock_requests.get.return_value = mock_resp
182
+
183
+ await client.search_direct(anchors)
184
+
185
+ call_args = mock_requests.get.call_args
186
+ params = call_args.kwargs.get("params", call_args[1].get("params", {}))
187
+ assert params["query.term"] == "ECOG 0-1 stage IV"
188
+
189
+ @pytest.mark.asyncio
190
+ async def test_search_direct_without_interventions_backwards_compatible(self, client):
191
+ """search_direct without new fields should still work (backward compatible)."""
192
+ anchors = SearchAnchors(
193
+ condition="NSCLC",
194
+ biomarkers=["EGFR exon 19 deletion"],
195
+ )
196
+
197
+ with patch("trialpath.services.mcp_client.requests") as mock_requests:
198
+ mock_resp = MagicMock()
199
+ mock_resp.json.return_value = {"studies": []}
200
+ mock_resp.raise_for_status = MagicMock()
201
+ mock_requests.get.return_value = mock_resp
202
+
203
+ await client.search_direct(anchors)
204
+
205
+ call_args = mock_requests.get.call_args
206
+ params = call_args.kwargs.get("params", call_args[1].get("params", {}))
207
+ assert params["query.cond"] == "NSCLC"
208
+ assert "query.intr" not in params
209
+ assert params["query.term"] == "EGFR"
210
+
211
+ @pytest.mark.asyncio
212
+ async def test_search_multi_variant_includes_intervention_variant(self, client):
213
+ """search_multi_variant should fire intervention-specific search variants."""
214
+ anchors = SearchAnchors(
215
+ condition="NSCLC",
216
+ interventions=["osimertinib", "erlotinib"],
217
+ eligibility_keywords=["ECOG 0-1"],
218
+ )
219
+
220
+ call_count = 0
221
+
222
+ async def mock_search_direct(a):
223
+ nonlocal call_count
224
+ call_count += 1
225
+ return [
226
+ {
227
+ "protocolSection": {
228
+ "identificationModule": {
229
+ "nctId": f"NCT0000000{call_count}",
230
+ "briefTitle": f"Trial {call_count}",
231
+ },
232
+ "statusModule": {},
233
+ "designModule": {},
234
+ "conditionsModule": {},
235
+ "eligibilityModule": {},
236
+ "contactsLocationsModule": {},
237
+ }
238
+ }
239
+ ]
240
+
241
+ with patch.object(client, "search_direct", side_effect=mock_search_direct):
242
+ results = await client.search_multi_variant(anchors)
243
+
244
+ # Variants: full(1) + broad(1) + interventions(2) + eligibility(1) = 5
245
+ assert call_count == 5
246
+ # All unique NCT IDs
247
+ assert len(results) == 5
248
+
249
+
250
  class TestNormalizeTrial:
251
  """Test normalize_trial conversion."""
252
 
trialpath/tests/test_models.py CHANGED
@@ -273,6 +273,39 @@ class TestSearchAnchors:
273
  assert anchors.geography.country == "DE"
274
  assert anchors.geography.max_distance_km == 200
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  def test_json_roundtrip(self):
277
  from trialpath.models.search_anchors import SearchAnchors
278
 
@@ -285,6 +318,20 @@ class TestSearchAnchors:
285
  restored = SearchAnchors.model_validate_json(json_str)
286
  assert restored == anchors
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  class TestTrialCandidate:
290
  """TrialCandidate v1 tests."""
 
273
  assert anchors.geography.country == "DE"
274
  assert anchors.geography.max_distance_km == 200
275
 
276
+ def test_search_anchors_with_interventions(self):
277
+ """interventions field should serialize correctly."""
278
+ from trialpath.models.search_anchors import SearchAnchors
279
+
280
+ anchors = SearchAnchors(
281
+ condition="NSCLC",
282
+ biomarkers=["EGFR exon 19 deletion"],
283
+ interventions=["osimertinib", "erlotinib"],
284
+ )
285
+ assert anchors.interventions == ["osimertinib", "erlotinib"]
286
+ data = anchors.model_dump()
287
+ assert data["interventions"] == ["osimertinib", "erlotinib"]
288
+
289
+ def test_search_anchors_with_eligibility_keywords(self):
290
+ """eligibility_keywords field should serialize correctly."""
291
+ from trialpath.models.search_anchors import SearchAnchors
292
+
293
+ anchors = SearchAnchors(
294
+ condition="NSCLC",
295
+ eligibility_keywords=["ECOG 0-1", "stage IV", "EGFR mutation"],
296
+ )
297
+ assert anchors.eligibility_keywords == ["ECOG 0-1", "stage IV", "EGFR mutation"]
298
+ data = anchors.model_dump()
299
+ assert data["eligibility_keywords"] == ["ECOG 0-1", "stage IV", "EGFR mutation"]
300
+
301
+ def test_search_anchors_defaults_empty_lists(self):
302
+ """New fields should default to empty lists for backward compatibility."""
303
+ from trialpath.models.search_anchors import SearchAnchors
304
+
305
+ anchors = SearchAnchors(condition="NSCLC")
306
+ assert anchors.interventions == []
307
+ assert anchors.eligibility_keywords == []
308
+
309
  def test_json_roundtrip(self):
310
  from trialpath.models.search_anchors import SearchAnchors
311
 
 
318
  restored = SearchAnchors.model_validate_json(json_str)
319
  assert restored == anchors
320
 
321
+ def test_json_roundtrip_with_new_fields(self):
322
+ """JSON roundtrip should preserve interventions and eligibility_keywords."""
323
+ from trialpath.models.search_anchors import SearchAnchors
324
+
325
+ anchors = SearchAnchors(
326
+ condition="NSCLC",
327
+ interventions=["osimertinib"],
328
+ eligibility_keywords=["ECOG 0-1", "stage IV"],
329
+ )
330
+ json_str = anchors.model_dump_json()
331
+ restored = SearchAnchors.model_validate_json(json_str)
332
+ assert restored.interventions == ["osimertinib"]
333
+ assert restored.eligibility_keywords == ["ECOG 0-1", "stage IV"]
334
+
335
 
336
  class TestTrialCandidate:
337
  """TrialCandidate v1 tests."""