from __future__ import annotations from dataclasses import dataclass from typing import Any import httpx import pytest from src.services import openclaw as openclaw_module from src.services.openclaw import OpenClawService class _FakeRegistry: def get_model(self, provider: str, use_case: str) -> str: return "qwen2.5:7b-instruct" def get_provider_options(self, provider: str) -> dict[str, Any]: return {"num_predict": 32, "temperature": 0.1, "top_p": 0.9} @dataclass class _FakeEndpoint: provider_name: str url: str class _FakeRoute: def __init__(self, endpoints: list[_FakeEndpoint]) -> None: self._endpoints = endpoints def all_endpoints_in_order(self) -> list[_FakeEndpoint]: return self._endpoints class _FakeManager: def __init__(self, endpoints: list[_FakeEndpoint]) -> None: self._endpoints = endpoints async def select_provider(self) -> _FakeRoute: return _FakeRoute(self._endpoints) class _FakeResponse: status_code = 200 def raise_for_status(self) -> None: return None def json(self) -> dict[str, Any]: return {"response": '{"action_title":"ok"}'} class _FakeClient: def __init__(self, fail_urls: set[str]) -> None: self.fail_urls = fail_urls self.posted_urls: list[str] = [] async def post(self, url: str, **kwargs: Any) -> _FakeResponse: self.posted_urls.append(url) if url in self.fail_urls: raise httpx.ConnectError("offline") return _FakeResponse() @pytest.mark.asyncio async def test_legacy_ollama_uses_failover_order_before_gemini( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(openclaw_module, "get_model_registry", lambda: _FakeRegistry()) monkeypatch.setattr(openclaw_module.settings, "OPENCLAW_TIMEOUT", 30) monkeypatch.setattr(openclaw_module.settings, "OLLAMA_DIAGNOSE_TIMEOUT_SECONDS", 200) monkeypatch.setattr( openclaw_module, "get_ollama_failover_manager", lambda: _FakeManager( [ _FakeEndpoint("ollama_gcp_a", "http://gcp-a:11435"), _FakeEndpoint("ollama_gcp_b", "http://gcp-b:11436"), _FakeEndpoint("ollama_local", "http://local-111:11434"), _FakeEndpoint("gemini", ""), ], ), ) client = _FakeClient(fail_urls={"http://gcp-a:11435/api/generate"}) service = object.__new__(OpenClawService) async def _get_client() -> _FakeClient: return client monkeypatch.setattr(service, "_get_client", _get_client) result, ok = await service._call_ollama("diagnose") assert ok is True assert result == '{"action_title":"ok"}' assert client.posted_urls == [ "http://gcp-a:11435/api/generate", "http://gcp-b:11436/api/generate", ] @pytest.mark.asyncio async def test_legacy_ollama_falls_back_to_configured_three_layer_urls( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(openclaw_module, "get_model_registry", lambda: _FakeRegistry()) monkeypatch.setattr(openclaw_module.settings, "OPENCLAW_TIMEOUT", 30) monkeypatch.setattr(openclaw_module.settings, "OLLAMA_DIAGNOSE_TIMEOUT_SECONDS", 200) monkeypatch.setattr(openclaw_module.settings, "OLLAMA_URL", "http://gcp-a:11435") monkeypatch.setattr(openclaw_module.settings, "OLLAMA_SECONDARY_URL", "http://gcp-b:11436") monkeypatch.setattr(openclaw_module.settings, "OLLAMA_FALLBACK_URL", "http://local-111:11434") monkeypatch.setattr( openclaw_module, "get_ollama_failover_manager", lambda: _FakeManager([_FakeEndpoint("gemini", "")]), ) client = _FakeClient( fail_urls={ "http://gcp-a:11435/api/generate", "http://gcp-b:11436/api/generate", }, ) service = object.__new__(OpenClawService) async def _get_client() -> _FakeClient: return client monkeypatch.setattr(service, "_get_client", _get_client) result, ok = await service._call_ollama("diagnose") assert ok is True assert result == '{"action_title":"ok"}' assert client.posted_urls == [ "http://gcp-a:11435/api/generate", "http://gcp-b:11436/api/generate", "http://local-111:11434/api/generate", ]