"""dingtalk entry 模块单元测试。 覆盖 ``channel_entry(host, manifest)`` 入口函数(F-03 单真相源签名): - 适配器注册(17 个适配器,均通过 registerAdapter 注册) - 生命周期钩子注册 - ``PluginManifest`` 返回值(manifest 直接复用入参,adapters 为运行时清单) F-03 单真相源改造后,``entry.py`` 不再构造声明性字段,所有声明性字段由 discover 阶段解析的 ``ChannelManifest`` 承载。本测试通过 ``load_manifest_from_dir`` 加载真实 ``manifest.json`` 作为入参。 注:entry.py 源码 docstring 描述"17 个适配器(16 列表 + 1 单实例)", 但实际 ``identity_resolver`` 同样通过 ``registerAdapter`` 注册,故 ``registerAdapter`` 调用次数 == ``_ADAPTER_TYPES`` 长度 == 17。 """ from __future__ import annotations import os import pytest from yuxi.channels.application.lifecycle.manifest_loader import load_manifest_from_dir from yuxi.channels.contract.plugin.manifest import ( ChannelManifest, PluginManifest, ) from yuxi.channels.plugins.dingtalk.entry import ( _ADAPTER_TYPES, channel_entry, ) from yuxi.channels.plugins.dingtalk.lifecycle import DingTalkLifecycleHandler pytestmark = pytest.mark.unit # ------------------------------------------------------------------ # 测试夹具 # ------------------------------------------------------------------ @pytest.fixture def dingtalk_manifest() -> ChannelManifest: """加载 dingtalk 插件真实 manifest.json 作为 entry 入参。""" plugin_dir = os.path.join( os.path.dirname(__file__), "..", "..", "..", "..", "..", "package", "yuxi", "channels", "plugins", "dingtalk", ) return load_manifest_from_dir(os.path.abspath(plugin_dir)) # ------------------------------------------------------------------ # channel_entry # ------------------------------------------------------------------ @pytest.mark.unit class TestChannelEntry: """channel_entry 入口函数测试(F-03 单真相源签名)。""" def test_channel_entry_is_callable(self): # Arrange / Act / Assert assert callable(channel_entry) def test_returns_plugin_manifest(self, fake_plugin_host, dingtalk_manifest): # Act result = channel_entry(fake_plugin_host, dingtalk_manifest) # Assert assert isinstance(result, PluginManifest) def test_returns_input_manifest_unchanged(self, fake_plugin_host, dingtalk_manifest): """F-03 单真相源:返回的 PluginManifest.manifest 应为入参 manifest 直接复用。""" # Act result = channel_entry(fake_plugin_host, dingtalk_manifest) # Assert assert result.manifest is dingtalk_manifest def test_registers_all_adapters(self, fake_plugin_host, dingtalk_manifest): """注册 17 个适配器(均通过 registerAdapter 注册)。""" # Act channel_entry(fake_plugin_host, dingtalk_manifest) # Assert assert fake_plugin_host.registerAdapter.call_count == 17 adapter_types = [call.args[0] for call in fake_plugin_host.registerAdapter.call_args_list] assert tuple(adapter_types) == _ADAPTER_TYPES def test_adapter_types_length_is_17(self): # Arrange / Act / Assert assert len(_ADAPTER_TYPES) == 17 def test_adapters_contain_expected_names(self): # Arrange expected = { "inbound", "outbound", "session", "status", "rich_message", "streaming", "mention", "message_ops", "directory", "command", "whitelist", "wizard", "doctor", "lifecycle", "probeable", "identity_resolver", "stream_connector", } # Act / Assert assert set(_ADAPTER_TYPES) == expected def test_no_login_adapter_registered(self, fake_plugin_host, dingtalk_manifest): """supports_qr_login=false,不注册 login_adapter。""" # Act channel_entry(fake_plugin_host, dingtalk_manifest) # Assert adapter_types = [call.args[0] for call in fake_plugin_host.registerAdapter.call_args_list] assert "login" not in adapter_types def test_registers_lifecycle_handler(self, fake_plugin_host, dingtalk_manifest): # Act channel_entry(fake_plugin_host, dingtalk_manifest) # Assert fake_plugin_host.registerLifecycleHandler.assert_called_once() handler = fake_plugin_host.registerLifecycleHandler.call_args.args[0] assert isinstance(handler, DingTalkLifecycleHandler) def test_manifest_adapters_match_registered_types(self, fake_plugin_host, dingtalk_manifest): # Act result = channel_entry(fake_plugin_host, dingtalk_manifest) # Assert assert result.adapters == _ADAPTER_TYPES assert len(result.adapters) == 17 def test_gets_required_ports(self, fake_plugin_host, dingtalk_manifest): """验证 entry 调用 getConfigPort/getLoggerPort/getCachePort/getPersistencePort。""" # Act channel_entry(fake_plugin_host, dingtalk_manifest) # Assert fake_plugin_host.getConfigPort.assert_called_once() fake_plugin_host.getLoggerPort.assert_called_once() fake_plugin_host.getCachePort.assert_called_once() fake_plugin_host.getPersistencePort.assert_called_once() def test_handler_receives_manifest_config_schema(self, fake_plugin_host, dingtalk_manifest): """F-03 单真相源:LifecycleHandler 接收 manifest.config_schema。""" # Act channel_entry(fake_plugin_host, dingtalk_manifest) # Assert - entry 不会因 config_schema 缺失而失败 assert dingtalk_manifest.config_schema is not None assert len(dingtalk_manifest.config_schema) > 0 # ------------------------------------------------------------------ # F-03 单真相源:验证 entry.py 不含硬编码声明 # ------------------------------------------------------------------ @pytest.mark.unit class TestNoHardcodedDeclarations: """F-03 单真相源:验证 entry.py 不含已被删除的硬编码声明。""" def test_no_accessible_ports_constant(self): import yuxi.channels.plugins.dingtalk.entry as entry_mod assert not hasattr(entry_mod, "_ACCESSIBLE_PORTS") def test_no_injectable_pipelines_constant(self): import yuxi.channels.plugins.dingtalk.entry as entry_mod assert not hasattr(entry_mod, "_INJECTABLE_PIPELINES") def test_no_resource_quota_constant(self): import yuxi.channels.plugins.dingtalk.entry as entry_mod assert not hasattr(entry_mod, "_RESOURCE_QUOTA") def test_no_build_config_schema_function(self): import yuxi.channels.plugins.dingtalk.entry as entry_mod assert not hasattr(entry_mod, "_build_config_schema") def test_source_no_channel_manifest_construction(self): """验证 entry.py 源码不含 ChannelManifest(...) 直接构造调用。""" import inspect from yuxi.channels.plugins.dingtalk import entry as entry_mod source = inspect.getsource(entry_mod) assert "ChannelManifest(" not in source def test_source_no_channel_capabilities_construction(self): """验证 entry.py 源码不含 ChannelCapabilities(...) 直接构造调用。""" import inspect from yuxi.channels.plugins.dingtalk import entry as entry_mod source = inspect.getsource(entry_mod) assert "ChannelCapabilities(" not in source def test_adapter_types_constant_preserved(self): """验证 _ADAPTER_TYPES 仍保留(运行时适配器清单,非声明性字段)。""" import yuxi.channels.plugins.dingtalk.entry as entry_mod assert hasattr(entry_mod, "_ADAPTER_TYPES") assert len(entry_mod._ADAPTER_TYPES) == 17