"""entry 模块单元测试。 覆盖 ``channel_entry(host, manifest)`` 入口函数(F-03 单真相源新签名): - 适配器注册(15 个适配器) - 生命周期钩子注册 - ``PluginManifest`` 返回值(manifest 直接复用入参,adapters 为运行时清单) F-03 单真相源改造后,``entry.py`` 不再构造声明性字段(capabilities / config_schema / accessible_ports / injectable_pipelines / resource_quota / env_vars / credential_strategy 等),所有声明性字段由 discover 阶段解析的 ``ChannelManifest`` 承载。本测试通过 ``load_manifest_from_dir`` 加载真实 ``manifest.json`` 作为入参,验证 entry 函数仅做适配器实例化与注册。 """ 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.qqbot.entry import ( _ADAPTER_TYPES, channel_entry, ) from yuxi.channels.plugins.qqbot.lifecycle import QQBotLifecycleHandler pytestmark = pytest.mark.unit # ------------------------------------------------------------------ # 测试夹具 # ------------------------------------------------------------------ @pytest.fixture def qqbot_manifest() -> ChannelManifest: """加载 qqbot 插件真实 manifest.json 作为 entry 入参。""" plugin_dir = os.path.join( os.path.dirname(__file__), "..", "..", "..", "..", "..", "package", "yuxi", "channels", "plugins", "qqbot", ) return load_manifest_from_dir(os.path.abspath(plugin_dir)) # ------------------------------------------------------------------ # channel_entry # ------------------------------------------------------------------ @pytest.mark.unit class TestChannelEntry: """channel_entry 入口函数测试(F-03 单真相源新签名)。""" def test_returns_plugin_manifest(self, fake_plugin_host, qqbot_manifest): # Act result = channel_entry(fake_plugin_host, qqbot_manifest) # Assert assert isinstance(result, PluginManifest) def test_returns_input_manifest_unchanged(self, fake_plugin_host, qqbot_manifest): """F-03 单真相源:返回的 PluginManifest.manifest 应为入参 manifest 直接复用。""" # Act result = channel_entry(fake_plugin_host, qqbot_manifest) # Assert - manifest 直接复用入参,不重新构造 assert result.manifest is qqbot_manifest def test_registers_all_15_adapters(self, fake_plugin_host, qqbot_manifest): # Act channel_entry(fake_plugin_host, qqbot_manifest) # Assert assert fake_plugin_host.registerAdapter.call_count == 15 adapter_types = [call.args[0] for call in fake_plugin_host.registerAdapter.call_args_list] assert tuple(adapter_types) == _ADAPTER_TYPES def test_registers_lifecycle_handler(self, fake_plugin_host, qqbot_manifest): # Act channel_entry(fake_plugin_host, qqbot_manifest) # Assert fake_plugin_host.registerLifecycleHandler.assert_called_once() handler = fake_plugin_host.registerLifecycleHandler.call_args.args[0] assert isinstance(handler, QQBotLifecycleHandler) def test_manifest_adapters_match_registered_types(self, fake_plugin_host, qqbot_manifest): # Act result = channel_entry(fake_plugin_host, qqbot_manifest) # Assert assert result.adapters == _ADAPTER_TYPES assert len(result.adapters) == 15 def test_gets_required_ports(self, fake_plugin_host, qqbot_manifest): """验证 entry 调用 getConfigPort/getLoggerPort/getCachePort。""" # Act channel_entry(fake_plugin_host, qqbot_manifest) # Assert fake_plugin_host.getConfigPort.assert_called_once() fake_plugin_host.getLoggerPort.assert_called_once() fake_plugin_host.getCachePort.assert_called_once() def test_handler_receives_manifest_config_schema(self, fake_plugin_host, qqbot_manifest): """F-03 单真相源:LifecycleHandler 接收 manifest.config_schema。""" # Act channel_entry(fake_plugin_host, qqbot_manifest) # Assert - 验证 manifest 的 config_schema 传递给 handler assert qqbot_manifest.config_schema is not None assert len(qqbot_manifest.config_schema) > 0 def test_adapter_types_contains_inbound(self): """_ADAPTER_TYPES 应包含 inbound。""" assert "inbound" in _ADAPTER_TYPES def test_adapter_types_contains_outbound(self): assert "outbound" in _ADAPTER_TYPES def test_adapter_types_contains_stream_connector(self): assert "stream_connector" in _ADAPTER_TYPES def test_adapter_types_not_contains_streaming(self): """QQ Bot v1 不实现 streaming 适配器。""" assert "streaming" not in _ADAPTER_TYPES def test_adapter_types_not_contains_command(self): """QQ Bot v1 不实现 command 适配器。""" assert "command" not in _ADAPTER_TYPES def test_adapter_types_not_contains_login(self): """QQ Bot v1 不实现 login 适配器。""" assert "login" not in _ADAPTER_TYPES # ------------------------------------------------------------------ # F-03 单真相源:验证 entry.py 不含硬编码声明 # ------------------------------------------------------------------ @pytest.mark.unit class TestNoHardcodedDeclarations: """F-03 单真相源:验证 entry.py 不含已被删除的硬编码声明。 entry.py 改造后应不再包含以下私有定义(声明性字段已收敛到 manifest.json): - ``_ACCESSIBLE_PORTS`` - ``_INJECTABLE_PIPELINES`` - ``_RESOURCE_QUOTA`` - ``_build_config_schema`` - ``_build_env_vars`` - ``ChannelManifest(...)`` 构造调用(manifest 应为入参复用) - ``ChannelCapabilities(...)`` 构造调用(能力声明应由 manifest 承载) """ def test_no_accessible_ports_constant(self): """验证 _ACCESSIBLE_PORTS 已被删除。""" import yuxi.channels.plugins.qqbot.entry as entry_mod assert not hasattr(entry_mod, "_ACCESSIBLE_PORTS") def test_no_injectable_pipelines_constant(self): """验证 _INJECTABLE_PIPELINES 已被删除。""" import yuxi.channels.plugins.qqbot.entry as entry_mod assert not hasattr(entry_mod, "_INJECTABLE_PIPELINES") def test_no_resource_quota_constant(self): """验证 _RESOURCE_QUOTA 已被删除。""" import yuxi.channels.plugins.qqbot.entry as entry_mod assert not hasattr(entry_mod, "_RESOURCE_QUOTA") def test_no_build_config_schema_function(self): """验证 _build_config_schema 已被删除。""" import yuxi.channels.plugins.qqbot.entry as entry_mod assert not hasattr(entry_mod, "_build_config_schema") def test_no_build_env_vars_function(self): """验证 _build_env_vars 已被删除。""" import yuxi.channels.plugins.qqbot.entry as entry_mod assert not hasattr(entry_mod, "_build_env_vars") def test_source_no_channel_manifest_construction(self): """验证 entry.py 源码不含 ChannelManifest(...) 直接构造调用。""" import inspect from yuxi.channels.plugins.qqbot 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.qqbot import entry as entry_mod source = inspect.getsource(entry_mod) assert "ChannelCapabilities(" not in source def test_source_no_credential_strategy_construction(self): """验证 entry.py 源码不含 CredentialStrategy(...) 直接构造调用。""" import inspect from yuxi.channels.plugins.qqbot import entry as entry_mod source = inspect.getsource(entry_mod) assert "CredentialStrategy(" not in source def test_source_no_resource_quota_construction(self): """验证 entry.py 源码不含 ResourceQuota(...) 直接构造调用。""" import inspect from yuxi.channels.plugins.qqbot import entry as entry_mod source = inspect.getsource(entry_mod) assert "ResourceQuota(" not in source def test_adapter_types_constant_preserved(self): """验证 _ADAPTER_TYPES 仍保留(运行时适配器清单,非声明性字段)。""" import yuxi.channels.plugins.qqbot.entry as entry_mod assert hasattr(entry_mod, "_ADAPTER_TYPES") assert len(entry_mod._ADAPTER_TYPES) == 15