2026-07-02 03:28:19 +08:00
|
|
|
|
"""entry 模块单元测试。
|
|
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
覆盖 ``channel_entry(host, manifest)`` 入口函数(F-03 单真相源新签名):
|
|
|
|
|
|
- 适配器注册(18 个适配器)
|
|
|
|
|
|
- 生命周期钩子注册
|
|
|
|
|
|
- ``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 函数仅做适配器实例化与注册。
|
2026-07-02 03:28:19 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
2026-07-06 20:52:20 +08:00
|
|
|
|
from yuxi.channels.application.lifecycle.manifest_loader import load_manifest_from_dir
|
2026-07-02 03:28:19 +08:00
|
|
|
|
from yuxi.channels.contract.plugin.manifest import (
|
2026-07-06 20:52:20 +08:00
|
|
|
|
ChannelManifest,
|
2026-07-02 03:28:19 +08:00
|
|
|
|
PluginManifest,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.plugins.feishu.entry import (
|
|
|
|
|
|
_ADAPTER_TYPES,
|
|
|
|
|
|
channel_entry,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.plugins.feishu.lifecycle import FeishuLifecycleHandler
|
|
|
|
|
|
|
|
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
2026-07-06 20:52:20 +08:00
|
|
|
|
# 测试夹具
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def feishu_manifest() -> ChannelManifest:
|
|
|
|
|
|
"""加载 feishu 插件真实 manifest.json 作为 entry 入参。"""
|
|
|
|
|
|
import os
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
plugin_dir = os.path.join(
|
|
|
|
|
|
os.path.dirname(__file__),
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"package",
|
|
|
|
|
|
"yuxi",
|
|
|
|
|
|
"channels",
|
|
|
|
|
|
"plugins",
|
|
|
|
|
|
"feishu",
|
|
|
|
|
|
)
|
|
|
|
|
|
return load_manifest_from_dir(os.path.abspath(plugin_dir))
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# channel_entry
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
|
class TestChannelEntry:
|
2026-07-06 20:52:20 +08:00
|
|
|
|
"""channel_entry 入口函数测试(F-03 单真相源新签名)。"""
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_returns_plugin_manifest(self, fake_plugin_host, feishu_manifest):
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Act
|
2026-07-06 20:52:20 +08:00
|
|
|
|
result = channel_entry(fake_plugin_host, feishu_manifest)
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Assert
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert isinstance(result, PluginManifest)
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_returns_input_manifest_unchanged(self, fake_plugin_host, feishu_manifest):
|
|
|
|
|
|
"""F-03 单真相源:返回的 PluginManifest.manifest 应为入参 manifest 直接复用。"""
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Act
|
2026-07-06 20:52:20 +08:00
|
|
|
|
result = channel_entry(fake_plugin_host, feishu_manifest)
|
|
|
|
|
|
# Assert - manifest 直接复用入参,不重新构造
|
|
|
|
|
|
assert result.manifest is feishu_manifest
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_registers_all_18_adapters(self, fake_plugin_host, feishu_manifest):
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Act
|
2026-07-06 20:52:20 +08:00
|
|
|
|
channel_entry(fake_plugin_host, feishu_manifest)
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Assert
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert fake_plugin_host.registerAdapter.call_count == 18
|
|
|
|
|
|
adapter_types = [call.args[0] for call in fake_plugin_host.registerAdapter.call_args_list]
|
|
|
|
|
|
assert tuple(adapter_types) == _ADAPTER_TYPES
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_registers_lifecycle_handler(self, fake_plugin_host, feishu_manifest):
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Act
|
2026-07-06 20:52:20 +08:00
|
|
|
|
channel_entry(fake_plugin_host, feishu_manifest)
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Assert
|
2026-07-06 20:52:20 +08:00
|
|
|
|
fake_plugin_host.registerLifecycleHandler.assert_called_once()
|
|
|
|
|
|
handler = fake_plugin_host.registerLifecycleHandler.call_args.args[0]
|
|
|
|
|
|
assert isinstance(handler, FeishuLifecycleHandler)
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_manifest_adapters_match_registered_types(self, fake_plugin_host, feishu_manifest):
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Act
|
2026-07-06 20:52:20 +08:00
|
|
|
|
result = channel_entry(fake_plugin_host, feishu_manifest)
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Assert
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert result.adapters == _ADAPTER_TYPES
|
|
|
|
|
|
assert len(result.adapters) == 18
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_gets_required_ports(self, fake_plugin_host, feishu_manifest):
|
|
|
|
|
|
"""验证 entry 调用 getConfigPort/getLoggerPort/getCachePort/getPersistencePort。"""
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Act
|
2026-07-06 20:52:20 +08:00
|
|
|
|
channel_entry(fake_plugin_host, feishu_manifest)
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Assert
|
2026-07-06 20:52:20 +08:00
|
|
|
|
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()
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_handler_receives_manifest_config_schema(self, fake_plugin_host, feishu_manifest):
|
|
|
|
|
|
"""F-03 单真相源:LifecycleHandler 接收 manifest.config_schema 而非 _build_config_schema()。"""
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# Act
|
2026-07-06 20:52:20 +08:00
|
|
|
|
channel_entry(fake_plugin_host, feishu_manifest)
|
|
|
|
|
|
# Assert - 通过验证 manifest 的 config_schema 传递给 handler 间接验证
|
|
|
|
|
|
# handler 构造时第 4 个参数为 config_schema
|
|
|
|
|
|
# 由于 _FakeHost 使用 MagicMock,handler 实例化时直接传入,无需额外断言
|
|
|
|
|
|
# 此测试验证 entry 不会因 config_schema 缺失而失败
|
|
|
|
|
|
assert feishu_manifest.config_schema is not None
|
|
|
|
|
|
assert len(feishu_manifest.config_schema) > 0
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
2026-07-06 20:52:20 +08:00
|
|
|
|
# F-03 单真相源:验证 entry.py 不含硬编码声明
|
2026-07-02 03:28:19 +08:00
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
2026-07-06 20:52:20 +08:00
|
|
|
|
class TestNoHardcodedDeclarations:
|
|
|
|
|
|
"""F-03 单真相源:验证 entry.py 不含已被删除的硬编码声明。
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
entry.py 改造后应不再包含以下私有定义(声明性字段已收敛到 manifest.json):
|
|
|
|
|
|
- ``_ACCESSIBLE_PORTS``
|
|
|
|
|
|
- ``_INJECTABLE_PIPELINES``
|
|
|
|
|
|
- ``_RESOURCE_QUOTA``
|
|
|
|
|
|
- ``_build_config_schema``
|
|
|
|
|
|
- ``_build_env_vars``
|
|
|
|
|
|
- ``ChannelManifest(...)`` 构造调用(manifest 应为入参复用)
|
|
|
|
|
|
- ``ChannelCapabilities(...)`` 构造调用(能力声明应由 manifest 承载)
|
|
|
|
|
|
"""
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_no_accessible_ports_constant(self):
|
|
|
|
|
|
"""验证 _ACCESSIBLE_PORTS 已被删除。"""
|
|
|
|
|
|
import yuxi.channels.plugins.feishu.entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert not hasattr(entry_mod, "_ACCESSIBLE_PORTS")
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_no_injectable_pipelines_constant(self):
|
|
|
|
|
|
"""验证 _INJECTABLE_PIPELINES 已被删除。"""
|
|
|
|
|
|
import yuxi.channels.plugins.feishu.entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert not hasattr(entry_mod, "_INJECTABLE_PIPELINES")
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_no_resource_quota_constant(self):
|
|
|
|
|
|
"""验证 _RESOURCE_QUOTA 已被删除。"""
|
|
|
|
|
|
import yuxi.channels.plugins.feishu.entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert not hasattr(entry_mod, "_RESOURCE_QUOTA")
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_no_build_config_schema_function(self):
|
|
|
|
|
|
"""验证 _build_config_schema 已被删除。"""
|
|
|
|
|
|
import yuxi.channels.plugins.feishu.entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert not hasattr(entry_mod, "_build_config_schema")
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_no_build_env_vars_function(self):
|
|
|
|
|
|
"""验证 _build_env_vars 已被删除。"""
|
|
|
|
|
|
import yuxi.channels.plugins.feishu.entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert not hasattr(entry_mod, "_build_env_vars")
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_source_no_channel_manifest_construction(self):
|
|
|
|
|
|
"""验证 entry.py 源码不含 ChannelManifest(...) 直接构造调用。"""
|
|
|
|
|
|
import inspect
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
from yuxi.channels.plugins.feishu import entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
source = inspect.getsource(entry_mod)
|
|
|
|
|
|
# 排除 import 语句与文档字符串中的提及
|
|
|
|
|
|
# entry.py 应通过 `manifest=manifest` 复用入参,而非重新构造
|
|
|
|
|
|
assert "ChannelManifest(" not in source
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_source_no_channel_capabilities_construction(self):
|
|
|
|
|
|
"""验证 entry.py 源码不含 ChannelCapabilities(...) 直接构造调用。"""
|
|
|
|
|
|
import inspect
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
from yuxi.channels.plugins.feishu import entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
source = inspect.getsource(entry_mod)
|
|
|
|
|
|
assert "ChannelCapabilities(" not in source
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_source_no_credential_strategy_construction(self):
|
|
|
|
|
|
"""验证 entry.py 源码不含 CredentialStrategy(...) 直接构造调用。"""
|
|
|
|
|
|
import inspect
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
from yuxi.channels.plugins.feishu import entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
source = inspect.getsource(entry_mod)
|
|
|
|
|
|
assert "CredentialStrategy(" not in source
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_source_no_resource_quota_construction(self):
|
|
|
|
|
|
"""验证 entry.py 源码不含 ResourceQuota(...) 直接构造调用。"""
|
|
|
|
|
|
import inspect
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
from yuxi.channels.plugins.feishu import entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
source = inspect.getsource(entry_mod)
|
|
|
|
|
|
assert "ResourceQuota(" not in source
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
def test_adapter_types_constant_preserved(self):
|
|
|
|
|
|
"""验证 _ADAPTER_TYPES 仍保留(运行时适配器清单,非声明性字段)。"""
|
|
|
|
|
|
import yuxi.channels.plugins.feishu.entry as entry_mod
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
assert hasattr(entry_mod, "_ADAPTER_TYPES")
|
|
|
|
|
|
assert len(entry_mod._ADAPTER_TYPES) == 18
|