| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- import importlib
- import io
- import unittest
- from app import GatewayApp
- from public_gateway import PublicGatewayApp
- from services.output_presenter import OutputPresenter
- COLUMNS = [
- ('order_number', '订单号'), ('customer_name', '客户'),
- ('product_name', '物流产品'), ('abnormal_status', '问题件状态'),
- ('abnormal_type', '问题件类型'), ('abnormal_desc', '问题件描述'),
- ('outbound_status', '排舱单状态'), ('inbound_date', '入库时间'),
- ('marker_name', '标记人'),
- ]
- STATUSES = [
- '待处理', '放行待审核', '包裹放行', '退件待审核', '退件驳回', '已退件',
- ]
- class RecordingApiClient:
- def __init__(self):
- self.calls = []
- def list_enabled_tools(self, request_id=''):
- return {
- 'code': 'MCP_0000',
- 'data': {
- 'tool_codes': [
- 'query_order_abnormal_list',
- 'list_order_abnormal_filter_options',
- ],
- },
- }
- def call_tool(self, tool_code, route_path, payload, request_id):
- self.calls.append((tool_code, route_path, payload, request_id))
- return {'code': 'MCP_0000', 'data': {}, 'meta': {}}
- class OrderAbnormalToolContractTest(unittest.TestCase):
- def tool_class(self, module_name, class_name):
- return getattr(importlib.import_module('tools.' + module_name), class_name)
- def test_query_schema_requires_status_and_forbids_identity(self):
- cls = self.tool_class(
- 'query_order_abnormal_list', 'QueryOrderAbnormalListTool'
- )
- metadata = cls().metadata()
- schema = metadata['input_schema']
- self.assertEqual('query_order_abnormal_list', metadata['name'])
- self.assertEqual('/mcp/tools/queryOrderAbnormalList', cls.route_path)
- self.assertFalse(schema['additionalProperties'])
- self.assertEqual(['abnormal_status'], schema['required'])
- self.assertEqual(STATUSES, schema['properties']['abnormal_status']['enum'])
- self.assertNotIn('default', schema['properties']['abnormal_status'])
- for forbidden in (
- 'company_id', 'admin_id', 'is_super', 'customer_id', 'product_id',
- ):
- self.assertNotIn(forbidden, schema['properties'])
- self.assertIn('使用场景:', metadata['description'])
- self.assertIn('禁止使用:', metadata['description'])
- self.assertIn('号码类型不明确时必须先询问用户', metadata['description'])
- self.assertIn('不得根据号码格式猜测', metadata['description'])
- self.assertIn('不得跨字段或跨工具试查', metadata['description'])
- self.assertIn('admin/Order/abnormal', metadata['description'])
- self.assertIn('订单号可选', metadata['description'])
- def test_query_forwards_optional_order_numbers(self):
- cls = self.tool_class(
- 'query_order_abnormal_list', 'QueryOrderAbnormalListTool'
- )
- client = RecordingApiClient()
- cls(client).call(
- abnormal_status='包裹放行',
- order_numbers=[' FMS-1 ', 'FMS-1'],
- page=2,
- limit=30,
- request_id='rq_abn',
- )
- self.assertEqual(
- (
- 'query_order_abnormal_list',
- '/mcp/tools/queryOrderAbnormalList',
- {
- 'abnormal_status': '包裹放行',
- 'order_numbers': ['FMS-1'],
- 'page': 2,
- 'limit': 30,
- },
- 'rq_abn',
- ),
- client.calls[-1],
- )
- cls(client).call(abnormal_status='待处理')
- self.assertEqual(
- {'abnormal_status': '待处理', 'page': 1, 'limit': 20},
- client.calls[-1][2],
- )
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='结案')
- with self.assertRaises(RuntimeError):
- cls().call(abnormal_status='待处理')
- def test_filter_schema_and_call(self):
- cls = self.tool_class(
- 'list_order_abnormal_filter_options',
- 'ListOrderAbnormalFilterOptionsTool',
- )
- metadata = cls().metadata()
- schema = metadata['input_schema']
- self.assertEqual('list_order_abnormal_filter_options', metadata['name'])
- self.assertEqual(
- '/mcp/tools/listOrderAbnormalFilterOptions', cls.route_path
- )
- self.assertEqual(['filter_type'], schema['required'])
- self.assertEqual(['问题件状态'], schema['properties']['filter_type']['enum'])
- self.assertIn('query_order_abnormal_list', metadata['description'])
- self.assertIn('禁止猜测', metadata['description'])
- client = RecordingApiClient()
- cls(client).call('问题件状态', keyword='待', page=2, limit=10)
- self.assertEqual(
- {
- 'filter_type': '问题件状态',
- 'keyword': '待',
- 'page': 2,
- 'limit': 10,
- },
- client.calls[-1][2],
- )
- with self.assertRaises(ValueError):
- cls(client).call('问题件类型')
- with self.assertRaises(RuntimeError):
- cls().call('问题件状态')
- with self.assertRaises(ValueError):
- cls(client).call('问题件状态', keyword='x' * 101)
- with self.assertRaises(ValueError):
- cls(client).call('问题件状态', page=0)
- with self.assertRaises(ValueError):
- cls(client).call('问题件状态', limit=True)
- def test_query_covers_remaining_validation_branches(self):
- cls = self.tool_class(
- 'query_order_abnormal_list', 'QueryOrderAbnormalListTool'
- )
- client = RecordingApiClient()
- with self.assertRaises(ValueError):
- cls(client).call(
- abnormal_status='待处理',
- order_numbers=['X%s' % i for i in range(201)],
- )
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='待处理', order_numbers='FMS-1')
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='待处理', order_numbers=[1])
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='待处理', order_numbers=[' '])
- with self.assertRaises(ValueError):
- cls(client).call(
- abnormal_status='待处理',
- order_numbers=['X' * 101],
- )
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='待处理', page=0)
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='待处理', limit=True)
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='待处理', order_numbers=[])
- with self.assertRaises(ValueError):
- cls(client).call(abnormal_status='待处理', page=101)
- def test_cli_forwards_status_and_order_numbers(self):
- client = RecordingApiClient()
- app = GatewayApp(api_client=client)
- with self.assertRaises(ValueError):
- app.run_cli([
- 'call', '--tool', 'query_order_abnormal_list',
- ], stdout=io.StringIO())
- app.run_cli([
- 'call', '--tool', 'query_order_abnormal_list',
- '--abnormal-status', '退件待审核',
- '--order-numbers', ' FMS-1,FMS-2 ',
- ], stdout=io.StringIO())
- self.assertEqual(
- {
- 'abnormal_status': '退件待审核',
- 'order_numbers': ['FMS-1', 'FMS-2'],
- 'page': 1,
- 'limit': 20,
- },
- client.calls[-1][2],
- )
- app.run_cli([
- 'call', '--tool', 'query_order_abnormal_list',
- '--abnormal-status', '待处理',
- ], stdout=io.StringIO())
- self.assertEqual(
- {'abnormal_status': '待处理', 'page': 1, 'limit': 20},
- client.calls[-1][2],
- )
- app.run_cli([
- 'call', '--tool', 'list_order_abnormal_filter_options',
- '--filter-type', '问题件状态',
- ], stdout=io.StringIO())
- self.assertEqual('list_order_abnormal_filter_options', client.calls[-1][0])
- with self.assertRaises(ValueError):
- app.run_cli([
- 'call', '--tool', 'list_order_abnormal_filter_options',
- ], stdout=io.StringIO())
- def test_presenter_renders_nine_columns_and_fails_closed(self):
- presenter = OutputPresenter()
- meta = {
- 'page': 1, 'limit': 20, 'has_more': False, 'request_id': 'rq_x',
- }
- columns = [{'key': key, 'name': name} for key, name in COLUMNS]
- record = {key: 'v' for key, _ in COLUMNS}
- record['abnormal_status'] = '待处理'
- ok = presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': columns,
- 'records': [record],
- },
- 'meta': meta,
- })
- self.assertFalse(ok['is_error'])
- self.assertEqual(
- ['订单号', '客户', '物流产品', '问题件状态', '问题件类型',
- '问题件描述', '排舱单状态', '入库时间', '标记人'],
- [item['label'] for item in ok['structured_content']['headers']],
- )
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': [],
- 'records': [],
- 'extra': 1,
- },
- 'meta': meta,
- })['is_error'])
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '结案',
- 'columns': columns,
- 'records': [],
- },
- 'meta': meta,
- })['is_error'])
- bad_columns = list(columns)
- bad_columns[0] = {'key': 'order_number', 'name': '错'}
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': bad_columns,
- 'records': [],
- },
- 'meta': meta,
- })['is_error'])
- bad_record = dict(record)
- bad_record['order_number'] = 1
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': columns,
- 'records': [bad_record],
- },
- 'meta': meta,
- })['is_error'])
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': columns,
- 'records': 'bad',
- },
- 'meta': meta,
- })['is_error'])
- extra_record = dict(record)
- extra_record['deal_remark'] = 'x'
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': columns,
- 'records': [extra_record],
- },
- 'meta': meta,
- })['is_error'])
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': None,
- 'records': [],
- },
- 'meta': meta,
- })['is_error'])
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': columns,
- 'records': [None],
- },
- 'meta': meta,
- })['is_error'])
- self.assertTrue(presenter.present('query_order_abnormal_list', {
- 'code': 'MCP_0000',
- 'data': {
- 'abnormal_status': '待处理',
- 'columns': columns,
- 'records': [],
- },
- 'meta': {
- 'page': 1, 'limit': 20, 'has_more': False,
- 'request_id': 'rq_x', 'total': 1,
- },
- })['is_error'])
- options = presenter.present('list_order_abnormal_filter_options', {
- 'code': 'MCP_0000',
- 'data': {
- 'records': [
- {'value': '待处理', 'label': '待处理', 'code': ''},
- ],
- },
- 'meta': meta,
- })
- self.assertFalse(options['is_error'])
- def test_local_public_registry_includes_both_tools(self):
- local = GatewayApp().registered_tool_names()
- public = PublicGatewayApp(None, None).registered_tool_names()
- self.assertEqual(local, public)
- self.assertEqual(30, len(local))
- self.assertEqual(29, len(OutputPresenter.SAFE_TOOLS))
- self.assertIn('query_order_abnormal_list', local)
- self.assertIn('list_order_abnormal_filter_options', local)
|