"""K1.3 接线测试:`_build_provider_adapter` 为 kimi-code 走 OAuth 路径 + 按需刷新。 不联网:refresh 路径 monkeypatch httpx + kimi_oauth.refresh。断言: - 凭据未临近过期 → 直接用现有 access token 建 KimiCodeAdapter(不刷新); - 临近过期 → 调 refresh + 持久化新包 + 用新 access token 建适配器; - 无 oauth 凭据 → LLM_UNAVAILABLE。 """ from __future__ import annotations import os from datetime import UTC, datetime, timedelta from typing import Any import pytest from fakes_providers import FakeCredentialStore from ww_api.services.credentials import STUB_OWNER_ID from ww_api.services.kimi_oauth import TokenSet, decrypt_oauth_bundle, encrypt_oauth_bundle from ww_llm_gateway.adapters.kimi_code import KIMI_CODE_PROVIDER, KimiCodeAdapter from ww_shared import AppError, ErrorCode _ENC_KEY = "imB6TEcV2AzlpXjR4NjTgK_9Zt-OoqaLFQc3XqBQ0CQ=" def _store_with_token(token: TokenSet) -> FakeCredentialStore: store = FakeCredentialStore() store.oauth[(STUB_OWNER_ID, KIMI_CODE_PROVIDER)] = encrypt_oauth_bundle(token, key=_ENC_KEY) return store def _set_env() -> None: os.environ["CREDENTIAL_ENC_KEY"] = _ENC_KEY from ww_config import get_settings get_settings.cache_clear() @pytest.mark.asyncio async def test_build_adapter_uses_existing_token_when_fresh() -> None: _set_env() from ww_api.services.project_deps import _build_provider_adapter fresh = TokenSet( access_token="fresh-acc", refresh_token="ref", expires_at=datetime.now(UTC) + timedelta(seconds=900), ) store = _store_with_token(fresh) adapter = await _build_provider_adapter(store, KIMI_CODE_PROVIDER) assert isinstance(adapter, KimiCodeAdapter) # 未刷新:oauth 凭据未变。 cred = store.oauth[(STUB_OWNER_ID, KIMI_CODE_PROVIDER)] assert decrypt_oauth_bundle(cred, key=_ENC_KEY).access_token == "fresh-acc" @pytest.mark.asyncio async def test_build_adapter_refreshes_when_near_expiry(monkeypatch: pytest.MonkeyPatch) -> None: _set_env() from ww_api.services import project_deps from ww_api.services.project_deps import _build_provider_adapter near = TokenSet( access_token="old-acc", refresh_token="old-ref", expires_at=datetime.now(UTC) + timedelta(seconds=30), # 临近过期 ) store = _store_with_token(near) refreshed = TokenSet( access_token="new-acc", refresh_token="new-ref", expires_at=datetime.now(UTC) + timedelta(seconds=900), ) async def _fake_refresh(_http: Any, _refresh_token: str) -> TokenSet: assert _refresh_token == "old-ref" return refreshed # 替换 refresh + httpx.AsyncClient(避免真联网)。 monkeypatch.setattr(project_deps, "kimi_refresh", _fake_refresh) class _FakeAsyncClient: async def __aenter__(self) -> _FakeAsyncClient: return self async def __aexit__(self, *exc: object) -> None: return None monkeypatch.setattr( "ww_api.services.project_deps.httpx.AsyncClient", lambda *a, **k: _FakeAsyncClient() ) adapter = await _build_provider_adapter(store, KIMI_CODE_PROVIDER) assert isinstance(adapter, KimiCodeAdapter) # 刷新后新包持久化(下次复用刷新结果)。 cred = store.oauth[(STUB_OWNER_ID, KIMI_CODE_PROVIDER)] assert decrypt_oauth_bundle(cred, key=_ENC_KEY).access_token == "new-acc" @pytest.mark.asyncio async def test_build_adapter_no_credential_returns_none() -> None: _set_env() from ww_api.services.project_deps import _build_provider_adapter store = FakeCredentialStore() # 无任何凭据 adapter = await _build_provider_adapter(store, KIMI_CODE_PROVIDER) assert adapter is None @pytest.mark.asyncio async def test_build_adapter_raises_when_oauth_missing_blob() -> None: """auth_type=oauth 但 oauth_enc=None(数据不一致)→ LLM_UNAVAILABLE。""" _set_env() from ww_api.services.credentials import StoredCredential from ww_api.services.project_deps import _resolve_kimi_code_token bad = StoredCredential( provider=KIMI_CODE_PROVIDER, api_key_enc=None, auth_type="oauth", oauth_enc=None ) with pytest.raises(AppError) as exc: await _resolve_kimi_code_token(FakeCredentialStore(), bad, _ENC_KEY) assert exc.value.code == ErrorCode.LLM_UNAVAILABLE