"""tools.py DTO 单元测试。 覆盖 ``ToolParameter`` / ``ChannelTool`` / ``ToolDefinition`` / ``ToolResult`` 的字段赋值、默认值、不可变语义与 ``__post_init__`` 校验逻辑。 """ from __future__ import annotations import dataclasses from typing import Any import pytest from yuxi.channels.contract.dtos.tools import ( ChannelTool, ToolDefinition, ToolParameter, ToolResult, ) from yuxi.channels.contract.errors import ValidationError pytestmark = pytest.mark.unit @pytest.mark.unit class TestToolParameter: """工具参数 DTO 测试。""" def test_required_fields_are_assigned(self): # Arrange # Act param = ToolParameter(name="count", type="int", description="数量") # Assert assert param.name == "count" assert param.type == "int" assert param.description == "数量" assert param.required is True assert param.default is None def test_with_optional_fields(self): # Arrange # Act param = ToolParameter( name="verbose", type="bool", description="详细模式", required=False, default=False, ) # Assert assert param.required is False assert param.default is False @pytest.mark.parametrize("ptype", ["str", "int", "bool", "json"]) def test_valid_types(self, ptype): # Arrange # Act param = ToolParameter(name="p", type=ptype, description="d") # type: ignore[arg-type] # Assert assert param.type == ptype def test_is_frozen(self): # Arrange param = ToolParameter(name="p", type="str", description="d") # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): param.name = "other" # type: ignore[misc] def test_empty_name_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ToolParameter(name="", type="str", description="d") assert exc_info.value.field == "name" def test_invalid_type_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ToolParameter(name="p", type="float", description="d") # type: ignore[arg-type] assert exc_info.value.field == "type" @pytest.mark.unit class TestChannelTool: """渠道工具 DTO 测试。""" def test_required_fields_are_assigned(self): # Arrange # Act tool = ChannelTool(name="search", description="搜索", handler_id="handler-1") # Assert assert tool.name == "search" assert tool.description == "搜索" assert tool.handler_id == "handler-1" assert tool.parameters == () assert tool.scope == "all" def test_with_parameters_and_scope(self): # Arrange params = (ToolParameter(name="q", type="str", description="query"),) # Act tool = ChannelTool( name="search", description="搜索", handler_id="handler-1", parameters=params, scope="private", ) # Assert assert tool.parameters == params assert tool.scope == "private" def test_is_frozen(self): # Arrange tool = ChannelTool(name="search", description="d", handler_id="h") # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): tool.name = "other" # type: ignore[misc] def test_empty_name_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ChannelTool(name="", description="d", handler_id="h") assert exc_info.value.field == "name" def test_empty_description_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ChannelTool(name="n", description="", handler_id="h") assert exc_info.value.field == "description" def test_empty_handler_id_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ChannelTool(name="n", description="d", handler_id="") assert exc_info.value.field == "handler_id" def test_invalid_scope_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ChannelTool(name="n", description="d", handler_id="h", scope="invalid") # type: ignore[arg-type] assert exc_info.value.field == "scope" @pytest.mark.unit class TestToolDefinition: """工具定义 DTO 测试。""" def test_required_fields_are_assigned(self): # Arrange schema: dict[str, Any] = {"type": "object"} # Act defn = ToolDefinition(name="search", description="搜索", parameters_schema=schema) # Assert assert defn.name == "search" assert defn.description == "搜索" assert defn.parameters_schema is schema assert defn.scope == "all" def test_with_scope(self): # Arrange # Act defn = ToolDefinition( name="search", description="d", parameters_schema={}, scope="group" ) # Assert assert defn.scope == "group" def test_is_frozen(self): # Arrange defn = ToolDefinition(name="n", description="d", parameters_schema={}) # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): defn.name = "other" # type: ignore[misc] def test_empty_name_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ToolDefinition(name="", description="d", parameters_schema={}) assert exc_info.value.field == "name" def test_empty_description_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ToolDefinition(name="n", description="", parameters_schema={}) assert exc_info.value.field == "description" def test_invalid_scope_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ToolDefinition(name="n", description="d", parameters_schema={}, scope="invalid") # type: ignore[arg-type] assert exc_info.value.field == "scope" @pytest.mark.unit class TestToolResult: """工具结果 DTO 测试。""" def test_required_fields_are_assigned(self): # Arrange # Act result = ToolResult(success=True) # Assert assert result.success is True assert result.result is None assert result.error is None def test_success_with_result(self): # Arrange # Act result = ToolResult(success=True, result={"data": "ok"}) # Assert assert result.result == {"data": "ok"} def test_failure_with_error(self): # Arrange # Act result = ToolResult(success=False, error="not found") # Assert assert result.success is False assert result.error == "not found" def test_is_frozen(self): # Arrange result = ToolResult(success=True) # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): result.success = False # type: ignore[misc]