| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- import copy
- import importlib.util
- import io
- import json
- import os
- import unittest
- from app import GatewayApp
- from public_gateway import PublicGatewayApp
- from services.output_presenter import OutputPresenter
- class RecordingApiClient:
- def __init__(self):
- self.calls = []
- def list_enabled_tools(self, request_id=''):
- return {
- 'code': 'MCP_0000',
- 'data': {'tool_codes': ['query_order_receivable_cost_details']},
- }
- 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': {}}
- class OrderReceivableCostDetailsToolTest(unittest.TestCase):
- def load_tool_class(self):
- path = os.path.join(
- os.path.dirname(os.path.dirname(__file__)),
- 'tools',
- 'query_order_receivable_cost_details.py',
- )
- self.assertTrue(os.path.isfile(path), '订单应收费用 Gateway 工具尚未实现')
- spec = importlib.util.spec_from_file_location('receivable_cost_tool', path)
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module.QueryOrderReceivableCostDetailsTool
- def test_closed_schema_requires_explicit_order_number_and_forwards_exactly(self):
- tool_class = self.load_tool_class()
- client = RecordingApiClient()
- metadata = tool_class(client).metadata()
- schema = metadata['input_schema']
- self.assertEqual('query_order_receivable_cost_details', metadata['name'])
- self.assertEqual(['order_number'], schema['required'])
- self.assertFalse(schema['additionalProperties'])
- self.assertEqual({'order_number', 'page', 'limit'}, set(schema['properties']))
- self.assertIn('明确订单号', metadata['description'])
- self.assertIn('不得使用参考号', metadata['description'])
- self.assertIn('不得模糊', metadata['description'])
- result = tool_class(client).call(
- order_number='ORD-260001', page=2, limit=30, request_id='rq_cost'
- )
- self.assertEqual('MCP_0000', result['code'])
- self.assertEqual((
- 'query_order_receivable_cost_details',
- '/mcp/tools/queryOrderReceivableCostDetails',
- {'order_number': 'ORD-260001', 'page': 2, 'limit': 30},
- 'rq_cost',
- ), client.calls[-1])
- def test_invalid_arguments_fail_before_backend_call(self):
- tool_class = self.load_tool_class()
- client = RecordingApiClient()
- tool = tool_class(client)
- for arguments in (
- {}, {'order_number': ''}, {'order_number': ' '},
- {'order_number': 123}, {'order_number': True},
- {'order_number': 'x' * 101}, {'order_number': 'ORD-1', 'page': 0},
- {'order_number': 'ORD-1', 'page': 101},
- {'order_number': 'ORD-1', 'limit': False},
- {'order_number': 'ORD-1', 'limit': 101},
- ):
- with self.subTest(arguments=arguments):
- with self.assertRaises((TypeError, ValueError)):
- tool.call(**arguments)
- self.assertEqual([], client.calls)
- with self.assertRaises(RuntimeError):
- tool_class().call(order_number='ORD-1')
- def test_registries_cli_and_payment_records_remain_available(self):
- client = RecordingApiClient()
- local = GatewayApp(api_client=client)
- public = PublicGatewayApp(None, None)
- self.assertEqual(local.registered_tool_names(), public.registered_tool_names())
- self.assertEqual(19, len(local.registered_tool_names()))
- self.assertIn('query_order_receivable_cost_details', local.registered_tool_names())
- self.assertIn('query_customer_payment_records', local.registered_tool_names())
- stdout = io.StringIO()
- local.run_cli([
- 'call', '--tool', 'query_order_receivable_cost_details',
- '--order-number', 'ORD-260001', '--page', '2', '--limit', '30',
- ], stdout=stdout)
- self.assertEqual('MCP_0000', json.loads(stdout.getvalue())['code'])
- self.assertEqual('ORD-260001', client.calls[-1][2]['order_number'])
- with self.assertRaises(ValueError):
- local.run_cli([
- 'call', '--tool', 'query_order_receivable_cost_details',
- ], stdout=io.StringIO())
- class OrderReceivableCostDetailsPresenterTest(unittest.TestCase):
- KEYS = [
- 'order_number', 'customer_name', 'cost_name', 'cost_customer_name',
- 'charge_weight', 'unit_price', 'quote_currency',
- 'receivable_original_amount', 'receivable_amount_cny',
- 'settlement_amount_cny', 'cost_confirmation_status',
- 'closing_status', 'verification_status',
- ]
- HEADERS = [
- '订单号', '客户名称', '费用项', '费用客户', '计费重', '计费单价',
- '原币币种', '应收原币', '应收金额(CNY)', '结算金额(CNY)',
- '费用确认状态', '关账状态', '核销状态',
- ]
- def payload(self):
- return {
- 'code': 'MCP_0000',
- 'data': {
- 'columns': [{'key': key, 'name': key} for key in self.KEYS],
- 'records': [{
- 'order_number': 'ORD-260001', 'customer_name': '主客户',
- 'cost_name': '海运费', 'cost_customer_name': '子客户(SC01)',
- 'charge_weight': 12.35, 'unit_price': 8.5,
- 'quote_currency': 'USD', 'receivable_original_amount': 10.0,
- 'receivable_amount_cny': 72.0, 'settlement_amount_cny': 39.0,
- 'cost_confirmation_status': '已确认', 'closing_status': '部分关账',
- 'verification_status': '是',
- }],
- },
- 'meta': {'page': 1, 'limit': 20, 'has_more': False},
- }
- def test_presenter_translates_exact_thirteen_columns(self):
- result = OutputPresenter().present(
- 'query_order_receivable_cost_details', self.payload()
- )
- self.assertFalse(result['is_error'])
- self.assertEqual(
- [{'label': header} for header in self.HEADERS],
- result['structured_content']['headers'],
- )
- self.assertEqual(13, len(result['structured_content']['rows'][0]))
- serialized = json.dumps(result, ensure_ascii=False)
- for key in self.KEYS:
- self.assertNotIn(key, serialized)
- def test_unknown_missing_reordered_and_bad_values_fail_closed(self):
- cases = []
- unknown = self.payload()
- unknown['data']['records'][0]['secret'] = 'hidden'
- cases.append(unknown)
- missing = self.payload()
- del missing['data']['records'][0]['cost_name']
- cases.append(missing)
- reordered = self.payload()
- reordered['data']['columns'].reverse()
- cases.append(reordered)
- bad_columns = self.payload()
- bad_columns['data']['columns'] = 'bad'
- cases.append(bad_columns)
- bad_column = self.payload()
- bad_column['data']['columns'][0]['internal'] = True
- cases.append(bad_column)
- extra_data = self.payload()
- extra_data['data']['internal'] = True
- cases.append(extra_data)
- bad_meta = self.payload()
- bad_meta['meta']['total'] = 1
- cases.append(bad_meta)
- bad_text = self.payload()
- bad_text['data']['records'][0]['order_number'] = []
- cases.append(bad_text)
- for amount in (True, '10.00', float('inf'), float('-inf'), float('nan')):
- bad_amount = self.payload()
- bad_amount['data']['records'][0]['receivable_amount_cny'] = amount
- cases.append(bad_amount)
- presenter = OutputPresenter()
- for payload in cases:
- with self.subTest(payload=payload):
- self.assertTrue(presenter.present(
- 'query_order_receivable_cost_details', payload
- )['is_error'])
- def test_payload_factory_does_not_share_nested_state(self):
- self.assertEqual(self.payload(), copy.deepcopy(self.payload()))
- if __name__ == '__main__':
- unittest.main()
|