Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

353 lines
12 KiB
Python

from __future__ import annotations
from pathlib import Path
import keyring
import keyring.errors
import pytest
from keyring.backend import KeyringBackend
from config.llm_auth.credentials import resolve_for_request, status
from config.llm_auth.records import resolve_provider_auth_record, save_provider_auth_record
from config.llm_credentials import resolve_llm_api_key
from integrations.llm_cli.base import CLIProbe
from integrations.llm_cli.codex_oauth import CodexOAuthResult
from surfaces.cli.llm_auth.providers import ProviderAuthProfile, resolve_auth_profile
from surfaces.cli.llm_auth.service import (
AuthSetupError,
configure_api_key_provider,
configure_cli_subscription_provider,
)
from surfaces.cli.wizard.config import ModelOption, ProviderOption
from surfaces.cli.wizard.validation import ValidationResult
from tests.shared.keyring_backend import MemoryKeyring
def test_resolve_auth_profile_accepts_subscription_aliases() -> None:
assert resolve_auth_profile("chatgpt").provider_value == "codex"
assert resolve_auth_profile("openai-codex").provider_value == "codex"
assert resolve_auth_profile("claude").provider_value == "claude-code"
assert resolve_auth_profile("deepseek").provider_value == "deepseek"
def test_configure_deepseek_api_key_stores_keyring_and_nonsecret_env(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
monkeypatch.setattr(
"surfaces.cli.wizard.store.get_store_path",
lambda: tmp_path / "opensre.json",
)
monkeypatch.setattr(
"surfaces.cli.llm_auth.service.validate_provider_credentials",
lambda **_kwargs: ValidationResult(ok=True, detail="ok"),
)
previous_backend = keyring.get_keyring()
keyring.set_keyring(MemoryKeyring())
try:
env_path = tmp_path / ".env"
result = configure_api_key_provider(
profile=resolve_auth_profile("deepseek"),
api_key="deepseek-secret",
model="deepseek-v4-flash",
env_path=env_path,
)
assert result.provider == "deepseek"
assert resolve_llm_api_key("DEEPSEEK_API_KEY") == "deepseek-secret"
env_content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=deepseek\n" in env_content
assert "DEEPSEEK_REASONING_MODEL=deepseek-v4-flash\n" in env_content
assert "DEEPSEEK_API_KEY=" not in env_content
assert resolve_provider_auth_record("deepseek")["source"] == "keyring"
finally:
keyring.set_keyring(previous_backend)
def test_configure_api_key_does_not_store_when_validation_fails(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
monkeypatch.setattr(
"surfaces.cli.llm_auth.service.validate_provider_credentials",
lambda **_kwargs: ValidationResult(ok=False, detail="rejected"),
)
previous_backend = keyring.get_keyring()
keyring.set_keyring(MemoryKeyring())
try:
with pytest.raises(AuthSetupError, match="rejected"):
configure_api_key_provider(
profile=resolve_auth_profile("deepseek"),
api_key="bad-key",
)
assert resolve_llm_api_key("DEEPSEEK_API_KEY") == ""
finally:
keyring.set_keyring(previous_backend)
class _FakeAdapter:
name = "fake"
binary_env_key = "FAKE_BIN"
install_hint = "install fake"
auth_hint = "Run: fake login"
min_version = None
default_exec_timeout_sec = 30.0
def detect(self) -> CLIProbe:
return CLIProbe(
installed=True,
version="1.0.0",
logged_in=True,
bin_path="/usr/bin/fake",
detail="Logged in.",
)
class _InconclusiveCodexAdapter:
name = "fake"
binary_env_key = "FAKE_BIN"
install_hint = "install fake"
auth_hint = "Run: fake login"
min_version = None
default_exec_timeout_sec = 30.0
def detect(self) -> CLIProbe:
return CLIProbe(
installed=True,
version="1.0.0",
logged_in=None,
bin_path="/usr/bin/fake",
detail="Login status unknown.",
)
def test_configure_cli_subscription_syncs_provider(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
monkeypatch.setattr(
"surfaces.cli.wizard.store.get_store_path",
lambda: tmp_path / "opensre.json",
)
fake_provider = ProviderOption(
value="codex",
label="OpenAI Codex CLI",
group="Local CLI providers",
api_key_env="",
model_env="CODEX_MODEL",
default_model="",
models=(ModelOption(value="", label="default"),),
credential_kind="cli",
adapter_factory=_FakeAdapter,
allow_custom_models=True,
)
monkeypatch.setattr(
"surfaces.cli.llm_auth.service.provider_for_profile", lambda _profile: fake_provider
)
previous_backend = keyring.get_keyring()
keyring.set_keyring(MemoryKeyring())
try:
env_path = tmp_path / ".env"
result = configure_cli_subscription_provider(
profile=ProviderAuthProfile(
name="chatgpt",
provider_value="codex",
label="ChatGPT subscription via Codex CLI",
kind="cli_subscription",
),
model="gpt-5-codex",
env_path=env_path,
)
assert result.provider == "codex"
env_content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=codex\n" in env_content
assert "CODEX_MODEL=gpt-5-codex\n" in env_content
assert resolve_provider_auth_record("codex")["source"] == "vendor-cli"
finally:
keyring.set_keyring(previous_backend)
def test_configure_cli_subscription_uses_managed_codex_oauth_when_status_probe_unknown(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
monkeypatch.setattr(
"surfaces.cli.wizard.store.get_store_path",
lambda: tmp_path / "opensre.json",
)
fake_provider = ProviderOption(
value="codex",
label="OpenAI Codex CLI",
group="Local CLI providers",
api_key_env="",
model_env="CODEX_MODEL",
default_model="",
models=(ModelOption(value="", label="default"),),
credential_kind="cli",
adapter_factory=_InconclusiveCodexAdapter,
allow_custom_models=True,
)
monkeypatch.setattr(
"surfaces.cli.llm_auth.service.provider_for_profile", lambda _profile: fake_provider
)
oauth_calls: list[object] = []
def _fake_codex_oauth_login() -> CodexOAuthResult:
oauth_calls.append(None)
return CodexOAuthResult(
account_id="account-123",
auth_path=tmp_path / "codex-home" / "auth.json",
detail="OpenAI OAuth tokens stored for Codex.",
)
monkeypatch.setattr(
"surfaces.cli.llm_auth.service.run_codex_oauth_login", _fake_codex_oauth_login
)
previous_backend = keyring.get_keyring()
keyring.set_keyring(MemoryKeyring())
try:
env_path = tmp_path / ".env"
result = configure_cli_subscription_provider(
profile=ProviderAuthProfile(
name="chatgpt",
provider_value="codex",
label="ChatGPT subscription via Codex CLI",
kind="cli_subscription",
),
model="gpt-5-codex",
env_path=env_path,
)
assert oauth_calls == [None]
assert result.provider == "codex"
assert result.source == "codex-oauth"
assert result.detail == "OpenAI OAuth tokens stored for Codex."
env_content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=openai\n" in env_content
assert "LLM_AUTH_METHOD=oauth\n" in env_content
assert "CODEX_MODEL=gpt-5-codex\n" in env_content
assert resolve_provider_auth_record("codex")["source"] == "codex-oauth"
finally:
keyring.set_keyring(previous_backend)
class _ErroringKeyring(KeyringBackend):
"""Simulates a keychain backend that can't be reached (e.g. no D-Bus session)."""
priority = 1.0
def get_password(self, _service: str, _username: str) -> str | None:
raise keyring.errors.KeyringError("Secret Service is not reachable")
def set_password(self, _service: str, _username: str, _password: str) -> None:
raise keyring.errors.KeyringError("Secret Service is not reachable")
def delete_password(self, _service: str, _username: str) -> None:
raise keyring.errors.KeyringError("Secret Service is not reachable")
def test_resolve_for_request_does_not_stale_verified_record_on_keyring_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A backend hiccup (regression for #3721) must not downgrade a verified credential.
Previously any ``keyring.errors.KeyringError`` during the on-demand keychain
read was swallowed into an empty string, which read as "credential missing"
and permanently marked the metadata record stale/unverified — even though
the secret itself was untouched.
"""
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
# OPENSRE_LLM_AUTH_METADATA_PATH is already redirected to this same tmp_path
# by the autouse ``_isolate_opensre_home_files`` fixture in tests/conftest.py.
save_provider_auth_record(
provider="deepseek",
auth_name="deepseek",
kind="api_key",
source="keyring",
detail="DEEPSEEK_API_KEY stored in the system keychain.",
verified=True,
stale=False,
env_var="DEEPSEEK_API_KEY",
)
previous_backend = keyring.get_keyring()
keyring.set_keyring(_ErroringKeyring())
try:
resolution = resolve_for_request("deepseek")
assert resolution.ok is False
assert "keychain" in resolution.detail.lower()
record = resolve_provider_auth_record("deepseek")
assert record["stale"] == "false"
assert record["verified"] == "true"
assert status("deepseek").verified is True
finally:
keyring.set_keyring(previous_backend)
def test_resolve_for_request_still_stales_when_credential_is_genuinely_absent(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A clean (error-free) keychain miss is real evidence and should still stale."""
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
# OPENSRE_LLM_AUTH_METADATA_PATH is already redirected by the autouse
# ``_isolate_opensre_home_files`` fixture in tests/conftest.py.
save_provider_auth_record(
provider="deepseek",
auth_name="deepseek",
kind="api_key",
source="keyring",
detail="DEEPSEEK_API_KEY stored in the system keychain.",
verified=True,
stale=False,
env_var="DEEPSEEK_API_KEY",
)
previous_backend = keyring.get_keyring()
keyring.set_keyring(MemoryKeyring())
try:
resolution = resolve_for_request("deepseek")
assert resolution.ok is False
record = resolve_provider_auth_record("deepseek")
assert record["stale"] == "true"
assert record["verified"] == "false"
finally:
keyring.set_keyring(previous_backend)
def test_codex_oauth_metadata_counts_as_prompt_safe_cli_status(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
save_provider_auth_record(
provider="codex",
auth_name="chatgpt",
kind="cli_subscription",
source="codex-oauth",
detail="OpenAI OAuth tokens stored for Codex.",
)
from config.llm_auth.credentials import status
result = status("codex")
assert result.configured is True
assert result.source == "cli"
assert result.detail == "OpenAI OAuth tokens stored for Codex."