test_inbound_receipt_tools.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import unittest
  2. from app import GatewayApp
  3. from public_gateway import PublicGatewayApp
  4. from services.output_presenter import OutputPresenter
  5. from tools.export_inbound_receipt import ExportInboundReceiptTool
  6. from tools.list_inbound_receipt_templates import ListInboundReceiptTemplatesTool
  7. class _Api:
  8. def __init__(self):
  9. self.calls = []
  10. def call_tool(self, tool_name, route_path, payload, request_id):
  11. self.calls.append((tool_name, route_path, payload, request_id))
  12. return {'code': 'MCP_0000', 'data': {}}
  13. class InboundReceiptToolTest(unittest.TestCase):
  14. def test_local_and_public_registries_match(self):
  15. local = set(GatewayApp(api_client=object()).registered_tool_names())
  16. public = set(PublicGatewayApp(object(), object()).registered_tool_names())
  17. self.assertEqual(local, public)
  18. self.assertIn('list_inbound_receipt_templates', local)
  19. self.assertIn('export_inbound_receipt', local)
  20. def test_list_tool_strips_and_dedupes_container_codes(self):
  21. api = _Api()
  22. tool = ListInboundReceiptTemplatesTool(api)
  23. tool.call([' CONT-1 ', 'CONT-1'], request_id='rq_list')
  24. self.assertEqual(
  25. ('list_inbound_receipt_templates', '/mcp/tools/listInboundReceiptTemplates',
  26. {'container_codes': ['CONT-1']}, 'rq_list'),
  27. api.calls[0]
  28. )
  29. self.assertIn('container_codes', tool.metadata()['input_schema']['required'])
  30. def test_export_requires_selected_template_and_does_not_wait(self):
  31. api = _Api()
  32. tool = ExportInboundReceiptTool(api)
  33. description = tool.metadata()['description']
  34. self.assertIn('query_export_task', description)
  35. self.assertIn('不会在本次调用中等待文件生成', description)
  36. tool.call(['CONT-1'], ' FMS-INBOUND-LAX ', request_id='rq_export')
  37. self.assertEqual('FMS-INBOUND-LAX', api.calls[0][2]['template_code'])
  38. with self.assertRaises(ValueError):
  39. tool.call(['CONT-1'], ' ')
  40. def test_presenter_shows_kind_and_passes_container_rejection(self):
  41. presenter = OutputPresenter()
  42. shown = presenter.present('list_inbound_receipt_templates', {
  43. 'code': 'MCP_0000',
  44. 'data': {
  45. 'receipt_kind': '直送入仓单',
  46. 'templates': [
  47. {'name': '直送柜', 'template_code': 'FMS-INBOUND-ZSG'},
  48. ],
  49. },
  50. 'meta': {'request_id': 'rq_ok'},
  51. })
  52. self.assertFalse(shown['is_error'])
  53. self.assertEqual('直送入仓单', shown['structured_content']['receipt_kind'])
  54. self.assertNotIn('id', shown['structured_content']['rows'][0])
  55. rejected = presenter.present('list_inbound_receipt_templates', {
  56. 'code': 'MCP_1601',
  57. 'msg': '这些柜号没有对应的排舱单:NONE-1',
  58. 'data': {},
  59. 'meta': {'request_id': 'rq_missing'},
  60. })
  61. self.assertTrue(rejected['is_error'])
  62. self.assertIn('NONE-1', rejected['text'])
  63. hidden = presenter.present('export_inbound_receipt', {
  64. 'code': 'MCP_1601',
  65. 'msg': 'SQLSTATE select * from fms_outbound',
  66. 'data': {},
  67. 'meta': {'request_id': 'rq_sql'},
  68. })
  69. self.assertNotIn('SQLSTATE', hidden['text'])
  70. self.assertIn('没有可导出的数据', hidden['text'])
  71. def test_container_codes_and_template_code_are_validated(self):
  72. listing = ListInboundReceiptTemplatesTool(api_client=_Api())
  73. exporting = ExportInboundReceiptTool(api_client=_Api())
  74. with self.assertRaises(RuntimeError):
  75. ListInboundReceiptTemplatesTool().call(['CONT-1'])
  76. with self.assertRaises(RuntimeError):
  77. ExportInboundReceiptTool().call(['CONT-1'], 'CODE')
  78. for value in (None, [], ['CONT-1'] * 201, [1], [' '], ['C' * 101]):
  79. with self.assertRaises(ValueError):
  80. listing.clean_container_codes(value)
  81. for value in (None, 1, ' ', 'C' * 101):
  82. with self.assertRaises(ValueError):
  83. exporting.call(['CONT-1'], value)
  84. presenter = OutputPresenter()
  85. ok = {
  86. 'receipt_kind': '非直送入仓单',
  87. 'templates': [{'name': ' 非直送柜 ', 'template_code': 'CODE'}],
  88. }
  89. shown = presenter.present('list_inbound_receipt_templates', {
  90. 'code': 'MCP_0000',
  91. 'data': ok,
  92. 'meta': {'request_id': 'rq_ok'},
  93. })
  94. self.assertFalse(shown['is_error'])
  95. for data in (
  96. dict(ok, extra=1),
  97. dict(ok, receipt_kind='其他'),
  98. dict(ok, templates=[]),
  99. dict(ok, templates=[{'name': '模板'}]),
  100. dict(ok, templates=[{'name': ' ', 'template_code': 'CODE'}]),
  101. dict(ok, templates=[{'name': '模板', 'template_code': 1}]),
  102. ):
  103. result = presenter.present('list_inbound_receipt_templates', {
  104. 'code': 'MCP_0000',
  105. 'data': data,
  106. 'meta': {'request_id': 'rq_bad'},
  107. })
  108. self.assertTrue(result['is_error'])
  109. for message in (
  110. '当前没有可用的入仓单模板',
  111. '所选模板不属于这次柜号对应的入仓单',
  112. '这些柜号没有对应的排舱单:',
  113. '这些柜号没有对应的排舱单:' + ('C' * 101),
  114. '这些柜号没有对应的排舱单:CONT-1、 CONT-2',
  115. '这些柜号没有对应的排舱单:CONT-1、\n',
  116. '这些柜号是直送柜:CONT-1。这些柜号是非直送柜:CONT-2。',
  117. '这些柜号是直送柜: CONT-1。这些柜号是非直送柜:CONT-2。',
  118. ):
  119. result = presenter.present('list_inbound_receipt_templates', {
  120. 'code': 'MCP_1601',
  121. 'msg': message,
  122. 'data': {},
  123. 'meta': {'request_id': 'rq_msg'},
  124. })
  125. if message.startswith('这些柜号是直送柜:CONT-1。') or message in (
  126. '当前没有可用的入仓单模板',
  127. '所选模板不属于这次柜号对应的入仓单',
  128. ):
  129. self.assertIn(message, result['text'])
  130. else:
  131. self.assertNotIn('CONT-1、', result['text'])
  132. if __name__ == '__main__':
  133. unittest.main()