Files
tick-stock-panel/backend/tests/test_ai_probe.py
T
shy3130 ecb49cbaca release: v0.3.6 — 核心+扩展架构完善版
合并 0.3.6 分支全部内容 (36 提交, 详细历史见 0.3.6 分支):

■ 数据与插件
- TdxQuant 数据源: 通达信量化接入 (实时/五档/日K/复权/分钟), tdxaidata 收进默认依赖
- 扩展表两级拉取配方: 概念/行业出厂即带完整可拉配置, 可作自定义接入参考示例
- 能力门禁日志带生效源; 标的池解析只走 instruments 维表

■ 页面扩展化
- 11 个官方页面全量迁移为单文件扩展 (custom/*/extension.tsx), 删目录即卸载
- 运行时页面包系统: 打包产物上传即挂载, 免重建宿主
- 新装用户扩展页默认隐藏, 老用户升级菜单无感

■ AI
- Responses 协议接入; 会话历史与长任务跨设备同步 (服务端注册表)
- 助手头部快捷入口 + 悬浮球拖拽 zoom 坐标系修复 + 系统设置开关

■ 界面
- 设置「关于」子菜单: 版权/更新/打赏 + 开源许可弹窗与合规声明双保险
- 侧栏两态收起、数据/AI 助手快捷行 (同步呼吸光晕)、核心/扩展 ext 标记
- 界面缩放默认 90%; 菜单设置徽标 Swiss 化; 策略池「叠加」分组
- 情绪评分口径可配置; 财务画像 5 张子表手动拉取

■ 文档与其他
- README 重构「核心+扩展」主线 + 架构图重画; 开放端点数核实为 65
- 版本号 bump 0.3.6; tdx 物化库测试夹具补 macOS dylib
2026-10-13 13:40:17 +08:00

162 lines
5.1 KiB
Python

"""AI 草稿连通性测试端点 (POST /api/settings/ai/test) 的契约测试。
核心契约: 测试只读表单值, 与保存完全解耦 —— 测试失败不得写任何配置,
状态栏也不得把未验证的草稿展示为已生效。失败以 200 + ok=false 回报,
由前端内联展示, 不触发全局 toast。
"""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from app import secrets_store
from app.api import settings as settings_api
from app.services import ai_provider
def test_probe_requires_base_url_model_and_key(monkeypatch):
monkeypatch.setattr(secrets_store, "get_ai_key", lambda: "")
with pytest.raises(ValueError, match="API 地址"):
import asyncio
asyncio.run(ai_provider.probe_ai_config(
provider="openai_compat", api_key="sk-x", base_url="", model="m",
))
with pytest.raises(ValueError, match="模型"):
import asyncio
asyncio.run(ai_provider.probe_ai_config(
provider="openai_compat", api_key="sk-x", base_url="https://x.example/v1", model="",
))
with pytest.raises(RuntimeError, match="API Key"):
import asyncio
asyncio.run(ai_provider.probe_ai_config(
provider="openai_compat", api_key="", base_url="https://x.example/v1", model="m",
))
def test_probe_codex_unavailable_raises(monkeypatch):
import asyncio
monkeypatch.setattr(ai_provider, "codex_cli_available", lambda: False)
with pytest.raises(RuntimeError, match="Codex CLI 不可用"):
asyncio.run(ai_provider.probe_ai_config(
provider="codex_cli", api_key="", base_url="", model="",
))
@pytest.mark.asyncio
async def test_probe_chat_protocol_uses_given_key_and_base(monkeypatch):
calls: list[dict] = []
class _Completions:
async def create(self, **kwargs):
calls.append(kwargs)
message = SimpleNamespace(content="OK")
return SimpleNamespace(choices=[SimpleNamespace(message=message)])
class _Chat:
completions = _Completions()
captured_client: dict = {}
class _Client:
def __init__(self, **kwargs):
captured_client.update(kwargs)
self.chat = _Chat()
import openai
monkeypatch.setattr(openai, "AsyncOpenAI", _Client)
text = await ai_provider.probe_ai_config(
provider="openai_compat",
api_key="sk-draft",
base_url="https://draft.example/v1",
model="draft-model",
)
assert text == "OK"
assert captured_client["api_key"] == "sk-draft"
assert "draft.example" in captured_client["base_url"]
assert calls == [{
"model": "draft-model",
"messages": [{"role": "user", "content": "Reply exactly: OK"}],
}]
@pytest.mark.asyncio
async def test_probe_responses_protocol_calls_responses(monkeypatch):
calls: list[dict] = []
class _Responses:
async def create(self, **kwargs):
calls.append(kwargs)
return SimpleNamespace(output_text="ok")
class _Client:
def __init__(self, **kwargs):
self.responses = _Responses()
import openai
monkeypatch.setattr(openai, "AsyncOpenAI", _Client)
text = await ai_provider.probe_ai_config(
provider="openai_compat",
api_key="sk-draft",
base_url="https://draft.example/v1",
model="draft-model",
protocol="responses",
)
assert text == "ok"
assert calls[0]["model"] == "draft-model"
assert calls[0]["input"] == [{"role": "user", "content": "Reply exactly: OK"}]
@pytest.mark.asyncio
async def test_test_endpoint_failure_reports_error_without_saving(monkeypatch):
"""关键回归: 测试失败 → ok=false 回报, 且不调用任何 secrets 写入。"""
saved: list = []
monkeypatch.setattr(settings_api.secrets_store, "save", lambda updates: saved.append(updates))
async def boom(**kwargs):
raise RuntimeError("模型或接口地址不存在")
monkeypatch.setattr(ai_provider, "probe_ai_config", boom)
result = await settings_api.test_ai_settings(settings_api.AiTestIn(
provider="openai_compat",
base_url="https://bad.example/v1",
api_key="sk-bad",
model="no-such-model",
))
assert result["ok"] is False
assert "不存在" in result["error"]
assert saved == [] # 未写入任何配置
@pytest.mark.asyncio
async def test_test_endpoint_falls_back_to_saved_key(monkeypatch):
"""表单 Key 留空时回退已保存 Key (改模型/协议不必重输)。"""
seen: dict = {}
async def fake_probe(**kwargs):
seen.update(kwargs)
return "OK"
monkeypatch.setattr(ai_provider, "probe_ai_config", fake_probe)
monkeypatch.setattr(settings_api.secrets_store, "get_ai_key", lambda: "sk-saved")
result = await settings_api.test_ai_settings(settings_api.AiTestIn(
provider="openai_compat",
base_url="https://api.example/v1",
api_key=None,
model="m1",
protocol="responses",
))
assert result["ok"] is True
assert seen["api_key"] == "sk-saved"
assert seen["protocol"] == "responses"