fix(mcp): 异常面适配真实 bleak——重试/错误码映射覆盖异常全家

- C1: _run_with_retry 捕获面 TransportError → (base+local 两个 TransportError,
  BleakError, OSError, asyncio.TimeoutError);真实掉线 bleak 裸抛
  BleakError("Not connected") 不再绕过重连重试
- C2/I1a: _map_transport_error 接受异常全家,类型优先(asyncio.TimeoutError、
  BleakDeviceNotFoundError → CONNECT_TIMEOUT)再按中英文消息映射;_call /
  scan_devices / device_status 的 except 同步扩展,device_status 不再把传输层
  错误兜底成 INTERNAL
- I2: _ensure_connected 的 __aenter__ 半途失败补 __aexit__,防半开连接泄漏
- M1: status() 先算 idle_seconds 再 _touch(),读数不再被自身归零
- docstring 校准:scan_devices 返回形状 data.devices;device_status 注明
  触发连接守候且刷新空闲计时
- 新测试 8 个(先红后绿):raw BleakError 重连成功/耗尽上抛、aenter 清理、
  idle_seconds 语义、local 中文 TransportError 两条映射、raw BleakError 映射、
  device_status 传输错误映射

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
chenweiandClaude committed 2026-07-30 23:55:31 +00:00
1 parent 16dbf494c9
commit 1f78587bbd
4 files changed
+164 -15

No files matched your search

+42 -1
View File
@@ -4,6 +4,7 @@ import io
import json
import pytest
from bleak.exc import BleakError
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import create_connected_server_and_client_session
from PIL import Image
@@ -11,6 +12,7 @@ from PIL import Image
from ppclock.client import PPClient
from ppclock.mcp_tools import decode_image_source, register_tools
from ppclock.transports.base import TransportError
from ppclock.transports.local import TransportError as LocalTransportError
class FakeTransport:
@@ -47,9 +49,10 @@ class FakeTransport:
class FakeManager:
"""模拟 DeviceManager:直接对 fake client 执行 op,或抛预置错误。"""
def __init__(self, error=None):
def __init__(self, error=None, status_error=None):
self.client = PPClient(FakeTransport())
self.error = error
self.status_error = status_error
self.ops = 0
async def run(self, op):
@@ -59,6 +62,8 @@ class FakeManager:
return await op(self.client)
async def status(self):
if self.status_error is not None:
raise self.status_error
return {"connected": True, "mac": "AA:BB:CC:DD:EE:FF",
"idle_seconds": 0.0, "connect_count": 1,
"connect_timeout": 90.0, "idle_timeout": 300.0,
@@ -144,6 +149,42 @@ class TestTools:
assert r["error"]["code"] == "CONNECT_TIMEOUT"
assert "hint" in r["error"]
@pytest.mark.asyncio
async def test_local_not_found_mapped(self):
"""真实 BLE 扫描落空:local.TransportError(中文)→ DEVICE_NOT_FOUND。"""
m = FakeManager(error=LocalTransportError("未找到名称前缀 'NRF-' 的设备"))
r = await call(make_session_coro(m), "set_time", {})
assert r["ok"] is False
assert r["error"]["code"] == "DEVICE_NOT_FOUND"
assert "hint" in r["error"]
@pytest.mark.asyncio
async def test_local_connect_failure_mapped(self):
"""已知 MAC 守候未连上(bleak 报 not found 被包装成"连接失败")→ CONNECT_TIMEOUT。"""
m = FakeManager(error=LocalTransportError(
"连接失败: Device with address AA:BB:CC:DD:EE:FF was not found."))
r = await call(make_session_coro(m), "set_time", {})
assert r["ok"] is False
assert r["error"]["code"] == "CONNECT_TIMEOUT"
assert "hint" in r["error"]
@pytest.mark.asyncio
async def test_raw_bleak_error_mapped(self):
"""写路径 bleak 裸抛 BleakError(不包装)→ BLE_ERROR。"""
m = FakeManager(error=BleakError("Not connected"))
r = await call(make_session_coro(m), "set_time", {})
assert r["ok"] is False
assert r["error"]["code"] == "BLE_ERROR"
@pytest.mark.asyncio
async def test_device_status_transport_error_mapped(self):
"""device_status 的传输层错误与其它工具同映射,不兜底 INTERNAL。"""
m = FakeManager(status_error=LocalTransportError("等待设备 ID 应答超时"))
r = await call(make_session_coro(m), "device_status", {})
assert r["ok"] is False
assert r["error"]["code"] == "CONNECT_TIMEOUT"
assert "hint" in r["error"]
@pytest.mark.asyncio
async def test_device_status(self):
r = await call(make_session_coro(FakeManager()), "device_status", {})