本次提交清理了多个单元测试文件中的多余空行,调整了部分导入的顺序,同时修复了一处feishu错误翻译的导入顺序问题,移除了application/conftest.py中未使用的fake_report_repo fixture,优化代码格式提升可读性。
944 lines
29 KiB
Python
944 lines
29 KiB
Python
"""配置 / 传输 / 能力 DTO 单元测试。
|
||
|
||
覆盖 ``yuxi.channels.contract.dtos`` 模块下 config / transport / capability
|
||
三个子模块的 DTO 构造、默认值、不可变语义与 ``__post_init__`` 校验逻辑。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import dataclasses
|
||
from datetime import datetime
|
||
from typing import Literal, get_args, get_origin
|
||
from unittest.mock import MagicMock
|
||
|
||
import pytest
|
||
from yuxi.channels.contract.dtos.capability import (
|
||
CapabilityResult,
|
||
ChannelCapabilities,
|
||
)
|
||
from yuxi.channels.contract.dtos.common import Operator, OperatorRole
|
||
from yuxi.channels.contract.dtos.config import (
|
||
BatchConfigUpdateSuccessItem,
|
||
BatchUpdateConfigCmd,
|
||
BatchUpdateConfigResult,
|
||
ConfigExportCmd,
|
||
ConfigExportResult,
|
||
ConfigField,
|
||
ConfigHistoryEntry,
|
||
ConfigHotReloadMode,
|
||
ConfigScope,
|
||
ConfigUpdateItem,
|
||
ConfigValue,
|
||
ConfigVersion,
|
||
ImportConfigCmd,
|
||
ImportConfigResult,
|
||
RollbackConfigCmd,
|
||
UpdateConfigCmd,
|
||
)
|
||
from yuxi.channels.contract.dtos.transport import (
|
||
PollingConfig,
|
||
PollResult,
|
||
StreamConfig,
|
||
StreamConnection,
|
||
TransportConfig,
|
||
TransportErrorCategory,
|
||
)
|
||
from yuxi.channels.contract.errors import ValidationError
|
||
|
||
pytestmark = pytest.mark.unit
|
||
|
||
|
||
def _make_operator() -> Operator:
|
||
"""构造测试用操作人。"""
|
||
return Operator(user_id="u-1", role=OperatorRole.ADMIN_USER)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# config.py - 枚举
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigHotReloadMode:
|
||
"""配置热更新模式枚举测试。"""
|
||
|
||
def test_enum_values_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
# Assert - 校验四个枚举成员的字符串值
|
||
assert ConfigHotReloadMode.OFF == "off"
|
||
assert ConfigHotReloadMode.HOT == "hot"
|
||
assert ConfigHotReloadMode.RESTART == "restart"
|
||
assert ConfigHotReloadMode.HYBRID == "hybrid"
|
||
|
||
def test_enum_inherits_from_str(self):
|
||
# Arrange
|
||
# Act
|
||
# Assert - StrEnum 应继承 str 并支持字符串比较
|
||
assert issubclass(ConfigHotReloadMode, str)
|
||
assert isinstance(ConfigHotReloadMode.HOT, str)
|
||
|
||
def test_enum_has_four_members(self):
|
||
# Arrange
|
||
# Act
|
||
members = list(ConfigHotReloadMode)
|
||
# Assert
|
||
assert len(members) == 4
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigScope:
|
||
"""配置作用域枚举测试。"""
|
||
|
||
def test_enum_values_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
# Assert - 校验四个作用域枚举成员的字符串值
|
||
assert ConfigScope.GLOBAL == "global"
|
||
assert ConfigScope.CHANNEL == "channel"
|
||
assert ConfigScope.ACCOUNT == "account"
|
||
assert ConfigScope.PLUGIN == "plugin"
|
||
|
||
def test_enum_inherits_from_str(self):
|
||
# Arrange
|
||
# Act
|
||
# Assert - StrEnum 应继承 str 并支持字符串比较
|
||
assert issubclass(ConfigScope, str)
|
||
assert isinstance(ConfigScope.GLOBAL, str)
|
||
|
||
def test_enum_has_four_members(self):
|
||
# Arrange
|
||
# Act
|
||
members = list(ConfigScope)
|
||
# Assert
|
||
assert len(members) == 4
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# config.py - 配置版本与字段
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigVersion:
|
||
"""配置版本 DTO 测试。"""
|
||
|
||
def test_fields_are_assigned(self):
|
||
# Arrange
|
||
now = datetime(2026, 1, 1, 12, 0, 0)
|
||
# Act
|
||
version = ConfigVersion(key="timeout", version=3, updated_at=now)
|
||
# Assert
|
||
assert version.key == "timeout"
|
||
assert version.version == 3
|
||
assert version.updated_at == now
|
||
|
||
def test_updated_at_accepts_none(self):
|
||
# Arrange
|
||
# Act
|
||
version = ConfigVersion(key="timeout", version=0, updated_at=None)
|
||
# Assert
|
||
assert version.updated_at is None
|
||
|
||
def test_is_frozen(self):
|
||
# Arrange
|
||
version = ConfigVersion(key="timeout", version=1, updated_at=None)
|
||
# Act / Assert - 冻结 dataclass 不允许修改属性
|
||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||
version.version = 2 # type: ignore[misc]
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigField:
|
||
"""配置字段元信息 DTO 测试。"""
|
||
|
||
def test_required_fields_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
field = ConfigField(key="timeout", type="int")
|
||
# Assert
|
||
assert field.key == "timeout"
|
||
assert field.type == "int"
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
field = ConfigField(key="timeout", type="int")
|
||
# Assert - 校验默认值
|
||
assert field.required is False
|
||
assert field.default is None
|
||
assert field.hot_reloadable is True
|
||
assert field.sensitive is False
|
||
assert field.constraints is None
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
constraints = {"min": 1, "max": 100}
|
||
# Act
|
||
field = ConfigField(
|
||
key="retry",
|
||
type="int",
|
||
required=True,
|
||
default=5,
|
||
hot_reloadable=False,
|
||
constraints=constraints,
|
||
)
|
||
# Assert
|
||
assert field.required is True
|
||
assert field.default == 5
|
||
assert field.hot_reloadable is False
|
||
assert field.constraints == constraints
|
||
|
||
def test_sensitive_field_is_assigned(self):
|
||
# Arrange / Act - F-01 sensitive 字段默认 False,可显式声明 True
|
||
field = ConfigField(key="bot_token", type="str", required=True, sensitive=True)
|
||
# Assert
|
||
assert field.sensitive is True
|
||
|
||
def test_is_frozen(self):
|
||
# Arrange
|
||
field = ConfigField(key="timeout", type="int")
|
||
# Act / Assert
|
||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||
field.key = "other" # type: ignore[misc]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# config.py - 更新 / 回滚命令
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestUpdateConfigCmd:
|
||
"""更新配置命令 DTO 测试。"""
|
||
|
||
def test_valid_construction_with_defaults(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act
|
||
cmd = UpdateConfigCmd(key="timeout", value=30, operator=operator)
|
||
# Assert - 默认作用域 GLOBAL,target 与 expected_version 为 None
|
||
assert cmd.key == "timeout"
|
||
assert cmd.value == 30
|
||
assert cmd.operator is operator
|
||
assert cmd.scope is ConfigScope.GLOBAL
|
||
assert cmd.target is None
|
||
assert cmd.expected_version is None
|
||
|
||
def test_valid_construction_with_all_fields(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act
|
||
cmd = UpdateConfigCmd(
|
||
key="timeout",
|
||
value=30,
|
||
operator=operator,
|
||
scope=ConfigScope.CHANNEL,
|
||
target="feishu",
|
||
expected_version=2,
|
||
)
|
||
# Assert
|
||
assert cmd.scope is ConfigScope.CHANNEL
|
||
assert cmd.target == "feishu"
|
||
assert cmd.expected_version == 2
|
||
|
||
def test_empty_key_raises_validation_error(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - key 为空字符串应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
UpdateConfigCmd(key="", value=30, operator=operator)
|
||
assert exc_info.value.field == "key"
|
||
|
||
def test_non_positive_expected_version_raises(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - expected_version <= 0 应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
UpdateConfigCmd(key="timeout", value=30, operator=operator, expected_version=0)
|
||
assert exc_info.value.field == "expected_version"
|
||
|
||
def test_target_required_for_channel_scope(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - CHANNEL 作用域下 target 为空应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
UpdateConfigCmd(key="timeout", value=30, operator=operator, scope=ConfigScope.CHANNEL)
|
||
assert exc_info.value.field == "target"
|
||
|
||
def test_target_required_for_account_scope(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - ACCOUNT 作用域下 target 为空应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
UpdateConfigCmd(key="timeout", value=30, operator=operator, scope=ConfigScope.ACCOUNT)
|
||
assert exc_info.value.field == "target"
|
||
|
||
def test_target_required_for_plugin_scope(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - PLUGIN 作用域下 target 为空应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
UpdateConfigCmd(key="timeout", value=30, operator=operator, scope=ConfigScope.PLUGIN)
|
||
assert exc_info.value.field == "target"
|
||
|
||
def test_is_frozen(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
cmd = UpdateConfigCmd(key="timeout", value=30, operator=operator)
|
||
# Act / Assert
|
||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||
cmd.value = 60 # type: ignore[misc]
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestRollbackConfigCmd:
|
||
"""回滚配置命令 DTO 测试。"""
|
||
|
||
def test_valid_construction_with_defaults(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act
|
||
cmd = RollbackConfigCmd(key="timeout", target_version=1, operator=operator)
|
||
# Assert
|
||
assert cmd.key == "timeout"
|
||
assert cmd.target_version == 1
|
||
assert cmd.operator is operator
|
||
assert cmd.scope is ConfigScope.GLOBAL
|
||
assert cmd.target is None
|
||
|
||
def test_empty_key_raises_validation_error(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - key 为空字符串应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
RollbackConfigCmd(key="", target_version=1, operator=operator)
|
||
assert exc_info.value.field == "key"
|
||
|
||
def test_non_positive_target_version_raises(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - target_version <= 0 应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
RollbackConfigCmd(key="timeout", target_version=0, operator=operator)
|
||
assert exc_info.value.field == "target_version"
|
||
|
||
def test_target_required_for_channel_scope(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - CHANNEL 作用域下 target 为空应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
RollbackConfigCmd(
|
||
key="timeout",
|
||
target_version=1,
|
||
operator=operator,
|
||
scope=ConfigScope.CHANNEL,
|
||
)
|
||
assert exc_info.value.field == "target"
|
||
|
||
def test_target_required_for_account_scope(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - ACCOUNT 作用域下 target 为空应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
RollbackConfigCmd(
|
||
key="timeout",
|
||
target_version=1,
|
||
operator=operator,
|
||
scope=ConfigScope.ACCOUNT,
|
||
)
|
||
assert exc_info.value.field == "target"
|
||
|
||
def test_target_required_for_plugin_scope(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act / Assert - PLUGIN 作用域下 target 为空应抛出 ValidationError
|
||
with pytest.raises(ValidationError) as exc_info:
|
||
RollbackConfigCmd(
|
||
key="timeout",
|
||
target_version=1,
|
||
operator=operator,
|
||
scope=ConfigScope.PLUGIN,
|
||
)
|
||
assert exc_info.value.field == "target"
|
||
|
||
def test_is_frozen(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
cmd = RollbackConfigCmd(key="timeout", target_version=1, operator=operator)
|
||
# Act / Assert
|
||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||
cmd.target_version = 2 # type: ignore[misc]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# config.py - 配置值与历史
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigValue:
|
||
"""配置值 DTO 测试。"""
|
||
|
||
def test_fields_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
value = ConfigValue(
|
||
key="timeout",
|
||
value=30,
|
||
version=2,
|
||
scope=ConfigScope.GLOBAL,
|
||
)
|
||
# Assert
|
||
assert value.key == "timeout"
|
||
assert value.value == 30
|
||
assert value.version == 2
|
||
assert value.scope is ConfigScope.GLOBAL
|
||
|
||
def test_is_frozen(self):
|
||
# Arrange
|
||
value = ConfigValue(key="timeout", value=30, version=2, scope=ConfigScope.GLOBAL)
|
||
# Act / Assert
|
||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||
value.value = 60 # type: ignore[misc]
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigHistoryEntry:
|
||
"""配置历史版本条目 DTO 测试。"""
|
||
|
||
def test_fields_are_assigned(self):
|
||
# Arrange
|
||
now = datetime(2026, 1, 1, 12, 0, 0)
|
||
# Act
|
||
entry = ConfigHistoryEntry(version=2, value=30, updated_at=now)
|
||
# Assert
|
||
assert entry.version == 2
|
||
assert entry.value == 30
|
||
assert entry.updated_at == now
|
||
|
||
def test_updated_at_accepts_none(self):
|
||
# Arrange
|
||
# Act
|
||
entry = ConfigHistoryEntry(version=1, value="v", updated_at=None)
|
||
# Assert - 历史数据未存储时间戳时为 None
|
||
assert entry.updated_at is None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# config.py - 导入 / 导出
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigExportCmd:
|
||
"""配置导出命令 DTO 测试。"""
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act
|
||
cmd = ConfigExportCmd(operator=operator)
|
||
# Assert
|
||
assert cmd.operator is operator
|
||
assert cmd.scope is ConfigScope.GLOBAL
|
||
assert cmd.target is None
|
||
|
||
def test_custom_scope_and_target(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act
|
||
cmd = ConfigExportCmd(operator=operator, scope=ConfigScope.CHANNEL, target="feishu")
|
||
# Assert
|
||
assert cmd.scope is ConfigScope.CHANNEL
|
||
assert cmd.target == "feishu"
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigExportResult:
|
||
"""配置导出结果 DTO 测试。"""
|
||
|
||
def test_required_fields_are_assigned(self):
|
||
# Arrange
|
||
now = datetime(2026, 1, 1, 12, 0, 0)
|
||
data = {"timeout": 30}
|
||
# Act
|
||
result = ConfigExportResult(
|
||
scope=ConfigScope.GLOBAL,
|
||
config_data=data,
|
||
version=1,
|
||
exported_at=now,
|
||
)
|
||
# Assert
|
||
assert result.scope is ConfigScope.GLOBAL
|
||
assert result.config_data == data
|
||
assert result.version == 1
|
||
assert result.exported_at == now
|
||
assert result.target is None
|
||
|
||
def test_target_is_optional(self):
|
||
# Arrange
|
||
now = datetime(2026, 1, 1, 12, 0, 0)
|
||
# Act
|
||
result = ConfigExportResult(
|
||
scope=ConfigScope.CHANNEL,
|
||
config_data={},
|
||
version=1,
|
||
exported_at=now,
|
||
target="feishu",
|
||
)
|
||
# Assert
|
||
assert result.target == "feishu"
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestImportConfigCmd:
|
||
"""配置导入命令 DTO 测试。"""
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
data = {"timeout": 30}
|
||
# Act
|
||
cmd = ImportConfigCmd(operator=operator, config_data=data)
|
||
# Assert
|
||
assert cmd.operator is operator
|
||
assert cmd.config_data == data
|
||
assert cmd.scope is ConfigScope.GLOBAL
|
||
assert cmd.target is None
|
||
assert cmd.overwrite is False
|
||
assert cmd.dry_run is False
|
||
|
||
def test_custom_flags(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
# Act
|
||
cmd = ImportConfigCmd(
|
||
operator=operator,
|
||
config_data={},
|
||
scope=ConfigScope.CHANNEL,
|
||
target="feishu",
|
||
overwrite=True,
|
||
dry_run=True,
|
||
)
|
||
# Assert
|
||
assert cmd.scope is ConfigScope.CHANNEL
|
||
assert cmd.target == "feishu"
|
||
assert cmd.overwrite is True
|
||
assert cmd.dry_run is True
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestImportConfigResult:
|
||
"""配置导入结果 DTO 测试。"""
|
||
|
||
def test_required_fields_are_assigned(self):
|
||
# Arrange
|
||
now = datetime(2026, 1, 1, 12, 0, 0)
|
||
failed_keys = ("bad_key", "missing_key")
|
||
# Act
|
||
result = ImportConfigResult(
|
||
imported_count=5,
|
||
skipped_count=2,
|
||
failed_count=2,
|
||
failed_keys=failed_keys,
|
||
imported_at=now,
|
||
)
|
||
# Assert
|
||
assert result.imported_count == 5
|
||
assert result.skipped_count == 2
|
||
assert result.failed_count == 2
|
||
assert result.failed_keys == failed_keys
|
||
assert result.imported_at == now
|
||
|
||
def test_failed_keys_is_tuple(self):
|
||
# Arrange
|
||
now = datetime(2026, 1, 1, 12, 0, 0)
|
||
# Act
|
||
result = ImportConfigResult(
|
||
imported_count=0,
|
||
skipped_count=0,
|
||
failed_count=0,
|
||
failed_keys=(),
|
||
imported_at=now,
|
||
)
|
||
# Assert - failed_keys 支持空元组
|
||
assert result.failed_keys == ()
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# config.py - 批量更新
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigUpdateItem:
|
||
"""单条配置更新项 DTO 测试。"""
|
||
|
||
def test_required_fields_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
item = ConfigUpdateItem(key="timeout", value=30)
|
||
# Assert
|
||
assert item.key == "timeout"
|
||
assert item.value == 30
|
||
assert item.expected_version is None
|
||
|
||
def test_expected_version_can_be_set(self):
|
||
# Arrange
|
||
# Act
|
||
item = ConfigUpdateItem(key="timeout", value=30, expected_version=2)
|
||
# Assert
|
||
assert item.expected_version == 2
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestBatchConfigUpdateSuccessItem:
|
||
"""批量配置更新成功条目 DTO 测试。"""
|
||
|
||
def test_fields_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
item = BatchConfigUpdateSuccessItem(key="timeout", new_version=3)
|
||
# Assert
|
||
assert item.key == "timeout"
|
||
assert item.new_version == 3
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestBatchUpdateConfigCmd:
|
||
"""批量更新配置命令 DTO 测试。"""
|
||
|
||
def test_required_fields_are_assigned(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
updates = (ConfigUpdateItem(key="timeout", value=30),)
|
||
# Act
|
||
cmd = BatchUpdateConfigCmd(operator=operator, updates=updates)
|
||
# Assert
|
||
assert cmd.operator is operator
|
||
assert cmd.updates == updates
|
||
assert cmd.scope is ConfigScope.GLOBAL
|
||
assert cmd.target is None
|
||
assert cmd.stop_on_error is False
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
operator = _make_operator()
|
||
updates = (ConfigUpdateItem(key="a", value=1), ConfigUpdateItem(key="b", value=2))
|
||
# Act
|
||
cmd = BatchUpdateConfigCmd(
|
||
operator=operator,
|
||
updates=updates,
|
||
scope=ConfigScope.CHANNEL,
|
||
target="feishu",
|
||
stop_on_error=True,
|
||
)
|
||
# Assert
|
||
assert cmd.scope is ConfigScope.CHANNEL
|
||
assert cmd.target == "feishu"
|
||
assert cmd.stop_on_error is True
|
||
assert len(cmd.updates) == 2
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestBatchUpdateConfigResult:
|
||
"""批量更新配置结果 DTO 测试。"""
|
||
|
||
def test_fields_are_assigned(self):
|
||
# Arrange
|
||
succeeded = (BatchConfigUpdateSuccessItem(key="timeout", new_version=2),)
|
||
failed: tuple = ()
|
||
# Act
|
||
result = BatchUpdateConfigResult(total=1, succeeded=succeeded, failed=failed)
|
||
# Assert
|
||
assert result.total == 1
|
||
assert result.succeeded == succeeded
|
||
assert result.failed == failed
|
||
|
||
def test_empty_result(self):
|
||
# Arrange
|
||
# Act
|
||
result = BatchUpdateConfigResult(total=0, succeeded=(), failed=())
|
||
# Assert
|
||
assert result.total == 0
|
||
assert result.succeeded == ()
|
||
assert result.failed == ()
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# transport.py
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestTransportErrorCategory:
|
||
"""传输错误分类类型测试。"""
|
||
|
||
def test_is_literal_type(self):
|
||
# Arrange
|
||
# Act
|
||
origin = get_origin(TransportErrorCategory)
|
||
# Assert - 应为 Literal 类型别名
|
||
assert origin is Literal
|
||
|
||
def test_has_four_values(self):
|
||
# Arrange
|
||
# Act
|
||
args = get_args(TransportErrorCategory)
|
||
# Assert
|
||
assert args == ("auth_expired", "rate_limited", "transient", "permanent")
|
||
assert len(args) == 4
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestPollingConfig:
|
||
"""轮询模式配置 DTO 测试。"""
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
config = PollingConfig()
|
||
# Assert
|
||
assert config.long_poll_timeout_ms == 0
|
||
assert config.poll_interval_ms == 0
|
||
assert config.max_batch_size == 100
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
config = PollingConfig(long_poll_timeout_ms=5000, poll_interval_ms=1000, max_batch_size=50)
|
||
# Assert
|
||
assert config.long_poll_timeout_ms == 5000
|
||
assert config.poll_interval_ms == 1000
|
||
assert config.max_batch_size == 50
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestStreamConfig:
|
||
"""流式连接配置 DTO 测试。"""
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
config = StreamConfig()
|
||
# Assert
|
||
assert config.heartbeat_interval_ms == 30000
|
||
assert config.connect_timeout_ms == 10000
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
config = StreamConfig(heartbeat_interval_ms=15000, connect_timeout_ms=5000)
|
||
# Assert
|
||
assert config.heartbeat_interval_ms == 15000
|
||
assert config.connect_timeout_ms == 5000
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestTransportConfig:
|
||
"""传输层全局配置 DTO 测试。"""
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
config = TransportConfig()
|
||
# Assert
|
||
assert config.stall_timeout_ms == 120000
|
||
assert config.backoff_schedule == (1.0, 2.0, 5.0, 10.0, 30.0)
|
||
assert config.backoff_jitter == 0.2
|
||
assert config.max_restart_attempts is None
|
||
assert config.graceful_shutdown_timeout_s == 10.0
|
||
|
||
def test_backoff_schedule_is_tuple(self):
|
||
# Arrange
|
||
# Act
|
||
config = TransportConfig()
|
||
# Assert - backoff_schedule 应为 tuple 保证不可变
|
||
assert isinstance(config.backoff_schedule, tuple)
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
config = TransportConfig(
|
||
stall_timeout_ms=60000,
|
||
backoff_schedule=(1.0, 2.0),
|
||
backoff_jitter=0.5,
|
||
max_restart_attempts=3,
|
||
graceful_shutdown_timeout_s=30.0,
|
||
)
|
||
# Assert
|
||
assert config.stall_timeout_ms == 60000
|
||
assert config.backoff_schedule == (1.0, 2.0)
|
||
assert config.backoff_jitter == 0.5
|
||
assert config.max_restart_attempts == 3
|
||
assert config.graceful_shutdown_timeout_s == 30.0
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestPollResult:
|
||
"""轮询结果 DTO 测试。"""
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
result = PollResult()
|
||
# Assert
|
||
assert result.messages == ()
|
||
assert result.next_cursor is None
|
||
assert result.error is None
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
messages = ({"id": "1"}, {"id": "2"})
|
||
# Act
|
||
result = PollResult(messages=messages, next_cursor="cursor-1")
|
||
# Assert
|
||
assert result.messages == messages
|
||
assert result.next_cursor == "cursor-1"
|
||
assert result.error is None
|
||
|
||
def test_messages_is_tuple(self):
|
||
# Arrange
|
||
# Act
|
||
result = PollResult(messages=({"id": "1"},))
|
||
# Assert
|
||
assert isinstance(result.messages, tuple)
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestStreamConnection:
|
||
"""流式连接句柄 DTO 测试。"""
|
||
|
||
def test_required_callable_fields_are_assigned(self):
|
||
# Arrange
|
||
receive = MagicMock()
|
||
send = MagicMock()
|
||
close = MagicMock()
|
||
# Act
|
||
conn = StreamConnection(receive=receive, send=send, close=close)
|
||
# Assert
|
||
assert conn.receive is receive
|
||
assert conn.send is send
|
||
assert conn.close is close
|
||
|
||
def test_extra_defaults_to_none(self):
|
||
# Arrange
|
||
receive = MagicMock()
|
||
send = MagicMock()
|
||
close = MagicMock()
|
||
# Act
|
||
conn = StreamConnection(receive=receive, send=send, close=close)
|
||
# Assert
|
||
assert conn.extra is None
|
||
|
||
def test_extra_can_be_set(self):
|
||
# Arrange
|
||
receive = MagicMock()
|
||
send = MagicMock()
|
||
close = MagicMock()
|
||
# Act
|
||
conn = StreamConnection(receive=receive, send=send, close=close, extra={"session": "s1"})
|
||
# Assert
|
||
assert conn.extra == {"session": "s1"}
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# capability.py
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestChannelCapabilities:
|
||
"""渠道能力集合 DTO 测试。"""
|
||
|
||
def test_all_defaults_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
caps = ChannelCapabilities()
|
||
# Assert - 所有布尔能力默认 False
|
||
assert caps.rich_message is False
|
||
assert caps.streaming is False
|
||
assert caps.typing_indicator is False
|
||
assert caps.message_edit is False
|
||
assert caps.message_recall is False
|
||
assert caps.supports_reaction is False
|
||
assert caps.supports_pin is False
|
||
assert caps.supports_card_update is False
|
||
assert caps.supports_card_update_streaming is False
|
||
assert caps.supports_image_inbound is False
|
||
assert caps.supports_video_inbound is False
|
||
assert caps.supports_image_outbound is False
|
||
assert caps.supports_video_outbound is False
|
||
assert caps.mention is False
|
||
assert caps.command is False
|
||
assert caps.directory is False
|
||
assert caps.doctor is False
|
||
assert caps.whitelist is False
|
||
assert caps.wizard is False
|
||
assert caps.tools is False
|
||
assert caps.status is False
|
||
assert caps.probeable is False
|
||
assert caps.identity_resolver is False
|
||
assert caps.lifecycle is False
|
||
assert caps.supports_qr_login is False
|
||
assert caps.agent_collaboration is False
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
caps = ChannelCapabilities(
|
||
rich_message=True,
|
||
streaming=True,
|
||
wizard=True,
|
||
tools=True,
|
||
supports_card_update_streaming=True,
|
||
)
|
||
# Assert
|
||
assert caps.rich_message is True
|
||
assert caps.streaming is True
|
||
assert caps.wizard is True
|
||
assert caps.tools is True
|
||
assert caps.supports_card_update_streaming is True
|
||
|
||
def test_is_frozen(self):
|
||
# Arrange
|
||
caps = ChannelCapabilities()
|
||
# Act / Assert - 冻结 dataclass 不允许修改属性
|
||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||
caps.rich_message = True # type: ignore[misc]
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestCapabilityResult:
|
||
"""能力探测结果 DTO 测试。"""
|
||
|
||
def test_required_fields_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
result = CapabilityResult(capability="streaming", declared=True)
|
||
# Assert
|
||
assert result.capability == "streaming"
|
||
assert result.declared is True
|
||
|
||
def test_defaults_are_correct(self):
|
||
# Arrange
|
||
# Act
|
||
result = CapabilityResult(capability="streaming", declared=True)
|
||
# Assert - proven 与 degraded 默认 False
|
||
assert result.proven is False
|
||
assert result.degraded is False
|
||
|
||
def test_custom_values_are_assigned(self):
|
||
# Arrange
|
||
# Act
|
||
result = CapabilityResult(
|
||
capability="streaming",
|
||
declared=True,
|
||
proven=True,
|
||
degraded=False,
|
||
)
|
||
# Assert
|
||
assert result.proven is True
|
||
assert result.degraded is False
|