@@ -170,4 +170,187 @@ async def test_get_data_sources_handles_missing_repository_ids(mock_get_api_key)
170170 workspace = data_sources [0 ]
171171 assert workspace ["id" ] == "workspace-1"
172172 assert workspace ["name" ] == "Test Workspace"
173- assert "repositoryIds" not in workspace
173+ assert "repositoryIds" not in workspace
174+
175+
176+ def _ctx_with_response (json_return , headers = None ):
177+ """Builds a mocked Context whose client.get returns a response with the given JSON body."""
178+ mock_ctx = MagicMock (spec = Context )
179+ mock_ctx .info = AsyncMock ()
180+ mock_ctx .warning = AsyncMock ()
181+ mock_ctx .error = AsyncMock ()
182+
183+ mock_response = MagicMock ()
184+ mock_response .json .return_value = json_return
185+ mock_response .headers = headers or {}
186+ mock_response .raise_for_status = MagicMock ()
187+
188+ mock_client = AsyncMock ()
189+ mock_client .get = AsyncMock (return_value = mock_response )
190+
191+ mock_lifespan_context = MagicMock ()
192+ mock_lifespan_context .base_url = "https://api.example.com"
193+ mock_lifespan_context .client = mock_client
194+ mock_ctx .request_context .lifespan_context = mock_lifespan_context
195+ return mock_ctx , mock_client
196+
197+
198+ @pytest .mark .asyncio
199+ @patch ('tools.datasources.get_api_key_from_context' )
200+ async def test_get_data_sources_with_query_passes_query_param (mock_get_api_key ):
201+ """When a query is supplied, it is forwarded to the listing endpoint as the `query` param."""
202+ mock_get_api_key .return_value = "test-key"
203+ mock_ctx , mock_client = _ctx_with_response ([
204+ {"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" , "relevanceReason" : "handles OAuth" },
205+ ])
206+
207+ await get_data_sources (mock_ctx , alive_only = True , query = "add OAuth to checkout" )
208+
209+ call_args = mock_client .get .call_args
210+ assert call_args .args [0 ] == "/api/datasources/ready"
211+ assert call_args .kwargs ["params" ] == {"query" : "add OAuth to checkout" }
212+
213+
214+ @pytest .mark .asyncio
215+ @patch ('tools.datasources.get_api_key_from_context' )
216+ async def test_get_data_sources_without_query_sends_no_query_param (mock_get_api_key ):
217+ """Without a query, no `query` param is sent (legacy behavior unchanged)."""
218+ mock_get_api_key .return_value = "test-key"
219+ mock_ctx , mock_client = _ctx_with_response ([
220+ {"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" },
221+ ])
222+
223+ await get_data_sources (mock_ctx , alive_only = True )
224+
225+ call_args = mock_client .get .call_args
226+ assert call_args .kwargs .get ("params" ) is None
227+
228+
229+ @pytest .mark .asyncio
230+ @patch ('tools.datasources.get_api_key_from_context' )
231+ async def test_get_data_sources_surfaces_relevance_reason (mock_get_api_key ):
232+ """relevanceReason is preserved per item for the client (wrapped shape when query is set)."""
233+ mock_get_api_key .return_value = "test-key"
234+ mock_ctx , _ = _ctx_with_response ([
235+ {"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" , "relevanceReason" : "implements the checkout flow" },
236+ ])
237+
238+ result = await get_data_sources (mock_ctx , alive_only = True , query = "checkout" )
239+
240+ payload = result
241+ assert payload ["dataSources" ][0 ]["relevanceReason" ] == "implements the checkout flow"
242+
243+
244+ @pytest .mark .asyncio
245+ @patch ('tools.datasources.get_api_key_from_context' )
246+ async def test_get_data_sources_filtered_hint_reports_total_and_omitted (mock_get_api_key ):
247+ """Filtered success surfaces how many sources exist beyond the shown subset and how to get them."""
248+ mock_get_api_key .return_value = "test-key"
249+ mock_ctx , _ = _ctx_with_response (
250+ [{"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" , "relevanceReason" : "checkout flow" }],
251+ headers = {"X-CodeAlive-Total-Data-Sources" : "25" },
252+ )
253+
254+ result = await get_data_sources (mock_ctx , alive_only = True , query = "checkout" )
255+
256+ payload = result
257+ assert len (payload ["dataSources" ]) == 1
258+ assert "1 of 25" in payload ["message" ]
259+ assert "omitted" in payload ["message" ].lower ()
260+ assert "without a query" in payload ["message" ].lower ()
261+
262+
263+ @pytest .mark .asyncio
264+ @patch ('tools.datasources.get_api_key_from_context' )
265+ async def test_get_data_sources_filtered_hint_without_total_header (mock_get_api_key ):
266+ """Filtered success without the total header still hints that sources were omitted."""
267+ mock_get_api_key .return_value = "test-key"
268+ mock_ctx , _ = _ctx_with_response (
269+ [{"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" , "relevanceReason" : "checkout flow" }],
270+ )
271+
272+ result = await get_data_sources (mock_ctx , alive_only = True , query = "checkout" )
273+
274+ payload = result
275+ assert "omitted" in payload ["message" ].lower ()
276+ assert "without a query" in payload ["message" ].lower ()
277+
278+
279+ @pytest .mark .asyncio
280+ @patch ('tools.datasources.get_api_key_from_context' )
281+ async def test_get_data_sources_filtered_hint_with_malformed_total_header (mock_get_api_key ):
282+ """A malformed total header is treated as absent rather than raising."""
283+ mock_get_api_key .return_value = "test-key"
284+ mock_ctx , _ = _ctx_with_response (
285+ [{"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" , "relevanceReason" : "checkout flow" }],
286+ headers = {"X-CodeAlive-Total-Data-Sources" : "not-a-number" },
287+ )
288+
289+ result = await get_data_sources (mock_ctx , alive_only = True , query = "checkout" )
290+
291+ payload = result
292+ assert "omitted" in payload ["message" ].lower ()
293+ assert "without a query" in payload ["message" ].lower ()
294+
295+
296+ @pytest .mark .asyncio
297+ @patch ('tools.datasources.get_api_key_from_context' )
298+ async def test_get_data_sources_all_relevant_hint_reports_no_omission (mock_get_api_key ):
299+ """When every available source is relevant, the hint says so instead of claiming omissions."""
300+ mock_get_api_key .return_value = "test-key"
301+ mock_ctx , _ = _ctx_with_response (
302+ [{"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" , "relevanceReason" : "checkout flow" }],
303+ headers = {"X-CodeAlive-Total-Data-Sources" : "1" },
304+ )
305+
306+ result = await get_data_sources (mock_ctx , alive_only = True , query = "checkout" )
307+
308+ payload = result
309+ assert "all 1" in payload ["message" ].lower ()
310+ assert "omitted" not in payload ["message" ].lower ()
311+
312+
313+ @pytest .mark .asyncio
314+ @patch ('tools.datasources.get_api_key_from_context' )
315+ async def test_get_data_sources_failopen_hint_when_no_reasons_present (mock_get_api_key ):
316+ """Query supplied but no item carries relevanceReason → the filter did not run (fail-open,
317+ disabled, or an older backend); the hint must say the FULL list is returned."""
318+ mock_get_api_key .return_value = "test-key"
319+ mock_ctx , _ = _ctx_with_response ([
320+ {"id" : "repo-1" , "name" : "Repo" , "type" : "Repository" },
321+ {"id" : "repo-2" , "name" : "Other" , "type" : "Repository" },
322+ ])
323+
324+ result = await get_data_sources (mock_ctx , alive_only = True , query = "checkout" )
325+
326+ payload = result
327+ assert len (payload ["dataSources" ]) == 2
328+ assert "unavailable" in payload ["message" ].lower ()
329+ assert "full" in payload ["message" ].lower ()
330+
331+
332+ @pytest .mark .asyncio
333+ @patch ('tools.datasources.get_api_key_from_context' )
334+ async def test_get_data_sources_empty_with_query_returns_no_relevant_hint (mock_get_api_key ):
335+ """Empty result WITH a query returns a 'no relevant' hint, not 'add a repository'."""
336+ mock_get_api_key .return_value = "test-key"
337+ mock_ctx , _ = _ctx_with_response ([])
338+
339+ result = await get_data_sources (mock_ctx , alive_only = True , query = "something unrelated" )
340+
341+ assert result ["dataSources" ] == []
342+ assert "relevant" in result ["hint" ].lower ()
343+ assert "add a repository" not in result ["hint" ].lower ()
344+
345+
346+ @pytest .mark .asyncio
347+ @patch ('tools.datasources.get_api_key_from_context' )
348+ async def test_get_data_sources_empty_without_query_keeps_add_repository_hint (mock_get_api_key ):
349+ """Empty result WITHOUT a query keeps the existing 'add a repository' hint."""
350+ mock_get_api_key .return_value = "test-key"
351+ mock_ctx , _ = _ctx_with_response ([])
352+
353+ result = await get_data_sources (mock_ctx , alive_only = True )
354+
355+ assert result ["dataSources" ] == []
356+ assert "add a repository" in result ["hint" ].lower ()
0 commit comments