test_order_receivable_cost_details_tool.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import copy
  2. import importlib.util
  3. import io
  4. import json
  5. import os
  6. import unittest
  7. from app import GatewayApp
  8. from public_gateway import PublicGatewayApp
  9. from services.output_presenter import OutputPresenter
  10. class RecordingApiClient:
  11. def __init__(self):
  12. self.calls = []
  13. def list_enabled_tools(self, request_id=''):
  14. return {
  15. 'code': 'MCP_0000',
  16. 'data': {'tool_codes': ['query_order_receivable_cost_details']},
  17. }
  18. def call_tool(self, tool_code, route_path, payload, request_id):
  19. self.calls.append((tool_code, route_path, payload, request_id))
  20. return {'code': 'MCP_0000', 'data': {}}
  21. class OrderReceivableCostDetailsToolTest(unittest.TestCase):
  22. def load_tool_class(self):
  23. path = os.path.join(
  24. os.path.dirname(os.path.dirname(__file__)),
  25. 'tools',
  26. 'query_order_receivable_cost_details.py',
  27. )
  28. self.assertTrue(os.path.isfile(path), '订单应收费用 Gateway 工具尚未实现')
  29. spec = importlib.util.spec_from_file_location('receivable_cost_tool', path)
  30. module = importlib.util.module_from_spec(spec)
  31. spec.loader.exec_module(module)
  32. return module.QueryOrderReceivableCostDetailsTool
  33. def test_closed_schema_requires_explicit_order_number_and_forwards_exactly(self):
  34. tool_class = self.load_tool_class()
  35. client = RecordingApiClient()
  36. metadata = tool_class(client).metadata()
  37. schema = metadata['input_schema']
  38. self.assertEqual('query_order_receivable_cost_details', metadata['name'])
  39. self.assertEqual(['order_number'], schema['required'])
  40. self.assertFalse(schema['additionalProperties'])
  41. self.assertEqual({'order_number', 'page', 'limit'}, set(schema['properties']))
  42. self.assertIn('明确订单号', metadata['description'])
  43. self.assertIn('不得使用参考号', metadata['description'])
  44. self.assertIn('不得模糊', metadata['description'])
  45. result = tool_class(client).call(
  46. order_number='ORD-260001', page=2, limit=30, request_id='rq_cost'
  47. )
  48. self.assertEqual('MCP_0000', result['code'])
  49. self.assertEqual((
  50. 'query_order_receivable_cost_details',
  51. '/mcp/tools/queryOrderReceivableCostDetails',
  52. {'order_number': 'ORD-260001', 'page': 2, 'limit': 30},
  53. 'rq_cost',
  54. ), client.calls[-1])
  55. def test_invalid_arguments_fail_before_backend_call(self):
  56. tool_class = self.load_tool_class()
  57. client = RecordingApiClient()
  58. tool = tool_class(client)
  59. for arguments in (
  60. {}, {'order_number': ''}, {'order_number': ' '},
  61. {'order_number': 123}, {'order_number': True},
  62. {'order_number': 'x' * 101}, {'order_number': 'ORD-1', 'page': 0},
  63. {'order_number': 'ORD-1', 'page': 101},
  64. {'order_number': 'ORD-1', 'limit': False},
  65. {'order_number': 'ORD-1', 'limit': 101},
  66. ):
  67. with self.subTest(arguments=arguments):
  68. with self.assertRaises((TypeError, ValueError)):
  69. tool.call(**arguments)
  70. self.assertEqual([], client.calls)
  71. with self.assertRaises(RuntimeError):
  72. tool_class().call(order_number='ORD-1')
  73. def test_registries_cli_and_payment_records_remain_available(self):
  74. client = RecordingApiClient()
  75. local = GatewayApp(api_client=client)
  76. public = PublicGatewayApp(None, None)
  77. self.assertEqual(local.registered_tool_names(), public.registered_tool_names())
  78. self.assertEqual(22, len(local.registered_tool_names()))
  79. self.assertIn('query_order_receivable_cost_details', local.registered_tool_names())
  80. self.assertIn('query_customer_payment_records', local.registered_tool_names())
  81. stdout = io.StringIO()
  82. local.run_cli([
  83. 'call', '--tool', 'query_order_receivable_cost_details',
  84. '--order-number', 'ORD-260001', '--page', '2', '--limit', '30',
  85. ], stdout=stdout)
  86. self.assertEqual('MCP_0000', json.loads(stdout.getvalue())['code'])
  87. self.assertEqual('ORD-260001', client.calls[-1][2]['order_number'])
  88. with self.assertRaises(ValueError):
  89. local.run_cli([
  90. 'call', '--tool', 'query_order_receivable_cost_details',
  91. ], stdout=io.StringIO())
  92. class OrderReceivableCostDetailsPresenterTest(unittest.TestCase):
  93. KEYS = [
  94. 'order_number', 'customer_name', 'cost_name', 'cost_customer_name',
  95. 'charge_weight', 'unit_price', 'quote_currency',
  96. 'receivable_original_amount', 'receivable_amount_cny',
  97. 'settlement_amount_cny', 'cost_confirmation_status',
  98. 'closing_status', 'verification_status',
  99. ]
  100. HEADERS = [
  101. '订单号', '客户名称', '费用项', '费用客户', '计费重', '计费单价',
  102. '原币币种', '应收原币', '应收金额(CNY)', '结算金额(CNY)',
  103. '费用确认状态', '关账状态', '核销状态',
  104. ]
  105. def payload(self):
  106. return {
  107. 'code': 'MCP_0000',
  108. 'data': {
  109. 'columns': [{'key': key, 'name': key} for key in self.KEYS],
  110. 'records': [{
  111. 'order_number': 'ORD-260001', 'customer_name': '主客户',
  112. 'cost_name': '海运费', 'cost_customer_name': '子客户(SC01)',
  113. 'charge_weight': 12.35, 'unit_price': 8.5,
  114. 'quote_currency': 'USD', 'receivable_original_amount': 10.0,
  115. 'receivable_amount_cny': 72.0, 'settlement_amount_cny': 39.0,
  116. 'cost_confirmation_status': '已确认', 'closing_status': '部分关账',
  117. 'verification_status': '是',
  118. }],
  119. },
  120. 'meta': {'page': 1, 'limit': 20, 'has_more': False},
  121. }
  122. def test_presenter_translates_exact_thirteen_columns(self):
  123. result = OutputPresenter().present(
  124. 'query_order_receivable_cost_details', self.payload()
  125. )
  126. self.assertFalse(result['is_error'])
  127. self.assertEqual(
  128. [{'label': header} for header in self.HEADERS],
  129. result['structured_content']['headers'],
  130. )
  131. self.assertEqual(13, len(result['structured_content']['rows'][0]))
  132. serialized = json.dumps(result, ensure_ascii=False)
  133. for key in self.KEYS:
  134. self.assertNotIn(key, serialized)
  135. def test_unknown_missing_reordered_and_bad_values_fail_closed(self):
  136. cases = []
  137. unknown = self.payload()
  138. unknown['data']['records'][0]['secret'] = 'hidden'
  139. cases.append(unknown)
  140. missing = self.payload()
  141. del missing['data']['records'][0]['cost_name']
  142. cases.append(missing)
  143. reordered = self.payload()
  144. reordered['data']['columns'].reverse()
  145. cases.append(reordered)
  146. bad_columns = self.payload()
  147. bad_columns['data']['columns'] = 'bad'
  148. cases.append(bad_columns)
  149. bad_column = self.payload()
  150. bad_column['data']['columns'][0]['internal'] = True
  151. cases.append(bad_column)
  152. extra_data = self.payload()
  153. extra_data['data']['internal'] = True
  154. cases.append(extra_data)
  155. bad_meta = self.payload()
  156. bad_meta['meta']['total'] = 1
  157. cases.append(bad_meta)
  158. bad_text = self.payload()
  159. bad_text['data']['records'][0]['order_number'] = []
  160. cases.append(bad_text)
  161. for amount in (True, '10.00', float('inf'), float('-inf'), float('nan')):
  162. bad_amount = self.payload()
  163. bad_amount['data']['records'][0]['receivable_amount_cny'] = amount
  164. cases.append(bad_amount)
  165. presenter = OutputPresenter()
  166. for payload in cases:
  167. with self.subTest(payload=payload):
  168. self.assertTrue(presenter.present(
  169. 'query_order_receivable_cost_details', payload
  170. )['is_error'])
  171. def test_payload_factory_does_not_share_nested_state(self):
  172. self.assertEqual(self.payload(), copy.deepcopy(self.payload()))
  173. if __name__ == '__main__':
  174. unittest.main()