Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion backend/database/tool_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,13 @@ def add_tool_field(tool_info):
query = session.query(ToolInfo).filter(
ToolInfo.tool_id == tool_info["tool_id"])
tool = query.first()
if tool is None:
# tool_id has no FK constraint, so a stale ToolInstance can outlive its ToolInfo row
logger.warning("add_tool_field: no ToolInfo found for tool_id=%s, skipping",
tool_info.get("tool_id"))
return None
# add tool params
tool_params = tool.params
tool_params = tool.params or []
for ele in tool_params:
param_name = ele["name"]
instance_value = tool_info["params"].get(param_name)
Expand Down Expand Up @@ -440,6 +445,8 @@ def search_tools_for_sub_agent(agent_id, tenant_id, version_no: int = 0):
for tool_instance in tool_instances:
tool_instance_dict = as_dict(tool_instance)
new_tool_instance_dict = add_tool_field(tool_instance_dict)
if new_tool_instance_dict is None:
continue

tools_list.append(new_tool_instance_dict)
return tools_list
Expand Down
71 changes: 71 additions & 0 deletions test/backend/database/test_tool_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,77 @@ def test_add_tool_field(monkeypatch, mock_session):
assert result["source"] == "test_source"


def test_add_tool_field_missing_tool_info_returns_none(monkeypatch, mock_session):
"""A ToolInstance whose tool_id has no matching ToolInfo row must not crash."""
session, query = mock_session

mock_first = MagicMock(return_value=None)
mock_filter = MagicMock()
mock_filter.first = mock_first
query.filter.return_value = mock_filter

mock_ctx = MagicMock()
mock_ctx.__enter__.return_value = session
mock_ctx.__exit__.return_value = None
monkeypatch.setattr(
"backend.database.tool_db.get_db_session", lambda: mock_ctx)

tool_info = {"tool_id": 999, "params": {}}
result = add_tool_field(tool_info)

assert result is None


def test_add_tool_field_null_params_defaults_to_empty_list(monkeypatch, mock_session):
"""A ToolInfo row with params=NULL must not crash the default-value merge loop."""
session, query = mock_session
mock_tool_info = MockToolInfo()
mock_tool_info.params = None

mock_first = MagicMock(return_value=mock_tool_info)
mock_filter = MagicMock()
mock_filter.first = mock_first
query.filter.return_value = mock_filter

mock_ctx = MagicMock()
mock_ctx.__enter__.return_value = session
mock_ctx.__exit__.return_value = None
monkeypatch.setattr(
"backend.database.tool_db.get_db_session", lambda: mock_ctx)
monkeypatch.setattr("backend.database.tool_db.as_dict",
lambda obj: obj.__dict__)

tool_info = {"tool_id": 1, "params": {"param1": "value1"}}
result = add_tool_field(tool_info)

assert result["params"] == []


def test_search_tools_for_sub_agent_skips_orphaned_tool(monkeypatch, mock_session):
"""An orphaned ToolInstance (add_tool_field returns None) must be skipped, not appended."""
session, query = mock_session
mock_tool_instance = MockToolInstance()

mock_all = MagicMock(return_value=[mock_tool_instance])
mock_filter = MagicMock()
mock_filter.all = mock_all
query.filter.return_value = mock_filter

mock_ctx = MagicMock()
mock_ctx.__enter__.return_value = session
mock_ctx.__exit__.return_value = None
monkeypatch.setattr(
"backend.database.tool_db.get_db_session", lambda: mock_ctx)
monkeypatch.setattr("backend.database.tool_db.as_dict",
lambda obj: obj.__dict__)
monkeypatch.setattr(
"backend.database.tool_db.add_tool_field", lambda data: None)

result = search_tools_for_sub_agent(1, "tenant1")

assert result == []


def test_search_tools_for_sub_agent(monkeypatch, mock_session):
"""Test searching tools for sub-agent"""
session, query = mock_session
Expand Down