test_semantic_logic.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. """
  2. 语义逻辑审查模块单元测试
  3. 测试 semantic_logic.py 中的 SemanticLogicReviewer 类
  4. """
  5. import pytest
  6. import asyncio
  7. import sys
  8. import os
  9. from unittest.mock import Mock, patch, AsyncMock, MagicMock
  10. from typing import Dict, Any
  11. # 添加项目根目录到路径
  12. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
  13. from core.construction_review.component.reviewers.semantic_logic import (
  14. SemanticLogicReviewer,
  15. semantic_logic_reviewer,
  16. SEMANTIC_LOGIC_MODEL_CONFIG
  17. )
  18. from core.construction_review.component.reviewers.base_reviewer import ReviewResult
  19. class TestSemanticLogicReviewer:
  20. """语义逻辑审查器测试类"""
  21. @pytest.fixture
  22. def reviewer(self):
  23. """创建审查器实例"""
  24. return SemanticLogicReviewer()
  25. @pytest.fixture
  26. def mock_state(self):
  27. """创建模拟的状态字典"""
  28. mock_progress_manager = AsyncMock()
  29. mock_progress_manager.update_stage_progress = AsyncMock()
  30. return {
  31. "progress_manager": mock_progress_manager,
  32. "callback_task_id": "test_task_123"
  33. }
  34. @pytest.fixture
  35. def sample_review_content(self):
  36. """示例审查内容"""
  37. return """
  38. 施工方案概述:
  39. 本工程为高速公路桥梁施工项目,主要包括桥墩基础施工、桥梁上部结构施工等内容。
  40. 施工工期为12个月,计划2024年3月开工,2025年3月竣工。
  41. """
  42. @pytest.fixture
  43. def sample_review_references(self):
  44. """示例审查参考"""
  45. return "参考标准:《公路桥涵施工技术规范》JTG/T 3650-2020"
  46. def test_reviewer_initialization(self, reviewer):
  47. """测试审查器初始化"""
  48. assert reviewer is not None
  49. assert reviewer.model == SEMANTIC_LOGIC_MODEL_CONFIG["model"]
  50. assert reviewer.temperature == SEMANTIC_LOGIC_MODEL_CONFIG["temperature"]
  51. assert reviewer.max_tokens == SEMANTIC_LOGIC_MODEL_CONFIG["max_tokens"]
  52. assert reviewer.client is not None
  53. def test_global_singleton_instance(self):
  54. """测试全局单例实例"""
  55. assert semantic_logic_reviewer is not None
  56. assert isinstance(semantic_logic_reviewer, SemanticLogicReviewer)
  57. def test_model_config(self):
  58. """测试模型配置"""
  59. assert SEMANTIC_LOGIC_MODEL_CONFIG["base_url"] == "http://192.168.91.253:8003/v1"
  60. assert SEMANTIC_LOGIC_MODEL_CONFIG["api_key"] == "sk-123456"
  61. assert SEMANTIC_LOGIC_MODEL_CONFIG["model"] == "qwen3-30b"
  62. assert SEMANTIC_LOGIC_MODEL_CONFIG["temperature"] == 0.7
  63. assert SEMANTIC_LOGIC_MODEL_CONFIG["max_tokens"] == 2000
  64. @pytest.mark.asyncio
  65. async def test_check_semantic_logic_success(
  66. self,
  67. reviewer,
  68. sample_review_content,
  69. sample_review_references,
  70. mock_state
  71. ):
  72. """测试语义逻辑检查成功场景"""
  73. # 模拟 OpenAI API 响应
  74. mock_response = MagicMock()
  75. mock_response.choices = [MagicMock()]
  76. mock_response.choices[0].message.content = "审查结果:内容逻辑清晰,无明显问题。"
  77. # 模拟提示词模板
  78. mock_prompt_template = MagicMock()
  79. mock_message = MagicMock()
  80. mock_message.type = "user"
  81. mock_message.content = "请审查以下内容"
  82. mock_prompt_template.format_messages.return_value = [mock_message]
  83. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  84. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  85. mock_create.return_value = mock_response
  86. mock_get_prompt.return_value = mock_prompt_template
  87. # 执行测试
  88. result = await reviewer.check_semantic_logic(
  89. trace_id="test_trace_001",
  90. review_content=sample_review_content,
  91. review_references=sample_review_references,
  92. review_location_label="第一章",
  93. state=mock_state,
  94. stage_name="basic_check"
  95. )
  96. # 验证结果
  97. assert isinstance(result, ReviewResult)
  98. assert result.success is True
  99. assert result.details["name"] == "semantic_check"
  100. assert "审查结果" in result.details["response"]
  101. assert result.error_message is None
  102. assert result.execution_time is not None
  103. assert result.execution_time > 0
  104. # 验证 API 调用
  105. mock_create.assert_called_once()
  106. call_kwargs = mock_create.call_args.kwargs
  107. assert call_kwargs["model"] == "qwen3-30b"
  108. assert call_kwargs["temperature"] == 0.7
  109. assert call_kwargs["max_tokens"] == 2000
  110. # 验证进度管理器被调用
  111. mock_state["progress_manager"].update_stage_progress.assert_called()
  112. @pytest.mark.asyncio
  113. async def test_check_semantic_logic_without_state(
  114. self,
  115. reviewer,
  116. sample_review_content
  117. ):
  118. """测试没有状态字典的语义逻辑检查"""
  119. mock_response = MagicMock()
  120. mock_response.choices = [MagicMock()]
  121. mock_response.choices[0].message.content = "审查通过"
  122. mock_prompt_template = MagicMock()
  123. mock_message = MagicMock()
  124. mock_message.type = "system"
  125. mock_message.content = "系统提示"
  126. mock_prompt_template.format_messages.return_value = [mock_message]
  127. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  128. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  129. mock_create.return_value = mock_response
  130. mock_get_prompt.return_value = mock_prompt_template
  131. result = await reviewer.check_semantic_logic(
  132. trace_id="test_trace_002",
  133. review_content=sample_review_content,
  134. state=None,
  135. stage_name=None
  136. )
  137. assert result.success is True
  138. assert result.details["name"] == "semantic_check"
  139. @pytest.mark.asyncio
  140. async def test_check_semantic_logic_api_error(
  141. self,
  142. reviewer,
  143. sample_review_content,
  144. mock_state
  145. ):
  146. """测试 API 调用失败场景"""
  147. mock_prompt_template = MagicMock()
  148. mock_message = MagicMock()
  149. mock_message.type = "user"
  150. mock_message.content = "测试内容"
  151. mock_prompt_template.format_messages.return_value = [mock_message]
  152. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  153. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  154. mock_create.side_effect = Exception("API连接失败")
  155. mock_get_prompt.return_value = mock_prompt_template
  156. result = await reviewer.check_semantic_logic(
  157. trace_id="test_trace_003",
  158. review_content=sample_review_content,
  159. state=mock_state,
  160. stage_name="basic_check"
  161. )
  162. # 验证错误处理
  163. assert isinstance(result, ReviewResult)
  164. assert result.success is False
  165. assert result.error_message is not None
  166. assert "API连接失败" in result.error_message
  167. assert result.execution_time is not None
  168. # 验证错误通知被发送
  169. mock_state["progress_manager"].update_stage_progress.assert_called()
  170. @pytest.mark.asyncio
  171. async def test_check_semantic_logic_empty_content(
  172. self,
  173. reviewer,
  174. mock_state
  175. ):
  176. """测试空内容场景"""
  177. mock_response = MagicMock()
  178. mock_response.choices = [MagicMock()]
  179. mock_response.choices[0].message.content = "内容为空,无法审查"
  180. mock_prompt_template = MagicMock()
  181. mock_message = MagicMock()
  182. mock_message.type = "user"
  183. mock_message.content = ""
  184. mock_prompt_template.format_messages.return_value = [mock_message]
  185. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  186. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  187. mock_create.return_value = mock_response
  188. mock_get_prompt.return_value = mock_prompt_template
  189. result = await reviewer.check_semantic_logic(
  190. trace_id="test_trace_004",
  191. review_content="",
  192. state=mock_state,
  193. stage_name="basic_check"
  194. )
  195. assert result.success is True
  196. assert result.details["name"] == "semantic_check"
  197. @pytest.mark.asyncio
  198. async def test_check_semantic_logic_with_references(
  199. self,
  200. reviewer,
  201. sample_review_content,
  202. sample_review_references
  203. ):
  204. """测试带参考信息的语义逻辑检查"""
  205. mock_response = MagicMock()
  206. mock_response.choices = [MagicMock()]
  207. mock_response.choices[0].message.content = "根据参考标准,内容符合要求"
  208. mock_prompt_template = MagicMock()
  209. mock_message = MagicMock()
  210. mock_message.type = "user"
  211. mock_message.content = "审查内容"
  212. mock_prompt_template.format_messages.return_value = [mock_message]
  213. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  214. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  215. mock_create.return_value = mock_response
  216. mock_get_prompt.return_value = mock_prompt_template
  217. result = await reviewer.check_semantic_logic(
  218. trace_id="test_trace_005",
  219. review_content=sample_review_content,
  220. review_references=sample_review_references
  221. )
  222. assert result.success is True
  223. assert "参考标准" in result.details["response"]
  224. # 验证提示词模板被正确调用
  225. mock_get_prompt.assert_called_once()
  226. call_kwargs = mock_get_prompt.call_args.kwargs
  227. assert call_kwargs["review_content"] == sample_review_content
  228. assert call_kwargs["review_references"] == sample_review_references
  229. @pytest.mark.asyncio
  230. async def test_message_format_conversion(self, reviewer, sample_review_content):
  231. """测试消息格式转换"""
  232. mock_response = MagicMock()
  233. mock_response.choices = [MagicMock()]
  234. mock_response.choices[0].message.content = "测试响应"
  235. # 模拟不同类型的消息
  236. mock_prompt_template = MagicMock()
  237. mock_system_msg = MagicMock()
  238. mock_system_msg.type = "system"
  239. mock_system_msg.content = "系统提示"
  240. mock_user_msg = MagicMock()
  241. mock_user_msg.type = "user"
  242. mock_user_msg.content = "用户输入"
  243. mock_unknown_msg = MagicMock()
  244. mock_unknown_msg.type = "unknown"
  245. mock_unknown_msg.content = "未知类型"
  246. mock_prompt_template.format_messages.return_value = [
  247. mock_system_msg,
  248. mock_user_msg,
  249. mock_unknown_msg
  250. ]
  251. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  252. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  253. mock_create.return_value = mock_response
  254. mock_get_prompt.return_value = mock_prompt_template
  255. result = await reviewer.check_semantic_logic(
  256. trace_id="test_trace_006",
  257. review_content=sample_review_content
  258. )
  259. # 验证消息格式转换
  260. call_kwargs = mock_create.call_args.kwargs
  261. messages = call_kwargs["messages"]
  262. assert len(messages) == 3
  263. assert messages[0]["role"] == "system"
  264. assert messages[0]["content"] == "系统提示"
  265. assert messages[1]["role"] == "user"
  266. assert messages[1]["content"] == "用户输入"
  267. assert messages[2]["role"] == "user" # unknown类型应转为user
  268. assert messages[2]["content"] == "未知类型"
  269. @pytest.mark.asyncio
  270. async def test_execution_time_tracking(self, reviewer, sample_review_content):
  271. """测试执行时间跟踪"""
  272. mock_response = MagicMock()
  273. mock_response.choices = [MagicMock()]
  274. mock_response.choices[0].message.content = "测试"
  275. mock_prompt_template = MagicMock()
  276. mock_message = MagicMock()
  277. mock_message.type = "user"
  278. mock_message.content = "测试"
  279. mock_prompt_template.format_messages.return_value = [mock_message]
  280. async def slow_api_call(*args, **kwargs):
  281. """模拟慢速API调用"""
  282. await asyncio.sleep(0.1) # 模拟100ms延迟
  283. return mock_response
  284. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  285. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  286. mock_create.side_effect = slow_api_call
  287. mock_get_prompt.return_value = mock_prompt_template
  288. result = await reviewer.check_semantic_logic(
  289. trace_id="test_trace_007",
  290. review_content=sample_review_content
  291. )
  292. # 验证执行时间大于100ms
  293. assert result.execution_time >= 0.1
  294. assert result.execution_time < 1.0 # 应该不会太长
  295. class TestIntegration:
  296. """集成测试类"""
  297. @pytest.mark.asyncio
  298. @pytest.mark.integration
  299. async def test_full_workflow(self):
  300. """测试完整工作流程(需要实际API可用)"""
  301. # 注意:此测试需要实际的API服务可用
  302. # 在CI/CD环境中可能需要跳过
  303. pytest.skip("需要实际API服务,跳过集成测试")
  304. reviewer = SemanticLogicReviewer()
  305. result = await reviewer.check_semantic_logic(
  306. trace_id="integration_test_001",
  307. review_content="测试施工方案内容",
  308. review_references="测试参考标准"
  309. )
  310. assert isinstance(result, ReviewResult)
  311. assert result.execution_time is not None
  312. class TestEdgeCases:
  313. """边界情况测试类"""
  314. @pytest.mark.asyncio
  315. async def test_very_long_content(self, reviewer):
  316. """测试超长内容"""
  317. long_content = "测试内容 " * 10000 # 非常长的内容
  318. mock_response = MagicMock()
  319. mock_response.choices = [MagicMock()]
  320. mock_response.choices[0].message.content = "内容过长"
  321. mock_prompt_template = MagicMock()
  322. mock_message = MagicMock()
  323. mock_message.type = "user"
  324. mock_message.content = long_content
  325. mock_prompt_template.format_messages.return_value = [mock_message]
  326. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  327. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  328. mock_create.return_value = mock_response
  329. mock_get_prompt.return_value = mock_prompt_template
  330. result = await reviewer.check_semantic_logic(
  331. trace_id="test_edge_001",
  332. review_content=long_content
  333. )
  334. assert result.success is True
  335. @pytest.mark.asyncio
  336. async def test_special_characters(self, reviewer):
  337. """测试特殊字符"""
  338. special_content = "测试内容包含特殊字符:@#$%^&*(){}[]|\\:;\"'<>,.?/~`"
  339. mock_response = MagicMock()
  340. mock_response.choices = [MagicMock()]
  341. mock_response.choices[0].message.content = "特殊字符处理正常"
  342. mock_prompt_template = MagicMock()
  343. mock_message = MagicMock()
  344. mock_message.type = "user"
  345. mock_message.content = special_content
  346. mock_prompt_template.format_messages.return_value = [mock_message]
  347. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  348. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  349. mock_create.return_value = mock_response
  350. mock_get_prompt.return_value = mock_prompt_template
  351. result = await reviewer.check_semantic_logic(
  352. trace_id="test_edge_002",
  353. review_content=special_content
  354. )
  355. assert result.success is True
  356. @pytest.mark.asyncio
  357. async def test_unicode_content(self, reviewer):
  358. """测试Unicode字符"""
  359. unicode_content = "测试内容包含各种语言:English, 中文, 日本語, 한국어, Русский, العربية, 🚀🎉"
  360. mock_response = MagicMock()
  361. mock_response.choices = [MagicMock()]
  362. mock_response.choices[0].message.content = "Unicode处理正常"
  363. mock_prompt_template = MagicMock()
  364. mock_message = MagicMock()
  365. mock_message.type = "user"
  366. mock_message.content = unicode_content
  367. mock_prompt_template.format_messages.return_value = [mock_message]
  368. with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
  369. patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
  370. mock_create.return_value = mock_response
  371. mock_get_prompt.return_value = mock_prompt_template
  372. result = await reviewer.check_semantic_logic(
  373. trace_id="test_edge_003",
  374. review_content=unicode_content
  375. )
  376. assert result.success is True
  377. if __name__ == "__main__":
  378. # 运行测试
  379. pytest.main([__file__, "-v", "-s", "--tb=short"])