@@ -372,6 +372,117 @@ async def test_explicit_profile_maps_correctly(self, mock_get_api_key):
372372
373373 call_args = mock_client .post .call_args
374374 assert call_args [1 ]["json" ]["profile" ] == "InheritanceOnly"
375+ # No data_source supplied => omitted from the body.
376+ assert "dataSource" not in call_args [1 ]["json" ]
377+
378+ @pytest .mark .asyncio
379+ @patch ("tools.artifact_relationships.get_api_key_from_context" )
380+ async def test_forwards_data_source (self , mock_get_api_key ):
381+ mock_get_api_key .return_value = "test_key"
382+
383+ ctx = MagicMock (spec = Context )
384+ ctx .debug = AsyncMock ()
385+ ctx .error = AsyncMock ()
386+
387+ mock_response = MagicMock ()
388+ mock_response .json .return_value = {
389+ "sourceIdentifier" : "id" ,
390+ "profile" : "CallsOnly" ,
391+ "found" : True ,
392+ "relationships" : [],
393+ }
394+ mock_response .raise_for_status = MagicMock ()
395+
396+ mock_client = AsyncMock ()
397+ mock_client .post .return_value = mock_response
398+
399+ mock_context = MagicMock ()
400+ mock_context .client = mock_client
401+ mock_context .base_url = "https://app.codealive.ai"
402+ ctx .request_context .lifespan_context = mock_context
403+
404+ await get_artifact_relationships (
405+ ctx = ctx ,
406+ identifier = "id" ,
407+ data_source = "backend" ,
408+ )
409+
410+ assert mock_client .post .call_args [1 ]["json" ]["dataSource" ] == "backend"
411+
412+ @pytest .mark .asyncio
413+ @patch ("tools.artifact_relationships.get_api_key_from_context" )
414+ async def test_whitespace_data_source_omitted (self , mock_get_api_key ):
415+ """A whitespace-only data_source normalizes to None: not sent to the backend
416+ and not echoed in the not-found hint (preserves the 409-on-ambiguity fallback)."""
417+ mock_get_api_key .return_value = "test_key"
418+
419+ ctx = MagicMock (spec = Context )
420+ ctx .debug = AsyncMock ()
421+ ctx .error = AsyncMock ()
422+
423+ mock_response = MagicMock ()
424+ mock_response .json .return_value = {
425+ "sourceIdentifier" : "id" ,
426+ "profile" : "CallsOnly" ,
427+ "found" : False ,
428+ }
429+ mock_response .raise_for_status = MagicMock ()
430+
431+ mock_client = AsyncMock ()
432+ mock_client .post .return_value = mock_response
433+
434+ mock_context = MagicMock ()
435+ mock_context .client = mock_client
436+ mock_context .base_url = "https://app.codealive.ai"
437+ ctx .request_context .lifespan_context = mock_context
438+
439+ result = await get_artifact_relationships (
440+ ctx = ctx ,
441+ identifier = "id" ,
442+ data_source = " " ,
443+ )
444+
445+ assert "dataSource" not in mock_client .post .call_args [1 ]["json" ]
446+ # The confusing `... in data source " "` hint must not appear.
447+ assert '" "' not in result ["hint" ]
448+
449+ @pytest .mark .asyncio
450+ @patch ("tools.artifact_relationships.get_api_key_from_context" )
451+ async def test_ambiguous_409_surfaces_candidate_data_sources (self , mock_get_api_key ):
452+ import httpx
453+
454+ mock_get_api_key .return_value = "test_key"
455+
456+ ctx = MagicMock (spec = Context )
457+ ctx .debug = AsyncMock ()
458+ ctx .error = AsyncMock ()
459+
460+ mock_response = MagicMock ()
461+ mock_response .status_code = 409
462+ mock_response .text = (
463+ '{"detail": "Identifier matches 2 data sources: '
464+ "Name='backend' Id='ds-main', Name='backend-legacy' Id='ds-master'\" }"
465+ )
466+ mock_response .raise_for_status .side_effect = httpx .HTTPStatusError (
467+ "Conflict" , request = MagicMock (), response = mock_response
468+ )
469+
470+ mock_client = AsyncMock ()
471+ mock_client .post .return_value = mock_response
472+
473+ mock_context = MagicMock ()
474+ mock_context .client = mock_client
475+ mock_context .base_url = "https://app.codealive.ai"
476+ ctx .request_context .lifespan_context = mock_context
477+
478+ with pytest .raises (ToolError ) as exc :
479+ await get_artifact_relationships (ctx = ctx , identifier = "org/repo::path::Symbol" )
480+
481+ message = str (exc .value )
482+ assert "409" in message
483+ # The candidate data sources from the backend 409 must be surfaced, plus the data_source retry hint.
484+ assert "backend" in message and "backend-legacy" in message
485+ assert "data_source" in message
375486
376487 @pytest .mark .asyncio
377488 async def test_empty_identifier_raises_tool_error (self ):
@@ -446,3 +557,21 @@ async def test_not_found_response_renders_correctly(self, mock_get_api_key):
446557
447558 assert data ["found" ] is False
448559 assert "relationships" not in data
560+
561+ def test_not_found_hint_with_data_source_suggests_retry_or_omit (self ):
562+ payload = _build_relationships_dict (
563+ {"sourceIdentifier" : "org/repo::path::S" , "profile" : "CallsOnly" , "found" : False },
564+ data_source = "backend" ,
565+ )
566+ hint = payload ["hint" ]
567+ assert "backend" in hint
568+ assert "data_source" in hint
569+ assert "omit" in hint .lower ()
570+
571+ def test_not_found_hint_without_data_source_is_generic (self ):
572+ payload = _build_relationships_dict (
573+ {"sourceIdentifier" : "org/repo::path::S" , "profile" : "CallsOnly" , "found" : False },
574+ )
575+ hint = payload ["hint" ]
576+ assert "data_source" not in hint
577+ assert "fresh identifier" in hint
0 commit comments