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

601 lines
22 KiB
Python

from __future__ import annotations
import pytest
from yuxi.channels.adapters.qqbot.analysis.integrator import Integrator
from yuxi.channels.adapters.qqbot.analysis.models import (
AgentReport,
AgentRole,
Finding,
FindingCategory,
FindingSeverity,
FixProposal,
GapAnalysisReport,
ResearchSource,
)
from yuxi.channels.adapters.qqbot.analysis.orchestrator import AnalysisOrchestrator
from yuxi.channels.adapters.qqbot.analysis.researcher_func import FunctionalResearcher
from yuxi.channels.adapters.qqbot.analysis.researcher_quality import QualityResearcher
from yuxi.channels.adapters.qqbot.analysis.researcher_web import WebResearcher
def _make_finding(
fid: str,
category: FindingCategory,
severity: FindingSeverity = FindingSeverity.MEDIUM,
) -> Finding:
return Finding(
id=fid,
category=category,
severity=severity,
title=f"Test finding {fid}",
description=f"Description for {fid}",
location="adapter.py:test",
suggestion="Fix it",
)
# ==================== Web Researcher Tests ====================
class TestWebResearcher:
@pytest.mark.asyncio
async def test_research_returns_report(self):
researcher = WebResearcher()
report = await researcher.research()
assert isinstance(report, AgentReport)
assert report.agent_role == AgentRole.WEB_RESEARCHER
assert len(report.findings) > 0
assert report.execution_time_ms > 0
assert report.sources_collected > 0
assert report.summary
@pytest.mark.asyncio
async def test_findings_include_all_categories(self):
researcher = WebResearcher()
report = await researcher.research()
categories = {f.category for f in report.findings}
assert FindingCategory.MISSING_FEATURE in categories
assert FindingCategory.STANDARD_GAP in categories
@pytest.mark.asyncio
async def test_missing_message_types_detected(self):
researcher = WebResearcher()
report = await researcher.research()
web001 = next((f for f in report.findings if f.id == "WEB-001"), None)
assert web001 is not None
assert web001.severity == FindingSeverity.HIGH
assert "Markdown" in web001.title
@pytest.mark.asyncio
async def test_missing_events_detected(self):
researcher = WebResearcher()
report = await researcher.research()
web005 = next((f for f in report.findings if f.id == "WEB-005"), None)
assert web005 is not None
assert "成员事件" in web005.title or "JOIN" in web005.description
@pytest.mark.asyncio
async def test_streaming_gaps_detected(self):
researcher = WebResearcher()
report = await researcher.research()
web008 = next((f for f in report.findings if f.id == "WEB-008"), None)
assert web008 is not None
assert "流式" in web008.title
@pytest.mark.asyncio
async def test_platform_standard_gaps_detected(self):
researcher = WebResearcher()
report = await researcher.research()
web013 = next((f for f in report.findings if f.id == "WEB-013"), None)
assert web013 is not None
assert "Session" in web013.title or "Resume" in web013.description
# ==================== Functional Researcher Tests ====================
class TestFunctionalResearcher:
@pytest.mark.asyncio
async def test_research_returns_report(self):
researcher = FunctionalResearcher()
report = await researcher.research()
assert isinstance(report, AgentReport)
assert report.agent_role == AgentRole.FUNCTIONAL_RESEARCHER
assert len(report.findings) > 0
assert report.execution_time_ms > 0
assert report.summary
@pytest.mark.asyncio
async def test_unimplemented_interfaces_detected(self):
researcher = FunctionalResearcher()
report = await researcher.research()
interface_findings = [f for f in report.findings if f.category == FindingCategory.MISSING_INTERFACE]
assert len(interface_findings) >= 3
titles = {f.title for f in interface_findings}
assert any("编辑" in t or "edit" in t.lower() for t in titles)
assert any("删除" in t or "delete" in t.lower() for t in titles)
assert any("Reaction" in t or "表情" in t for t in titles)
@pytest.mark.asyncio
async def test_partial_implementations_detected(self):
researcher = FunctionalResearcher()
report = await researcher.research()
partial = [f for f in report.findings if "不完整" in f.title]
assert len(partial) > 0
@pytest.mark.asyncio
async def test_data_flow_issues_detected(self):
researcher = FunctionalResearcher()
report = await researcher.research()
func007 = next((f for f in report.findings if f.id == "FUNC-007"), None)
assert func007 is not None
assert "队列" in func007.title or "queue" in func007.title.lower()
@pytest.mark.asyncio
async def test_cross_module_issues_detected(self):
researcher = FunctionalResearcher()
report = await researcher.research()
func010 = next((f for f in report.findings if f.id == "FUNC-010"), None)
assert func010 is not None
@pytest.mark.asyncio
async def test_functional_gaps_detected(self):
researcher = FunctionalResearcher()
report = await researcher.research()
func012 = next((f for f in report.findings if f.id == "FUNC-012"), None)
assert func012 is not None
assert "去重" in func012.title or "dedup" in func012.title.lower() or "重复" in func012.title
# ==================== Quality Researcher Tests ====================
class TestQualityResearcher:
@pytest.mark.asyncio
async def test_research_returns_report(self):
researcher = QualityResearcher()
report = await researcher.research()
assert isinstance(report, AgentReport)
assert report.agent_role == AgentRole.QUALITY_RESEARCHER
assert len(report.findings) > 0
assert report.execution_time_ms > 0
assert report.summary
@pytest.mark.asyncio
async def test_error_handling_issues_detected(self):
researcher = QualityResearcher()
report = await researcher.research()
error_findings = [f for f in report.findings if f.category == FindingCategory.ERROR_HANDLING]
assert len(error_findings) > 0
@pytest.mark.asyncio
async def test_wait_for_hello_issue_detected(self):
researcher = QualityResearcher()
report = await researcher.research()
qual001 = next((f for f in report.findings if f.id == "QUAL-001"), None)
assert qual001 is not None
assert "HELLO" in qual001.title.upper()
@pytest.mark.asyncio
async def test_heartbeat_issue_detected(self):
researcher = QualityResearcher()
report = await researcher.research()
qual002 = next((f for f in report.findings if f.id == "QUAL-002"), None)
assert qual002 is not None
assert "心跳" in qual002.title or "heartbeat" in qual002.title.lower()
@pytest.mark.asyncio
async def test_security_issues_detected(self):
researcher = QualityResearcher()
report = await researcher.research()
security_findings = [f for f in report.findings if f.category == FindingCategory.SECURITY]
assert len(security_findings) > 0
@pytest.mark.asyncio
async def test_ed25519_validation_issue_detected(self):
researcher = QualityResearcher()
report = await researcher.research()
qual011 = next((f for f in report.findings if f.id == "QUAL-011"), None)
assert qual011 is not None
assert "ed25519" in qual011.title.lower() or "签名" in qual011.title or "密钥" in qual011.title
@pytest.mark.asyncio
async def test_code_quality_issues_detected(self):
researcher = QualityResearcher()
report = await researcher.research()
quality_findings = [f for f in report.findings if f.category == FindingCategory.CODE_QUALITY]
assert len(quality_findings) > 0
@pytest.mark.asyncio
async def test_logic_bugs_detected(self):
researcher = QualityResearcher()
report = await researcher.research()
logic_findings = [f for f in report.findings if f.category == FindingCategory.LOGIC_BUG]
assert len(logic_findings) > 0
# ==================== Integrator Tests ====================
class TestIntegrator:
@pytest.mark.asyncio
async def test_integrate_empty_reports(self):
integrator = Integrator()
gap_report = await integrator.integrate([])
assert isinstance(gap_report, GapAnalysisReport)
assert gap_report.module_name == "qqbot"
assert gap_report.total_findings == 0
assert gap_report.critical_count == 0
assert gap_report.summary
@pytest.mark.asyncio
async def test_integrate_with_findings(self):
web_report = AgentReport(
agent_role=AgentRole.WEB_RESEARCHER,
findings=[
_make_finding("W-001", FindingCategory.MISSING_FEATURE, FindingSeverity.HIGH),
_make_finding("W-002", FindingCategory.STANDARD_GAP),
],
summary="Web findings",
)
func_report = AgentReport(
agent_role=AgentRole.FUNCTIONAL_RESEARCHER,
findings=[
_make_finding("F-001", FindingCategory.MISSING_INTERFACE, FindingSeverity.CRITICAL),
_make_finding("F-002", FindingCategory.PERFORMANCE, FindingSeverity.LOW),
],
summary="Func findings",
)
quality_report = AgentReport(
agent_role=AgentRole.QUALITY_RESEARCHER,
findings=[
_make_finding("Q-001", FindingCategory.SECURITY, FindingSeverity.CRITICAL),
_make_finding("Q-002", FindingCategory.ERROR_HANDLING, FindingSeverity.HIGH),
_make_finding("Q-003", FindingCategory.CODE_QUALITY, FindingSeverity.LOW),
],
summary="Quality findings",
)
integrator = Integrator()
gap_report = await integrator.integrate([web_report, func_report, quality_report])
assert gap_report.total_findings == 7
assert gap_report.critical_count == 2
assert len(gap_report.missing_features) == 1
assert len(gap_report.missing_interfaces) == 1
assert len(gap_report.standard_gaps) == 1
assert len(gap_report.security_issues) == 1
assert len(gap_report.error_handling_issues) == 1
assert len(gap_report.code_quality_issues) == 1
assert len(gap_report.performance_bottlenecks) == 1
assert len(gap_report.sub_agent_reports) == 3
@pytest.mark.asyncio
async def test_integrate_category_routing(self):
all_categories = [
(FindingCategory.MISSING_FEATURE, "missing_features"),
(FindingCategory.MISSING_INTERFACE, "missing_interfaces"),
(FindingCategory.STANDARD_GAP, "standard_gaps"),
(FindingCategory.PERFORMANCE, "performance_bottlenecks"),
(FindingCategory.ERROR_HANDLING, "error_handling_issues"),
(FindingCategory.CODE_QUALITY, "code_quality_issues"),
(FindingCategory.SECURITY, "security_issues"),
(FindingCategory.LOGIC_BUG, "logic_bugs"),
]
for category, field_name in all_categories:
report = AgentReport(
agent_role=AgentRole.QUALITY_RESEARCHER,
findings=[_make_finding("T-001", category)],
)
integrator = Integrator()
gap_report = await integrator.integrate([report])
assert len(getattr(gap_report, field_name)) == 1, f"Category {category} should route to {field_name}"
# ==================== Orchestrator Tests ====================
class TestOrchestrator:
@pytest.mark.asyncio
async def test_run_diagnostics(self):
orchestrator = AnalysisOrchestrator()
gap_report = await orchestrator.run_diagnostics()
assert isinstance(gap_report, GapAnalysisReport)
assert gap_report.module_name == "qqbot"
assert gap_report.total_findings > 0
assert gap_report.summary
assert len(gap_report.sub_agent_reports) == 3
@pytest.mark.asyncio
async def test_run_diagnostics_has_all_sub_reports(self):
orchestrator = AnalysisOrchestrator()
gap_report = await orchestrator.run_diagnostics()
roles = {r.agent_role for r in gap_report.sub_agent_reports}
assert AgentRole.WEB_RESEARCHER in roles
assert AgentRole.FUNCTIONAL_RESEARCHER in roles
assert AgentRole.QUALITY_RESEARCHER in roles
@pytest.mark.asyncio
async def test_run_full_analysis_no_apply(self):
orchestrator = AnalysisOrchestrator()
gap_report, fix_report = await orchestrator.run_full_analysis(apply_fixes=False)
assert isinstance(gap_report, GapAnalysisReport)
assert len(fix_report.proposals) > 0
assert fix_report.applied_count == 0
@pytest.mark.asyncio
async def test_run_auto_fix_generates_proposals(self):
orchestrator = AnalysisOrchestrator()
fix_report = await orchestrator.run_auto_fix()
assert len(fix_report.proposals) > 0
assert isinstance(fix_report.summary, str)
@pytest.mark.asyncio
async def test_gap_report_has_all_category_lists(self):
orchestrator = AnalysisOrchestrator()
gap_report = await orchestrator.run_diagnostics()
assert isinstance(gap_report.missing_features, list)
assert isinstance(gap_report.missing_interfaces, list)
assert isinstance(gap_report.standard_gaps, list)
assert isinstance(gap_report.performance_bottlenecks, list)
assert isinstance(gap_report.error_handling_issues, list)
assert isinstance(gap_report.code_quality_issues, list)
assert isinstance(gap_report.security_issues, list)
assert isinstance(gap_report.logic_bugs, list)
# ==================== Fixer Tests ====================
class TestFixer:
@pytest.fixture
def sample_report(self):
return GapAnalysisReport(
module_name="qqbot",
all_findings=[
_make_finding("QUAL-001", FindingCategory.ERROR_HANDLING, FindingSeverity.HIGH),
_make_finding("QUAL-011", FindingCategory.SECURITY, FindingSeverity.HIGH),
_make_finding("QUAL-007", FindingCategory.CODE_QUALITY, FindingSeverity.MEDIUM),
],
error_handling_issues=[
_make_finding("QUAL-001", FindingCategory.ERROR_HANDLING, FindingSeverity.HIGH),
],
security_issues=[
_make_finding("QUAL-011", FindingCategory.SECURITY, FindingSeverity.HIGH),
],
code_quality_issues=[
_make_finding("QUAL-007", FindingCategory.CODE_QUALITY, FindingSeverity.MEDIUM),
],
logic_bugs=[],
)
@pytest.mark.asyncio
async def test_generate_proposals(self, sample_report):
from yuxi.channels.adapters.qqbot.analysis.fixer import Fixer
fixer = Fixer()
proposals = await fixer.generate_proposals(sample_report)
assert len(proposals) > 0
assert all(isinstance(p, FixProposal) for p in proposals)
@pytest.mark.asyncio
async def test_proposals_have_ids(self, sample_report):
from yuxi.channels.adapters.qqbot.analysis.fixer import Fixer
fixer = Fixer()
proposals = await fixer.generate_proposals(sample_report)
ids = {p.id for p in proposals}
assert len(ids) == len(proposals)
@pytest.mark.asyncio
async def test_proposals_include_security_fix(self, sample_report):
from yuxi.channels.adapters.qqbot.analysis.fixer import Fixer
fixer = Fixer()
proposals = await fixer.generate_proposals(sample_report)
security_patches = [p for p in proposals if p.fix_type == "security_patch"]
assert len(security_patches) > 0
@pytest.mark.asyncio
async def test_proposals_include_bug_fix(self, sample_report):
from yuxi.channels.adapters.qqbot.analysis.fixer import Fixer
fixer = Fixer()
proposals = await fixer.generate_proposals(sample_report)
bug_fixes = [p for p in proposals if p.fix_type == "fix_bug"]
assert len(bug_fixes) > 0
@pytest.mark.asyncio
async def test_proposals_include_refactor(self, sample_report):
from yuxi.channels.adapters.qqbot.analysis.fixer import Fixer
fixer = Fixer()
proposals = await fixer.generate_proposals(sample_report)
refactors = [p for p in proposals if p.fix_type == "refactor"]
assert len(refactors) > 0
@pytest.mark.asyncio
async def test_apply_with_empty_proposals(self, sample_report):
from yuxi.channels.adapters.qqbot.analysis.fixer import Fixer
fixer = Fixer()
fix_report = await fixer.apply_proposals([])
assert fix_report.applied_count == 0
assert fix_report.skipped_count == 0
# ==================== Models Tests ====================
class TestModels:
def test_finding_creation(self):
finding = Finding(
id="TEST-001",
category=FindingCategory.SECURITY,
severity=FindingSeverity.CRITICAL,
title="Test finding",
description="Test",
location="adapter.py:100",
suggestion="Fix it",
sources=[
ResearchSource(
url="https://example.com",
title="Source",
relevance=0.95,
)
],
)
assert finding.id == "TEST-001"
assert finding.severity == FindingSeverity.CRITICAL
def test_gap_report_properties(self):
report = GapAnalysisReport(
all_findings=[
_make_finding("A", FindingCategory.SECURITY, FindingSeverity.CRITICAL),
_make_finding("B", FindingCategory.ERROR_HANDLING, FindingSeverity.HIGH),
_make_finding("C", FindingCategory.CODE_QUALITY),
],
)
assert report.total_findings == 3
assert report.critical_count == 1
def test_fix_proposal_creation(self):
proposal = FixProposal(
id="FIX-001",
finding_id="QUAL-001",
title="Fix _wait_for_hello",
description="Fix the HELLO handling",
fix_type="fix_bug",
estimated_effort="low",
affected_files=["adapter.py"],
test_suggestions=["Test HELLO after non-HELLO message"],
)
assert proposal.fix_type == "fix_bug"
assert not proposal.applied
def test_agent_report_summary(self):
report = AgentReport(
agent_role=AgentRole.WEB_RESEARCHER,
findings=[
_make_finding("W1", FindingCategory.MISSING_FEATURE),
_make_finding("W2", FindingCategory.STANDARD_GAP),
],
summary="Web research completed",
execution_time_ms=150.0,
sources_collected=10,
)
assert report.agent_role == AgentRole.WEB_RESEARCHER
assert len(report.findings) == 2
assert report.execution_time_ms == 150.0
def test_finding_severity_ordering(self):
severities = [
FindingSeverity.CRITICAL,
FindingSeverity.HIGH,
FindingSeverity.MEDIUM,
FindingSeverity.LOW,
FindingSeverity.INFO,
]
assert len(severities) == 5
assert len(set(severities)) == 5
# ==================== Integration: Full Flow Tests ====================
class TestFullAnalysisFlow:
@pytest.mark.asyncio
async def test_all_three_researchers_produce_findings(self):
web = WebResearcher()
func = FunctionalResearcher()
quality = QualityResearcher()
web_report = await web.research()
func_report = await func.research()
quality_report = await quality.research()
assert len(web_report.findings) > 0
assert len(func_report.findings) > 0
assert len(quality_report.findings) > 0
@pytest.mark.asyncio
async def test_integration_produces_comprehensive_report(self):
web = WebResearcher()
func = FunctionalResearcher()
quality = QualityResearcher()
web_report = await web.research()
func_report = await func.research()
quality_report = await quality.research()
integrator = Integrator()
gap_report = await integrator.integrate([web_report, func_report, quality_report])
assert gap_report.total_findings > 0
assert gap_report.summary
total_by_category = (
len(gap_report.missing_features)
+ len(gap_report.missing_interfaces)
+ len(gap_report.standard_gaps)
+ len(gap_report.performance_bottlenecks)
+ len(gap_report.error_handling_issues)
+ len(gap_report.code_quality_issues)
+ len(gap_report.security_issues)
+ len(gap_report.logic_bugs)
)
assert total_by_category == gap_report.total_findings
@pytest.mark.asyncio
async def test_diagnostics_report_contains_all_sections(self):
orchestrator = AnalysisOrchestrator()
gap_report = await orchestrator.run_diagnostics()
non_empty_sections = [
name for name in [
"missing_features",
"missing_interfaces",
"standard_gaps",
"error_handling_issues",
"code_quality_issues",
"security_issues",
"logic_bugs",
]
if len(getattr(gap_report, name)) > 0
]
assert len(non_empty_sections) >= 4