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