test_review_fixes.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. """业务流程回归测试:使用模拟模型、代理和数据库,不访问外部服务。"""
  2. import asyncio
  3. import json
  4. import sys
  5. import unittest
  6. from pathlib import Path
  7. from unittest.mock import AsyncMock, Mock
  8. from langchain_core.messages import AIMessage
  9. from langchain_core.runnables import RunnableLambda
  10. from chat_service import ChatService
  11. from memory import ConversationMemory
  12. class FlowTests(unittest.IsolatedAsyncioTestCase):
  13. def service(self):
  14. service = ChatService.__new__(ChatService)
  15. service.memory = ConversationMemory()
  16. service.messages = []
  17. service.conversation_history = ""
  18. service.llm = RunnableLambda(lambda _: AIMessage(content="汇总结果"))
  19. return service
  20. async def test_city_stream_supplies_weather_even_when_unavailable(self):
  21. for weather in ("成都小雨", ""):
  22. service = self.service()
  23. prompts = []
  24. def model(prompt):
  25. prompts.append(prompt.to_string())
  26. return AIMessage(content="接待建议")
  27. service.llm = RunnableLambda(model)
  28. service._fetch_weather_for_city_service = AsyncMock(return_value=weather)
  29. service.intent_agent = Mock(
  30. return_value=(["city_service"], {"city_service": "成都接待"}, "")
  31. )
  32. reply = "".join([x async for x in service.chat_stream("成都接待")])
  33. self.assertEqual(reply, "接待建议")
  34. self.assertIn("目的地天气:" + weather, prompts[0])
  35. service._fetch_weather_for_city_service.assert_awaited_once_with("成都接待")
  36. async def test_multi_intent_stream_matches_saved_reply(self):
  37. service = self.service()
  38. service.intent_agent = Mock(
  39. return_value=(
  40. ["weather", "train"],
  41. {"weather": "天气", "train": "高铁"},
  42. "",
  43. )
  44. )
  45. async def call(intent, query):
  46. yield query
  47. service._call_agent_intent_stream = call
  48. reply = "".join([x async for x in service.chat_stream("天气和高铁")])
  49. self.assertEqual(reply, "天气\n\n高铁")
  50. self.assertEqual(service.messages[-1]["content"], reply)
  51. async def test_dependencies_reach_later_agent_in_both_modes(self):
  52. for streaming in (False, True):
  53. service = self.service()
  54. calls = []
  55. async def call(intent, query):
  56. calls.append((intent, query))
  57. return "航班MU123,800元" if intent == "flight" else "模拟预订"
  58. service._call_agent_intent = call
  59. # 刻意传入逆序计划,验证实际执行仍满足依赖。
  60. steps = [
  61. {
  62. "step": 2,
  63. "intent": "order",
  64. "action": "订最便宜的航班",
  65. "depends_on": 1,
  66. },
  67. {"step": 1, "intent": "flight", "action": "查询航班", "depends_on": 0},
  68. ]
  69. queries = {"flight": "查询航班", "order": "预订查询结果"}
  70. if streaming:
  71. _ = [x async for x in service._react_loop_stream(steps, queries)]
  72. else:
  73. await service.react_loop(steps, queries)
  74. self.assertEqual([x[0] for x in calls], ["flight", "order"])
  75. payload = json.loads(calls[1][1])
  76. self.assertEqual(
  77. payload["previous_results"][0]["result"], "航班MU123,800元"
  78. )
  79. self.assertEqual(payload["action"], "订最便宜的航班")
  80. async def test_invalid_plans_execute_nothing(self):
  81. for steps in (
  82. [{"step": 1, "depends_on": 2}],
  83. [{"step": 1, "depends_on": 2}, {"step": 2, "depends_on": 1}],
  84. [{"step": 1}, {"step": 1}],
  85. ):
  86. service = self.service()
  87. service.execute_step = AsyncMock()
  88. with self.assertRaises(ValueError):
  89. await service._execute_plan(steps, {})
  90. service.execute_step.assert_not_called()
  91. async def test_independent_steps_still_run_concurrently(self):
  92. service = self.service()
  93. started = set()
  94. ready = asyncio.Event()
  95. async def execute(step, queries, previous):
  96. started.add(step["step"])
  97. if len(started) == 2:
  98. ready.set()
  99. await asyncio.wait_for(ready.wait(), timeout=1)
  100. return "ok"
  101. service.execute_step = execute
  102. result = await service._execute_plan([{"step": 1}, {"step": 2}], {})
  103. self.assertEqual(len(result), 2)
  104. def test_complex_goes_to_planning(self):
  105. self.assertFalse(self.service()._should_skip_planning(["complex"]))
  106. self.assertTrue(self.service()._should_skip_planning(["weather"]))
  107. class ProfileTests(unittest.TestCase):
  108. def test_replace_deletes_and_merge_keeps_other_keys(self):
  109. memory = ConversationMemory()
  110. memory.update_profile({"座位": "二等座", "舱位": "经济舱"})
  111. memory.update_profile({"座位": "一等座"})
  112. self.assertIn("舱位", memory.user_profile)
  113. memory.update_profile({"座位": "一等座"}, replace=True)
  114. self.assertEqual(memory.user_profile, {"座位": "一等座"})
  115. memory.update_profile({}, replace=True)
  116. self.assertEqual(memory.user_profile, {})
  117. def test_database_replace_deletes_stale_rows_and_commits(self):
  118. memory = ConversationMemory()
  119. db = Mock()
  120. memory.set_db_connection(db)
  121. memory.update_profile({"座位": "二等座"}, replace=True)
  122. calls = db.cursor.return_value.execute.call_args_list
  123. self.assertEqual(calls[0].args, ("DELETE FROM user_profiles",))
  124. self.assertEqual(calls[1].args[1], ("座位", "二等座", "二等座"))
  125. db.commit.assert_called_once()
  126. memory.update_profile({}, replace=True)
  127. self.assertEqual(
  128. db.cursor.return_value.execute.call_args.args,
  129. ("DELETE FROM user_profiles",),
  130. )
  131. def test_failed_replace_rolls_back_memory_and_database(self):
  132. memory = ConversationMemory()
  133. memory.user_profile = {"座位": "二等座"}
  134. db = Mock()
  135. db.cursor.return_value.execute.side_effect = RuntimeError("模拟数据库失败")
  136. memory.set_db_connection(db)
  137. with self.assertRaises(RuntimeError):
  138. memory.update_profile({}, replace=True)
  139. self.assertEqual(memory.user_profile, {"座位": "二等座"})
  140. db.rollback.assert_called_once()
  141. db.commit.assert_not_called()
  142. class SimulationTests(unittest.TestCase):
  143. def test_all_booking_tools_explicitly_report_simulation_without_database(self):
  144. sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
  145. from mcp_server.mcp_ticket_server import TicketService
  146. from mcp_server.mcp_trip_server import TripService
  147. ticket = TicketService.__new__(TicketService)
  148. trip = TripService.__new__(TripService)
  149. replies = [
  150. ticket.order_train("2026-10-01", "G1", "二等座", 1),
  151. ticket.order_flight("2026-10-01", "MU123", "经济舱", 1),
  152. ticket.order_event("2026-10-01", "活动", "场馆", "普通票", 1),
  153. trip.order_car_rental("2026-10-01", "SUV", 1),
  154. trip.order_meeting_package("2026-10-01", "套餐", 1),
  155. trip.order_insurance("综合型", "2026-10-01", 1),
  156. ]
  157. for reply in replies:
  158. self.assertIn("模拟", reply)
  159. self.assertIn("未实际下单", reply)
  160. if __name__ == "__main__":
  161. unittest.main()