Skip to content

Commit 552b78f

Browse files
committed
✨ Agent duplicate name handling logic improvement #1622
[Specification Details] 1.Add test cases.
1 parent daad039 commit 552b78f

File tree

2 files changed

+527
-0
lines changed

2 files changed

+527
-0
lines changed

test/backend/services/test_agent_service.py

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nextra"
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

Comments
 (0)