"""配置 / 传输 / 能力 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