Files
easy_tdx_max/tests/unit/test_web_spa_api_guard.py
T
GitHub 4bd5b5d833 release: v1.29.0 — 借鉴社区 Fork 六项特性:ZIG 策略 + 交易时段感知刷新 + 120M K 线 + 逐 bar 衍生字段 + 159 龙头池 + 多 Provider LLM 直连
- ZIG 右侧突破回补策略:MyTT 新增 ZIG 之字转向(未来函数,含前视偏差警示);
  波谷启动建仓挂硬止损(OCO)→ 见顶清仓记前高 → 右侧突破回补;路径依赖不实现
  entry_exit_masks(向量化守护测试白名单);含寻优预设网格与 --strategy-file 独立文件
- 交易时段感知刷新:realtime/session.py(09:15~11:30:30 / 13:00~15:05)+
  GET /market/session;看板 30/60/120s 轮询休市自动暂停(三态状态栏 + 开关持久化
  + 手动刷新不受限);SSE/WS 既有会话语义不动
- 120 分钟 K 线:/bars?category=MIN_120(MAC 原生 Period.MINS×120 优先,
  2×60M 相邻聚合兜底,标准客户端上限 400 根);前端周期选择器同步
- 逐 bar 衍生字段:/bars 与 /bars/index 附带 pre_close/change/change_pct/
  amplitude_pct(pre_close≤0.01 兜底防除零)
- 159 只核心龙头池:数据资产取自 Fork(东财全行业龙头名单,四组分层);
  universe=core 接入 screen scan / SignalScanner / StrengthRanker / market strength;
  GET /market/core-leaders + WebUI「龙头池」页(搜索/个股详情)
- 多 Provider LLM 直连:easy_tdx.ai + /llm/*(DeepSeek/通义/智谱/Kimi/MiniMax/
  OpenAI/Claude/Ollama/自定义,openai 兼容 + anthropic 原生双协议);
  配置落盘 ~/.easy_tdx/llm.json(WebUI「AI 设置」页 ⇆ 手工编辑双向兼容,
  文件>环境变量>预设;key 脱敏回显/CLEAR 清除);「AI 解读」后台任务化
  (复用 task_runner,提交+轮询,不占 HTTP 连接);思考型模型空白正文防御
  (reasoning_content 耗尽 max_tokens → 可操作报错;默认 16000);
  AI 解读历史页(自动归档 Prompt/正文/策略上下文 + 去回测带参引导)
- WebUI 加固:SPA fallback 对未知 /api/* 返回 JSON 404(不再 200 HTML 伪装解析错);
  index.html 一律 Cache-Control: no-store(防缓存旧资源引用);路由兜底重定向;
  全局风险提示常驻底栏 + 龙头池/AI 解读针对性免责声明
- 测试:新增 9 个单测文件共 59 例;黄金基线仅新增 zig_breakout 条目(其余零漂移);
  全量 1448 例通过
2026-09-02 20:19:17 +08:00

85 lines
3.0 KiB
Python
Raw 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.
"""SPA fallback 的 /api 守卫测试(v1.29)。
背景(实测踩坑):未注册的 ``/api/*`` 路径会掉进 StaticFiles 的 SPA
fallback 返回 200 + index.html——前端 ``resp.ok`` 为 true、``resp.json()``
抛 ``Unexpected token '<'``,把"服务是旧版本/端点不存在"伪装成前端解析
错误。守护:未知 /api 路径必须返回 JSON 404,前端路由路径仍回 index.html。
"""
from __future__ import annotations
from pathlib import Path
import pytest
fastapi_testclient = pytest.importorskip("fastapi.testclient")
def _make_app_with_ui(tmp_path: Path):
"""带假前端 dist 的 appindex.html + 一个资产文件)。"""
from easy_tdx.web.app import _create_app
dist = tmp_path / "dist"
dist.mkdir()
(dist / "index.html").write_text("<!doctype html><title>spa</title>", encoding="utf-8")
(dist / "test-asset.txt").write_text("asset", encoding="utf-8")
import easy_tdx.web.app as app_mod
original = app_mod._resolve_web_dist_dir
app_mod._resolve_web_dist_dir = lambda: dist # type: ignore[assignment]
try:
return _create_app(enable_mac=False, enable_ui=True)
finally:
app_mod._resolve_web_dist_dir = original # type: ignore[assignment]
@pytest.fixture()
def client(tmp_path):
app = _make_app_with_ui(tmp_path)
with fastapi_testclient.TestClient(app) as c:
yield c
def test_unknown_api_path_returns_json_404(client):
"""未注册的 /api 路径:JSON 404,绝不能是 200 HTMLSPA fallback)。"""
resp = client.get("/api/v1/llm/config-not-exist")
assert resp.status_code == 404
assert resp.headers["content-type"].startswith("application/json")
assert "<!doctype" not in resp.text.lower()
def test_unknown_api_post_returns_404_not_html(client):
resp = client.post("/api/v1/no-such-endpoint", json={})
assert resp.status_code in (404, 405)
assert "<!doctype" not in resp.text.lower()
def test_spa_route_still_serves_index(client):
"""前端路由路径(如 /llm)仍回 index.htmlSPA 刷新场景)。"""
resp = client.get("/llm")
assert resp.status_code == 200
assert "spa" in resp.text
def test_static_asset_served(client):
assert client.get("/test-asset.txt").text == "asset"
def test_registered_api_route_unaffected(client):
"""已注册端点正常返回 JSON(守卫只拦未匹配路径)。"""
resp = client.get("/api/v1/market/session")
assert resp.status_code == 200
assert resp.json()["session_desc"]
def test_index_html_no_store(client):
"""入口 index.html 永远 no-store:防浏览器缓存旧资源引用(强刷仍见旧版)。"""
for path in ("/", "/llm", "/ai-history"):
resp = client.get(path)
assert resp.status_code == 200
assert resp.headers.get("cache-control") == "no-store", path
# 哈希文件名的静态资产不受影响(默认缓存语义)
asset = client.get("/test-asset.txt")
assert asset.headers.get("cache-control") != "no-store"