1. 新增QQBot、Wecom、Dingtalk、Instagram、WechatMp等渠道的单元测试夹具与测试用例 2. 修复WechatILink的缓存键顺序与测试用例 3. 补全Feishu适配器的日志断言与测试逻辑 4. 清理WechatILink冗余的clear_context_token测试代码
237 lines
9.1 KiB
Python
237 lines
9.1 KiB
Python
"""dingtalk 插件 manifest.json 一致性测试。
|
||
|
||
验证 manifest.json 关键不变量:
|
||
- 标识字段(id / channel_type / provides)一致。
|
||
- capabilities 关键能力声明与 entry.py 注册的适配器一致。
|
||
- transport_mode 为 stream,与仅注册 StreamConnector 的传输模式一致。
|
||
- failure_policy 为 degrade。
|
||
- max_message_length 为 5000。
|
||
- accessible_ports 含 ConfigPort / CachePort / LoggerPort 等基础端口。
|
||
- config_schema 含 client_id / client_secret / robot_code 等必填项。
|
||
- capability_requirements 中引用的所有配置键均在 config_schema 中声明
|
||
(F-03 单真相源:声明性字段交叉引用完整)。
|
||
|
||
测试通过 ``load_manifest_from_dir`` 加载真实 ``manifest.json``,确保校验
|
||
基于实际文件内容而非硬编码预期值。
|
||
"""
|
||
|
||
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
|
||
|
||
pytestmark = pytest.mark.unit
|
||
|
||
|
||
@pytest.fixture
|
||
def dingtalk_manifest() -> ChannelManifest:
|
||
"""加载 dingtalk 插件真实 manifest.json。"""
|
||
plugin_dir = os.path.join(
|
||
os.path.dirname(__file__),
|
||
"..",
|
||
"..",
|
||
"..",
|
||
"..",
|
||
"..",
|
||
"package",
|
||
"yuxi",
|
||
"channels",
|
||
"plugins",
|
||
"dingtalk",
|
||
)
|
||
return load_manifest_from_dir(os.path.abspath(plugin_dir))
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# 标识字段一致性
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestManifestIdentity:
|
||
"""manifest 标识字段(id / channel_type / provides)一致性。"""
|
||
|
||
def test_id_is_com_yuxi_channels_dingtalk(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.id == "com.yuxi.channels.dingtalk"
|
||
|
||
def test_channel_type_is_dingtalk(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.channel_type == "dingtalk"
|
||
|
||
def test_provides_contains_dingtalk(self, dingtalk_manifest):
|
||
assert "dingtalk" in dingtalk_manifest.provides
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# capabilities 关键能力声明
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestCapabilities:
|
||
"""capabilities 关键能力声明与 entry.py 注册的适配器一致。"""
|
||
|
||
def test_rich_message_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.rich_message is True
|
||
|
||
def test_streaming_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.streaming is True
|
||
|
||
def test_message_recall_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.message_recall is True
|
||
|
||
def test_typing_indicator_is_false(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.typing_indicator is False
|
||
|
||
def test_supports_card_update_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.supports_card_update is True
|
||
|
||
def test_supports_card_update_streaming_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.supports_card_update_streaming is True
|
||
|
||
def test_identity_resolver_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.identity_resolver is True
|
||
|
||
def test_lifecycle_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.lifecycle is True
|
||
|
||
def test_status_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.status is True
|
||
|
||
def test_probeable_is_true(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.capabilities.probeable is True
|
||
|
||
def test_supports_qr_login_is_false(self, dingtalk_manifest):
|
||
"""supports_qr_login=false,entry.py 不注册 login_adapter。"""
|
||
assert dingtalk_manifest.capabilities.supports_qr_login is False
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# transport_mode / failure_policy / max_message_length
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestTransportAndPolicy:
|
||
"""transport_mode / failure_policy / max_message_length 声明。"""
|
||
|
||
def test_transport_mode_is_stream(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.transport_mode == "stream"
|
||
|
||
def test_failure_policy_is_degrade(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.failure_policy == "degrade"
|
||
|
||
def test_max_message_length_is_5000(self, dingtalk_manifest):
|
||
assert dingtalk_manifest.max_message_length == 5000
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# accessible_ports
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestAccessiblePorts:
|
||
"""accessible_ports 含基础端口与钉钉所需端口。"""
|
||
|
||
def test_contains_config_port(self, dingtalk_manifest):
|
||
assert "ConfigPort" in dingtalk_manifest.accessible_ports
|
||
|
||
def test_contains_cache_port(self, dingtalk_manifest):
|
||
assert "CachePort" in dingtalk_manifest.accessible_ports
|
||
|
||
def test_contains_logger_port(self, dingtalk_manifest):
|
||
assert "LoggerPort" in dingtalk_manifest.accessible_ports
|
||
|
||
def test_contains_persistence_port(self, dingtalk_manifest):
|
||
assert "PersistencePort" in dingtalk_manifest.accessible_ports
|
||
|
||
def test_contains_tracer_port(self, dingtalk_manifest):
|
||
assert "TracerPort" in dingtalk_manifest.accessible_ports
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# config_schema 必填项
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConfigSchema:
|
||
"""config_schema 含钉钉必填配置项。"""
|
||
|
||
def test_contains_client_id(self, dingtalk_manifest):
|
||
keys = {f.key for f in dingtalk_manifest.config_schema}
|
||
assert "client_id" in keys
|
||
|
||
def test_contains_client_secret(self, dingtalk_manifest):
|
||
keys = {f.key for f in dingtalk_manifest.config_schema}
|
||
assert "client_secret" in keys
|
||
|
||
def test_contains_robot_code(self, dingtalk_manifest):
|
||
keys = {f.key for f in dingtalk_manifest.config_schema}
|
||
assert "robot_code" in keys
|
||
|
||
def test_client_id_is_required(self, dingtalk_manifest):
|
||
field = next(f for f in dingtalk_manifest.config_schema if f.key == "client_id")
|
||
assert field.required is True
|
||
|
||
def test_client_secret_is_required(self, dingtalk_manifest):
|
||
field = next(f for f in dingtalk_manifest.config_schema if f.key == "client_secret")
|
||
assert field.required is True
|
||
|
||
def test_robot_code_is_required(self, dingtalk_manifest):
|
||
field = next(f for f in dingtalk_manifest.config_schema if f.key == "robot_code")
|
||
assert field.required is True
|
||
|
||
def test_stream_endpoint_path_in_config_schema(self, dingtalk_manifest):
|
||
"""stream_connector 读取 stream_endpoint_path,应在 config_schema 声明。"""
|
||
keys = {f.key for f in dingtalk_manifest.config_schema}
|
||
assert "stream_endpoint_path" in keys
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# capability_requirements 交叉引用完整性
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestCapabilityRequirementsConsistency:
|
||
"""capability_requirements 交叉引用完整性校验。
|
||
|
||
每项 capability_requirements 为 ``(能力字段名, (所需配置键清单))``,
|
||
所有引用的配置键必须在 config_schema 中声明(F-03 单真相源)。
|
||
"""
|
||
|
||
def test_capability_requirements_not_empty(self, dingtalk_manifest):
|
||
assert len(dingtalk_manifest.capability_requirements) > 0
|
||
|
||
def test_all_referenced_config_keys_exist_in_schema(self, dingtalk_manifest):
|
||
schema_keys = {f.key for f in dingtalk_manifest.config_schema}
|
||
for _capability, required_keys in dingtalk_manifest.capability_requirements:
|
||
for key in required_keys:
|
||
assert key in schema_keys, f"capability_requirements 引用的配置键 '{key}' 未在 config_schema 中声明"
|
||
|
||
def test_rich_message_requires_app_credentials(self, dingtalk_manifest):
|
||
"""rich_message 依赖 client_id / client_secret / robot_code。"""
|
||
req = dict(dingtalk_manifest.capability_requirements)
|
||
assert "rich_message" in req
|
||
required = set(req["rich_message"])
|
||
assert "client_id" in required
|
||
assert "client_secret" in required
|
||
assert "robot_code" in required
|
||
|
||
def test_streaming_requires_app_credentials(self, dingtalk_manifest):
|
||
req = dict(dingtalk_manifest.capability_requirements)
|
||
assert "streaming" in req
|
||
required = set(req["streaming"])
|
||
assert {"client_id", "client_secret", "robot_code"} <= required
|
||
|
||
def test_lifecycle_probeable_status_have_empty_requirements(self, dingtalk_manifest):
|
||
"""lifecycle / probeable / status 为基础能力,无额外配置依赖。"""
|
||
req = dict(dingtalk_manifest.capability_requirements)
|
||
for cap in ("lifecycle", "probeable", "status"):
|
||
assert cap in req
|
||
assert req[cap] == ()
|