test_grammar_check_chain.py 17 KB

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