chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
"""Unit tests for src/agents/_mcp_tool_metadata.py pure helpers.
|
||||
|
||||
The module resolves MCP tool display metadata (title / description) from
|
||||
either dict payloads or attribute-bearing objects. It feeds hosted-MCP
|
||||
tool definitions into the model and into traces, but had no direct
|
||||
test file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from agents._mcp_tool_metadata import (
|
||||
MCPToolMetadata,
|
||||
collect_mcp_list_tools_metadata,
|
||||
extract_mcp_tool_metadata,
|
||||
resolve_mcp_tool_description,
|
||||
resolve_mcp_tool_description_for_model,
|
||||
resolve_mcp_tool_title,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ToolObj:
|
||||
"""Tiny attribute-bearing stand-in for an MCP tool object."""
|
||||
|
||||
name: str | None = None
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
annotations: Any = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Annotations:
|
||||
title: str | None = None
|
||||
|
||||
|
||||
class TestResolveMCPToolTitle:
|
||||
def test_explicit_title_wins(self) -> None:
|
||||
tool = {"title": "Explicit", "annotations": {"title": "Annotated"}}
|
||||
assert resolve_mcp_tool_title(tool) == "Explicit"
|
||||
|
||||
def test_falls_back_to_annotations_title(self) -> None:
|
||||
tool = {"annotations": {"title": "Annotated"}}
|
||||
assert resolve_mcp_tool_title(tool) == "Annotated"
|
||||
|
||||
def test_returns_none_when_neither_present(self) -> None:
|
||||
assert resolve_mcp_tool_title({}) is None
|
||||
|
||||
def test_skips_empty_explicit_title(self) -> None:
|
||||
tool = {"title": "", "annotations": {"title": "Annotated"}}
|
||||
assert resolve_mcp_tool_title(tool) == "Annotated"
|
||||
|
||||
def test_skips_non_string_explicit_title(self) -> None:
|
||||
tool = {"title": 123, "annotations": {"title": "Annotated"}}
|
||||
assert resolve_mcp_tool_title(tool) == "Annotated"
|
||||
|
||||
def test_works_with_attribute_objects(self) -> None:
|
||||
tool = _ToolObj(title="Explicit")
|
||||
assert resolve_mcp_tool_title(tool) == "Explicit"
|
||||
|
||||
def test_works_with_attribute_annotations(self) -> None:
|
||||
tool = _ToolObj(annotations=_Annotations(title="Annotated"))
|
||||
assert resolve_mcp_tool_title(tool) == "Annotated"
|
||||
|
||||
def test_handles_missing_annotations_attribute(self) -> None:
|
||||
tool = _ToolObj()
|
||||
assert resolve_mcp_tool_title(tool) is None
|
||||
|
||||
|
||||
class TestResolveMCPToolDescription:
|
||||
def test_returns_description(self) -> None:
|
||||
assert resolve_mcp_tool_description({"description": "Long form"}) == "Long form"
|
||||
|
||||
def test_returns_none_when_empty(self) -> None:
|
||||
assert resolve_mcp_tool_description({"description": ""}) is None
|
||||
|
||||
def test_returns_none_when_missing(self) -> None:
|
||||
assert resolve_mcp_tool_description({}) is None
|
||||
|
||||
def test_returns_none_when_non_string(self) -> None:
|
||||
assert resolve_mcp_tool_description({"description": 123}) is None
|
||||
|
||||
def test_works_with_attribute_object(self) -> None:
|
||||
assert resolve_mcp_tool_description(_ToolObj(description="Long form")) == "Long form"
|
||||
|
||||
|
||||
class TestResolveMCPToolDescriptionForModel:
|
||||
def test_uses_description_when_present(self) -> None:
|
||||
tool = {"description": "Long form", "title": "Short"}
|
||||
assert resolve_mcp_tool_description_for_model(tool) == "Long form"
|
||||
|
||||
def test_falls_back_to_title_when_description_missing(self) -> None:
|
||||
assert resolve_mcp_tool_description_for_model({"title": "Short"}) == "Short"
|
||||
|
||||
def test_falls_back_to_annotations_title(self) -> None:
|
||||
tool = {"annotations": {"title": "Annotated"}}
|
||||
assert resolve_mcp_tool_description_for_model(tool) == "Annotated"
|
||||
|
||||
def test_returns_empty_string_when_nothing_resolvable(self) -> None:
|
||||
assert resolve_mcp_tool_description_for_model({}) == ""
|
||||
|
||||
|
||||
class TestExtractMCPToolMetadata:
|
||||
def test_collects_both_fields(self) -> None:
|
||||
tool = {"description": "Long form", "title": "Short"}
|
||||
assert extract_mcp_tool_metadata(tool) == MCPToolMetadata(
|
||||
description="Long form", title="Short"
|
||||
)
|
||||
|
||||
def test_returns_empty_metadata_when_nothing_present(self) -> None:
|
||||
assert extract_mcp_tool_metadata({}) == MCPToolMetadata()
|
||||
|
||||
|
||||
class TestCollectMCPListToolsMetadata:
|
||||
def test_collects_from_raw_payload(self) -> None:
|
||||
items = [
|
||||
{
|
||||
"type": "mcp_list_tools",
|
||||
"server_label": "github",
|
||||
"tools": [
|
||||
{"name": "search", "description": "Search repos", "title": "Search"},
|
||||
{"name": "create", "description": "Create issue"},
|
||||
],
|
||||
}
|
||||
]
|
||||
result = collect_mcp_list_tools_metadata(items)
|
||||
assert result == {
|
||||
("github", "search"): MCPToolMetadata(description="Search repos", title="Search"),
|
||||
("github", "create"): MCPToolMetadata(description="Create issue"),
|
||||
}
|
||||
|
||||
def test_unwraps_run_item_with_raw_item(self) -> None:
|
||||
@dataclass
|
||||
class _RunItem:
|
||||
raw_item: Any
|
||||
|
||||
run_item = _RunItem(
|
||||
raw_item={
|
||||
"type": "mcp_list_tools",
|
||||
"server_label": "internal",
|
||||
"tools": [{"name": "ping", "description": "Ping"}],
|
||||
}
|
||||
)
|
||||
result = collect_mcp_list_tools_metadata([run_item])
|
||||
assert result == {("internal", "ping"): MCPToolMetadata(description="Ping")}
|
||||
|
||||
def test_skips_items_without_correct_type(self) -> None:
|
||||
items = [
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"server_label": "github",
|
||||
"tools": [{"name": "ignored"}],
|
||||
}
|
||||
]
|
||||
assert collect_mcp_list_tools_metadata(items) == {}
|
||||
|
||||
def test_skips_items_without_server_label(self) -> None:
|
||||
items = [
|
||||
{
|
||||
"type": "mcp_list_tools",
|
||||
"tools": [{"name": "search"}],
|
||||
}
|
||||
]
|
||||
assert collect_mcp_list_tools_metadata(items) == {}
|
||||
|
||||
def test_skips_items_with_non_list_tools(self) -> None:
|
||||
items = [
|
||||
{
|
||||
"type": "mcp_list_tools",
|
||||
"server_label": "github",
|
||||
"tools": "not-a-list",
|
||||
}
|
||||
]
|
||||
assert collect_mcp_list_tools_metadata(items) == {}
|
||||
|
||||
def test_skips_tools_without_name(self) -> None:
|
||||
items = [
|
||||
{
|
||||
"type": "mcp_list_tools",
|
||||
"server_label": "github",
|
||||
"tools": [
|
||||
{"description": "no name"},
|
||||
{"name": "", "description": "empty name"},
|
||||
{"name": "good", "description": "kept"},
|
||||
],
|
||||
}
|
||||
]
|
||||
result = collect_mcp_list_tools_metadata(items)
|
||||
assert result == {("github", "good"): MCPToolMetadata(description="kept")}
|
||||
|
||||
def test_returns_empty_for_empty_input(self) -> None:
|
||||
assert collect_mcp_list_tools_metadata([]) == {}
|
||||
|
||||
def test_later_entry_for_same_key_wins(self) -> None:
|
||||
items = [
|
||||
{
|
||||
"type": "mcp_list_tools",
|
||||
"server_label": "github",
|
||||
"tools": [{"name": "search", "description": "first"}],
|
||||
},
|
||||
{
|
||||
"type": "mcp_list_tools",
|
||||
"server_label": "github",
|
||||
"tools": [{"name": "search", "description": "second"}],
|
||||
},
|
||||
]
|
||||
result = collect_mcp_list_tools_metadata(items)
|
||||
assert result == {("github", "search"): MCPToolMetadata(description="second")}
|
||||
Reference in New Issue
Block a user