ForcePilot/backend/test/unit/channels/test_channels_nostr_analysis.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

265 lines
10 KiB
Python

from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.nostr.config import NostrConfig, NostrConfigError
from yuxi.channels.adapters.nostr.crypto import NostrCryptoError
from yuxi.channels.adapters.nostr.probe import ProbeResult
class TestNostrConfigValidation:
def test_valid_config_passes_validation(self):
cfg = NostrConfig(
private_key="nsec1test",
relays=["wss://test.relay"],
dm_policy="pairing",
streaming_mode="block",
)
assert cfg.dm_policy == "pairing"
def test_invalid_dm_policy_raises_error(self):
with pytest.raises(NostrConfigError, match="dm_policy 值无效"):
NostrConfig(dm_policy="invalid_policy")
def test_invalid_streaming_mode_raises_error(self):
with pytest.raises(NostrConfigError, match="streaming_mode 值无效"):
NostrConfig(streaming_mode="invalid_mode")
def test_invalid_reconnect_interval_raises_error(self):
with pytest.raises(NostrConfigError, match="reconnect_interval_sec"):
NostrConfig(reconnect_interval_sec=0)
def test_invalid_relay_timeout_raises_error(self):
with pytest.raises(NostrConfigError, match="relay_timeout_sec"):
NostrConfig(relay_timeout_sec=0)
def test_invalid_degraded_threshold_raises_error(self):
with pytest.raises(NostrConfigError, match="relay_degraded_threshold"):
NostrConfig(relay_degraded_threshold=0)
def test_invalid_degraded_threshold_above_one_raises_error(self):
with pytest.raises(NostrConfigError, match="relay_degraded_threshold"):
NostrConfig(relay_degraded_threshold=1.5)
def test_invalid_relay_url_raises_error(self):
with pytest.raises(NostrConfigError, match="Relay URL 必须以 ws:// 或 wss:// 开头"):
NostrConfig(relays=["http://invalid.relay"])
def test_all_valid_dm_policies_pass(self):
for policy in ("pairing", "open", "whitelist"):
cfg = NostrConfig(dm_policy=policy)
assert cfg.dm_policy == policy
def test_all_valid_streaming_modes_pass(self):
for mode in ("off", "block", "progress"):
cfg = NostrConfig(streaming_mode=mode)
assert cfg.streaming_mode == mode
def test_from_dict_with_invalid_config_still_validates(self):
with pytest.raises(NostrConfigError):
NostrConfig.from_dict({
"accounts": {"default": {"dm_policy": "bad"}}
})
def test_wss_relay_url_valid(self):
cfg = NostrConfig(relays=["wss://relay.example.com"])
assert len(cfg.relays) == 1
def test_ws_relay_url_valid(self):
cfg = NostrConfig(relays=["ws://relay.example.com"])
assert len(cfg.relays) == 1
class TestNostrCryptoDecryptNip17:
@pytest.mark.asyncio
async def test_decrypt_nip17_raises_on_none_unwrapped(self):
with patch(
"yuxi.channels.adapters.nostr.crypto.Event"
) as mock_event_class, patch(
"yuxi.channels.adapters.nostr.crypto.UnwrappedGift"
) as mock_gift_class:
mock_event = MagicMock()
mock_event_class.from_json.return_value = mock_event
mock_gift = MagicMock()
mock_gift.from_gift_wrap.return_value = None
mock_gift_class.from_gift_wrap.return_value = None
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
crypto = create_crypto_instance()
with pytest.raises(NostrCryptoError, match="Gift Wrap 解封返回空"):
await crypto.decrypt_nip17('{"id":"test"}')
@pytest.mark.asyncio
async def test_decrypt_nip17_raises_on_none_rumor(self):
with patch(
"yuxi.channels.adapters.nostr.crypto.Event"
) as mock_event_class, patch(
"yuxi.channels.adapters.nostr.crypto.UnwrappedGift"
) as mock_gift_class:
mock_event = MagicMock()
mock_event_class.from_json.return_value = mock_event
mock_gift = MagicMock()
mock_gift.from_gift_wrap.return_value = mock_gift
mock_gift_class.from_gift_wrap.return_value = mock_gift
mock_gift.rumor.return_value = None
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
crypto = create_crypto_instance()
with pytest.raises(NostrCryptoError, match="Rumor 为空"):
await crypto.decrypt_nip17('{"id":"test"}')
class TestProbeResult:
def test_probe_result_is_dataclass(self):
result = ProbeResult(url="wss://test.relay", connected=True, latency_ms=100.0)
assert result.url == "wss://test.relay"
assert result.connected is True
assert result.latency_ms == 100.0
assert result.error is None
def test_probe_result_with_error(self):
result = ProbeResult(
url="wss://bad.relay", connected=False, error="Connection refused"
)
assert result.connected is False
assert result.error == "Connection refused"
def test_probe_result_repr(self):
result = ProbeResult(url="wss://test.relay", connected=True, latency_ms=50.0)
repr_str = repr(result)
assert "ProbeResult" in repr_str
def create_crypto_instance():
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
with patch(
"yuxi.channels.adapters.nostr.crypto.Keys"
) as mock_keys_class:
mock_keys = MagicMock()
mock_public_key = MagicMock()
mock_public_key.to_bech32.return_value = "npub1test"
mock_public_key.to_hex.return_value = "abcdef"
mock_secret_key = MagicMock()
mock_secret_key.to_bech32.return_value = "nsec1test"
mock_keys.public_key.return_value = mock_public_key
mock_keys.secret_key.return_value = mock_secret_key
mock_keys_class.parse.return_value = mock_keys
return NostrCrypto("nsec1test")
class TestHealthCheckGuards:
@pytest.mark.asyncio
async def test_health_check_total_zero_returns_unhealthy(self):
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
adapter = NostrAdapter(config={"private_key": None, "relays": []})
adapter._nostr_config = NostrConfig(relays=[])
adapter._relay_manager = MagicMock()
adapter._relay_manager.active_count.return_value = (0, 0)
result = await adapter.health_check()
assert result.status == "unhealthy"
assert "没有 Relay" in result.last_error
@pytest.mark.asyncio
async def test_health_check_no_crypto_still_healthy(self):
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
adapter = NostrAdapter(config={"private_key": None, "relays": []})
adapter._nostr_config = NostrConfig(relays=["wss://test.relay"])
adapter._relay_manager = MagicMock()
adapter._relay_manager.active_count.return_value = (2, 3)
adapter._crypto = None
result = await adapter.health_check()
assert result.status == "healthy"
assert result.metadata["npub"] == ""
class TestAnalysisCoordinator:
@pytest.mark.asyncio
async def test_coordinator_generates_complete_report(self):
from yuxi.channels.adapters.nostr.analysis.coordinator import (
AnalysisCoordinator,
)
coordinator = AnalysisCoordinator("package/yuxi/channels/adapters/nostr")
report = await coordinator.run()
assert report.module_name == "NostrAdapter"
assert len(report.sub_agent_reports) == 3
assert report.sub_agent_reports[0].agent_id == "web_researcher"
assert report.sub_agent_reports[1].agent_id == "code_analyzer"
assert report.sub_agent_reports[2].agent_id == "problem_finder"
assert all(r.status == "completed" for r in report.sub_agent_reports)
assert len(report.code_issues) > 0
assert len(report.fix_proposals) > 0
assert report.generated_at != ""
def test_report_generator_markdown_output(self):
from yuxi.channels.adapters.nostr.analysis.coordinator import (
AnalysisCoordinator,
)
from yuxi.channels.adapters.nostr.analysis.reporter import ReportGenerator
import asyncio
coordinator = AnalysisCoordinator("package/yuxi/channels/adapters/nostr")
report = asyncio.run(coordinator.run())
md = ReportGenerator.to_markdown(report)
assert "# NostrAdapter 功能欠缺分析报告" in md
assert "缺失的核心功能点" in md
assert "未实现的接口或方法" in md
assert "与行业标准的差距" in md
assert "潜在的性能瓶颈" in md
assert "代码问题详细清单" in md
assert "代码修复方案" in md
assert "子Agent报告摘要" in md
@pytest.mark.asyncio
async def test_web_researcher_produces_findings(self):
from yuxi.channels.adapters.nostr.analysis.web_researcher import (
WebResearcher,
)
researcher = WebResearcher()
report = await researcher.research()
assert report.status == "completed"
assert len(report.findings) > 0
assert report.findings[0].category in (
"NIP标准协议", "DM加密演变", "Relay管理", "事件处理", "同类实现参考"
)
@pytest.mark.asyncio
async def test_code_analyzer_finds_interface_gaps(self):
from yuxi.channels.adapters.nostr.analysis.code_analyzer import CodeAnalyzer
analyzer = CodeAnalyzer("package/yuxi/channels/adapters/nostr")
report = await analyzer.analyze()
assert report.status == "completed"
assert len(report.findings) >= 10
@pytest.mark.asyncio
async def test_problem_finder_detects_critical_issues(self):
from yuxi.channels.adapters.nostr.analysis.problem_finder import (
ProblemFinder,
)
finder = ProblemFinder("package/yuxi/channels/adapters/nostr")
report = await finder.analyze()
assert report.status == "completed"
assert len(report.findings) >= 20
critical_count = sum(
1 for f in report.findings if hasattr(f, "severity") and f.severity == "critical"
)
assert critical_count >= 1