Files
writer-work-flow/apps/api/tests/test_kimi_code_wiring.py
Yaojia Wang 765dbdfbd4 feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。
M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变);
  网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则;
  前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。
K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径;
  coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。
本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400);
  GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化;
  字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。
门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:39:58 +02:00

126 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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