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

73 lines
2.7 KiB
Python

"""secrets.json 作为统一配置源的外部编辑契约。
用户可以直接用编辑器修改 secrets.json (AI + 数据源 Key 同文件), 保存后
下一次请求即读取最新内容 —— 不需要重启服务。这里锁定三条:
① file_path() 指向 data/user_data/secrets.json;
② 外部改写文件后 load()/get_ai_config() 立刻反映新值 (mtime+size 签名缓存失效);
③ GET /api/settings 暴露 secrets_path 供设置页展示。
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from app import secrets_store
@pytest.fixture()
def data_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
from app.config import settings
monkeypatch.setattr(settings, "data_dir", tmp_path, raising=False)
yield tmp_path
def test_file_path_points_to_user_data_secrets(data_dir: Path) -> None:
assert secrets_store.file_path() == data_dir / "user_data" / "secrets.json"
def test_external_edit_is_picked_up_without_restart(data_dir: Path) -> None:
secrets_store.save({"ai_model": "old-model", "ai_base_url": "https://old.example/v1"})
assert secrets_store.get_ai_config("ai_model") == "old-model"
# 模拟用户编辑器直接覆写文件 (绕过 secrets_store.save)。
# 内容长度与 mtime 都变化, 签名缓存必然失效。
path = data_dir / "user_data" / "secrets.json"
path.write_text(json.dumps({
"ai_model": "brand-new-model",
"ai_base_url": "https://new.example/v1",
"ai_api_key": "sk-new",
}, indent=2), encoding="utf-8")
assert secrets_store.get_ai_config("ai_model") == "brand-new-model"
assert secrets_store.get_ai_config("ai_base_url") == "https://new.example/v1"
assert secrets_store.get_ai_key() == "sk-new"
def test_external_edit_with_same_size_still_reloads(data_dir: Path) -> None:
"""同长度改写: 签名含 mtime_ns, 不能因 size 相同而吃到旧缓存。"""
import os
import time
path = data_dir / "user_data" / "secrets.json"
secrets_store.save({"ai_model": "model-aaaa"})
assert secrets_store.get_ai_config("ai_model") == "model-aaaa"
path.write_text(json.dumps({"ai_model": "model-bbbb"}), encoding="utf-8")
# 强制 mtime 前进 (文件系统时间戳粒度可能吞掉快速连续写入)
future = time.time() + 5
os.utime(path, (future, future))
assert secrets_store.get_ai_config("ai_model") == "model-bbbb"
def test_get_settings_exposes_secrets_path(data_dir: Path) -> None:
from app.api import settings as settings_api
payload = settings_api.get_settings()
assert payload["secrets_path"].endswith("secrets.json")
assert str(data_dir) in payload["secrets_path"] or Path(payload["secrets_path"]).name == "secrets.json"