chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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
|
||||
Reference in New Issue
Block a user