feat: add openclaw_connect_metadata function and update compatibility check logic, fixes #156
This commit is contained in:
@@ -7,7 +7,12 @@ from dataclasses import dataclass
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.services.openclaw.gateway_rpc import GatewayConfig, OpenClawGatewayError, openclaw_call
|
from app.services.openclaw.gateway_rpc import (
|
||||||
|
GatewayConfig,
|
||||||
|
OpenClawGatewayError,
|
||||||
|
openclaw_call,
|
||||||
|
openclaw_connect_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
_VERSION_PATTERN = re.compile(r"(?i)v?(?P<version>\d+(?:\.\d+)+)")
|
_VERSION_PATTERN = re.compile(r"(?i)v?(?P<version>\d+(?:\.\d+)+)")
|
||||||
_PRIMARY_VERSION_PATHS: tuple[tuple[str, ...], ...] = (
|
_PRIMARY_VERSION_PATHS: tuple[tuple[str, ...], ...] = (
|
||||||
@@ -192,12 +197,27 @@ async def _fetch_schema_metadata(config: GatewayConfig) -> object | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_connect_metadata(config: GatewayConfig) -> object | None:
|
||||||
|
try:
|
||||||
|
return await openclaw_connect_metadata(config=config)
|
||||||
|
except OpenClawGatewayError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def check_gateway_runtime_compatibility(
|
async def check_gateway_runtime_compatibility(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
*,
|
*,
|
||||||
minimum_version: str | None = None,
|
minimum_version: str | None = None,
|
||||||
) -> GatewayVersionCheckResult:
|
) -> GatewayVersionCheckResult:
|
||||||
"""Fetch runtime metadata and evaluate gateway version compatibility."""
|
"""Fetch runtime metadata and evaluate gateway version compatibility."""
|
||||||
|
connect_payload = await _fetch_connect_metadata(config)
|
||||||
|
current_version = extract_gateway_version(connect_payload)
|
||||||
|
if current_version is not None:
|
||||||
|
return evaluate_gateway_version(
|
||||||
|
current_version=current_version,
|
||||||
|
minimum_version=minimum_version,
|
||||||
|
)
|
||||||
|
|
||||||
schema_payload = await _fetch_schema_metadata(config)
|
schema_payload = await _fetch_schema_metadata(config)
|
||||||
current_version = extract_gateway_version(schema_payload)
|
current_version = extract_gateway_version(schema_payload)
|
||||||
if current_version is not None:
|
if current_version is not None:
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ async def _ensure_connected(
|
|||||||
ws: websockets.ClientConnection,
|
ws: websockets.ClientConnection,
|
||||||
first_message: str | bytes | None,
|
first_message: str | bytes | None,
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
) -> None:
|
) -> object:
|
||||||
if first_message:
|
if first_message:
|
||||||
if isinstance(first_message, bytes):
|
if isinstance(first_message, bytes):
|
||||||
first_message = first_message.decode("utf-8")
|
first_message = first_message.decode("utf-8")
|
||||||
@@ -272,7 +272,7 @@ async def _ensure_connected(
|
|||||||
"params": _build_connect_params(config),
|
"params": _build_connect_params(config),
|
||||||
}
|
}
|
||||||
await ws.send(json.dumps(response))
|
await ws.send(json.dumps(response))
|
||||||
await _await_response(ws, connect_id)
|
return await _await_response(ws, connect_id)
|
||||||
|
|
||||||
|
|
||||||
async def openclaw_call(
|
async def openclaw_call(
|
||||||
@@ -327,6 +327,48 @@ async def openclaw_call(
|
|||||||
raise OpenClawGatewayError(str(exc)) from exc
|
raise OpenClawGatewayError(str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def openclaw_connect_metadata(*, config: GatewayConfig) -> object:
|
||||||
|
"""Open a gateway connection and return the connect/hello payload."""
|
||||||
|
gateway_url = _build_gateway_url(config)
|
||||||
|
started_at = perf_counter()
|
||||||
|
logger.debug(
|
||||||
|
"gateway.rpc.connect_metadata.start gateway_url=%s",
|
||||||
|
_redacted_url_for_log(gateway_url),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
async with websockets.connect(gateway_url, ping_interval=None) as ws:
|
||||||
|
first_message = None
|
||||||
|
try:
|
||||||
|
first_message = await asyncio.wait_for(ws.recv(), timeout=2)
|
||||||
|
except TimeoutError:
|
||||||
|
first_message = None
|
||||||
|
metadata = await _ensure_connected(ws, first_message, config)
|
||||||
|
logger.debug(
|
||||||
|
"gateway.rpc.connect_metadata.success duration_ms=%s",
|
||||||
|
int((perf_counter() - started_at) * 1000),
|
||||||
|
)
|
||||||
|
return metadata
|
||||||
|
except OpenClawGatewayError:
|
||||||
|
logger.warning(
|
||||||
|
"gateway.rpc.connect_metadata.gateway_error duration_ms=%s",
|
||||||
|
int((perf_counter() - started_at) * 1000),
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except (
|
||||||
|
TimeoutError,
|
||||||
|
ConnectionError,
|
||||||
|
OSError,
|
||||||
|
ValueError,
|
||||||
|
WebSocketException,
|
||||||
|
) as exc: # pragma: no cover - network/protocol errors
|
||||||
|
logger.error(
|
||||||
|
"gateway.rpc.connect_metadata.transport_error duration_ms=%s error_type=%s",
|
||||||
|
int((perf_counter() - started_at) * 1000),
|
||||||
|
exc.__class__.__name__,
|
||||||
|
)
|
||||||
|
raise OpenClawGatewayError(str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
message: str,
|
message: str,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ async def test_check_gateway_runtime_compatibility_prefers_schema_version(
|
|||||||
return {"version": "2026.2.13"}
|
return {"version": "2026.2.13"}
|
||||||
raise AssertionError(f"unexpected method: {method}")
|
raise AssertionError(f"unexpected method: {method}")
|
||||||
|
|
||||||
|
async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None:
|
||||||
|
_ = config
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata)
|
||||||
monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call)
|
monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call)
|
||||||
|
|
||||||
result = await gateway_compat.check_gateway_runtime_compatibility(
|
result = await gateway_compat.check_gateway_runtime_compatibility(
|
||||||
@@ -62,6 +67,34 @@ async def test_check_gateway_runtime_compatibility_prefers_schema_version(
|
|||||||
assert result.current_version == "2026.2.13"
|
assert result.current_version == "2026.2.13"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_gateway_runtime_compatibility_prefers_connect_metadata(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None:
|
||||||
|
_ = config
|
||||||
|
return {"server": {"version": "2026.2.21-2"}}
|
||||||
|
|
||||||
|
async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object:
|
||||||
|
_ = (params, config)
|
||||||
|
calls.append(method)
|
||||||
|
raise AssertionError(f"unexpected method: {method}")
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata)
|
||||||
|
monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call)
|
||||||
|
|
||||||
|
result = await gateway_compat.check_gateway_runtime_compatibility(
|
||||||
|
GatewayConfig(url="ws://gateway.example/ws"),
|
||||||
|
minimum_version="2026.1.30",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls == []
|
||||||
|
assert result.compatible is True
|
||||||
|
assert result.current_version == "2026.2.21-2"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_gateway_runtime_compatibility_falls_back_to_health(
|
async def test_check_gateway_runtime_compatibility_falls_back_to_health(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
@@ -77,6 +110,11 @@ async def test_check_gateway_runtime_compatibility_falls_back_to_health(
|
|||||||
raise OpenClawGatewayError("unknown method")
|
raise OpenClawGatewayError("unknown method")
|
||||||
return {"version": "2026.2.0"}
|
return {"version": "2026.2.0"}
|
||||||
|
|
||||||
|
async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None:
|
||||||
|
_ = config
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata)
|
||||||
monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call)
|
monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call)
|
||||||
|
|
||||||
result = await gateway_compat.check_gateway_runtime_compatibility(
|
result = await gateway_compat.check_gateway_runtime_compatibility(
|
||||||
@@ -104,6 +142,11 @@ async def test_check_gateway_runtime_compatibility_uses_health_when_status_has_n
|
|||||||
return {"uptime": 1234}
|
return {"uptime": 1234}
|
||||||
return {"version": "2026.2.0"}
|
return {"version": "2026.2.0"}
|
||||||
|
|
||||||
|
async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None:
|
||||||
|
_ = config
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata)
|
||||||
monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call)
|
monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call)
|
||||||
|
|
||||||
result = await gateway_compat.check_gateway_runtime_compatibility(
|
result = await gateway_compat.check_gateway_runtime_compatibility(
|
||||||
|
|||||||
Reference in New Issue
Block a user