This commit is contained in:
@@ -152,6 +152,7 @@ class MCPResult:
|
||||
cost_usd: float = 0.0
|
||||
error: Optional[str] = None
|
||||
output_size: int = 0
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -196,7 +197,7 @@ def _async_write_mcp_call(
|
||||
'args': json.dumps(args_redacted),
|
||||
'osz': result.output_size,
|
||||
'dur': result.duration_ms,
|
||||
'status': 'ok' if result.success else 'error',
|
||||
'status': result.status or ('ok' if result.success else 'error'),
|
||||
'err': (result.error or '')[:4000] if result.error else None,
|
||||
'cost': result.cost_usd,
|
||||
'cache': result.cache_hit,
|
||||
@@ -260,6 +261,7 @@ class MCPRouter:
|
||||
success=True, server=server, tool=tool,
|
||||
data=cached, cache_hit=True, duration_ms=0,
|
||||
output_size=len(json.dumps(cached, ensure_ascii=False)),
|
||||
status='cache_only',
|
||||
)
|
||||
_async_write_mcp_call(caller, server, tool, args, result, request_id)
|
||||
return result
|
||||
@@ -274,10 +276,12 @@ class MCPRouter:
|
||||
duration_ms = int((time.monotonic() - t0) * 1000)
|
||||
|
||||
if resp.status_code != 200:
|
||||
status = 'rate_limited' if resp.status_code == 429 else 'error'
|
||||
result = MCPResult(
|
||||
success=False, server=server, tool=tool,
|
||||
duration_ms=duration_ms,
|
||||
error=f'HTTP {resp.status_code}: {resp.text[:200]}',
|
||||
status=status,
|
||||
)
|
||||
_async_write_mcp_call(caller, server, tool, args, result, request_id)
|
||||
return result
|
||||
@@ -299,6 +303,7 @@ class MCPRouter:
|
||||
success=True, server=server, tool=tool,
|
||||
data=data, cache_hit=False,
|
||||
duration_ms=duration_ms, output_size=output_size,
|
||||
status='ok',
|
||||
)
|
||||
_async_write_mcp_call(caller, server, tool, args, result, request_id)
|
||||
return result
|
||||
@@ -308,6 +313,7 @@ class MCPRouter:
|
||||
result = MCPResult(
|
||||
success=False, server=server, tool=tool,
|
||||
duration_ms=duration_ms, error=f'timeout ({request_timeout}s)',
|
||||
status='timeout',
|
||||
)
|
||||
_async_write_mcp_call(caller, server, tool, args, result, request_id)
|
||||
return result
|
||||
@@ -318,6 +324,7 @@ class MCPRouter:
|
||||
success=False, server=server, tool=tool,
|
||||
duration_ms=duration_ms,
|
||||
error=f'{type(exc).__name__}: {str(exc)[:300]}',
|
||||
status='error',
|
||||
)
|
||||
_async_write_mcp_call(caller, server, tool, args, result, request_id)
|
||||
return result
|
||||
|
||||
@@ -103,8 +103,16 @@ def test_filesystem_registry_is_read_only(monkeypatch):
|
||||
|
||||
def test_successful_call_and_cache_hit(monkeypatch):
|
||||
monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
|
||||
import services.mcp_router as mr
|
||||
from services.mcp_router import mcp_router
|
||||
|
||||
written_statuses = []
|
||||
monkeypatch.setattr(
|
||||
mr,
|
||||
'_async_write_mcp_call',
|
||||
lambda caller, server, tool, args, result, request_id=None: written_statuses.append(result.status),
|
||||
)
|
||||
|
||||
fake_resp = MagicMock(status_code=200)
|
||||
fake_resp.json.return_value = {'results': [{'title': '商品 A', 'content': '熱銷'}]}
|
||||
fake_resp.text = '{"ok": true}'
|
||||
@@ -117,6 +125,7 @@ def test_successful_call_and_cache_hit(monkeypatch):
|
||||
)
|
||||
assert r1.success is True
|
||||
assert r1.cache_hit is False
|
||||
assert r1.status == 'ok'
|
||||
assert r1.data['results'][0]['title'] == '商品 A'
|
||||
assert mock_post.call_count == 1
|
||||
|
||||
@@ -127,7 +136,9 @@ def test_successful_call_and_cache_hit(monkeypatch):
|
||||
)
|
||||
assert r2.success is True
|
||||
assert r2.cache_hit is True
|
||||
assert r2.status == 'cache_only'
|
||||
assert mock_post.call_count == 1 # 沒再打 HTTP
|
||||
assert written_statuses == ['ok', 'cache_only']
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -146,6 +157,7 @@ def test_http_timeout_returns_failure(monkeypatch):
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert result.status == 'timeout'
|
||||
assert 'timeout' in (result.error or '').lower()
|
||||
|
||||
|
||||
@@ -167,9 +179,28 @@ def test_http_500_returns_failure(monkeypatch):
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert result.status == 'error'
|
||||
assert 'HTTP 500' in (result.error or '')
|
||||
|
||||
|
||||
def test_http_429_records_rate_limited_status(monkeypatch):
|
||||
monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
|
||||
from services.mcp_router import mcp_router
|
||||
|
||||
fake_resp = MagicMock(status_code=429)
|
||||
fake_resp.text = 'Too Many Requests'
|
||||
|
||||
with patch('services.mcp_router.requests.post', return_value=fake_resp):
|
||||
result = mcp_router.call(
|
||||
server='omnisearch', tool='tavily_search',
|
||||
args={'query': 'x'}, caller='mcp_collector',
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert result.status == 'rate_limited'
|
||||
assert 'HTTP 429' in (result.error or '')
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# T6: cache key 穩定性
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user