@@ -84,6 +84,13 @@ def _mock_context():
8484 _resolve_user_tenant_language ,
8585 _apply_duplicate_name_availability_rules ,
8686 _check_single_model_availability ,
87+ _normalize_language_key ,
88+ _render_prompt_template ,
89+ _format_existing_values ,
90+ _generate_unique_agent_name_with_suffix ,
91+ _generate_unique_display_name_with_suffix ,
92+ _generate_unique_value_with_suffix ,
93+ _regenerate_agent_value_with_llm ,
8794 )
8895 from consts .model import ExportAndImportAgentInfo , ExportAndImportDataFormat , MCPInfo , AgentRequest
8996
@@ -5202,6 +5209,340 @@ def mock_create_agent_impl(agent_info, tenant_id, user_id):
52025209 assert agent_info_dict ["enabled" ] is True
52035210
52045211
5212+ # =====================================================================
5213+ # Additional tests for internal helper functions and import logic
5214+ # =====================================================================
5215+
5216+
5217+ def test_normalize_language_key_variants ():
5218+ """_normalize_language_key should normalize various language inputs."""
5219+ from consts .const import LANGUAGE as LANG
5220+
5221+ assert _normalize_language_key ("zh-CN" ) == LANG ["ZH" ]
5222+ assert _normalize_language_key ("ZH" ) == LANG ["ZH" ]
5223+ assert _normalize_language_key ("en" ) == LANG ["EN" ]
5224+ assert _normalize_language_key ("EN-us" ) == LANG ["EN" ]
5225+ # Fallback when language is None or empty
5226+ assert _normalize_language_key ("" ) == LANG ["EN" ]
5227+ assert _normalize_language_key (None ) == LANG ["EN" ]
5228+
5229+
5230+ def test_render_prompt_template_success (monkeypatch ):
5231+ """_render_prompt_template should render a jinja2 template successfully."""
5232+
5233+ class FakeTemplate :
5234+ def __init__ (self , template_str ):
5235+ self .template_str = template_str
5236+
5237+ def render (self , ** context ):
5238+ # Very small fake renderer for test purposes
5239+ return self .template_str .format (** context )
5240+
5241+ monkeypatch .setattr (
5242+ agent_service , "Template" , FakeTemplate , raising = False
5243+ )
5244+
5245+ tpl = "Hello {name}"
5246+ rendered = _render_prompt_template (tpl , name = "World" )
5247+ assert rendered == "Hello World"
5248+
5249+
5250+ def test_render_prompt_template_on_error_returns_original (monkeypatch ):
5251+ """When Template.render fails, _render_prompt_template should return original template."""
5252+
5253+ class FailingTemplate :
5254+ def __init__ (self , template_str ):
5255+ self .template_str = template_str
5256+
5257+ def render (self , ** context ):
5258+ raise ValueError ("render failed" )
5259+
5260+ monkeypatch .setattr (
5261+ agent_service , "Template" , FailingTemplate , raising = False
5262+ )
5263+
5264+ tpl = "Broken {template"
5265+ # Should not raise; should return original string
5266+ assert _render_prompt_template (tpl , name = "x" ) == tpl
5267+
5268+
5269+ def test_format_existing_values_for_languages ():
5270+ """_format_existing_values should format values and handle empty cases."""
5271+ from consts .const import LANGUAGE as LANG
5272+
5273+ # Non-empty set
5274+ values = {"b" , "a" }
5275+ formatted = _format_existing_values (values , LANG ["EN" ])
5276+ assert formatted in {"a, b" , "b, a" } # order not guaranteed
5277+
5278+ # Empty set, English
5279+ assert _format_existing_values (set (), LANG ["EN" ]) == "None"
5280+ # Empty set, Chinese
5281+ assert _format_existing_values (set (), LANG ["ZH" ]).startswith ("无" )
5282+
5283+
5284+ def test_check_agent_value_duplicate_with_and_without_exclude ():
5285+ """_check_agent_value_duplicate should respect exclude_agent_id and cache."""
5286+ agents = [
5287+ {"agent_id" : 1 , "name" : "agent_one" },
5288+ {"agent_id" : 2 , "name" : "agent_two" },
5289+ ]
5290+
5291+ # Duplicate found
5292+ assert agent_service ._check_agent_value_duplicate (
5293+ "name" , "agent_one" , tenant_id = "t" , agents_cache = agents
5294+ )
5295+ # No duplicate
5296+ assert not agent_service ._check_agent_value_duplicate (
5297+ "name" , "agent_three" , tenant_id = "t" , agents_cache = agents
5298+ )
5299+ # Exclude matching id should skip that record
5300+ assert not agent_service ._check_agent_value_duplicate (
5301+ "name" , "agent_one" , tenant_id = "t" , exclude_agent_id = 1 , agents_cache = agents
5302+ )
5303+
5304+
5305+ def test_generate_unique_value_with_suffix_success ():
5306+ """_generate_unique_value_with_suffix should find first available suffix."""
5307+
5308+ taken = {"base_1" }
5309+
5310+ def dup_check (candidate , ** _ ):
5311+ return candidate in taken
5312+
5313+ result = _generate_unique_value_with_suffix (
5314+ "base" ,
5315+ tenant_id = "tenant" ,
5316+ duplicate_check_fn = dup_check ,
5317+ agents_cache = [],
5318+ max_suffix_attempts = 5 ,
5319+ )
5320+ # base_1 is taken, so should start from base_2
5321+ assert result == "base_2"
5322+
5323+
5324+ def test_generate_unique_value_with_suffix_exhausts_attempts ():
5325+ """When all candidates are duplicates, _generate_unique_value_with_suffix should raise."""
5326+
5327+ def always_duplicate (* args , ** kwargs ):
5328+ return True
5329+
5330+ with pytest .raises (ValueError , match = "Failed to generate unique value" ):
5331+ _generate_unique_value_with_suffix (
5332+ "dup" ,
5333+ tenant_id = "tenant" ,
5334+ duplicate_check_fn = always_duplicate ,
5335+ agents_cache = [],
5336+ max_suffix_attempts = 3 ,
5337+ )
5338+
5339+
5340+ def test_generate_unique_agent_and_display_name_wrappers (monkeypatch ):
5341+ """Wrapper helpers should delegate to _generate_unique_value_with_suffix."""
5342+ calls = []
5343+
5344+ def fake_generate (base_value , tenant_id , duplicate_check_fn , agents_cache , exclude_agent_id = None , max_suffix_attempts = 100 ):
5345+ calls .append (
5346+ (base_value , tenant_id , duplicate_check_fn , tuple (agents_cache ), exclude_agent_id , max_suffix_attempts )
5347+ )
5348+ return f"{ base_value } _unique"
5349+
5350+ monkeypatch .setattr (
5351+ agent_service , "_generate_unique_value_with_suffix" , fake_generate , raising = False
5352+ )
5353+
5354+ name = _generate_unique_agent_name_with_suffix (
5355+ "agent" , tenant_id = "t" , agents_cache = [{"agent_id" : 1 }], exclude_agent_id = 1
5356+ )
5357+ display = _generate_unique_display_name_with_suffix (
5358+ "Agent Display" , tenant_id = "t2" , agents_cache = [{"agent_id" : 2 }]
5359+ )
5360+
5361+ assert name == "agent_unique"
5362+ assert display == "Agent Display_unique"
5363+ # Ensure both calls delegated correctly
5364+ assert len (calls ) == 2
5365+ assert calls [0 ][0 ] == "agent"
5366+ assert calls [1 ][0 ] == "Agent Display"
5367+
5368+
5369+ def test_regenerate_agent_value_with_llm_success (monkeypatch ):
5370+ """_regenerate_agent_value_with_llm should return first non-duplicate LLM value."""
5371+
5372+ # Avoid dependency on real prompt templates
5373+ monkeypatch .setattr (
5374+ agent_service ,
5375+ "get_prompt_generate_prompt_template" ,
5376+ lambda lang : {},
5377+ raising = False ,
5378+ )
5379+
5380+ # Provide a fake LLM call that returns a new unique value
5381+ def fake_call_llm (model_id , user_prompt , system_prompt , callback , tenant_id ):
5382+ assert model_id == 1
5383+ assert tenant_id == "tenant"
5384+ # Callback is not used in this helper, but should be passed through
5385+ return "new_name\n extra"
5386+
5387+ # Ensure the dynamic import `from services.prompt_service import ...` in
5388+ # `_regenerate_agent_value_with_llm` can succeed by registering a fake
5389+ # module in `sys.modules` with the expected attribute.
5390+ fake_prompt_module = MagicMock ()
5391+ fake_prompt_module .call_llm_for_system_prompt = fake_call_llm
5392+ sys .modules ["services.prompt_service" ] = fake_prompt_module
5393+
5394+ result = _regenerate_agent_value_with_llm (
5395+ original_value = "old" ,
5396+ existing_values = ["existing" ],
5397+ task_description = "task" ,
5398+ model_id = 1 ,
5399+ tenant_id = "tenant" ,
5400+ language = "en" ,
5401+ system_prompt_key = "SYS_KEY" ,
5402+ user_prompt_key = "USER_KEY" ,
5403+ default_system_prompt = "sys" ,
5404+ default_user_prompt_builder = lambda ctx : "user" ,
5405+ fallback_fn = lambda base : f"fallback_{ base } " ,
5406+ )
5407+ assert result == "new_name"
5408+
5409+
5410+ def test_regenerate_agent_value_with_llm_fallback_on_error (monkeypatch ):
5411+ """When LLM keeps failing, _regenerate_agent_value_with_llm should use fallback."""
5412+
5413+ monkeypatch .setattr (
5414+ agent_service ,
5415+ "get_prompt_generate_prompt_template" ,
5416+ lambda lang : {},
5417+ raising = False ,
5418+ )
5419+
5420+ def failing_llm (* args , ** kwargs ):
5421+ raise RuntimeError ("llm failed" )
5422+
5423+ fake_prompt_module = MagicMock ()
5424+ fake_prompt_module .call_llm_for_system_prompt = failing_llm
5425+ sys .modules ["services.prompt_service" ] = fake_prompt_module
5426+
5427+ used = {}
5428+
5429+ def fallback (base ):
5430+ used ["called" ] = True
5431+ return f"fb_{ base } "
5432+
5433+ result = _regenerate_agent_value_with_llm (
5434+ original_value = "orig" ,
5435+ existing_values = ["a" , "b" ],
5436+ task_description = "task" ,
5437+ model_id = 1 ,
5438+ tenant_id = "tenant" ,
5439+ language = "en" ,
5440+ system_prompt_key = "SYS_KEY" ,
5441+ user_prompt_key = "USER_KEY" ,
5442+ default_system_prompt = "sys" ,
5443+ default_user_prompt_builder = lambda ctx : "user" ,
5444+ fallback_fn = fallback ,
5445+ )
5446+
5447+ assert result == "fb_orig"
5448+ assert used .get ("called" ) is True
5449+
5450+
5451+ @pytest .mark .asyncio
5452+ async def test_import_agent_impl_dfs_import_order (monkeypatch ):
5453+ """
5454+ import_agent_impl should handle DFS ordering and establish relationships correctly.
5455+ This covers the branch where managed agents are not yet imported (agent_stack.extend path).
5456+ """
5457+ # Mock user and tenant
5458+ monkeypatch .setattr (
5459+ "backend.services.agent_service.get_current_user_info" ,
5460+ lambda authorization : ("user1" , "tenant1" , "en" ),
5461+ raising = False ,
5462+ )
5463+
5464+ # Skip MCP handling by providing no mcp_info and making update_tool_list a no-op
5465+ from consts .model import ExportAndImportAgentInfo , ExportAndImportDataFormat
5466+
5467+ root_agent = ExportAndImportAgentInfo (
5468+ agent_id = 1 ,
5469+ name = "root" ,
5470+ display_name = "Root" ,
5471+ description = "root" ,
5472+ business_description = "root" ,
5473+ max_steps = 5 ,
5474+ provide_run_summary = False ,
5475+ duty_prompt = None ,
5476+ constraint_prompt = None ,
5477+ few_shots_prompt = None ,
5478+ enabled = True ,
5479+ tools = [],
5480+ managed_agents = [2 ],
5481+ )
5482+ child_agent = ExportAndImportAgentInfo (
5483+ agent_id = 2 ,
5484+ name = "child" ,
5485+ display_name = "Child" ,
5486+ description = "child" ,
5487+ business_description = "child" ,
5488+ max_steps = 5 ,
5489+ provide_run_summary = False ,
5490+ duty_prompt = None ,
5491+ constraint_prompt = None ,
5492+ few_shots_prompt = None ,
5493+ enabled = True ,
5494+ tools = [],
5495+ managed_agents = [],
5496+ )
5497+
5498+ export_data = ExportAndImportDataFormat (
5499+ agent_id = 1 ,
5500+ agent_info = {"1" : root_agent , "2" : child_agent },
5501+ mcp_info = [],
5502+ )
5503+
5504+ # Track import order and relationship creation
5505+ imported_ids = []
5506+
5507+ async def fake_import_agent_by_agent_id (import_agent_info , tenant_id , user_id , skip_duplicate_regeneration = False ):
5508+ # Assign synthetic new IDs based on source id
5509+ new_id = 100 + import_agent_info .agent_id
5510+ imported_ids .append (import_agent_info .agent_id )
5511+ return new_id
5512+
5513+ relationships = []
5514+
5515+ def fake_insert_related_agent (parent_agent_id , child_agent_id , tenant_id ):
5516+ relationships .append ((parent_agent_id , child_agent_id , tenant_id ))
5517+
5518+ async def fake_update_tool_list (tenant_id , user_id ):
5519+ return None
5520+
5521+ monkeypatch .setattr (
5522+ "backend.services.agent_service.import_agent_by_agent_id" ,
5523+ fake_import_agent_by_agent_id ,
5524+ raising = False ,
5525+ )
5526+ monkeypatch .setattr (
5527+ "backend.services.agent_service.insert_related_agent" ,
5528+ fake_insert_related_agent ,
5529+ raising = False ,
5530+ )
5531+ monkeypatch .setattr (
5532+ "backend.services.agent_service.update_tool_list" ,
5533+ fake_update_tool_list ,
5534+ raising = False ,
5535+ )
5536+
5537+ # Execute
5538+ await import_agent_impl (export_data , authorization = "Bearer token" , force_import = False )
5539+
5540+ # Child (2) must be imported before parent (1)
5541+ assert imported_ids == [2 , 1 ]
5542+ # Relationship should be created between new IDs 101 (child) and 100 (parent)
5543+ assert relationships == [(100 + 1 , 100 + 2 , "tenant1" )]
5544+
5545+
52055546# =====================================================================
52065547# Tests for _resolve_model_with_fallback helper function
52075548# =====================================================================
0 commit comments