"""feishu 插件 manifest.json 一致性测试。 验证 manifest.json 经 A1-A4 修正后的关键不变量: - A1:``capabilities.status`` / ``capabilities.probeable`` 为 ``true``, 与 entry.py 注册的 StatusAdapter / ProbeableAdapter 一致。 - A2:``transport_mode`` 为 ``"stream"``,与 feishu 仅注册 StreamConnector (无 Puller)的实际传输模式一致。 - A3:``config_schema`` 含 ``ws_endpoint_path`` 配置项(stream_connector 实际读取),不含死字段 ``ws_endpoint``。 - A4:``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 feishu_manifest() -> ChannelManifest: """加载 feishu 插件真实 manifest.json。""" plugin_dir = os.path.join( os.path.dirname(__file__), "..", "..", "..", "..", "..", "package", "yuxi", "channels", "plugins", "feishu", ) return load_manifest_from_dir(os.path.abspath(plugin_dir)) # ------------------------------------------------------------------ # A1: status / probeable 能力声明与适配器注册一致 # ------------------------------------------------------------------ @pytest.mark.unit class TestStatusProbeableCapabilities: """A1:status / probeable 声明为 true,与 entry.py 注册的适配器一致。""" def test_status_capability_is_true(self, feishu_manifest): assert feishu_manifest.capabilities.status is True def test_probeable_capability_is_true(self, feishu_manifest): assert feishu_manifest.capabilities.probeable is True # ------------------------------------------------------------------ # A2: transport_mode 声明 # ------------------------------------------------------------------ @pytest.mark.unit class TestTransportMode: """A2:transport_mode 为 stream,与仅注册 StreamConnector 一致。""" def test_transport_mode_is_stream(self, feishu_manifest): assert feishu_manifest.transport_mode == "stream" # ------------------------------------------------------------------ # A3: ws_endpoint_path 配置项存在,ws_endpoint 死字段已移除 # ------------------------------------------------------------------ @pytest.mark.unit class TestWsEndpointConfigKey: """A3:config_schema 含 ws_endpoint_path,不含死字段 ws_endpoint。""" def test_ws_endpoint_path_exists_in_config_schema(self, feishu_manifest): keys = {f.key for f in feishu_manifest.config_schema} assert "ws_endpoint_path" in keys def test_ws_endpoint_dead_field_removed(self, feishu_manifest): keys = {f.key for f in feishu_manifest.config_schema} assert "ws_endpoint" not in keys def test_ws_endpoint_path_has_default_value(self, feishu_manifest): """ws_endpoint_path 默认值应为飞书标准 WS 端点路径。""" field = next(f for f in feishu_manifest.config_schema if f.key == "ws_endpoint_path") assert field.default == "/open-apis/event/v1/get_ws_endpoint" # ------------------------------------------------------------------ # A4: capability_requirements 引用的配置键均在 config_schema 中声明 # ------------------------------------------------------------------ @pytest.mark.unit class TestCapabilityRequirementsConsistency: """A4:capability_requirements 交叉引用完整性校验。 每项 capability_requirements 为 ``(能力字段名, (所需配置键清单))``, 所有引用的配置键必须在 config_schema 中声明(F-03 单真相源)。 """ def test_capability_requirements_not_empty(self, feishu_manifest): assert len(feishu_manifest.capability_requirements) > 0 def test_all_referenced_config_keys_exist_in_schema(self, feishu_manifest): schema_keys = {f.key for f in feishu_manifest.config_schema} for _capability, required_keys in feishu_manifest.capability_requirements: for key in required_keys: assert key in schema_keys, f"capability_requirements 引用的配置键 '{key}' 未在 config_schema 中声明" def test_qr_login_requirements_reference_qr_redirect_uri(self, feishu_manifest): """supports_qr_login 依赖 enable_qr_login 与 qr_redirect_uri (与 login_adapter.py 实际读取的配置键一致)。""" req = dict(feishu_manifest.capability_requirements) assert "supports_qr_login" in req required = set(req["supports_qr_login"]) assert "enable_qr_login" in required assert "qr_redirect_uri" in required def test_media_capabilities_require_app_credentials(self, feishu_manifest): """媒体能力(图片/视频出入站)依赖 app_id 与 app_secret。""" req = dict(feishu_manifest.capability_requirements) for cap in ( "supports_image_inbound", "supports_image_outbound", "supports_video_outbound", ): assert cap in req required = set(req[cap]) assert "app_id" in required assert "app_secret" in required def test_lifecycle_probeable_status_have_empty_requirements(self, feishu_manifest): """lifecycle / probeable / status 为基础能力,无额外配置依赖。""" req = dict(feishu_manifest.capability_requirements) for cap in ("lifecycle", "probeable", "status"): assert cap in req assert req[cap] == () # ------------------------------------------------------------------ # F-03: qr_redirect_uri 配置键一致性(login_adapter 修复验证) # ------------------------------------------------------------------ @pytest.mark.unit class TestQrRedirectUriConfigKey: """验证 qr_redirect_uri 配置键在 manifest 中声明(login_adapter 读取此键)。 login_adapter.py 原 read ``qr_login_redirect_uri``(manifest 未声明), 已修正为 ``qr_redirect_uri``(与 manifest 及 wizard_adapter 一致)。 """ def test_qr_redirect_uri_in_config_schema(self, feishu_manifest): keys = {f.key for f in feishu_manifest.config_schema} assert "qr_redirect_uri" in keys def test_qr_login_redirect_uri_not_in_config_schema(self, feishu_manifest): """死键 qr_login_redirect_uri 不应出现在 config_schema。""" keys = {f.key for f in feishu_manifest.config_schema} assert "qr_login_redirect_uri" not in keys