4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
191 lines
5.9 KiB
Python
191 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import httpx
|
|
import pytest
|
|
from anthropic import AuthenticationError as AnthropicAuthError
|
|
from openai import AuthenticationError as OpenAIAuthError
|
|
|
|
from surfaces.cli.wizard.config import PROVIDER_BY_VALUE
|
|
from surfaces.cli.wizard.validation import _get_provider_base_url, validate_provider_credentials
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _preload_sdk_error_classes(monkeypatch) -> None:
|
|
"""Pre-populate the module-level ``*AuthError`` globals in validation.py.
|
|
|
|
``_load_anthropic_client``/``_load_openai_client`` lazily import the SDKs
|
|
and override the module-level ``Anthropic``/``OpenAI`` names when their
|
|
matching ``*AuthError`` is ``None``. If a test monkeypatches only the
|
|
client class (``Anthropic`` / ``OpenAI``), the first call to the loader
|
|
re-imports and silently replaces that monkeypatch with the real SDK —
|
|
which then hits the real network using any ``ANTHROPIC_API_KEY`` /
|
|
``OPENAI_API_KEY`` leaked in from the test environment.
|
|
|
|
By setting the ``*AuthError`` globals here to the real classes (already
|
|
imported at the top of this file), we short-circuit the loader's import
|
|
branch and make monkeypatches of ``Anthropic`` / ``OpenAI`` reliable.
|
|
"""
|
|
monkeypatch.setattr("surfaces.cli.wizard.validation.AnthropicAuthError", AnthropicAuthError)
|
|
monkeypatch.setattr("surfaces.cli.wizard.validation.OpenAIAuthError", OpenAIAuthError)
|
|
|
|
|
|
class _FakeAnthropicTextBlock:
|
|
type = "text"
|
|
|
|
def __init__(self, text: str) -> None:
|
|
self.text = text
|
|
|
|
|
|
class _FakeAnthropicResponse:
|
|
def __init__(self, text: str) -> None:
|
|
self.content = [_FakeAnthropicTextBlock(text)]
|
|
|
|
|
|
class _FakeAnthropicMessages:
|
|
def __init__(self, result: object) -> None:
|
|
self._result = result
|
|
|
|
def create(self, **_kwargs):
|
|
if isinstance(self._result, Exception):
|
|
raise self._result
|
|
return self._result
|
|
|
|
|
|
class _FakeAnthropicClient:
|
|
def __init__(self, result: object) -> None:
|
|
self.messages = _FakeAnthropicMessages(result)
|
|
|
|
|
|
class _FakeOpenAIMessage:
|
|
def __init__(self, content: str) -> None:
|
|
self.content = content
|
|
|
|
|
|
class _FakeOpenAIChoice:
|
|
def __init__(self, content: str) -> None:
|
|
self.message = _FakeOpenAIMessage(content)
|
|
|
|
|
|
class _FakeOpenAIResponse:
|
|
def __init__(self, content: str) -> None:
|
|
self.choices = [_FakeOpenAIChoice(content)]
|
|
|
|
|
|
class _FakeOpenAICompletions:
|
|
def __init__(self, result: object) -> None:
|
|
self._result = result
|
|
|
|
def create(self, **_kwargs):
|
|
if isinstance(self._result, Exception):
|
|
raise self._result
|
|
return self._result
|
|
|
|
|
|
class _FakeOpenAIChat:
|
|
def __init__(self, result: object) -> None:
|
|
self.completions = _FakeOpenAICompletions(result)
|
|
|
|
|
|
class _FakeOpenAIClient:
|
|
def __init__(self, result: object) -> None:
|
|
self.chat = _FakeOpenAIChat(result)
|
|
|
|
|
|
def _request(url: str) -> httpx.Request:
|
|
return httpx.Request("POST", url)
|
|
|
|
|
|
def test_validate_provider_credentials_returns_failure_for_bad_anthropic_key(monkeypatch) -> None:
|
|
auth_error = AnthropicAuthError(
|
|
"unauthorized",
|
|
response=httpx.Response(401, request=_request("https://api.anthropic.com/v1/messages")),
|
|
body=None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"surfaces.cli.wizard.validation.Anthropic",
|
|
lambda **_kwargs: _FakeAnthropicClient(auth_error),
|
|
)
|
|
|
|
result = validate_provider_credentials(
|
|
provider=PROVIDER_BY_VALUE["anthropic"],
|
|
api_key="bad-key",
|
|
model="claude-opus-4-5",
|
|
)
|
|
|
|
assert result.ok is False
|
|
assert result.detail == "Anthropic rejected the API key."
|
|
|
|
|
|
def test_validate_provider_credentials_returns_success_for_valid_anthropic_key(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
"surfaces.cli.wizard.validation.Anthropic",
|
|
lambda **_kwargs: _FakeAnthropicClient(_FakeAnthropicResponse("OpenSRE ready")),
|
|
)
|
|
|
|
result = validate_provider_credentials(
|
|
provider=PROVIDER_BY_VALUE["anthropic"],
|
|
api_key="good-key",
|
|
model="claude-opus-4-5",
|
|
)
|
|
|
|
assert result.ok is True
|
|
assert result.detail == "Anthropic API key validated."
|
|
assert result.sample_response == "OpenSRE ready"
|
|
|
|
|
|
def test_validate_provider_credentials_returns_failure_for_bad_openai_key(monkeypatch) -> None:
|
|
auth_error = OpenAIAuthError(
|
|
"unauthorized",
|
|
response=httpx.Response(
|
|
401, request=_request("https://api.openai.com/v1/chat/completions")
|
|
),
|
|
body=None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"surfaces.cli.wizard.validation.OpenAI",
|
|
lambda **_kwargs: _FakeOpenAIClient(auth_error),
|
|
)
|
|
|
|
result = validate_provider_credentials(
|
|
provider=PROVIDER_BY_VALUE["openai"],
|
|
api_key="bad-key",
|
|
model="gpt-5-mini",
|
|
)
|
|
|
|
assert result.ok is False
|
|
assert result.detail == "OpenAI rejected the API key."
|
|
|
|
|
|
def test_validate_provider_credentials_returns_success_for_valid_openai_key(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
"surfaces.cli.wizard.validation.OpenAI",
|
|
lambda **_kwargs: _FakeOpenAIClient(_FakeOpenAIResponse("OpenSRE ready")),
|
|
)
|
|
|
|
result = validate_provider_credentials(
|
|
provider=PROVIDER_BY_VALUE["openai"],
|
|
api_key="good-key",
|
|
model="gpt-5-mini",
|
|
)
|
|
|
|
assert result.ok is True
|
|
assert result.detail == "OpenAI API key validated."
|
|
assert result.sample_response == "OpenSRE ready"
|
|
|
|
|
|
def test_get_provider_base_url_deepseek() -> None:
|
|
from config.config import DEEPSEEK_BASE_URL
|
|
|
|
assert _get_provider_base_url("deepseek") == DEEPSEEK_BASE_URL
|
|
|
|
|
|
def test_get_provider_base_url_groq() -> None:
|
|
from config.config import GROQ_BASE_URL
|
|
|
|
assert _get_provider_base_url("groq") == GROQ_BASE_URL
|
|
|
|
|
|
def test_get_provider_base_url_openai_returns_none() -> None:
|
|
"""Native OpenAI should return None (uses default base_url)."""
|
|
assert _get_provider_base_url("openai") is None
|