"""M8 MCP 服务测试。 验证工具存在 + 格式化逻辑 + 降级行为,不依赖真实嵌入/检索。 """ from __future__ import annotations from unittest.mock import patch from mcp_server.tools import ( _fmt_results, mcp, search_company_news, search_news, search_sentiment_trend, search_stock_events, ) # --------------------------------------------------------------------------- # # 工具存在性 # --------------------------------------------------------------------------- # def test_mcp_server_has_name() -> None: assert mcp.name == "A股DeepResearch" def test_all_five_tools_registered() -> None: tool_names = [getattr(t, "name", "") for t in mcp._tool_manager._tools.values()] # type: ignore[union-attr] expected = { "search_news", "search_company_news", "search_industry_news", "search_stock_events", "search_sentiment_trend", } assert set(tool_names) == expected # --------------------------------------------------------------------------- # # _fmt_results # --------------------------------------------------------------------------- # def _hit(title: str = "测试标题", source: str = "cls", score: float = 0.9, sentiment: str = "positive", stock_codes: list[str] | None = None, company_names: list[str] | None = None, summary: str = "摘要", publish_time: str = "2026-06-16T10:00:00", url: str = "https://example.com/1") -> dict: return { "title": title, "url": url, "source": source, "score": score, "publish_time": publish_time, "event": { "sentiment": sentiment, "importance": 4, "event_type": "重大合同", "stock_codes": stock_codes or [], "company_names": company_names or [], "industries": ["动力电池"], "summary": summary, }, } def test_fmt_results_contains_title_and_source() -> None: hits = [_hit("宁德时代签百亿合同", "cls")] out = _fmt_results(hits, "宁德时代") assert "百亿合同" in out assert "cls" in out assert "0.9" in out def test_fmt_results_includes_event_fields() -> None: hits = [_hit( company_names=["宁德时代"], stock_codes=["300750"], )] out = _fmt_results(hits, "查询") assert "300750" in out assert "宁德时代" in out assert "动力电池" in out assert "摘要" in out def test_fmt_results_empty_returns_hint() -> None: out = _fmt_results([], "无结果") assert "未找到" in out and "无结果" in out def test_fmt_results_multiple_hits() -> None: hits = [_hit(f"测试{i}") for i in range(3)] out = _fmt_results(hits, "查询") assert "共 3 条" in out # --------------------------------------------------------------------------- # # 工具:降级行为(无精确命中时走纯语义) # --------------------------------------------------------------------------- # @patch("mcp_server.tools._search") def test_search_company_news_falls_back_on_empty(mock_search) -> None: """company 精确命中 0 条时,降级为无 filter 纯语义搜索。""" mock_search.side_effect = [ [], # 第一次:精确匹配 0 条 [_hit("fallback")], # 降级: 纯语义 ] out = search_company_news("查询", company="不存在的公司") assert "fallback" in out @patch("mcp_server.tools._search") def test_search_stock_events_normalizes_code(mock_search) -> None: """stock_code 应去掉后缀,统一大写。""" mock_search.return_value = [_hit("结果")] out = search_stock_events("查询", stock_code="300750.SZ") mock_search.assert_called() # code 应为 '300750' assert "结果" in out @patch("mcp_server.tools._search") def test_search_sentiment_trend_includes_stats(mock_search) -> None: mock_search.return_value = [ _hit("a", sentiment="positive"), _hit("b", sentiment="positive"), _hit("c", sentiment="negative"), _hit("d", sentiment="neutral"), ] out = search_sentiment_trend("查询", sentiment="all", top_k=10) assert "利好 2" in out assert "利空 1" in out assert "中性 1" in out assert "共 4 条" in out @patch("mcp_server.tools._search") def test_search_news_passthrough(mock_search) -> None: mock_search.return_value = [_hit("结果")] out = search_news("查询") assert "结果" in out