"""yuxi.channels.contract.plugin 模块单元测试。 覆盖插件协议包入口与各子模块: - ``__init__.py`` 的 ``__all__`` 导出完整性 - ``manifest.py`` 的枚举与 dataclass(FailurePolicy / CredentialType / AcquireMethod / PluginDependency / ResourceQuota / CredentialStrategy / ChannelManifest / PluginManifest) - ``lifecycle.py`` 的枚举与 Protocol(LifecycleState / LifecycleHook / LifecycleHookHandler) - ``extension_point.py`` 的枚举、dataclass 与 Protocol(ConflictStrategy / FailureStrategy / StageSlot / EventSubscription / ConfigSource / Stage) - ``capability.py`` 的 dataclass 与 Protocol(CapabilityDeclaration / CapabilityProof / CapabilityProver) - ``entry.py`` 的 Protocol 与类型别名(PluginHost / CHANNEL_ENTRY) """ from __future__ import annotations from datetime import datetime from typing import Protocol, get_args, get_origin from collections.abc import Callable from enum import StrEnum from unittest.mock import MagicMock import pytest from yuxi.channels.contract.dtos.capability import ChannelCapabilities from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.dtos.config import ConfigField from yuxi.channels.contract.plugin import ( CHANNEL_ENTRY, AcquireMethod, CapabilityDeclaration, CapabilityProof, CapabilityProver, ChannelManifest, ConfigSource, ConflictStrategy, CredentialStrategy, CredentialType, EventSubscription, FailurePolicy, FailureStrategy, LifecycleHook, LifecycleHookHandler, LifecycleState, PluginDependency, PluginHost, PluginManifest, ResourceQuota, Stage, StageSlot, ) pytestmark = pytest.mark.unit # ─── 辅助工厂 ──────────────────────────────────────────────────────────── def _build_channel_manifest(**kwargs) -> ChannelManifest: """构造最小可用 ChannelManifest 实例,便于测试覆写字段。""" defaults: dict = dict( id="com.yuxi.channels.feishu", name="feishu", version="1.0.0", channel_type=ChannelType("feishu"), provides=("inbound", "outbound"), entry_module="yuxi.channels.plugins.feishu.entry", capabilities=ChannelCapabilities(), config_schema=(ConfigField(key="app_id", type="str"),), ) defaults.update(kwargs) return ChannelManifest(**defaults) # ─── __all__ 导出完整性 ────────────────────────────────────────────────── @pytest.mark.unit class TestPluginExports: """plugin 包 ``__all__`` 导出完整性。""" def test_all_contains_expected_core_names(self): # Arrange - 期望 __all__ 中包含的核心协议与值对象名称 from yuxi.channels.contract.plugin import __all__ as plugin_all expected = { # manifest "AcquireMethod", "ChannelManifest", "ConfigField", "CredentialStrategy", "CredentialType", "FailurePolicy", "PluginDependency", "PluginManifest", "ResourceQuota", # entry "CHANNEL_ENTRY", "PluginHost", # capability "CapabilityDeclaration", "CapabilityProof", "CapabilityProver", # lifecycle "LifecycleHook", "LifecycleHookHandler", "LifecycleState", # extension_point "ConflictStrategy", "ConfigSource", "EventSubscription", "FailureStrategy", "Stage", "StageSlot", } # Act # Assert assert expected.issubset(set(plugin_all)) def test_all_contains_adapter_names(self): # Arrange - 期望 __all__ 中包含的适配器协议名称(抽样) from yuxi.channels.contract.plugin import __all__ as plugin_all expected_adapters = { "InboundAdapter", "OutboundAdapter", "SessionAdapter", "RichMessageAdapter", "StreamingAdapter", "DirectoryAdapter", "CommandAdapter", "WizardAdapter", "DoctorAdapter", "WhitelistAdapter", "ToolsAdapter", "MessageOpsAdapter", "StatusAdapter", "MentionAdapter", "IdentityResolverAdapter", "LifecycleAdapter", "LoginAdapter", "ProbeableAdapter", "ContentModerationAdapter", "PullerAdapter", "StreamConnectorAdapter", "WebhookTestAdapter", "AttachmentUploadAdapter", } # Act # Assert assert expected_adapters.issubset(set(plugin_all)) def test_all_names_importable(self): # Arrange from yuxi.channels.contract import plugin as mod # Act # Assert - __all__ 中每个名称必须可在模块属性中找到 for name in mod.__all__: assert hasattr(mod, name), f"{name} 在 __all__ 但未定义" def test_all_list_meets_minimum_count(self): # Arrange - 当前 __all__ 至少 40 个名称(23 适配器 + 17 协议/枚举/值对象) from yuxi.channels.contract.plugin import __all__ as plugin_all # Act # Assert assert len(plugin_all) >= 40 def test_all_names_unique(self): # Arrange from yuxi.channels.contract.plugin import __all__ as plugin_all # Act # Assert assert len(plugin_all) == len(set(plugin_all)) # ─── manifest.py 枚举 ─────────────────────────────────────────────────── @pytest.mark.unit class TestManifestEnums: """manifest.py 的 FailurePolicy / CredentialType / AcquireMethod 枚举。""" def test_failure_policy_inherits_strenum(self): # Arrange # Act # Assert assert issubclass(FailurePolicy, StrEnum) def test_failure_policy_values(self): # Arrange # Act # Assert - 三个取值,字符串值与枚举名一致(小写) assert FailurePolicy.DEGRADE == "degrade" assert FailurePolicy.CIRCUIT_BREAK == "circuit_break" assert FailurePolicy.ISOLATE == "isolate" def test_failure_policy_member_count(self): # Arrange # Act # Assert assert len(list(FailurePolicy)) == 3 def test_failure_policy_str_value(self): # Arrange - StrEnum 的 str() 返回值本身 # Act # Assert assert str(FailurePolicy.DEGRADE) == "degrade" def test_credential_type_inherits_strenum(self): # Arrange # Act # Assert assert issubclass(CredentialType, StrEnum) def test_credential_type_values(self): # Arrange # Act # Assert assert CredentialType.STATIC == "static" assert CredentialType.DYNAMIC == "dynamic" assert CredentialType.HYBRID == "hybrid" def test_credential_type_member_count(self): # Arrange # Act # Assert assert len(list(CredentialType)) == 3 def test_acquire_method_inherits_strenum(self): # Arrange # Act # Assert assert issubclass(AcquireMethod, StrEnum) def test_acquire_method_values(self): # Arrange # Act # Assert assert AcquireMethod.MANUAL == "manual" assert AcquireMethod.QR_LOGIN == "qr_login" assert AcquireMethod.OAUTH == "oauth" assert AcquireMethod.WEBHOOK_VERIFY == "webhook_verify" def test_acquire_method_member_count(self): # Arrange # Act # Assert assert len(list(AcquireMethod)) == 4 # ─── PluginDependency ─────────────────────────────────────────────────── @pytest.mark.unit class TestPluginDependency: """PluginDependency 不可变值对象。""" def test_required_fields_set(self): # Arrange # Act dep = PluginDependency(plugin_id="p-1", version_range="^1.0.0") # Assert assert dep.plugin_id == "p-1" assert dep.version_range == "^1.0.0" def test_is_frozen_dataclass(self): # Arrange dep = PluginDependency(plugin_id="p-1", version_range="^1.0.0") # Act & Assert - frozen dataclass 字段不可变 with pytest.raises(Exception): dep.plugin_id = "p-2" # type: ignore[misc] def test_version_range_immutable(self): # Arrange dep = PluginDependency(plugin_id="p-1", version_range="^1.0.0") # Act & Assert with pytest.raises(Exception): dep.version_range = "^2.0.0" # type: ignore[misc] # ─── ResourceQuota ────────────────────────────────────────────────────── @pytest.mark.unit class TestResourceQuota: """ResourceQuota 不可变值对象,所有字段可选。""" def test_all_fields_default_none(self): # Arrange # Act quota = ResourceQuota() # Assert - 所有字段默认 None assert quota.max_cpu is None assert quota.max_memory is None assert quota.max_connections is None assert quota.max_calls_per_sec is None def test_fields_settable(self): # Arrange # Act quota = ResourceQuota( max_cpu="500m", max_memory="512Mi", max_connections=100, max_calls_per_sec=50, ) # Assert assert quota.max_cpu == "500m" assert quota.max_memory == "512Mi" assert quota.max_connections == 100 assert quota.max_calls_per_sec == 50 def test_is_frozen_dataclass(self): # Arrange quota = ResourceQuota(max_cpu="500m") # Act & Assert with pytest.raises(Exception): quota.max_cpu = "1000m" # type: ignore[misc] def test_max_connections_immutable(self): # Arrange quota = ResourceQuota(max_connections=10) # Act & Assert with pytest.raises(Exception): quota.max_connections = 20 # type: ignore[misc] # ─── CredentialStrategy ───────────────────────────────────────────────── @pytest.mark.unit class TestCredentialStrategy: """CredentialStrategy 不可变值对象。""" def test_required_fields_set(self): # Arrange # Act strategy = CredentialStrategy( type=CredentialType.STATIC, acquire_method=AcquireMethod.MANUAL, required_fields=("app_id", "app_secret"), ) # Assert assert strategy.type is CredentialType.STATIC assert strategy.acquire_method is AcquireMethod.MANUAL assert strategy.required_fields == ("app_id", "app_secret") def test_optional_fields_defaults(self): # Arrange # Act strategy = CredentialStrategy( type=CredentialType.DYNAMIC, acquire_method=AcquireMethod.QR_LOGIN, required_fields=("token",), ) # Assert - optional_fields 默认空元组,supports_rotation/supports_revocation 默认 False assert strategy.optional_fields == () assert strategy.supports_rotation is False assert strategy.supports_revocation is False def test_optional_fields_settable(self): # Arrange # Act strategy = CredentialStrategy( type=CredentialType.HYBRID, acquire_method=AcquireMethod.OAUTH, required_fields=("app_id",), optional_fields=("user_token",), supports_rotation=True, supports_revocation=True, ) # Assert assert strategy.optional_fields == ("user_token",) assert strategy.supports_rotation is True assert strategy.supports_revocation is True def test_is_frozen_dataclass(self): # Arrange strategy = CredentialStrategy( type=CredentialType.STATIC, acquire_method=AcquireMethod.MANUAL, required_fields=("app_id",), ) # Act & Assert with pytest.raises(Exception): strategy.type = CredentialType.DYNAMIC # type: ignore[misc] def test_required_fields_immutable(self): # Arrange strategy = CredentialStrategy( type=CredentialType.STATIC, acquire_method=AcquireMethod.MANUAL, required_fields=("app_id",), ) # Act & Assert with pytest.raises(Exception): strategy.required_fields = ("other",) # type: ignore[misc] # ─── ChannelManifest ──────────────────────────────────────────────────── @pytest.mark.unit class TestChannelManifest: """ChannelManifest 不可变值对象。""" def test_required_fields_set(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.id == "com.yuxi.channels.feishu" assert manifest.name == "feishu" assert manifest.version == "1.0.0" assert manifest.channel_type is ChannelType("feishu") assert manifest.provides == ("inbound", "outbound") assert manifest.entry_module == "yuxi.channels.plugins.feishu.entry" assert isinstance(manifest.capabilities, ChannelCapabilities) assert isinstance(manifest.config_schema, tuple) assert isinstance(manifest.config_schema[0], ConfigField) def test_lifecycle_default(self): # Arrange # Act manifest = _build_channel_manifest() # Assert - 默认生命周期为 init/start/stop/unload assert manifest.lifecycle == ("init", "start", "stop", "unload") def test_failure_policy_default(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.failure_policy is FailurePolicy.DEGRADE def test_critical_default_false(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.critical is False def test_requires_dm_pairing_default_true(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.requires_dm_pairing is True def test_requires_outbound_delivery_default_true(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.requires_outbound_delivery is True def test_credential_strategy_default_none(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.credential_strategy is None def test_depends_default_empty_tuple(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.depends == () def test_compatibility_default_none(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.compatibility is None def test_resource_quota_default_none(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.resource_quota is None def test_accessible_ports_default_empty_tuple(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.accessible_ports == () def test_injectable_pipelines_default_empty_tuple(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.injectable_pipelines == () def test_skills_default_empty_tuple(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.skills == () def test_env_vars_default_empty_tuple(self): # Arrange # Act manifest = _build_channel_manifest() # Assert assert manifest.env_vars == () def test_credential_strategy_settable(self): # Arrange strategy = CredentialStrategy( type=CredentialType.STATIC, acquire_method=AcquireMethod.MANUAL, required_fields=("app_id",), ) # Act manifest = _build_channel_manifest(credential_strategy=strategy) # Assert assert manifest.credential_strategy is strategy def test_is_frozen_dataclass(self): # Arrange manifest = _build_channel_manifest() # Act & Assert with pytest.raises(Exception): manifest.id = "other" # type: ignore[misc] def test_critical_immutable(self): # Arrange manifest = _build_channel_manifest() # Act & Assert with pytest.raises(Exception): manifest.critical = True # type: ignore[misc] # ─── PluginManifest ───────────────────────────────────────────────────── @pytest.mark.unit class TestPluginManifest: """PluginManifest 不可变值对象。""" def test_required_field_manifest(self): # Arrange channel_manifest = _build_channel_manifest() # Act plugin_manifest = PluginManifest(manifest=channel_manifest) # Assert assert plugin_manifest.manifest is channel_manifest def test_adapters_default_empty_tuple(self): # Arrange # Act plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Assert assert plugin_manifest.adapters == () def test_stages_default_empty_tuple(self): # Arrange # Act plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Assert assert plugin_manifest.stages == () def test_event_subscriptions_default_empty_tuple(self): # Arrange # Act plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Assert assert plugin_manifest.event_subscriptions == () def test_config_sources_default_empty_tuple(self): # Arrange # Act plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Assert assert plugin_manifest.config_sources == () def test_installed_at_default_none(self): # Arrange # Act plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Assert assert plugin_manifest.installed_at is None def test_install_source_default_none(self): # Arrange # Act plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Assert assert plugin_manifest.install_source is None def test_install_version_default_none(self): # Arrange # Act plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Assert assert plugin_manifest.install_version is None def test_optional_fields_settable(self): # Arrange now = datetime(2026, 1, 1, 0, 0, 0) # Act plugin_manifest = PluginManifest( manifest=_build_channel_manifest(), adapters=("InboundAdapter",), stages=("stage-1",), event_subscriptions=("PairingApproved",), config_sources=("source-1",), installed_at=now, install_source="/path/to/plugin", install_version="1.2.3", ) # Assert assert plugin_manifest.adapters == ("InboundAdapter",) assert plugin_manifest.stages == ("stage-1",) assert plugin_manifest.event_subscriptions == ("PairingApproved",) assert plugin_manifest.config_sources == ("source-1",) assert plugin_manifest.installed_at == now assert plugin_manifest.install_source == "/path/to/plugin" assert plugin_manifest.install_version == "1.2.3" def test_is_frozen_dataclass(self): # Arrange plugin_manifest = PluginManifest(manifest=_build_channel_manifest()) # Act & Assert with pytest.raises(Exception): plugin_manifest.manifest = _build_channel_manifest(id="other") # type: ignore[misc] # ─── lifecycle.py 枚举 ────────────────────────────────────────────────── @pytest.mark.unit class TestLifecycleEnums: """lifecycle.py 的 LifecycleState / LifecycleHook 枚举。""" def test_lifecycle_state_inherits_strenum(self): # Arrange # Act # Assert assert issubclass(LifecycleState, StrEnum) def test_lifecycle_state_member_count(self): # Arrange # Act # Assert - 11 个状态 assert len(list(LifecycleState)) == 11 def test_lifecycle_state_values(self): # Arrange # Act # Assert assert LifecycleState.DISCOVERED == "discovered" assert LifecycleState.RESOLVED == "resolved" assert LifecycleState.LOADED == "loaded" assert LifecycleState.INITIALIZED == "initialized" assert LifecycleState.STARTED == "started" assert LifecycleState.PAUSED == "paused" assert LifecycleState.STOPPED == "stopped" assert LifecycleState.UNLOADED == "unloaded" assert LifecycleState.FAILED == "failed" assert LifecycleState.NOT_INSTALLED == "not_installed" assert LifecycleState.INSTALLED == "installed" def test_lifecycle_hook_inherits_strenum(self): # Arrange # Act # Assert assert issubclass(LifecycleHook, StrEnum) def test_lifecycle_hook_member_count(self): # Arrange # Act # Assert - 8 个钩子 assert len(list(LifecycleHook)) == 8 def test_lifecycle_hook_values(self): # Arrange # Act # Assert assert LifecycleHook.INIT == "init" assert LifecycleHook.START == "start" assert LifecycleHook.STOP == "stop" assert LifecycleHook.PAUSE == "pause" assert LifecycleHook.RESUME == "resume" assert LifecycleHook.UNLOAD == "unload" assert LifecycleHook.RECONFIGURE == "reconfigure" assert LifecycleHook.FAIL == "fail" # ─── LifecycleHookHandler Protocol ────────────────────────────────────── @pytest.mark.unit class TestLifecycleHookHandlerProtocol: """LifecycleHookHandler 为 runtime_checkable Protocol。""" def test_is_protocol_subclass(self): # Arrange # Act # Assert assert issubclass(LifecycleHookHandler, Protocol) def test_runtime_checkable_isinstance_works(self): # Arrange - MagicMock 支持任意属性访问,runtime_checkable Protocol 的 # isinstance 检查仅校验属性存在性 # Act # Assert assert isinstance(MagicMock(), LifecycleHookHandler) def test_protocol_has_expected_methods(self): # Arrange - LifecycleHookHandler 定义了 8 个异步钩子方法 expected_methods = [ "onInit", "onStart", "onStop", "onPause", "onResume", "onUnload", "onReconfigure", "onFail", ] # Act # Assert for name in expected_methods: assert hasattr(LifecycleHookHandler, name), f"缺少方法 {name}" # ─── extension_point.py 枚举 ──────────────────────────────────────────── @pytest.mark.unit class TestExtensionPointEnums: """extension_point.py 的 ConflictStrategy / FailureStrategy 枚举。""" def test_conflict_strategy_inherits_strenum(self): # Arrange # Act # Assert assert issubclass(ConflictStrategy, StrEnum) def test_conflict_strategy_member_count(self): # Arrange # Act # Assert - 3 个冲突策略 assert len(list(ConflictStrategy)) == 3 def test_conflict_strategy_values(self): # Arrange # Act # Assert assert ConflictStrategy.CHAIN == "chain" assert ConflictStrategy.REJECT == "reject" assert ConflictStrategy.OVERRIDE == "override" def test_failure_strategy_inherits_strenum(self): # Arrange # Act # Assert assert issubclass(FailureStrategy, StrEnum) def test_failure_strategy_member_count(self): # Arrange # Act # Assert - 4 个阶段失败策略 assert len(list(FailureStrategy)) == 4 def test_failure_strategy_values(self): # Arrange # Act # Assert assert FailureStrategy.TERMINATE == "terminate" assert FailureStrategy.SKIP == "skip" assert FailureStrategy.COMPENSATE == "compensate" assert FailureStrategy.DEGRADE == "degrade" # ─── StageSlot ────────────────────────────────────────────────────────── @pytest.mark.unit class TestStageSlot: """StageSlot 不可变值对象。""" def test_required_fields_set(self): # Arrange - stage 字段为 Stage Protocol 实例,使用 MagicMock 模拟 stage = MagicMock() # Act slot = StageSlot(pipeline="inbound", anchor="before:route", stage=stage) # Assert assert slot.pipeline == "inbound" assert slot.anchor == "before:route" assert slot.stage is stage def test_priority_default_100(self): # Arrange # Act slot = StageSlot(pipeline="inbound", anchor="before:route", stage=MagicMock()) # Assert assert slot.priority == 100 def test_multi_default_false(self): # Arrange # Act slot = StageSlot(pipeline="inbound", anchor="before:route", stage=MagicMock()) # Assert assert slot.multi is False def test_conflict_strategy_default_chain(self): # Arrange # Act slot = StageSlot(pipeline="inbound", anchor="before:route", stage=MagicMock()) # Assert assert slot.conflict_strategy is ConflictStrategy.CHAIN def test_failure_policy_default_degrade(self): # Arrange # Act slot = StageSlot(pipeline="inbound", anchor="before:route", stage=MagicMock()) # Assert assert slot.failure_policy is FailurePolicy.DEGRADE def test_optional_fields_settable(self): # Arrange # Act slot = StageSlot( pipeline="outbound", anchor="after:format", stage=MagicMock(), priority=50, multi=True, conflict_strategy=ConflictStrategy.REJECT, failure_policy=FailurePolicy.ISOLATE, ) # Assert assert slot.priority == 50 assert slot.multi is True assert slot.conflict_strategy is ConflictStrategy.REJECT assert slot.failure_policy is FailurePolicy.ISOLATE def test_is_frozen_dataclass(self): # Arrange slot = StageSlot(pipeline="inbound", anchor="before:route", stage=MagicMock()) # Act & Assert with pytest.raises(Exception): slot.pipeline = "outbound" # type: ignore[misc] def test_priority_immutable(self): # Arrange slot = StageSlot(pipeline="inbound", anchor="before:route", stage=MagicMock()) # Act & Assert with pytest.raises(Exception): slot.priority = 200 # type: ignore[misc] # ─── EventSubscription ────────────────────────────────────────────────── @pytest.mark.unit class TestEventSubscription: """EventSubscription 不可变值对象。""" def test_required_fields_set(self): # Arrange - handler 字段为 EventHandler Protocol 实例,使用 MagicMock 模拟 handler = MagicMock() # Act sub = EventSubscription(event_type="PairingApproved", handler=handler) # Assert assert sub.event_type == "PairingApproved" assert sub.handler is handler def test_priority_default_100(self): # Arrange # Act sub = EventSubscription(event_type="PairingApproved", handler=MagicMock()) # Assert assert sub.priority == 100 def test_failure_policy_default_degrade(self): # Arrange # Act sub = EventSubscription(event_type="PairingApproved", handler=MagicMock()) # Assert assert sub.failure_policy is FailurePolicy.DEGRADE def test_optional_fields_settable(self): # Arrange # Act sub = EventSubscription( event_type="ConfigChanged", handler=MagicMock(), priority=50, failure_policy=FailurePolicy.CIRCUIT_BREAK, ) # Assert assert sub.priority == 50 assert sub.failure_policy is FailurePolicy.CIRCUIT_BREAK def test_is_frozen_dataclass(self): # Arrange sub = EventSubscription(event_type="PairingApproved", handler=MagicMock()) # Act & Assert with pytest.raises(Exception): sub.event_type = "other" # type: ignore[misc] # ─── ConfigSource ─────────────────────────────────────────────────────── @pytest.mark.unit class TestConfigSource: """ConfigSource 不可变值对象。""" def test_required_fields_set(self): # Arrange - loader 字段为 ConfigLoader Protocol 实例,使用 MagicMock 模拟 loader = MagicMock() # Act source = ConfigSource(source_id="src-1", loader=loader) # Assert assert source.source_id == "src-1" assert source.loader is loader def test_decryptor_default_none(self): # Arrange # Act source = ConfigSource(source_id="src-1", loader=MagicMock()) # Assert assert source.decryptor is None def test_failure_policy_default_degrade(self): # Arrange # Act source = ConfigSource(source_id="src-1", loader=MagicMock()) # Assert assert source.failure_policy is FailurePolicy.DEGRADE def test_optional_fields_settable(self): # Arrange # Act source = ConfigSource( source_id="src-1", loader=MagicMock(), decryptor=MagicMock(), failure_policy=FailurePolicy.ISOLATE, ) # Assert assert source.decryptor is not None assert source.failure_policy is FailurePolicy.ISOLATE def test_is_frozen_dataclass(self): # Arrange source = ConfigSource(source_id="src-1", loader=MagicMock()) # Act & Assert with pytest.raises(Exception): source.source_id = "other" # type: ignore[misc] # ─── Stage Protocol ───────────────────────────────────────────────────── @pytest.mark.unit class TestStageProtocol: """Stage 为 runtime_checkable Protocol。""" def test_is_protocol_subclass(self): # Arrange # Act # Assert assert issubclass(Stage, Protocol) def test_runtime_checkable_isinstance_works(self): # Arrange - MagicMock 支持任意属性访问 # Act # Assert assert isinstance(MagicMock(), Stage) def test_protocol_has_expected_attributes(self): # Arrange - Stage 定义了 id/reads/writes/idempotent/thread_safe/failure/compensate 属性 expected_attrs = [ "id", "reads", "writes", "idempotent", "thread_safe", "failure", "compensate", "process", ] # Act # Assert for name in expected_attrs: assert hasattr(Stage, name), f"Stage 缺少属性 {name}" # ─── CapabilityDeclaration ────────────────────────────────────────────── @pytest.mark.unit class TestCapabilityDeclaration: """CapabilityDeclaration 不可变值对象。""" def test_capabilities_field_set(self): # Arrange caps = ChannelCapabilities(rich_message=True, streaming=True) # Act decl = CapabilityDeclaration(capabilities=caps) # Assert assert decl.capabilities is caps assert decl.capabilities.rich_message is True assert decl.capabilities.streaming is True def test_default_capabilities(self): # Arrange # Act decl = CapabilityDeclaration(capabilities=ChannelCapabilities()) # Assert assert isinstance(decl.capabilities, ChannelCapabilities) assert decl.capabilities.rich_message is False def test_is_frozen_dataclass(self): # Arrange decl = CapabilityDeclaration(capabilities=ChannelCapabilities()) # Act & Assert with pytest.raises(Exception): decl.capabilities = ChannelCapabilities() # type: ignore[misc] # ─── CapabilityProof ──────────────────────────────────────────────────── @pytest.mark.unit class TestCapabilityProof: """CapabilityProof 不可变值对象。""" def test_required_fields_set(self): # Arrange now = datetime(2026, 1, 1, 0, 0, 0) # Act proof = CapabilityProof( capability_name="streaming", proven=True, proof_timestamp=now, ) # Assert assert proof.capability_name == "streaming" assert proof.proven is True assert proof.proof_timestamp == now def test_cache_ttl_default_300(self): # Arrange # Act proof = CapabilityProof( capability_name="streaming", proven=False, proof_timestamp=datetime(2026, 1, 1, 0, 0, 0), ) # Assert assert proof.cache_ttl == 300 def test_cache_ttl_settable(self): # Arrange # Act proof = CapabilityProof( capability_name="streaming", proven=True, proof_timestamp=datetime(2026, 1, 1, 0, 0, 0), cache_ttl=600, ) # Assert assert proof.cache_ttl == 600 def test_is_frozen_dataclass(self): # Arrange proof = CapabilityProof( capability_name="streaming", proven=True, proof_timestamp=datetime(2026, 1, 1, 0, 0, 0), ) # Act & Assert with pytest.raises(Exception): proof.capability_name = "other" # type: ignore[misc] def test_proven_immutable(self): # Arrange proof = CapabilityProof( capability_name="streaming", proven=True, proof_timestamp=datetime(2026, 1, 1, 0, 0, 0), ) # Act & Assert with pytest.raises(Exception): proof.proven = False # type: ignore[misc] # ─── CapabilityProver Protocol ────────────────────────────────────────── @pytest.mark.unit class TestCapabilityProverProtocol: """CapabilityProver 为 runtime_checkable Protocol。""" def test_is_protocol_subclass(self): # Arrange # Act # Assert assert issubclass(CapabilityProver, Protocol) def test_runtime_checkable_isinstance_works(self): # Arrange - MagicMock 支持任意属性访问 # Act # Assert assert isinstance(MagicMock(), CapabilityProver) def test_protocol_has_prove_capability_method(self): # Arrange # Act # Assert assert hasattr(CapabilityProver, "proveCapability") # ─── PluginHost Protocol ──────────────────────────────────────────────── @pytest.mark.unit class TestPluginHostProtocol: """PluginHost 为 runtime_checkable Protocol。""" def test_is_protocol_subclass(self): # Arrange # Act # Assert assert issubclass(PluginHost, Protocol) def test_runtime_checkable_isinstance_works(self): # Arrange - MagicMock 支持任意属性访问,runtime_checkable Protocol 的 # isinstance 检查仅校验属性存在性 # Act # Assert assert isinstance(MagicMock(), PluginHost) def test_protocol_has_port_getter_methods(self): # Arrange - PluginHost 定义了若干 getXxxPort 方法 expected_methods = [ "getConfigPort", "getLoggerPort", "getPersistencePort", "getCachePort", "getTracerPort", "getQueuePort", "getConversationPort", "getAgentRunPort", "getIdentityResolverPort", "getChannelAccountRepositoryPort", "getChannelSessionRepositoryPort", "getPairingRepositoryPort", "getAuditLogRepositoryPort", "getOutboxRepositoryPort", "getUserIdentityRepositoryPort", "getIdempotencyRepositoryPort", "getPersistenceHealthPort", ] # Act # Assert for name in expected_methods: assert hasattr(PluginHost, name), f"PluginHost 缺少方法 {name}" def test_protocol_has_register_methods(self): # Arrange - PluginHost 定义了若干 registerXxx 方法 expected_methods = [ "registerAdapter", "registerStageSlot", "registerEventSubscription", "registerConfigSource", "registerCapabilityProver", "registerMatchTier", "registerMatcher", ] # Act # Assert for name in expected_methods: assert hasattr(PluginHost, name), f"PluginHost 缺少方法 {name}" def test_protocol_no_declare_methods(self): # Arrange - PluginHost 不再定义 declareXxx 方法 # (能力边界声明已统一到 ChannelManifest 静态字段) removed_methods = [ "declareAccessiblePorts", "declareInjectablePipelines", "declareResourceQuota", ] # Act # Assert for name in removed_methods: assert not hasattr(PluginHost, name), ( f"PluginHost 不应再定义 {name}(已统一到 ChannelManifest)" ) def test_protocol_has_publish_event_method(self): # Arrange # Act # Assert assert hasattr(PluginHost, "publishEvent") # ─── CHANNEL_ENTRY 类型别名 ───────────────────────────────────────────── @pytest.mark.unit class TestChannelEntryAlias: """CHANNEL_ENTRY 为 Callable 类型别名。""" def test_is_callable_origin(self): # Arrange # Act origin = get_origin(CHANNEL_ENTRY) # Assert - CHANNEL_ENTRY 的 origin 为 collections.abc.Callable assert origin is Callable def test_args_contain_plugin_host_and_manifest(self): # Arrange - CHANNEL_ENTRY = Callable[[PluginHost], PluginManifest] # Act args = get_args(CHANNEL_ENTRY) # Assert - 参数列表含 PluginHost,返回类型为 PluginManifest assert PluginManifest in args # args 结构为 ([PluginHost], PluginManifest),参数列表在 args[0] param_args = args[0] assert PluginHost in param_args def test_callable_with_plugin_host_returns_manifest(self): # Arrange - 构造一个符合 CHANNEL_ENTRY 签名的函数 def entry(host: PluginHost) -> PluginManifest: return PluginManifest(manifest=_build_channel_manifest()) # Act result = entry(MagicMock()) # Assert assert isinstance(result, PluginManifest)