- 新增多个业务域的__init__.py模块文件,规范包导出结构 - 调整多个DTO文件的导入路径,统一模块组织方式 - 移除测试文件中多余的空行与导入语句 - 优化部分业务模块的包层级划分
218 lines
7.9 KiB
Python
218 lines
7.9 KiB
Python
"""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.plugin.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
|