1. 删除了 test/unit/external_systems/framework/ 下的废弃空测试目录 2. 修复多处测试断言逻辑、参数传递与测试数据构造 3. 新增日志级别、缓存令牌、流事件等DTO单元测试 4. 补充路由绑定、会话仓储、outbox仓储的测试覆盖 5. 更新测试用例中的异常类型、参数校验与业务逻辑断言
236 lines
7.4 KiB
Python
236 lines
7.4 KiB
Python
"""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]
|