test_grammar_check_chain.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 语法审查 (GrammarCheckReviewer.check_grammar) 全链路测试
  5. 测试范围(仅限 sensitive_word_check.py 自身逻辑):
  6. 1. Prompt 模板:YAML 配置结构 → prompt_loader 加载 → 变量填充
  7. 2. 模型调用:check_grammar → model_client.get_model_generate_invoke(function_name="sensitive_check")
  8. 3. 结果封装:模型响应 → ReviewResult(details={name, response}, execution_time)
  9. 4. 异常处理:超时/错误 → ReviewResult(success=False)
  10. 5. 推送集成:state.progress_manager 异步推送
  11. 用法:
  12. cd <project_root>
  13. set PYTHONPATH=<project_root>
  14. pytest utils_test/Sensitive_Test/test_grammar_check_chain.py -v
  15. """
  16. import asyncio
  17. import sys, os, json, types
  18. from pathlib import Path
  19. from typing import Dict, Any, Optional
  20. from dataclasses import dataclass
  21. from unittest.mock import AsyncMock, MagicMock, patch
  22. import pytest
  23. # -----------------------------------------------------------
  24. # 项目根目录
  25. # -----------------------------------------------------------
  26. current_dir = Path(__file__).parent.absolute()
  27. project_root = current_dir.parent.parent
  28. sys.path.insert(0, str(project_root))
  29. # -----------------------------------------------------------
  30. # 只 Mock 导致导入链断裂的 modules,不引入多余依赖
  31. # -----------------------------------------------------------
  32. # directory_extraction.py 引用了不存在的 PydanticOutputParser
  33. _mock_dir_ext = types.ModuleType(
  34. "core.construction_review.component.reviewers.utils.directory_extraction"
  35. )
  36. _mock_dir_ext.extract_basis_with_langchain_qwen = MagicMock()
  37. _mock_dir_ext.BasisItems = MagicMock()
  38. _mock_dir_ext.BasisItem = MagicMock()
  39. sys.modules["core.construction_review.component.reviewers.utils.directory_extraction"] = (
  40. _mock_dir_ext
  41. )
  42. # langfuse 未安装
  43. sys.modules.setdefault("langfuse", MagicMock())
  44. # langchain_openai 未安装
  45. _mock_lc_openai = types.ModuleType("langchain_openai")
  46. _mock_lc_openai.ChatOpenAI = MagicMock()
  47. _mock_lc_openai.OpenAIEmbeddings = MagicMock()
  48. sys.modules.setdefault("langchain_openai", _mock_lc_openai)
  49. sys.modules.setdefault("langchain_openai.chat_models", MagicMock())
  50. sys.modules.setdefault("langchain_openai.embeddings", MagicMock())
  51. # ============================================================
  52. # 核心测试:GrammarCheckReviewer 链路
  53. # ============================================================
  54. class TestGrammarCheckChain:
  55. """sensitive_word_check.py 审查链路测试"""
  56. # --------------------------------------------------------
  57. # 1. Prompt 模板配置
  58. # --------------------------------------------------------
  59. def test_prompt_template_structure(self):
  60. """验证 YAML 模板包含必要字段和变量"""
  61. import yaml
  62. prompt_dir = (
  63. project_root
  64. / "core"
  65. / "construction_review"
  66. / "component"
  67. / "reviewers"
  68. / "prompt"
  69. )
  70. with open(prompt_dir / "basic_reviewers.yaml", "r", encoding="utf-8") as f:
  71. config = yaml.safe_load(f)
  72. cfg = config.get("sensitive_word_check", {})
  73. assert cfg, "sensitive_word_check 键不存在"
  74. assert "system_prompt" in cfg
  75. assert "user_prompt_template" in cfg
  76. tmpl = cfg["user_prompt_template"]
  77. assert "{review_content}" in tmpl
  78. assert "{review_references}" in tmpl
  79. def test_prompt_loader_loads_template(self):
  80. """prompt_loader 能正常加载 sensitive_word_check 模板"""
  81. from core.construction_review.component.reviewers.utils.prompt_loader import (
  82. prompt_loader,
  83. )
  84. prompts = prompt_loader.list_available_prompts("basic")
  85. assert "sensitive_word_check" in prompts
  86. template = prompt_loader.get_prompt_template(
  87. "basic",
  88. "sensitive_word_check",
  89. review_content="测试内容abc123",
  90. review_references="测试参考",
  91. )
  92. messages = template.format_messages()
  93. assert len(messages) == 2
  94. assert messages[0].type == "system"
  95. assert messages[1].type == "human"
  96. assert "测试内容abc123" in messages[1].content
  97. assert "测试参考" in messages[1].content
  98. # --------------------------------------------------------
  99. # 2. 模型调用参数构建
  100. # --------------------------------------------------------
  101. def test_check_grammar_calls_model_with_function_name(self):
  102. """验证 check_grammar 以 function_name=sensitive_check 调用模型"""
  103. from core.construction_review.component.reviewers.sensitive_word_check import (
  104. GrammarCheckReviewer,
  105. )
  106. reviewer = GrammarCheckReviewer()
  107. reviewer.model_client = MagicMock()
  108. reviewer.model_client.get_model_generate_invoke = AsyncMock(return_value="无明显问题")
  109. async def run():
  110. await reviewer.check_grammar(
  111. trace_id="trace_001", review_content="测试内容。"
  112. )
  113. kwargs = reviewer.model_client.get_model_generate_invoke.call_args[1]
  114. assert kwargs.get("function_name") == "sensitive_check", (
  115. f"期望 sensitive_check,实际: {kwargs.get('function_name')}"
  116. )
  117. assert kwargs.get("trace_id") == "trace_001"
  118. messages = kwargs.get("messages", [])
  119. assert len(messages) == 2
  120. assert "测试内容。" in messages[1].content
  121. asyncio.run(run())
  122. # --------------------------------------------------------
  123. # 3. 正常审查结果封装
  124. # --------------------------------------------------------
  125. def test_success_result_structure(self):
  126. """验证成功时返回正确的 ReviewResult"""
  127. from core.construction_review.component.reviewers.sensitive_word_check import (
  128. GrammarCheckReviewer,
  129. )
  130. from core.construction_review.component.reviewers.base_reviewer import ReviewResult
  131. reviewer = GrammarCheckReviewer()
  132. reviewer.model_client = MagicMock()
  133. mock_resp = json.dumps({"issue_point": "测试问题"}, ensure_ascii=False)
  134. reviewer.model_client.get_model_generate_invoke = AsyncMock(return_value=mock_resp)
  135. async def run():
  136. result = await reviewer.check_grammar(
  137. trace_id="t_succ", review_content="测试。"
  138. )
  139. assert isinstance(result, ReviewResult)
  140. assert result.success is True
  141. assert result.error_message is None
  142. assert result.details.get("name") == "sensitive_word_check"
  143. assert result.details.get("response") == mock_resp
  144. assert isinstance(result.execution_time, (int, float))
  145. assert result.execution_time >= 0
  146. asyncio.run(run())
  147. def test_model_json_response_preserved(self):
  148. """模型 JSON 响应完整保留"""
  149. from core.construction_review.component.reviewers.sensitive_word_check import (
  150. GrammarCheckReviewer,
  151. )
  152. reviewer = GrammarCheckReviewer()
  153. reviewer.model_client = MagicMock()
  154. resp = json.dumps({
  155. "issue_point": "绝对化用语",
  156. "location": "第一章",
  157. "suggestion": "修改建议",
  158. "risk_level": "中风险",
  159. }, ensure_ascii=False)
  160. reviewer.model_client.get_model_generate_invoke = AsyncMock(return_value=resp)
  161. async def run():
  162. result = await reviewer.check_grammar(
  163. trace_id="t_json", review_content="绝对不会出现问题。"
  164. )
  165. assert result.success is True
  166. assert "绝对化用语" in result.details["response"]
  167. asyncio.run(run())
  168. def test_execution_time_recorded(self):
  169. """耗时被正确记录"""
  170. from core.construction_review.component.reviewers.sensitive_word_check import (
  171. GrammarCheckReviewer,
  172. )
  173. reviewer = GrammarCheckReviewer()
  174. reviewer.model_client = MagicMock()
  175. async def slow(*a, **kw):
  176. await asyncio.sleep(0.05)
  177. return "无明显问题"
  178. reviewer.model_client.get_model_generate_invoke = AsyncMock(side_effect=slow)
  179. async def run():
  180. result = await reviewer.check_grammar(
  181. trace_id="t_time", review_content="测试。"
  182. )
  183. assert result.execution_time >= 0.04
  184. asyncio.run(run())
  185. # --------------------------------------------------------
  186. # 4. 异常处理
  187. # --------------------------------------------------------
  188. def test_timeout_returns_error(self):
  189. """超时 → success=False"""
  190. from core.construction_review.component.reviewers.sensitive_word_check import (
  191. GrammarCheckReviewer,
  192. )
  193. reviewer = GrammarCheckReviewer()
  194. reviewer.model_client = MagicMock()
  195. reviewer.model_client.get_model_generate_invoke = AsyncMock(
  196. side_effect=TimeoutError("模型调用超时")
  197. )
  198. async def run():
  199. result = await reviewer.check_grammar(
  200. trace_id="t_to", review_content="测试。"
  201. )
  202. assert result.success is False
  203. assert result.error_message is not None
  204. assert "超时" in result.error_message
  205. assert result.details.get("name") == "sensitive_word_check"
  206. assert result.execution_time is not None
  207. asyncio.run(run())
  208. def test_api_error_returns_error(self):
  209. """API 异常 → success=False"""
  210. from core.construction_review.component.reviewers.sensitive_word_check import (
  211. GrammarCheckReviewer,
  212. )
  213. reviewer = GrammarCheckReviewer()
  214. reviewer.model_client = MagicMock()
  215. reviewer.model_client.get_model_generate_invoke = AsyncMock(
  216. side_effect=RuntimeError("API 500")
  217. )
  218. async def run():
  219. result = await reviewer.check_grammar(
  220. trace_id="t_api", review_content="测试。"
  221. )
  222. assert result.success is False
  223. assert "语法检查失败" in result.error_message
  224. asyncio.run(run())
  225. def test_empty_content_handled(self):
  226. """空内容不崩溃"""
  227. from core.construction_review.component.reviewers.sensitive_word_check import (
  228. GrammarCheckReviewer,
  229. )
  230. reviewer = GrammarCheckReviewer()
  231. reviewer.model_client = MagicMock()
  232. reviewer.model_client.get_model_generate_invoke = AsyncMock(return_value="无明显问题")
  233. async def run():
  234. result = await reviewer.check_grammar(
  235. trace_id="t_empty", review_content=""
  236. )
  237. assert result.success is True
  238. asyncio.run(run())
  239. # --------------------------------------------------------
  240. # 5. Progress Manager 推送
  241. # --------------------------------------------------------
  242. def test_progress_push_on_success(self):
  243. """成功时推送进度"""
  244. from core.construction_review.component.reviewers.sensitive_word_check import (
  245. GrammarCheckReviewer,
  246. )
  247. reviewer = GrammarCheckReviewer()
  248. reviewer.model_client = MagicMock()
  249. reviewer.model_client.get_model_generate_invoke = AsyncMock(return_value="无明显问题")
  250. pm = AsyncMock()
  251. pm.update_stage_progress = AsyncMock()
  252. state = {"progress_manager": pm, "callback_task_id": "cb_001"}
  253. async def run():
  254. result = await reviewer.check_grammar(
  255. trace_id="t_ps",
  256. review_content="测试。",
  257. state=state,
  258. stage_name="test_stage",
  259. )
  260. assert result.success is True
  261. # asyncio.create_task 是 fire-and-forget,需要 yield 让 task 执行
  262. await asyncio.sleep(0)
  263. pm.update_stage_progress.assert_awaited_once()
  264. kw = pm.update_stage_progress.call_args[1]
  265. assert kw["callback_task_id"] == "cb_001"
  266. assert kw["stage_name"] == "test_stage"
  267. assert kw["status"] == "processing"
  268. assert "sensitive_word_check" in kw["message"]
  269. assert len(kw["issues"]) == 1
  270. assert kw["issues"][0]["name"] == "sensitive_word_check"
  271. assert kw["issues"][0]["success"] is True
  272. asyncio.run(run())
  273. def test_progress_push_on_failure(self):
  274. """失败时也推送"""
  275. from core.construction_review.component.reviewers.sensitive_word_check import (
  276. GrammarCheckReviewer,
  277. )
  278. reviewer = GrammarCheckReviewer()
  279. reviewer.model_client = MagicMock()
  280. reviewer.model_client.get_model_generate_invoke = AsyncMock(
  281. side_effect=RuntimeError("err")
  282. )
  283. pm = AsyncMock()
  284. pm.update_stage_progress = AsyncMock()
  285. state = {"progress_manager": pm, "callback_task_id": "cb_002"}
  286. async def run():
  287. result = await reviewer.check_grammar(
  288. trace_id="t_pf",
  289. review_content="测试。",
  290. state=state,
  291. stage_name="test",
  292. )
  293. assert result.success is False
  294. await asyncio.sleep(0) # yield 让 create_task 执行
  295. pm.update_stage_progress.assert_awaited_once()
  296. issues = pm.update_stage_progress.call_args[1]["issues"]
  297. assert issues[0]["success"] is False
  298. asyncio.run(run())
  299. def test_no_state_skips_push(self):
  300. """不传 state 不推送"""
  301. from core.construction_review.component.reviewers.sensitive_word_check import (
  302. GrammarCheckReviewer,
  303. )
  304. reviewer = GrammarCheckReviewer()
  305. reviewer.model_client = MagicMock()
  306. reviewer.model_client.get_model_generate_invoke = AsyncMock(return_value="无明显问题")
  307. async def run():
  308. result = await reviewer.check_grammar(
  309. trace_id="t_ns", review_content="测试。"
  310. )
  311. assert result.success is True
  312. asyncio.run(run())
  313. # --------------------------------------------------------
  314. # 6. 模块导出
  315. # --------------------------------------------------------
  316. def test_module_exports(self):
  317. """验证模块导出全局实例"""
  318. from core.construction_review.component.reviewers.sensitive_word_check import (
  319. sensitive_word_check_reviewer,
  320. GrammarCheckReviewer,
  321. )
  322. assert isinstance(sensitive_word_check_reviewer, GrammarCheckReviewer)
  323. def test_default_model_client(self):
  324. """默认 model_client 指向全局实例"""
  325. from core.construction_review.component.reviewers.sensitive_word_check import (
  326. GrammarCheckReviewer,
  327. )
  328. from foundation.ai.agent.generate.model_generate import generate_model_client
  329. inst = GrammarCheckReviewer()
  330. assert inst.model_client is generate_model_client
  331. # --------------------------------------------------------
  332. # 7. 全链路集成
  333. # --------------------------------------------------------
  334. def test_full_chain_success(self):
  335. """全链路成功:加载 prompt → 调用模型 → ReviewResult"""
  336. from core.construction_review.component.reviewers.sensitive_word_check import (
  337. GrammarCheckReviewer,
  338. )
  339. reviewer = GrammarCheckReviewer()
  340. reviewer.model_client = MagicMock()
  341. reviewer.model_client.get_model_generate_invoke = AsyncMock(
  342. return_value="无明显问题"
  343. )
  344. async def run():
  345. result = await reviewer.check_grammar(
  346. trace_id="chain_ok",
  347. review_content="本方案编制依据GB50300-2013。",
  348. )
  349. assert reviewer.model_client.get_model_generate_invoke.awaited
  350. assert result.success is True
  351. assert result.execution_time is not None
  352. asyncio.run(run())
  353. def test_full_chain_error(self):
  354. """全链路异常:模型抛错 → ReviewResult(success=False)"""
  355. from core.construction_review.component.reviewers.sensitive_word_check import (
  356. GrammarCheckReviewer,
  357. )
  358. reviewer = GrammarCheckReviewer()
  359. reviewer.model_client = MagicMock()
  360. reviewer.model_client.get_model_generate_invoke = AsyncMock(
  361. side_effect=TimeoutError("超时")
  362. )
  363. async def run():
  364. result = await reviewer.check_grammar(
  365. trace_id="chain_err", review_content="测试。"
  366. )
  367. assert result.success is False
  368. assert result.error_message is not None
  369. asyncio.run(run())
  370. # ============================================================
  371. # 入口
  372. # ============================================================
  373. if __name__ == "__main__":
  374. raise SystemExit(pytest.main([__file__, "-v", "--capture=no"]))