|
@@ -0,0 +1,197 @@
|
|
|
|
|
+import unittest
|
|
|
|
|
+
|
|
|
|
|
+from app import GatewayApp
|
|
|
|
|
+from public_gateway import PublicGatewayApp
|
|
|
|
|
+from tools.export_out_of_province_port_data import (
|
|
|
|
|
+ ExportOutOfProvincePortDataTool,
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class RecordingApiClient:
|
|
|
|
|
+ def __init__(self):
|
|
|
|
|
+ self.last_call = None
|
|
|
|
|
+
|
|
|
|
|
+ def call_tool(self, tool_code, route_path, payload, request_id):
|
|
|
|
|
+ self.last_call = {
|
|
|
|
|
+ 'tool_code': tool_code,
|
|
|
|
|
+ 'route_path': route_path,
|
|
|
|
|
+ 'payload': payload,
|
|
|
|
|
+ 'request_id': request_id,
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'code': 'MCP_0000',
|
|
|
|
|
+ 'data': {'file_url': 'https://files.test/port.zip'},
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def list_enabled_tools(self):
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'code': 'MCP_0000',
|
|
|
|
|
+ 'data': {'tool_codes': ['export_out_of_province_port_data']},
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class PublicSessionStore:
|
|
|
|
|
+ def get(self, gateway_session_id):
|
|
|
|
|
+ if gateway_session_id == 'GWS_test':
|
|
|
|
|
+ return {'mcp_token': 'MT_test'}
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class PublicApiClient:
|
|
|
|
|
+ def __init__(self, enabled=True):
|
|
|
|
|
+ self.enabled = enabled
|
|
|
|
|
+ self.calls = []
|
|
|
|
|
+
|
|
|
|
|
+ def list_enabled_tools(self, token):
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'code': 'MCP_0000',
|
|
|
|
|
+ 'data': {'tool_codes': (
|
|
|
|
|
+ ['export_out_of_province_port_data'] if self.enabled else []
|
|
|
|
|
+ )},
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def call_tool(self, **kwargs):
|
|
|
|
|
+ self.calls.append(kwargs)
|
|
|
|
|
+ return {'code': 'MCP_0000'}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class ExportOutOfProvincePortDataToolTest(unittest.TestCase):
|
|
|
|
|
+ def test_metadata_explains_business_fields_and_ai_decision_boundaries(self):
|
|
|
|
|
+ metadata = ExportOutOfProvincePortDataTool().metadata()
|
|
|
|
|
+ schema = metadata['input_schema']
|
|
|
|
|
+ properties = schema['properties']
|
|
|
|
|
+
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ ['outbound_numbers', 'file_type'],
|
|
|
|
|
+ schema['required'],
|
|
|
|
|
+ )
|
|
|
|
|
+ self.assertFalse(schema['additionalProperties'])
|
|
|
|
|
+ numbers = properties['outbound_numbers']
|
|
|
|
|
+ self.assertEqual('array', numbers['type'])
|
|
|
|
|
+ self.assertEqual('string', numbers['items']['type'])
|
|
|
|
|
+ self.assertEqual(1, numbers['items']['minLength'])
|
|
|
|
|
+ self.assertEqual(100, numbers['items']['maxLength'])
|
|
|
|
|
+ self.assertEqual(1, numbers['minItems'])
|
|
|
|
|
+ self.assertEqual(100, numbers['maxItems'])
|
|
|
|
|
+ self.assertTrue(numbers['uniqueItems'])
|
|
|
|
|
+ self.assertIsInstance(numbers['examples'][0], list)
|
|
|
|
|
+ self.assertGreater(len(numbers['examples'][0]), 1)
|
|
|
|
|
+
|
|
|
|
|
+ file_type = properties['file_type']
|
|
|
|
|
+ self.assertEqual(['NB', 'SH', 'MS'], file_type['enum'])
|
|
|
|
|
+ self.assertEqual(['NB'], file_type['examples'])
|
|
|
|
|
+
|
|
|
|
|
+ description = metadata['description']
|
|
|
|
|
+ for phrase in (
|
|
|
|
|
+ '排舱单列表',
|
|
|
|
|
+ '不是系统订单号',
|
|
|
|
|
+ '不得根据号码格式猜测',
|
|
|
|
|
+ '先询问',
|
|
|
|
|
+ '一次只能选择一种资料类型',
|
|
|
|
|
+ ):
|
|
|
|
|
+ self.assertIn(phrase, description)
|
|
|
|
|
+ for phrase in (
|
|
|
|
|
+ '排舱单号',
|
|
|
|
|
+ '不是系统订单号',
|
|
|
|
|
+ '不得根据号码格式猜测',
|
|
|
|
|
+ ):
|
|
|
|
|
+ self.assertIn(phrase, numbers['description'])
|
|
|
|
|
+ for phrase in ('NB=宁波', 'SH=上海', 'MS=美森', '先询问'):
|
|
|
|
|
+ self.assertIn(phrase, file_type['description'])
|
|
|
|
|
+
|
|
|
|
|
+ def test_call_normalizes_and_forwards_only_supported_fields(self):
|
|
|
|
|
+ client = RecordingApiClient()
|
|
|
|
|
+ tool = ExportOutOfProvincePortDataTool(api_client=client)
|
|
|
|
|
+
|
|
|
|
|
+ result = tool.call(
|
|
|
|
|
+ [' PC001 ', 'PC002', 'PC001'],
|
|
|
|
|
+ ' nb ',
|
|
|
|
|
+ request_id='rq_port',
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ self.assertEqual('MCP_0000', result['code'])
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ 'export_out_of_province_port_data',
|
|
|
|
|
+ client.last_call['tool_code'],
|
|
|
|
|
+ )
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ '/mcp/tools/exportOutOfProvincePortData',
|
|
|
|
|
+ client.last_call['route_path'],
|
|
|
|
|
+ )
|
|
|
|
|
+ self.assertEqual({
|
|
|
|
|
+ 'outbound_numbers': ['PC001', 'PC002'],
|
|
|
|
|
+ 'file_type': 'NB',
|
|
|
|
|
+ }, client.last_call['payload'])
|
|
|
|
|
+ self.assertEqual('rq_port', client.last_call['request_id'])
|
|
|
|
|
+
|
|
|
|
|
+ def test_call_rejects_invalid_inputs(self):
|
|
|
|
|
+ tool = ExportOutOfProvincePortDataTool(api_client=RecordingApiClient())
|
|
|
|
|
+
|
|
|
|
|
+ invalid_numbers = (
|
|
|
|
|
+ [],
|
|
|
|
|
+ 'PC001',
|
|
|
|
|
+ [''],
|
|
|
|
|
+ ['PC001', 2],
|
|
|
|
|
+ ['X' * 101],
|
|
|
|
|
+ ['PC{0}'.format(index) for index in range(101)],
|
|
|
|
|
+ )
|
|
|
|
|
+ for numbers in invalid_numbers:
|
|
|
|
|
+ with self.subTest(numbers=numbers):
|
|
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
|
|
+ tool.call(numbers, 'NB')
|
|
|
|
|
+ with self.assertRaisesRegex(ValueError, 'file_type must be NB, SH, or MS'):
|
|
|
|
|
+ tool.call(['PC001'], 'OTHER')
|
|
|
|
|
+ with self.assertRaisesRegex(RuntimeError, 'api client is required'):
|
|
|
|
|
+ ExportOutOfProvincePortDataTool().call(['PC001'], 'NB')
|
|
|
|
|
+
|
|
|
|
|
+ def test_local_and_public_gateways_expose_enabled_tool(self):
|
|
|
|
|
+ local_names = {
|
|
|
|
|
+ item['name']
|
|
|
|
|
+ for item in GatewayApp(api_client=RecordingApiClient()).list_tools()
|
|
|
|
|
+ }
|
|
|
|
|
+ public_names = {
|
|
|
|
|
+ item['name']
|
|
|
|
|
+ for item in PublicGatewayApp(
|
|
|
|
|
+ PublicSessionStore(),
|
|
|
|
|
+ PublicApiClient(),
|
|
|
|
|
+ ).list_tools('GWS_test')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ self.assertIn('export_out_of_province_port_data', local_names)
|
|
|
|
|
+ self.assertIn('export_out_of_province_port_data', public_names)
|
|
|
|
|
+
|
|
|
|
|
+ def test_public_gateway_hides_disabled_tool(self):
|
|
|
|
|
+ names = {
|
|
|
|
|
+ item['name']
|
|
|
|
|
+ for item in PublicGatewayApp(
|
|
|
|
|
+ PublicSessionStore(),
|
|
|
|
|
+ PublicApiClient(enabled=False),
|
|
|
|
|
+ ).list_tools('GWS_test')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ self.assertNotIn('export_out_of_province_port_data', names)
|
|
|
|
|
+
|
|
|
|
|
+ def test_public_gateway_forwards_scoped_token_route_and_payload(self):
|
|
|
|
|
+ api_client = PublicApiClient()
|
|
|
|
|
+ gateway = PublicGatewayApp(PublicSessionStore(), api_client)
|
|
|
|
|
+
|
|
|
|
|
+ gateway.call_tool(
|
|
|
|
|
+ 'GWS_test',
|
|
|
|
|
+ 'export_out_of_province_port_data',
|
|
|
|
|
+ {'outbound_numbers': ['PC001'], 'file_type': 'NB'},
|
|
|
|
|
+ request_id='rq_public_port',
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ self.assertEqual('MT_test', api_client.calls[0]['token'])
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ '/mcp/tools/exportOutOfProvincePortData',
|
|
|
|
|
+ api_client.calls[0]['route_path'],
|
|
|
|
|
+ )
|
|
|
|
|
+ self.assertEqual({
|
|
|
|
|
+ 'outbound_numbers': ['PC001'],
|
|
|
|
|
+ 'file_type': 'NB',
|
|
|
|
|
+ }, api_client.calls[0]['payload'])
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == '__main__':
|
|
|
|
|
+ unittest.main()
|