ForcePilot/backend/test/unit/channels/plugins/dingtalk/test_manifest.py

237 lines
9.1 KiB
Python
Raw Normal View History

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