Bladeren bron

Merge branch 'master' into master_task_710395169

jackson 2 weken geleden
bovenliggende
commit
332cd3cb71

+ 13 - 0
tests/test_query_freightower_track_tool.py

@@ -160,6 +160,19 @@ class QueryFreightowerTrackToolTest(unittest.TestCase):
         bad_value = sample_data()
         bad_value['tracks'][0]['time'] = 1
         cases.append(bad_value)
+        bad_schedule = sample_data()
+        bad_schedule['schedule']['vessel'] = 1
+        cases.append(bad_schedule)
+        bad_port_keys = sample_data()
+        bad_port_keys['ports'] = [{'place_type': '目的港'}]
+        cases.append(bad_port_keys)
+        bad_port_value = sample_data()
+        bad_port_value['ports'][0]['eta'] = 1
+        cases.append(bad_port_value)
+        cases.append(dict(sample_data(), ports=['x'], tracks=[1]))
+        bad_track_keys = sample_data()
+        bad_track_keys['tracks'] = [{'time': '2026-09-20'}]
+        cases.append(bad_track_keys)
         cases.append({'schedule': {}})
         for data in cases:
             with self.subTest(data=data):

+ 158 - 0
tests/test_query_goods_info_tool.py

@@ -0,0 +1,158 @@
+import io
+import unittest
+from unittest.mock import Mock, patch
+
+from app import GatewayApp
+from services.output_presenter import OutputPresenter
+from tools.query_goods_info import QueryGoodsInfoTool
+
+
+def goods_columns():
+    return [
+        {'key': key, 'name': name}
+        for key, name in OutputPresenter.GOODS_INFO_COLUMNS
+    ]
+
+
+def goods_record():
+    return {key: key + '-v' for key, _name in OutputPresenter.GOODS_INFO_COLUMNS}
+
+
+class QueryGoodsInfoToolTest(unittest.TestCase):
+    def test_call_forwards_sku_name_and_page(self):
+        client = Mock()
+        client.call_tool.return_value = {'code': 'MCP_0000'}
+        QueryGoodsInfoTool(client).call(
+            sku=' SKU-1 ',
+            cn_name=' 衬衫 ',
+            page=2,
+            limit=100,
+            request_id='rq_goods',
+            ignored=True,
+        )
+        client.call_tool.assert_called_once_with(
+            'query_goods_info',
+            '/mcp/tools/queryGoodsInfo',
+            {
+                'page': 2,
+                'limit': 100,
+                'sku': 'SKU-1',
+                'cn_name': '衬衫',
+            },
+            'rq_goods',
+        )
+
+    def test_call_accepts_either_sku_or_name(self):
+        client = Mock()
+        client.call_tool.return_value = {'code': 'MCP_0000'}
+        tool = QueryGoodsInfoTool(client)
+        tool.call(sku='SKU-1', cn_name='   ')
+        self.assertEqual({'page': 1, 'limit': 20, 'sku': 'SKU-1'}, client.call_tool.call_args[0][2])
+        tool.call(cn_name='衬衫', sku=None)
+        self.assertEqual(
+            {'page': 1, 'limit': 20, 'cn_name': '衬衫'},
+            client.call_tool.call_args[0][2],
+        )
+
+    def test_call_requires_api_client_and_a_lookup_field(self):
+        with self.assertRaises(RuntimeError):
+            QueryGoodsInfoTool().call(sku='SKU-1')
+        tool = QueryGoodsInfoTool(api_client=Mock())
+        with self.assertRaises(ValueError):
+            tool.call()
+        with self.assertRaises(ValueError):
+            tool.call(sku='', cn_name='')
+
+    def test_call_rejects_invalid_fields(self):
+        tool = QueryGoodsInfoTool(api_client=Mock())
+        with self.assertRaises(ValueError):
+            tool.call(sku=1)
+        with self.assertRaises(ValueError):
+            tool.call(cn_name=1)
+        with self.assertRaises(ValueError):
+            tool.call(sku='S' * 2001)
+        with self.assertRaises(ValueError):
+            tool.call(cn_name='名' * 101)
+        for page in (True, '1', 0, 101):
+            with self.subTest(page=page):
+                with self.assertRaises(ValueError):
+                    tool.call(sku='SKU-1', page=page)
+        with self.assertRaises(ValueError):
+            tool.call(sku='SKU-1', limit=0)
+
+    def test_cli_forwards_sku_or_name(self):
+        app = GatewayApp()
+        stdout = io.StringIO()
+        with self.assertRaises(ValueError):
+            app.run_cli(['call', '--tool', 'query_goods_info'], stdout=stdout)
+        with patch.object(app, 'call_tool', return_value={'code': 'MCP_0000'}) as call_tool:
+            self.assertEqual(0, app.run_cli([
+                'call', '--tool', 'query_goods_info', '--sku', 'SKU-1',
+            ], stdout=stdout))
+            self.assertEqual('SKU-1', call_tool.call_args[0][1]['sku'])
+            self.assertNotIn('cn_name', call_tool.call_args[0][1])
+            self.assertEqual(0, app.run_cli([
+                'call', '--tool', 'query_goods_info', '--cn-name', '衬衫',
+            ], stdout=stdout))
+            self.assertEqual('衬衫', call_tool.call_args[0][1]['cn_name'])
+            self.assertNotIn('sku', call_tool.call_args[0][1])
+            self.assertEqual(0, app.run_cli([
+                'call', '--tool', 'query_goods_info',
+                '--sku', 'SKU-1', '--cn-name', '衬衫',
+            ], stdout=stdout))
+            payload = call_tool.call_args[0][1]
+            self.assertEqual('SKU-1', payload['sku'])
+            self.assertEqual('衬衫', payload['cn_name'])
+
+    def test_presenter_renders_goods_table(self):
+        record = goods_record()
+        result = OutputPresenter().present('query_goods_info', {
+            'code': 'MCP_0000',
+            'data': {'columns': goods_columns(), 'records': [record]},
+            'meta': {'page': 1, 'limit': 20, 'has_more': False, 'request_id': 'rq_g'},
+        })
+        self.assertFalse(result['is_error'])
+        self.assertEqual(24, len(result['structured_content']['headers']))
+        self.assertEqual('sku-v', result['structured_content']['rows'][0][0])
+        self.assertIn('SKU', result['text'])
+        empty = OutputPresenter().present('query_goods_info', {
+            'code': 'MCP_0000',
+            'data': {'columns': goods_columns(), 'records': []},
+            'meta': {'page': 1, 'limit': 20, 'has_more': False},
+        })
+        self.assertFalse(empty['is_error'])
+        self.assertEqual([], empty['structured_content']['rows'])
+
+    def test_presenter_rejects_invalid_goods_payload(self):
+        presenter = OutputPresenter()
+        columns = goods_columns()
+        record = goods_record()
+        meta = {'page': 1, 'limit': 20, 'has_more': False}
+        bad_column = list(columns)
+        bad_column[0] = {'key': 'sku', 'name': '错'}
+        bad_record = dict(record)
+        bad_record['sku'] = 1
+        extra_record = dict(record)
+        extra_record['secret'] = 'x'
+        cases = [
+            {'columns': columns},
+            {'columns': 'bad', 'records': []},
+            {'columns': columns, 'records': None},
+            {'columns': columns[:1], 'records': []},
+            {'columns': columns, 'records': []},
+            {'columns': bad_column, 'records': []},
+            {'columns': columns, 'records': ['x']},
+            {'columns': columns, 'records': [extra_record]},
+            {'columns': columns, 'records': [bad_record]},
+        ]
+        metas = [meta, meta, meta, meta, {'request_id': 'rq'}, meta, meta, meta, meta]
+        for data, raw_meta in zip(cases, metas):
+            with self.subTest(data=data):
+                result = presenter.present('query_goods_info', {
+                    'code': 'MCP_0000',
+                    'data': data,
+                    'meta': raw_meta,
+                })
+                self.assertTrue(result['is_error'])
+                self.assertNotIn('secret', result['text'])
+                self.assertNotIn('sku-v', result['text'])

+ 102 - 1
tests/test_trial_goods_tax_tool.py

@@ -1,5 +1,7 @@
+import io
+import math
 import unittest
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
 
 from app import GatewayApp
 from public_gateway import PublicGatewayApp
@@ -51,6 +53,68 @@ class TrialGoodsTaxToolTest(unittest.TestCase):
                 price=0, qty=1, gross_weight=1, box=1,
             )
 
+    def test_call_requires_api_client(self):
+        with self.assertRaises(RuntimeError):
+            TrialGoodsTaxTool().call(
+                sku='SKU-1', departure_country='CN', destination_country='US',
+                price=1, qty=1, gross_weight=1, box=1,
+            )
+
+    def test_call_rejects_invalid_fields(self):
+        tool = TrialGoodsTaxTool(api_client=Mock())
+        cases = [
+            {'sku': 1},
+            {'sku': ''},
+            {'sku': 'S' * 101},
+            {'departure_country': None},
+            {'departure_country': 'A'},
+            {'departure_country': 'ABCD'},
+            {'departure_country': 'C1'},
+            {'price': True},
+            {'qty': '1'},
+            {'gross_weight': math.inf},
+            {'box': 1000000001},
+        ]
+        base = {
+            'sku': 'SKU-1', 'departure_country': 'CN', 'destination_country': 'USA',
+            'price': 1, 'qty': 1, 'gross_weight': 1, 'box': 1,
+        }
+        for override in cases:
+            with self.subTest(override=override):
+                payload = dict(base)
+                payload.update(override)
+                with self.assertRaises(ValueError):
+                    tool.call(**payload)
+
+    def test_cli_forwards_trial_fields(self):
+        app = GatewayApp()
+        with patch.object(app, 'call_tool', return_value={'code': 'MCP_0000'}) as call_tool:
+            code = app.run_cli([
+                'call', '--tool', 'trial_goods_tax',
+                '--sku', 'SKU-1',
+                '--departure-country', 'CN',
+                '--destination-country', 'US',
+                '--price', '10',
+                '--qty', '2',
+                '--gross-weight', '1.5',
+                '--box', '1',
+            ], stdout=io.StringIO())
+        self.assertEqual(0, code)
+        self.assertEqual(
+            {
+                'page': 1,
+                'limit': 20,
+                'sku': 'SKU-1',
+                'departure_country': 'CN',
+                'destination_country': 'US',
+                'price': 10.0,
+                'qty': 2.0,
+                'gross_weight': 1.5,
+                'box': 1.0,
+            },
+            call_tool.call_args[0][1],
+        )
+
     def test_registries_match(self):
         local = GatewayApp().registered_tool_names()
         public = PublicGatewayApp(None, None).registered_tool_names()
@@ -83,3 +147,40 @@ class TrialGoodsTaxToolTest(unittest.TestCase):
         })
         self.assertTrue(result['is_error'])
         self.assertEqual('未找到已审核商品', result['structured_content']['message'])
+
+    def test_presenter_rejects_invalid_tax_payload(self):
+        presenter = OutputPresenter()
+        valid = {
+            'estimated_tax': 12,
+            'sku': 'SKU-1',
+            'departure_country': 'CN',
+            'destination_country': 'USA',
+        }
+        ok = presenter.present('trial_goods_tax', {
+            'code': 'MCP_0000',
+            'data': valid,
+        })
+        self.assertFalse(ok['is_error'])
+        self.assertEqual(12, ok['structured_content']['estimated_tax'])
+        cases = [
+            {'estimated_tax': 1},
+            dict(valid, estimated_tax=True),
+            dict(valid, estimated_tax='12'),
+            dict(valid, estimated_tax=math.nan),
+            dict(valid, sku=1),
+            dict(valid, sku='  '),
+            dict(valid, departure_country=1),
+            dict(valid, departure_country='C1'),
+            dict(valid, departure_country='cn'),
+            dict(valid, departure_country='A'),
+            dict(valid, departure_country='ABCD'),
+            dict(valid, destination_country='us'),
+        ]
+        for data in cases:
+            with self.subTest(data=data):
+                result = presenter.present('trial_goods_tax', {
+                    'code': 'MCP_0000',
+                    'data': data,
+                })
+                self.assertTrue(result['is_error'])
+                self.assertNotIn('SKU-1', result['text'])

+ 0 - 2
tools/trial_goods_tax.py

@@ -76,8 +76,6 @@ class TrialGoodsTaxTool:
             'gross_weight': self._positive(gross_weight, 'gross_weight'),
             'box': self._positive(box, 'box'),
         }
-        if not payload['sku'] or not payload['departure_country'] or not payload['destination_country']:
-            raise ValueError('sku, departure_country and destination_country are required')
         return self.api_client.call_tool(
             self.name, self.route_path, payload, request_id
         )