| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- """Unit tests for BaseTool functionality."""
- import pytest
- from agents import RunContextWrapper
- from pydantic import BaseModel
- from agency_swarm.context import MasterContext
- from agency_swarm.tools.base_tool import BaseTool, classproperty
- class SampleTool(BaseTool):
- name: str = "sample"
- def run(self) -> str:
- return "ok"
- def test_classproperty_descriptor_supports_plain_and_basetool_access() -> None:
- """classproperty should resolve for plain classes and BaseTool subclasses."""
- class Demo:
- _value = "value"
- @classproperty
- def prop(cls):
- return cls._value
- assert Demo.prop == "value"
- assert Demo().prop == "value"
- class ToolWithClassProp(BaseTool):
- name: str = "name"
- @classproperty
- def computed(cls):
- return "computed-value"
- def run(self) -> str:
- return self.name
- tool = ToolWithClassProp()
- assert tool.name == "name"
- assert tool.computed == "computed-value"
- def test_base_tool_requires_run_implementation() -> None:
- """BaseTool and incomplete subclasses should remain abstract."""
- with pytest.raises(TypeError, match="Can't instantiate abstract class BaseTool"):
- BaseTool()
- with pytest.raises(TypeError, match="Can't instantiate abstract class.*run"):
- class IncompleteTool(BaseTool):
- pass
- IncompleteTool()
- def test_base_tool_initialization_and_tool_config_defaults() -> None:
- """Concrete tools should initialize context wrapper and preserve ToolConfig defaults/overrides."""
- tool = SampleTool()
- assert tool.name == "sample"
- assert isinstance(tool._context, RunContextWrapper)
- assert isinstance(tool._context.context, MasterContext)
- assert tool.ToolConfig.strict is False
- assert tool.ToolConfig.one_call_at_a_time is False
- class StrictSequentialTool(BaseTool):
- class ToolConfig:
- strict = True
- one_call_at_a_time = True
- custom_flag = "kept"
- def run(self) -> str:
- return "ok"
- tool = StrictSequentialTool()
- assert tool.ToolConfig.strict is True
- assert tool.ToolConfig.one_call_at_a_time is True
- assert tool.ToolConfig.custom_flag == "kept"
- def test_openai_schema_descriptions_required_fields_and_fallback() -> None:
- """OpenAI schema should include docstring details and apply fallback descriptions when absent."""
- class DocumentedTool(BaseTool):
- """A tool for schema checks.
- Args:
- name: Name field description
- count: Count field description
- """
- name: str
- count: int = 5
- def run(self) -> str:
- return f"{self.name}:{self.count}"
- schema = DocumentedTool.openai_schema
- params = schema["parameters"]
- assert schema["name"] == "DocumentedTool"
- assert "A tool for schema checks." in schema["description"]
- assert params["properties"]["name"]["description"] == "Name field description"
- assert params["properties"]["count"]["description"] == "Count field description"
- assert "name" in params["required"]
- assert "count" not in params["required"]
- class UndocumentedTool(BaseTool):
- def run(self) -> str:
- return "ok"
- schema = UndocumentedTool.openai_schema
- assert schema["description"] == "`UndocumentedTool` tool"
- def test_openai_schema_strict_mode_applies_additional_properties_to_main_and_defs() -> None:
- """Strict mode should set additionalProperties=False on root and nested definitions."""
- class NestedModel(BaseModel):
- nested_field: str
- optional_field: int | None = None
- class StrictTool(BaseTool):
- class ToolConfig:
- strict = True
- name: str
- payload: list[NestedModel]
- def run(self) -> str:
- return "ok"
- schema = StrictTool.openai_schema
- assert schema["strict"] is True
- assert schema["parameters"]["additionalProperties"] is False
- defs = schema["parameters"].get("$defs", {})
- for definition in defs.values():
- assert definition["additionalProperties"] is False
- def test_base_tool_context_property_and_removed_shared_state() -> None:
- """context property should proxy MasterContext and _shared_state should be unavailable."""
- tool = SampleTool()
- assert isinstance(tool.context, MasterContext)
- tool._context = None
- assert tool.context is None
- with pytest.raises(AttributeError, match=r"_shared_state"):
- _ = tool._shared_state
|