Files
tick-stock-panel/backend/tests/test_custom_assistant_sessions.py
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

98 lines
3.4 KiB
Python

"""AI 助手会话历史服务端存储 (跨设备同步) 的服务与 API 测试。
契约: GET 空结构、PUT 保存往返、结构裁剪 (无 id 会话剔除 / 消息截尾 200 /
会话上限 20)、损坏文件按空处理、API 挂载在 assistant 扩展路由下。
"""
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.custom.assistant import sessions as assistant_sessions
from app.custom.assistant.routes import build_router
@pytest.fixture
def client(tmp_path: Path) -> TestClient:
app = FastAPI()
app.include_router(build_router())
app.state.repo = SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path))
return TestClient(app)
def _session(sid: str, n_msgs: int = 1) -> dict:
return {
"id": sid,
"title": f"会话{sid}",
"createdAt": 1700000000000,
"messages": [
{"id": f"m{i}", "role": "user", "content": f"hi {i}", "ts": i}
for i in range(n_msgs)
],
}
def test_api_roundtrip_and_empty_state(client: TestClient) -> None:
assert client.get("/api/custom/assistant/sessions").json() == {
"sessions": [],
"activeId": "",
}
r = client.put(
"/api/custom/assistant/sessions",
json={"sessions": [_session("s1"), _session("s2")], "activeId": "s2"},
)
assert r.status_code == 200 and r.json() == {"ok": True, "sessions": 2}
got = client.get("/api/custom/assistant/sessions").json()
assert [s["id"] for s in got["sessions"]] == ["s1", "s2"]
assert got["activeId"] == "s2"
def test_sanitize_trims_and_drops(client: TestClient, tmp_path: Path) -> None:
# 无 id/非字符串 id 的会话剔除
bad = [{"title": "no id"}, {"id": 123, "messages": []}]
# 消息超 200 截尾
long = _session("long", n_msgs=250)
# 会话超 20 截断 (服务层: API 层 max_length=20 会先拒绝更大的载荷)
many = [_session(f"s{i}") for i in range(25)]
n = assistant_sessions.save_sessions(
tmp_path, {"sessions": [*bad, long, *many], "activeId": "s0"}
)
assert n == assistant_sessions.MAX_SESSIONS
got = assistant_sessions.load_sessions(tmp_path)
ids = [s["id"] for s in got["sessions"]]
assert len(ids) == 20
assert "long" in ids # 输入顺序靠前, 保留
assert len(next(s for s in got["sessions"] if s["id"] == "long")["messages"]) == 200
# API 层: 结构非法的会话静默剔除, 合法照常保存
r = client.put(
"/api/custom/assistant/sessions",
json={"sessions": [*bad, _session("ok1")], "activeId": "ok1"},
)
assert r.status_code == 200 and r.json() == {"ok": True, "sessions": 1}
ids2 = [s["id"] for s in client.get("/api/custom/assistant/sessions").json()["sessions"]]
assert ids2 == ["ok1"]
def test_corrupted_file_treated_as_empty(tmp_path: Path) -> None:
p = tmp_path / "user_data" / "assistant_sessions.json"
p.parent.mkdir(parents=True)
p.write_text("{not json", encoding="utf-8")
assert assistant_sessions.load_sessions(tmp_path) == {"sessions": [], "activeId": ""}
def test_put_rejects_oversized_session_list(client: TestClient) -> None:
r = client.put(
"/api/custom/assistant/sessions",
json={"sessions": [_session(f"s{i}") for i in range(21)]},
)
assert r.status_code == 422 # 超过 max_length=20, 防御异常客户端