test_export_receivable_cost_list_tool.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import io
  2. import unittest
  3. from app import GatewayApp
  4. from public_gateway import PublicGatewayApp
  5. from services.output_presenter import OutputPresenter
  6. from tools.export_receivable_cost_list import ExportReceivableCostListTool
  7. class RecordingApiClient:
  8. def __init__(self):
  9. self.calls = []
  10. def list_enabled_tools(self, request_id=''):
  11. return {
  12. 'code': 'MCP_0000',
  13. 'data': {'tool_codes': ['export_receivable_cost_list']},
  14. }
  15. def call_tool(self, tool_code, route_path, payload, request_id):
  16. self.calls.append((tool_code, route_path, payload, request_id))
  17. return {
  18. 'code': 'MCP_0000',
  19. 'data': {
  20. 'task_ref': 'mexp_test',
  21. 'status': 'queued',
  22. 'retry_after_seconds': 10,
  23. },
  24. }
  25. class ExportReceivableCostListToolTest(unittest.TestCase):
  26. def test_schema_is_closed_and_matches_list_filters_without_pagination(self):
  27. metadata = ExportReceivableCostListTool().metadata()
  28. schema = metadata['input_schema']
  29. self.assertEqual('export_receivable_cost_list', metadata['name'])
  30. self.assertFalse(schema['additionalProperties'])
  31. self.assertEqual({
  32. 'reference_numbers', 'tracking_numbers', 'order_numbers',
  33. 'bill_numbers', 'business_date_start', 'business_date_end',
  34. 'customer_id', 'sub_customer_id', 'billing_status',
  35. 'verification_status', 'document_type', 'cost_type_id',
  36. }, set(schema['properties']))
  37. self.assertNotIn('page', schema['properties'])
  38. self.assertNotIn('limit', schema['properties'])
  39. self.assertNotIn('ids', schema['properties'])
  40. self.assertIn('明确要求导出应收费用单', metadata['description'])
  41. self.assertIn('query_receivable_cost_list', metadata['description'])
  42. self.assertIn('query_export_task', metadata['description'])
  43. self.assertIn('禁止在一次调用内轮询', metadata['description'])
  44. def test_call_forwards_exact_filters(self):
  45. client = RecordingApiClient()
  46. result = ExportReceivableCostListTool(client).call(
  47. order_numbers=[' ORDER-1 '],
  48. billing_status=0,
  49. )
  50. self.assertEqual('MCP_0000', result['code'])
  51. self.assertEqual(
  52. (
  53. 'export_receivable_cost_list',
  54. '/mcp/tools/exportReceivableCostList',
  55. {
  56. 'order_numbers': ['ORDER-1'],
  57. 'billing_status': 0,
  58. },
  59. 'rq_export_receivable_cost_list',
  60. ),
  61. client.calls[0],
  62. )
  63. def test_call_validation_boundaries(self):
  64. tool = ExportReceivableCostListTool()
  65. with self.assertRaisesRegex(RuntimeError, 'api client is required'):
  66. tool.call(order_numbers=['O1'])
  67. client = RecordingApiClient()
  68. tool = ExportReceivableCostListTool(client)
  69. with self.assertRaisesRegex(ValueError, 'non-empty list'):
  70. tool.call(order_numbers='O1')
  71. with self.assertRaisesRegex(ValueError, 'must be strings'):
  72. tool.call(order_numbers=[1])
  73. with self.assertRaisesRegex(ValueError, '1 to 100 chars'):
  74. tool.call(order_numbers=[''])
  75. with self.assertRaisesRegex(ValueError, '1 to 100 chars'):
  76. tool.call(order_numbers=['x' * 101])
  77. with self.assertRaisesRegex(ValueError, 'at most 200'):
  78. tool.call(order_numbers=['n{0}'.format(i) for i in range(201)])
  79. with self.assertRaisesRegex(ValueError, 'both start and end'):
  80. tool.call(business_date_start='2026-07-01')
  81. with self.assertRaisesRegex(ValueError, 'within 31 days'):
  82. tool.call(
  83. business_date_start='2026-07-01',
  84. business_date_end='2026-08-01',
  85. )
  86. with self.assertRaisesRegex(ValueError, 'number filters or a business'):
  87. tool.call()
  88. with self.assertRaisesRegex(ValueError, 'requires customer_id'):
  89. tool.call(order_numbers=['O1'], sub_customer_id=9)
  90. ExportReceivableCostListTool(client).call(
  91. order_numbers=['ORDER-1', ' ORDER-1 '],
  92. )
  93. self.assertEqual({'order_numbers': ['ORDER-1']}, client.calls[-1][2])
  94. ExportReceivableCostListTool(client).call(
  95. business_date_start='2026-07-01',
  96. business_date_end='2026-07-31',
  97. customer_id=7,
  98. sub_customer_id=9,
  99. verification_status=-1,
  100. document_type=8,
  101. cost_type_id=12,
  102. )
  103. self.assertEqual(
  104. {
  105. 'business_date_start': '2026-07-01',
  106. 'business_date_end': '2026-07-31',
  107. 'customer_id': 7,
  108. 'sub_customer_id': 9,
  109. 'verification_status': -1,
  110. 'document_type': 8,
  111. 'cost_type_id': 12,
  112. },
  113. client.calls[-1][2],
  114. )
  115. def test_cli_forwards_export_filters(self):
  116. client = RecordingApiClient()
  117. app = GatewayApp(api_client=client)
  118. code = app.run_cli([
  119. 'call', '--tool', 'export_receivable_cost_list',
  120. '--order-numbers', 'ORDER-1',
  121. ], stdout=io.StringIO())
  122. self.assertEqual(0, code)
  123. self.assertEqual({'order_numbers': ['ORDER-1']}, client.calls[-1][2])
  124. code = app.run_cli([
  125. 'call', '--tool', 'export_receivable_cost_list',
  126. '--order-numbers', 'ORDER-1',
  127. '--business-date-start', '2026-07-01',
  128. '--business-date-end', '2026-07-31',
  129. '--customer-id', '7',
  130. '--sub-customer-id', '9',
  131. '--billing-status', '0',
  132. '--verification-status', '-1',
  133. '--document-type', '8',
  134. '--cost-type-id', '12',
  135. ], stdout=io.StringIO())
  136. self.assertEqual(0, code)
  137. self.assertEqual('export_receivable_cost_list', client.calls[-1][0])
  138. self.assertEqual(
  139. {
  140. 'order_numbers': ['ORDER-1'],
  141. 'business_date_start': '2026-07-01',
  142. 'business_date_end': '2026-07-31',
  143. 'customer_id': 7,
  144. 'sub_customer_id': 9,
  145. 'billing_status': 0,
  146. 'verification_status': -1,
  147. 'document_type': 8,
  148. 'cost_type_id': 12,
  149. },
  150. client.calls[-1][2],
  151. )
  152. def test_local_and_public_registries_include_export_tool(self):
  153. local = GatewayApp().registered_tool_names()
  154. public = PublicGatewayApp(None, None).registered_tool_names()
  155. self.assertEqual(local, public)
  156. self.assertEqual(25, len(local))
  157. self.assertIn('export_receivable_cost_list', local)
  158. self.assertEqual(24, len(OutputPresenter.SAFE_TOOLS))
  159. def test_presenter_reuses_queued_export_contract(self):
  160. presented = OutputPresenter().present(
  161. 'export_receivable_cost_list',
  162. {
  163. 'code': 'MCP_0000',
  164. 'data': {
  165. 'task_ref': 'mexp_test',
  166. 'status': 'queued',
  167. 'retry_after_seconds': 10,
  168. },
  169. },
  170. )
  171. self.assertFalse(presented['is_error'])
  172. task = presented['structured_content']['task']
  173. self.assertEqual('queued', task['status'])
  174. self.assertEqual('mexp_test', task['task_ref'])
  175. self.assertEqual(10, task['retry_after_seconds'])
  176. def test_wrong_route_path_fails_and_restore_passes(self):
  177. tool = ExportReceivableCostListTool()
  178. original = tool.route_path
  179. tool.route_path = '/mcp/tools/exportReceivableCostListWrong'
  180. self.assertNotEqual(
  181. '/mcp/tools/exportReceivableCostList',
  182. tool.route_path,
  183. )
  184. tool.route_path = original
  185. self.assertEqual(
  186. '/mcp/tools/exportReceivableCostList',
  187. tool.route_path,
  188. )
  189. if __name__ == '__main__':
  190. unittest.main()