test_arrange_destination_trailer_tool.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import unittest
  2. from app import GatewayApp
  3. from public_gateway import PublicGatewayApp
  4. from services.output_presenter import OutputPresenter
  5. from tools.arrange_destination_trailer import ArrangeDestinationTrailerTool
  6. class RecordingApiClient:
  7. def __init__(self):
  8. self.calls = []
  9. def call_tool(self, name, route_path, payload, request_id):
  10. self.calls.append((name, route_path, payload, request_id))
  11. return {'code': 'MCP_0000', 'data': {}, 'meta': {'request_id': request_id}}
  12. class ArrangeDestinationTrailerToolTest(unittest.TestCase):
  13. def test_metadata_rejects_unknown_fields(self):
  14. schema = ArrangeDestinationTrailerTool().metadata()['input_schema']
  15. self.assertFalse(schema['additionalProperties'])
  16. self.assertEqual(['trailer_provider'], schema['required'])
  17. self.assertNotIn('outbound_id', schema['properties'])
  18. def test_call_requires_a_number_and_posts_the_route(self):
  19. client = RecordingApiClient()
  20. tool = ArrangeDestinationTrailerTool(api_client=client)
  21. with self.assertRaises(ValueError):
  22. tool.call(trailer_provider='甲拖车-A')
  23. tool.call(trailer_provider='甲拖车-A', container_code='CONT1')
  24. self.assertEqual('arrange_destination_trailer', client.calls[0][0])
  25. self.assertEqual('/mcp/tools/arrangeDestinationTrailer', client.calls[0][1])
  26. self.assertEqual(
  27. {'trailer_provider': '甲拖车-A', 'container_code': 'CONT1'},
  28. client.calls[0][2],
  29. )
  30. def test_presenter_returns_only_three_labels(self):
  31. presenter = OutputPresenter()
  32. result = presenter.present('arrange_destination_trailer', {
  33. 'code': 'MCP_0000',
  34. 'data': {
  35. 'trailer_number': 'DET1',
  36. 'bl_number': 'BL1',
  37. 'container_code': 'CONT1',
  38. },
  39. 'meta': {'request_id': 'rq1'},
  40. })
  41. self.assertEqual({
  42. '拖车单号': 'DET1',
  43. '提单号': 'BL1',
  44. '柜号': 'CONT1',
  45. }, result['structured_content'])
  46. self.assertFalse(presenter.present('arrange_destination_trailer', {
  47. 'code': 'MCP_0000',
  48. 'data': {
  49. 'trailer_number': 'DET1',
  50. 'bl_number': 'BL1',
  51. 'container_code': 'CONT1',
  52. 'providers_id': 1,
  53. },
  54. 'meta': {'request_id': 'rq1'},
  55. })['is_error'] is False)
  56. self.assertTrue(presenter.present('arrange_destination_trailer', {
  57. 'code': 'MCP_0000',
  58. 'data': {
  59. 'trailer_number': 1,
  60. 'bl_number': 'BL1',
  61. 'container_code': 'CONT1',
  62. },
  63. 'meta': {'request_id': 'rq1'},
  64. })['is_error'])
  65. def test_call_rejects_bad_values_and_forwards_spoken_fields(self):
  66. tool = ArrangeDestinationTrailerTool()
  67. with self.assertRaises(RuntimeError):
  68. tool.call(trailer_provider='甲拖车-A', container_code='CONT1')
  69. client = RecordingApiClient()
  70. tool = ArrangeDestinationTrailerTool(api_client=client)
  71. with self.assertRaises(ValueError):
  72. tool.call(trailer_provider=' ', container_code='CONT1')
  73. with self.assertRaises(ValueError):
  74. tool.call(trailer_provider='甲拖车-A', container_code=1)
  75. with self.assertRaises(ValueError):
  76. tool.call(trailer_provider='甲拖车-A', bl_number='BL1', is_direct_send='未知')
  77. with self.assertRaises(ValueError):
  78. tool.call(
  79. trailer_provider='甲拖车-A',
  80. bl_number='BL1',
  81. est_pickup_time='2026-10-12',
  82. )
  83. with self.assertRaises(ValueError):
  84. tool.call(
  85. trailer_provider='甲拖车-A',
  86. bl_number='BL1',
  87. order_measures=[{'weight': '1'}],
  88. )
  89. with self.assertRaises(ValueError):
  90. tool.call(
  91. trailer_provider='甲拖车-A',
  92. bl_number='BL1',
  93. order_measures='1',
  94. )
  95. with self.assertRaises(ValueError):
  96. tool.call(
  97. trailer_provider='甲拖车-A',
  98. bl_number='BL1',
  99. order_measures=[1],
  100. )
  101. with self.assertRaises(ValueError):
  102. tool.call(
  103. trailer_provider='甲拖车-A',
  104. bl_number='BL1',
  105. order_measures=[{'order_number': 'SO1'}] * 501,
  106. )
  107. with self.assertRaises(ValueError):
  108. tool.call(trailer_provider='甲' * 201, container_code='CONT1')
  109. with self.assertRaises(ValueError):
  110. tool.call(
  111. trailer_provider='甲拖车-A',
  112. bl_number='BL1',
  113. direct_change_reason='其他',
  114. )
  115. tool.call(
  116. trailer_provider=' 甲拖车-A ',
  117. bl_number=' BL1 ',
  118. container_code=' CONT1 ',
  119. is_direct_send='否',
  120. warehouse_code='LAX1',
  121. warehouse_contact='张三',
  122. warehouse_tel='10086',
  123. follow_up_user='空',
  124. est_pickup_time='2026-10-12 08:00:00',
  125. is_direct_change='是',
  126. direct_change_reason='直送爆仓被拒',
  127. order_measures=[{
  128. 'order_number': 'SO1',
  129. 'weight': '1.5',
  130. 'volume': None,
  131. }],
  132. request_id='rq_case',
  133. )
  134. self.assertEqual({
  135. 'trailer_provider': '甲拖车-A',
  136. 'bl_number': 'BL1',
  137. 'container_code': 'CONT1',
  138. 'is_direct_send': '否',
  139. 'warehouse_code': 'LAX1',
  140. 'warehouse_contact': '张三',
  141. 'warehouse_tel': '10086',
  142. 'follow_up_user': '空',
  143. 'est_pickup_time': '2026-10-12 08:00:00',
  144. 'is_direct_change': '是',
  145. 'direct_change_reason': '直送爆仓被拒',
  146. 'order_measures': [{'order_number': 'SO1', 'weight': '1.5'}],
  147. }, client.calls[-1][2])
  148. self.assertEqual('rq_case', client.calls[-1][3])
  149. def test_presenter_keeps_known_arrange_errors(self):
  150. presenter = OutputPresenter()
  151. result = presenter.present('arrange_destination_trailer', {
  152. 'code': 'MCP_1401',
  153. 'msg': '请填写订单O1的重量和体积',
  154. 'meta': {'request_id': 'rq1'},
  155. })
  156. self.assertEqual('请填写订单O1的重量和体积', result['structured_content']['message'])
  157. self.assertFalse(result['structured_content']['retryable'])
  158. hidden = presenter.present('arrange_destination_trailer', {
  159. 'code': 'MCP_1401',
  160. 'msg': 'SQLSTATE[42S02]',
  161. 'meta': {'request_id': 'rq1'},
  162. })
  163. self.assertEqual('工具参数不正确,请检查后重试', hidden['structured_content']['message'])
  164. def test_registered_on_both_gateways(self):
  165. local = GatewayApp().registered_tool_names()
  166. public = PublicGatewayApp(None, None).registered_tool_names()
  167. self.assertEqual(local, public)
  168. self.assertIn('arrange_destination_trailer', local)
  169. self.assertEqual(51, len(local))
  170. self.assertIn('arrange_destination_trailer', OutputPresenter.SAFE_TOOLS)
  171. if __name__ == '__main__':
  172. unittest.main()