From 2d3c3ee3e4921d08e7ca5db09cbbef06ac19c885 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Mon, 23 Feb 2026 01:23:09 +0530 Subject: [PATCH] feat: add config fallback for gateway version compatibility check --- .../app/services/openclaw/gateway_compat.py | 24 ++++++- backend/tests/test_gateway_version_compat.py | 63 ++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/backend/app/services/openclaw/gateway_compat.py b/backend/app/services/openclaw/gateway_compat.py index ce1b99ec..9bccb17e 100644 --- a/backend/app/services/openclaw/gateway_compat.py +++ b/backend/app/services/openclaw/gateway_compat.py @@ -6,8 +6,11 @@ import re from dataclasses import dataclass from app.core.config import settings +from app.core.logging import get_logger from app.services.openclaw.gateway_rpc import ( GatewayConfig, + OpenClawGatewayError, + openclaw_call, openclaw_connect_metadata, ) @@ -16,6 +19,8 @@ _CALVER_PATTERN = re.compile( re.IGNORECASE, ) _CONNECT_VERSION_PATH: tuple[str, ...] = ("server", "version") +_CONFIG_VERSION_PATH: tuple[str, ...] = ("config", "meta", "lastTouchedVersion") +logger = get_logger(__name__) @dataclass(frozen=True, slots=True) @@ -84,6 +89,11 @@ def extract_connect_server_version(payload: object) -> str | None: return _coerce_version_string(_value_at_path(payload, _CONNECT_VERSION_PATH)) +def extract_config_last_touched_version(payload: object) -> str | None: + """Extract a runtime version hint from config.get payload.""" + return _coerce_version_string(_value_at_path(payload, _CONFIG_VERSION_PATH)) + + def evaluate_gateway_version( *, current_version: str | None, @@ -150,9 +160,21 @@ async def check_gateway_version_compatibility( *, minimum_version: str | None = None, ) -> GatewayVersionCheckResult: - """Use connect metadata server.version and evaluate compatibility.""" + """Evaluate gateway compatibility using connect metadata with config fallback.""" connect_payload = await openclaw_connect_metadata(config=config) current_version = extract_connect_server_version(connect_payload) + if current_version is None or _parse_version_parts(current_version) is None: + try: + config_payload = await openclaw_call("config.get", config=config) + except OpenClawGatewayError as exc: + logger.debug( + "gateway.compat.config_get_fallback_unavailable reason=%s", + str(exc), + ) + else: + fallback_version = extract_config_last_touched_version(config_payload) + if fallback_version is not None: + current_version = fallback_version return evaluate_gateway_version( current_version=current_version, minimum_version=minimum_version, diff --git a/backend/tests/test_gateway_version_compat.py b/backend/tests/test_gateway_version_compat.py index ee27a754..a33a84ea 100644 --- a/backend/tests/test_gateway_version_compat.py +++ b/backend/tests/test_gateway_version_compat.py @@ -35,6 +35,26 @@ def test_extract_connect_server_version_returns_none_when_server_version_missing assert gateway_compat.extract_connect_server_version(payload) is None +def test_extract_config_last_touched_version_reads_config_meta_last_touched_version() -> None: + payload = { + "config": { + "meta": {"lastTouchedVersion": "2026.2.9"}, + "wizard": {"lastRunVersion": "2026.2.8"}, + }, + "parsed": {"meta": {"lastTouchedVersion": "2026.2.7"}}, + } + + assert gateway_compat.extract_config_last_touched_version(payload) == "2026.2.9" + + +def test_extract_config_last_touched_version_returns_none_without_config_meta_last_touched_version() -> None: + payload = { + "config": {"wizard": {"lastRunVersion": "2026.2.9"}}, + } + + assert gateway_compat.extract_config_last_touched_version(payload) is None + + @pytest.mark.parametrize( ("current_version", "minimum_version", "expected_compatible"), [ @@ -97,7 +117,12 @@ async def test_check_gateway_version_compatibility_uses_connect_server_version_o "server": {"version": "2026.2.13"}, } + async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: + _ = (method, params, config) + raise AssertionError("config.get fallback should not run for valid connect version") + 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_version_compatibility( GatewayConfig(url="ws://gateway.example/ws"), @@ -116,7 +141,13 @@ async def test_check_gateway_version_compatibility_fails_without_server_version( _ = config return {"runtime": {"version": "2026.2.13"}} + async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: + _ = (params, config) + assert method == "config.get" + return {"config": {}} + 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_version_compatibility( GatewayConfig(url="ws://gateway.example/ws"), @@ -129,14 +160,44 @@ async def test_check_gateway_version_compatibility_fails_without_server_version( @pytest.mark.asyncio -async def test_check_gateway_version_compatibility_rejects_non_calver_server_version( +async def test_check_gateway_version_compatibility_uses_config_get_fallback_when_connect_is_dev( monkeypatch: pytest.MonkeyPatch, ) -> None: async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None: _ = config return {"server": {"version": "dev"}} + async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: + _ = (params, config) + assert method == "config.get" + return {"config": {"meta": {"lastTouchedVersion": "2026.2.9"}}} + 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_version_compatibility( + GatewayConfig(url="ws://gateway.example/ws"), + minimum_version="2026.1.30", + ) + + assert result.compatible is True + assert result.current_version == "2026.2.9" + + +@pytest.mark.asyncio +async def test_check_gateway_version_compatibility_rejects_non_calver_server_version_when_fallback_unavailable( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None: + _ = config + return {"server": {"version": "dev"}} + + async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: + _ = (method, params, config) + raise OpenClawGatewayError("method unavailable") + + 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_version_compatibility( GatewayConfig(url="ws://gateway.example/ws"),