1. 新增QQBot、Wecom、Dingtalk、Instagram、WechatMp等渠道的单元测试夹具与测试用例 2. 修复WechatILink的缓存键顺序与测试用例 3. 补全Feishu适配器的日志断言与测试逻辑 4. 清理WechatILink冗余的clear_context_token测试代码
259 lines
10 KiB
Python
259 lines
10 KiB
Python
"""qqbot 插件 _constants.py 模块测试。
|
||
|
||
验证共享常量模块的正确性:
|
||
- 模块导出完整(``__all__`` 与实际定义一致)。
|
||
- 常量值与 ``manifest.json`` 中对应字段对齐(F-03 单真相源):
|
||
- ``CHANNEL_TARGET`` 与 manifest ``channel_type`` 一致。
|
||
- ``HTTP_TIMEOUT_SECONDS`` 与 manifest ``http_timeout_ms.default`` 对齐。
|
||
- ``HTTP_MAX_CONNECTIONS`` 与 manifest ``resource_quota.max_connections`` 对齐。
|
||
- ``WS_HEARTBEAT_INTERVAL_MS`` 与 manifest ``ws_heartbeat_interval_ms.default`` 对齐。
|
||
- ``DEFAULT_INTENTS`` 与 manifest ``intents.default`` 对齐。
|
||
- ``MAX_MESSAGE_LENGTH`` 与 manifest ``max_message_length`` 对齐。
|
||
|
||
_constants.py 作为 ConfigPort 未命中时的本地回退默认值,其值必须与
|
||
manifest 声明保持同步,避免双源不一致。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
import pytest
|
||
from yuxi.channels.application.lifecycle.manifest_loader import load_manifest_from_dir
|
||
from yuxi.channels.plugins.qqbot import _constants
|
||
|
||
pytestmark = pytest.mark.unit
|
||
|
||
|
||
@pytest.fixture
|
||
def qqbot_manifest():
|
||
"""加载 qqbot 插件真实 manifest.json。"""
|
||
plugin_dir = os.path.join(
|
||
os.path.dirname(__file__),
|
||
"..",
|
||
"..",
|
||
"..",
|
||
"..",
|
||
"..",
|
||
"package",
|
||
"yuxi",
|
||
"channels",
|
||
"plugins",
|
||
"qqbot",
|
||
)
|
||
return load_manifest_from_dir(os.path.abspath(plugin_dir))
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# 模块导出完整性
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConstantsExports:
|
||
"""验证 _constants.py 模块导出完整。"""
|
||
|
||
def test_all_contains_channel_target(self):
|
||
assert "CHANNEL_TARGET" in _constants.__all__
|
||
|
||
def test_all_contains_qqbot_api_dep(self):
|
||
assert "QQBOT_API_DEP" in _constants.__all__
|
||
|
||
def test_all_contains_http_constants(self):
|
||
assert "HTTP_TIMEOUT_SECONDS" in _constants.__all__
|
||
assert "HTTP_MAX_CONNECTIONS" in _constants.__all__
|
||
assert "HTTP_MAX_KEEPALIVE_CONNECTIONS" in _constants.__all__
|
||
|
||
def test_all_contains_ws_constants(self):
|
||
assert "WS_HEARTBEAT_INTERVAL_MS" in _constants.__all__
|
||
assert "WS_STALL_TIMEOUT_MS" in _constants.__all__
|
||
|
||
def test_all_contains_cache_constants(self):
|
||
assert "USER_CACHE_TTL" in _constants.__all__
|
||
assert "IDENTITY_CACHE_TTL" in _constants.__all__
|
||
assert "TOKEN_DEFAULT_TTL" in _constants.__all__
|
||
|
||
def test_all_matches_defined_names(self):
|
||
"""``__all__`` 应恰好包含模块中定义的全部公开常量。"""
|
||
expected = {
|
||
"CHANNEL_TARGET",
|
||
"QQBOT_API_DEP",
|
||
"API_BASE_URL",
|
||
"TOKEN_ENDPOINT_PATH",
|
||
"GATEWAY_ENDPOINT_PATH",
|
||
"USERS_ENDPOINT_PREFIX",
|
||
"C2C_MESSAGES_PREFIX",
|
||
"GROUP_MESSAGES_PREFIX",
|
||
"MESSAGES_SUFFIX",
|
||
"FILES_SUFFIX",
|
||
"INTERACTIONS_PREFIX",
|
||
"HTTP_TIMEOUT_SECONDS",
|
||
"HTTP_MAX_CONNECTIONS",
|
||
"HTTP_MAX_KEEPALIVE_CONNECTIONS",
|
||
"WS_HEARTBEAT_INTERVAL_MS",
|
||
"WS_STALL_TIMEOUT_MS",
|
||
"CONNECT_TIMEOUT_MS",
|
||
"TOKEN_REFRESH_LEAD_TIME_SEC",
|
||
"TOKEN_DEFAULT_TTL",
|
||
"USER_CACHE_TTL",
|
||
"IDENTITY_CACHE_TTL",
|
||
"MSG_CACHE_TTL_SECONDS",
|
||
"DEFAULT_INTENTS",
|
||
"PASSIVE_REPLY_LIMIT_C2C",
|
||
"PASSIVE_REPLY_LIMIT_GROUP",
|
||
"PASSIVE_REPLY_TTL_C2C_MS",
|
||
"PASSIVE_REPLY_TTL_GROUP_MS",
|
||
"GROUP_ACTIVE_QPM_ACCOUNT_VERIFIED",
|
||
"GROUP_ACTIVE_QPM_ACCOUNT_UNVERIFIED",
|
||
"GROUP_ACTIVE_QPM_PER_GROUP",
|
||
"RECALL_TIME_LIMIT_SECONDS",
|
||
"INTERACTION_ACK_TIMEOUT_SECONDS",
|
||
"MAX_MESSAGE_LENGTH",
|
||
}
|
||
assert set(_constants.__all__) == expected
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# 常量值与 manifest 对齐(F-03 单真相源)
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConstantsAlignWithManifest:
|
||
"""验证 _constants.py 常量值与 manifest.json 声明对齐。"""
|
||
|
||
def test_channel_target_matches_manifest_channel_type(self, qqbot_manifest):
|
||
assert _constants.CHANNEL_TARGET == qqbot_manifest.channel_type
|
||
|
||
def test_channel_target_matches_manifest_provides(self, qqbot_manifest):
|
||
"""CHANNEL_TARGET 也应出现在 manifest provides 中。"""
|
||
assert _constants.CHANNEL_TARGET in qqbot_manifest.provides
|
||
|
||
def test_http_timeout_aligns_with_http_timeout_ms(self, qqbot_manifest):
|
||
"""HTTP_TIMEOUT_SECONDS(秒)与 manifest http_timeout_ms.default(毫秒)对齐。"""
|
||
field = next(f for f in qqbot_manifest.config_schema if f.key == "http_timeout_ms")
|
||
assert _constants.HTTP_TIMEOUT_SECONDS == field.default / 1000.0
|
||
|
||
def test_http_max_connections_aligns_with_resource_quota(self, qqbot_manifest):
|
||
"""HTTP_MAX_CONNECTIONS 与 manifest resource_quota.max_connections 对齐。"""
|
||
assert qqbot_manifest.resource_quota is not None
|
||
assert _constants.HTTP_MAX_CONNECTIONS == qqbot_manifest.resource_quota.max_connections
|
||
|
||
def test_ws_heartbeat_aligns_with_ws_heartbeat_interval_ms(self, qqbot_manifest):
|
||
"""WS_HEARTBEAT_INTERVAL_MS 与 manifest ws_heartbeat_interval_ms.default 对齐。"""
|
||
field = next(f for f in qqbot_manifest.config_schema if f.key == "ws_heartbeat_interval_ms")
|
||
assert _constants.WS_HEARTBEAT_INTERVAL_MS == field.default
|
||
|
||
def test_ws_stall_timeout_aligns_with_ws_stall_timeout_ms(self, qqbot_manifest):
|
||
"""WS_STALL_TIMEOUT_MS 与 manifest ws_stall_timeout_ms.default 对齐。"""
|
||
field = next(f for f in qqbot_manifest.config_schema if f.key == "ws_stall_timeout_ms")
|
||
assert _constants.WS_STALL_TIMEOUT_MS == field.default
|
||
|
||
def test_default_intents_aligns_with_intents_default(self, qqbot_manifest):
|
||
"""DEFAULT_INTENTS 与 manifest intents.default 对齐。"""
|
||
field = next(f for f in qqbot_manifest.config_schema if f.key == "intents")
|
||
assert _constants.DEFAULT_INTENTS == field.default
|
||
|
||
def test_max_message_length_aligns_with_manifest(self, qqbot_manifest):
|
||
"""MAX_MESSAGE_LENGTH 与 manifest max_message_length 对齐。"""
|
||
assert _constants.MAX_MESSAGE_LENGTH == qqbot_manifest.max_message_length
|
||
|
||
def test_user_cache_ttl_aligns_with_user_cache_ttl(self, qqbot_manifest):
|
||
"""USER_CACHE_TTL 与 manifest user_cache_ttl.default 对齐。"""
|
||
field = next(f for f in qqbot_manifest.config_schema if f.key == "user_cache_ttl")
|
||
assert _constants.USER_CACHE_TTL == field.default
|
||
|
||
def test_token_default_ttl_aligns_with_token_cache_ttl(self, qqbot_manifest):
|
||
"""TOKEN_DEFAULT_TTL 与 manifest token_cache_ttl.default 对齐。"""
|
||
field = next(f for f in qqbot_manifest.config_schema if f.key == "token_cache_ttl")
|
||
assert _constants.TOKEN_DEFAULT_TTL == field.default
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# 常量值合理性
|
||
# ------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestConstantsSanity:
|
||
"""验证 _constants.py 常量值的基本合理性。"""
|
||
|
||
def test_channel_target_is_qqbot(self):
|
||
assert _constants.CHANNEL_TARGET == "qqbot"
|
||
|
||
def test_qqbot_api_dep_is_string(self):
|
||
assert isinstance(_constants.QQBOT_API_DEP, str)
|
||
assert _constants.QQBOT_API_DEP == "qqbot_api"
|
||
|
||
def test_api_base_url_starts_with_https(self):
|
||
assert _constants.API_BASE_URL.startswith("https://")
|
||
|
||
def test_http_timeout_positive(self):
|
||
assert _constants.HTTP_TIMEOUT_SECONDS > 0
|
||
|
||
def test_http_max_connections_positive(self):
|
||
assert _constants.HTTP_MAX_CONNECTIONS > 0
|
||
|
||
def test_http_keepalive_le_max_connections(self):
|
||
"""keepalive 连接数上限不应超过最大连接数。"""
|
||
assert _constants.HTTP_MAX_KEEPALIVE_CONNECTIONS <= _constants.HTTP_MAX_CONNECTIONS
|
||
|
||
def test_ws_heartbeat_positive(self):
|
||
assert _constants.WS_HEARTBEAT_INTERVAL_MS > 0
|
||
|
||
def test_ws_stall_timeout_gt_heartbeat(self):
|
||
"""卡死检测超时应大于心跳间隔。"""
|
||
assert _constants.WS_STALL_TIMEOUT_MS > _constants.WS_HEARTBEAT_INTERVAL_MS
|
||
|
||
def test_default_intents_positive(self):
|
||
assert _constants.DEFAULT_INTENTS > 0
|
||
|
||
def test_token_default_ttl_positive(self):
|
||
assert _constants.TOKEN_DEFAULT_TTL > 0
|
||
|
||
def test_token_refresh_lead_time_positive(self):
|
||
assert _constants.TOKEN_REFRESH_LEAD_TIME_SEC > 0
|
||
|
||
def test_token_refresh_lead_time_lt_default_ttl(self):
|
||
"""刷新提前量应小于默认 TTL。"""
|
||
assert _constants.TOKEN_REFRESH_LEAD_TIME_SEC < _constants.TOKEN_DEFAULT_TTL
|
||
|
||
def test_user_cache_ttl_positive(self):
|
||
assert _constants.USER_CACHE_TTL > 0
|
||
|
||
def test_identity_cache_ttl_positive(self):
|
||
assert _constants.IDENTITY_CACHE_TTL > 0
|
||
|
||
def test_identity_cache_ttl_le_user_cache_ttl(self):
|
||
"""身份解析缓存 TTL 应小于等于用户缓存 TTL。"""
|
||
assert _constants.IDENTITY_CACHE_TTL <= _constants.USER_CACHE_TTL
|
||
|
||
def test_max_message_length_positive(self):
|
||
assert _constants.MAX_MESSAGE_LENGTH > 0
|
||
|
||
def test_msg_cache_ttl_positive(self):
|
||
assert _constants.MSG_CACHE_TTL_SECONDS > 0
|
||
|
||
def test_recall_time_limit_positive(self):
|
||
assert _constants.RECALL_TIME_LIMIT_SECONDS > 0
|
||
|
||
def test_interaction_ack_timeout_positive(self):
|
||
assert _constants.INTERACTION_ACK_TIMEOUT_SECONDS > 0
|
||
|
||
def test_passive_reply_limits_positive(self):
|
||
assert _constants.PASSIVE_REPLY_LIMIT_C2C > 0
|
||
assert _constants.PASSIVE_REPLY_LIMIT_GROUP > 0
|
||
|
||
def test_passive_reply_ttls_positive(self):
|
||
assert _constants.PASSIVE_REPLY_TTL_C2C_MS > 0
|
||
assert _constants.PASSIVE_REPLY_TTL_GROUP_MS > 0
|
||
|
||
def test_group_active_qpm_positive(self):
|
||
assert _constants.GROUP_ACTIVE_QPM_ACCOUNT_VERIFIED > 0
|
||
assert _constants.GROUP_ACTIVE_QPM_ACCOUNT_UNVERIFIED > 0
|
||
assert _constants.GROUP_ACTIVE_QPM_PER_GROUP > 0
|
||
|
||
def test_group_active_qpm_verified_gt_unverified(self):
|
||
"""认证账号 QPM 应大于等于未认证账号 QPM。"""
|
||
assert _constants.GROUP_ACTIVE_QPM_ACCOUNT_VERIFIED >= _constants.GROUP_ACTIVE_QPM_ACCOUNT_UNVERIFIED
|