ForcePilot/backend/test/unit/channels/plugins/dingtalk/test_manifest.py
Kris 532e7c59ec test: add and fix multi-channel unit tests
1. 新增QQBot、Wecom、Dingtalk、Instagram、WechatMp等渠道的单元测试夹具与测试用例
2. 修复WechatILink的缓存键顺序与测试用例
3. 补全Feishu适配器的日志断言与测试逻辑
4. 清理WechatILink冗余的clear_context_token测试代码
2026-07-08 22:59:24 +08:00

237 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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=falseentry.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] == ()