test_query_order_exact_tool.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import inspect
  2. import unittest
  3. from io import StringIO
  4. import app as gateway_app_module
  5. from app import GatewayApp, parse_int_list
  6. from public_gateway import PublicGatewayApp
  7. from tools.query_order_exact import QueryOrderExactTool
  8. class RecordingApiClient:
  9. def __init__(self):
  10. self.last_call = None
  11. def call_tool(self, tool_code, route_path, payload, request_id):
  12. self.last_call = {
  13. 'tool_code': tool_code,
  14. 'route_path': route_path,
  15. 'payload': payload,
  16. 'request_id': request_id,
  17. }
  18. return {'code': 'MCP_0000'}
  19. def list_enabled_tools(self):
  20. return {
  21. 'code': 'MCP_0000',
  22. 'data': {'tool_codes': ['query_order_exact']},
  23. }
  24. class PublicSessionStore:
  25. def get(self, gateway_session_id):
  26. if gateway_session_id == 'GWS_test':
  27. return {'mcp_token': 'MT_test'}
  28. return None
  29. class PublicApiClient:
  30. def list_enabled_tools(self, token):
  31. return {
  32. 'code': 'MCP_0000',
  33. 'data': {'tool_codes': ['query_order_exact']},
  34. }
  35. class QueryOrderExactToolTest(unittest.TestCase):
  36. def test_local_and_public_gateways_register_tool(self):
  37. local_names = {
  38. tool['name']
  39. for tool in GatewayApp(api_client=RecordingApiClient()).list_tools()
  40. }
  41. public_names = {
  42. tool['name']
  43. for tool in PublicGatewayApp(
  44. PublicSessionStore(),
  45. PublicApiClient(),
  46. ).list_tools('GWS_test')
  47. }
  48. self.assertIn('query_order_exact', local_names)
  49. self.assertIn('query_order_exact', public_names)
  50. def test_parse_int_list_accepts_comma_separated_ids(self):
  51. self.assertEqual([9, 12], parse_int_list('9, 12'))
  52. self.assertEqual([], parse_int_list(''))
  53. def test_metadata_exposes_all_exact_filters(self):
  54. schema = QueryOrderExactTool().metadata()['input_schema']
  55. for field in (
  56. 'order_number', 'reference_number', 'tracking_number',
  57. 'outbound_number', 'container_code', 'so_number', 'shipment_id',
  58. 'receiver_country', 'product_ids', 'customer_ids', 'sales_id',
  59. 'warehouse_ids', 'department_id', 'inbound_date_start',
  60. 'inbound_date_end', 'outbound_date_start', 'outbound_date_end',
  61. 'page', 'limit',
  62. ):
  63. self.assertIn(field, schema['properties'])
  64. for field in (
  65. 'order_numbers', 'reference_numbers', 'tracking_numbers',
  66. 'outbound_numbers', 'container_codes', 'so_numbers',
  67. ):
  68. self.assertIn(field, schema['properties'])
  69. self.assertEqual('array', schema['properties'][field]['type'])
  70. self.assertEqual(
  71. 'string',
  72. schema['properties'][field]['items']['type'],
  73. )
  74. self.assertEqual(100, schema['properties']['limit']['maximum'])
  75. def test_metadata_guides_ai_to_explicit_fields_without_fallback(self):
  76. metadata = QueryOrderExactTool().metadata()
  77. description = metadata['description']
  78. properties = metadata['input_schema']['properties']
  79. for phrase in (
  80. '单号类型不明确', '先询问用户', '不得改用其他字段',
  81. '同一字段使用 IN', '不同字段使用 AND', '排舱单号',
  82. '所有单号格式均为开放格式',
  83. '只能根据用户明确说出的业务类型',
  84. '前缀、长度、字符组合或示例',
  85. ):
  86. self.assertIn(phrase, description)
  87. self.assertNotIn('出库单号、柜号', description)
  88. single_number_fields = (
  89. 'order_number', 'reference_number', 'tracking_number',
  90. 'outbound_number', 'container_code', 'so_number', 'shipment_id',
  91. )
  92. batch_fields = (
  93. 'order_numbers', 'reference_numbers', 'tracking_numbers',
  94. 'outbound_numbers', 'container_codes', 'so_numbers',
  95. )
  96. for field in single_number_fields:
  97. self.assertTrue(properties[field]['description'])
  98. self.assertIsInstance(properties[field]['examples'][0], str)
  99. self.assertIn('用户明确', properties[field]['description'])
  100. for field in batch_fields:
  101. self.assertTrue(properties[field]['description'])
  102. self.assertIsInstance(properties[field]['examples'][0], list)
  103. self.assertGreater(len(properties[field]['examples'][0]), 1)
  104. self.assertIn('用户明确', properties[field]['description'])
  105. self.assertIn('系统订单号', properties['order_number']['description'])
  106. self.assertIn(
  107. '客户参考号',
  108. properties['reference_number']['description'],
  109. )
  110. self.assertIn('承运商', properties['tracking_number']['description'])
  111. outbound_description = properties['outbound_number']['description']
  112. self.assertIn('排舱单号', outbound_description)
  113. self.assertIn('不是海外仓出库单号', outbound_description)
  114. self.assertNotIn('以 PC 开头', outbound_description)
  115. self.assertNotIn(
  116. '每个号码均以 PC 开头',
  117. properties['outbound_numbers']['description'],
  118. )
  119. so_description = properties['so_number']['description']
  120. self.assertIn('格式不固定', so_description)
  121. self.assertIn('用户明确', so_description)
  122. self.assertEqual(
  123. '97964454',
  124. properties['so_number']['examples'][0],
  125. )
  126. self.assertEqual(
  127. ['97964454', 'OOLU12345678'],
  128. properties['so_numbers']['examples'][0],
  129. )
  130. self.assertIn('柜号', properties['container_code']['description'])
  131. self.assertIn('Shipping Order', so_description)
  132. for field in (
  133. 'receiver_country', 'product_ids', 'customer_ids',
  134. 'sales_id', 'warehouse_ids', 'department_id',
  135. 'inbound_date_start', 'inbound_date_end',
  136. 'outbound_date_start', 'outbound_date_end', 'page', 'limit',
  137. ):
  138. self.assertTrue(properties[field]['description'])
  139. def test_call_forwards_normalized_number_arrays(self):
  140. tool = QueryOrderExactTool(api_client=RecordingApiClient())
  141. self.assertIn('order_numbers', inspect.signature(tool.call).parameters)
  142. tool.call(
  143. order_number=' A ',
  144. order_numbers=[' B ', 'A'],
  145. tracking_numbers=['T1', 'T2'],
  146. )
  147. self.assertEqual('A', tool.api_client.last_call['payload']['order_number'])
  148. self.assertEqual(
  149. ['B', 'A'],
  150. tool.api_client.last_call['payload']['order_numbers'],
  151. )
  152. self.assertEqual(
  153. ['T1', 'T2'],
  154. tool.api_client.last_call['payload']['tracking_numbers'],
  155. )
  156. def test_call_rejects_invalid_number_arrays(self):
  157. tool = QueryOrderExactTool(api_client=RecordingApiClient())
  158. self.assertIn('order_numbers', inspect.signature(tool.call).parameters)
  159. invalid_values = ('A,B', [1], [''], [' '], ['X' * 101])
  160. for values in invalid_values:
  161. with self.subTest(values=values):
  162. with self.assertRaises(ValueError):
  163. tool.call(order_numbers=values)
  164. with self.assertRaisesRegex(ValueError, 'must not exceed 200'):
  165. tool.call(
  166. order_numbers=['O{0}'.format(i) for i in range(200)],
  167. so_numbers=['S1'],
  168. )
  169. def test_call_normalizes_exact_filters_and_id_arrays(self):
  170. client = RecordingApiClient()
  171. tool = QueryOrderExactTool(api_client=client)
  172. result = tool.call(
  173. order_number=' USC001 ',
  174. customer_ids=['9', 9, 0, -1, '12'],
  175. warehouse_ids=['-1', '5'],
  176. sales_id='7',
  177. page=0,
  178. limit=200,
  179. request_id='rq_exact',
  180. )
  181. self.assertEqual({'code': 'MCP_0000'}, result)
  182. self.assertEqual('query_order_exact', client.last_call['tool_code'])
  183. self.assertEqual('/mcp/tools/queryOrderExact', client.last_call['route_path'])
  184. self.assertEqual('rq_exact', client.last_call['request_id'])
  185. self.assertEqual({
  186. 'order_number': 'USC001',
  187. 'customer_ids': [9, 12],
  188. 'sales_id': 7,
  189. 'warehouse_ids': [-1, 5],
  190. 'page': 1,
  191. 'limit': 100,
  192. }, client.last_call['payload'])
  193. def test_call_requires_one_business_filter(self):
  194. tool = QueryOrderExactTool(api_client=RecordingApiClient())
  195. with self.assertRaisesRegex(ValueError, 'at least one exact order filter'):
  196. tool.call(page=1, limit=20)
  197. def test_call_requires_api_client(self):
  198. with self.assertRaisesRegex(RuntimeError, 'api client is required'):
  199. QueryOrderExactTool().call(order_number='USC001')
  200. def test_call_rejects_non_positive_scalar_ids(self):
  201. tool = QueryOrderExactTool(api_client=RecordingApiClient())
  202. for field in ('sales_id', 'department_id'):
  203. with self.subTest(field=field):
  204. with self.assertRaisesRegex(ValueError, 'must be greater than 0'):
  205. tool.call(order_number='O1', **{field: 0})
  206. def test_normalize_int_list_accepts_comma_separated_values(self):
  207. self.assertEqual(
  208. [9, 12],
  209. QueryOrderExactTool._normalize_int_list('9, 12, 9, 0'),
  210. )
  211. def test_normalize_string_list_deduplicates_values(self):
  212. self.assertEqual(
  213. ['A', 'B'],
  214. QueryOrderExactTool._normalize_string_list(
  215. ['A', 'B', 'A'],
  216. 'numbers',
  217. ),
  218. )
  219. def test_cli_forwards_exact_fields_and_id_lists(self):
  220. client = RecordingApiClient()
  221. output = StringIO()
  222. exit_code = GatewayApp(api_client=client).run_cli([
  223. 'call',
  224. '--tool', 'query_order_exact',
  225. '--order-number', 'USC001',
  226. '--customer-ids', '9,12',
  227. '--warehouse-ids=-1,5',
  228. '--inbound-date-start', '2026-07-01',
  229. ], stdout=output)
  230. self.assertEqual(0, exit_code)
  231. self.assertEqual({
  232. 'order_number': 'USC001',
  233. 'customer_ids': [9, 12],
  234. 'warehouse_ids': [-1, 5],
  235. 'inbound_date_start': '2026-07-01',
  236. 'page': 1,
  237. 'limit': 20,
  238. }, client.last_call['payload'])
  239. def test_cli_forwards_batch_number_lists(self):
  240. self.assertTrue(hasattr(gateway_app_module, 'parse_string_list'))
  241. self.assertEqual(
  242. ['A', 'B'],
  243. gateway_app_module.parse_string_list(' A, B, A '),
  244. )
  245. client = RecordingApiClient()
  246. output = StringIO()
  247. exit_code = GatewayApp(api_client=client).run_cli([
  248. 'call',
  249. '--tool', 'query_order_exact',
  250. '--order-numbers', 'A,B,A',
  251. '--tracking-numbers', 'T1,T2',
  252. ], stdout=output)
  253. self.assertEqual(0, exit_code)
  254. self.assertEqual(['A', 'B'], client.last_call['payload']['order_numbers'])
  255. self.assertEqual(
  256. ['T1', 'T2'],
  257. client.last_call['payload']['tracking_numbers'],
  258. )
  259. if __name__ == '__main__':
  260. unittest.main()