主站行情管理并入数据中枢
- 数据中枢数据源配置页新增「主站行情任务」卡:交易时段后台刷新开关、
手动后台刷新、历史区间回补(任务提交 + 轮询主站 job 记录亮灯),
数据与调度仍归主站,控制台只经桥接代操作
- 新增桥接端点 /api/hub-admin/market/{refresh,backfill};回补改为
market.backfill 后台任务(jobs.config.json 注册,锁独立,超时 30 分钟),
桥接调用秒回,不再阻塞
- 主站桌面端删除顶栏「行情管理」按钮与整个行情管理对话框,清理随之
失效的 CSS;移动端删除 system/admin 页与入口,管理员专区只留数据中枢
- Tushare Token / iFinD 凭证编辑沿用数据源页既有凭证区,无功能缺失
模型池收起修复
- 拉出模型清单后按钮切换为「收起列表」,收起只留一行摘要;再次点击
重新拉取并展开;勾选添加完成后清单自动收起(原逻辑保留)
- 交互全部沿用 HEL-558 已确认样式的既有按钮与提示组件,未新增视觉
自测
- verify_baseline 通过;pytest 492 项通过;数据中枢 240 项通过
- verify_datahub_console 新增 [11b] 行情任务桥接端到端段;UI 自测新增
行情任务卡开关往返、模型清单展开/收起/再展开/添加自动收起,日夜主题
与 1030 窄屏复验通过
- Playwright e2e 102/103:唯一失败项在基线提交上同样失败(本机字体度量
导致的头部溢出,与本卡无关)
Co-authored-by: multica-agent <github@multica.ai>
146 lines
5.9 KiB
Python
146 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from backend.application import (
|
|
AUTHENTICATED_POST_HANDLERS,
|
|
PUBLIC_POST_HANDLERS,
|
|
RequestHandler,
|
|
)
|
|
|
|
|
|
APP_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def class_methods(path: Path, class_name: str) -> set[str]:
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
owner = next(
|
|
node
|
|
for node in tree.body
|
|
if isinstance(node, ast.ClassDef) and node.name == class_name
|
|
)
|
|
return {
|
|
node.name
|
|
for node in owner.body
|
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
}
|
|
|
|
|
|
class HttpDispatchContractTests(unittest.TestCase):
|
|
@staticmethod
|
|
def handler(path: str, calls: list[str]) -> RequestHandler:
|
|
handler = RequestHandler.__new__(RequestHandler)
|
|
handler.path = path
|
|
handler.require_auth = lambda: calls.append("auth") or True
|
|
handler.require_csrf = lambda: calls.append("csrf") or True
|
|
handler.require_access = lambda method, route: (
|
|
calls.append(f"access:{method}:{route}") or True
|
|
)
|
|
return handler
|
|
|
|
def test_named_handlers_are_real_registered_post_routes(self) -> None:
|
|
all_handlers = {**PUBLIC_POST_HANDLERS, **AUTHENTICATED_POST_HANDLERS}
|
|
self.assertEqual(
|
|
set(PUBLIC_POST_HANDLERS) & set(AUTHENTICATED_POST_HANDLERS), set()
|
|
)
|
|
for path, handler_name in all_handlers.items():
|
|
with self.subTest(path=path):
|
|
route = RequestHandler.route_registry.resolve("POST", path)
|
|
self.assertIsNotNone(route)
|
|
self.assertTrue(callable(getattr(RequestHandler, handler_name)))
|
|
expected_access = "public" if path in PUBLIC_POST_HANDLERS else None
|
|
if expected_access:
|
|
self.assertEqual(route.access, expected_access)
|
|
else:
|
|
self.assertNotEqual(route.access, "public")
|
|
|
|
def test_public_post_dispatches_without_authentication(self) -> None:
|
|
for path, handler_name in PUBLIC_POST_HANDLERS.items():
|
|
with self.subTest(path=path):
|
|
calls: list[str] = []
|
|
handler = self.handler(path, calls)
|
|
handler.require_auth = lambda: (_ for _ in ()).throw(
|
|
AssertionError("public route required authentication")
|
|
)
|
|
setattr(handler, handler_name, lambda: calls.append(handler_name))
|
|
|
|
RequestHandler.do_POST(handler)
|
|
|
|
self.assertEqual(calls, [handler_name])
|
|
|
|
def test_authenticated_post_preserves_guard_order(self) -> None:
|
|
for path, handler_name in AUTHENTICATED_POST_HANDLERS.items():
|
|
with self.subTest(path=path):
|
|
calls: list[str] = []
|
|
handler = self.handler(path, calls)
|
|
setattr(handler, handler_name, lambda: calls.append(handler_name))
|
|
|
|
RequestHandler.do_POST(handler)
|
|
|
|
self.assertEqual(
|
|
calls,
|
|
["auth", "csrf", f"access:POST:{path}", handler_name],
|
|
)
|
|
|
|
def test_failed_access_never_dispatches_protected_handler(self) -> None:
|
|
path, handler_name = next(iter(AUTHENTICATED_POST_HANDLERS.items()))
|
|
calls: list[str] = []
|
|
handler = self.handler(path, calls)
|
|
handler.require_access = lambda method, route: (
|
|
calls.append(f"access:{method}:{route}") or False
|
|
)
|
|
setattr(handler, handler_name, lambda: calls.append(handler_name))
|
|
|
|
RequestHandler.do_POST(handler)
|
|
|
|
self.assertEqual(calls, ["auth", "csrf", f"access:POST:{path}"])
|
|
|
|
def test_application_composition_and_route_ownership_stay_narrow(self) -> None:
|
|
application = APP_ROOT / "backend" / "application.py"
|
|
dispatch = APP_ROOT / "backend" / "http" / "dispatch.py"
|
|
self.assertLessEqual(len(application.read_text(encoding="utf-8").splitlines()), 220)
|
|
self.assertLessEqual(len(dispatch.read_text(encoding="utf-8").splitlines()), 150)
|
|
self.assertEqual(class_methods(application, "DashboardService"), {"__init__"})
|
|
self.assertEqual(class_methods(application, "RequestHandler"), set())
|
|
self.assertEqual(
|
|
class_methods(dispatch, "ApplicationHttpDispatchMixin"),
|
|
{"_dispatch_named_handler", "do_GET", "do_POST", "do_DELETE"},
|
|
)
|
|
|
|
expected_route_owners = {
|
|
"accounts", "alerts", "auction", "dragon_tiger", "heaven", "market",
|
|
"mentor", "pools", "popularity", "review", "rotation", "screener",
|
|
"sentiment", "system", "themes",
|
|
}
|
|
route_files = {
|
|
path.parent.name: path
|
|
for path in (APP_ROOT / "backend" / "features").glob("*/routes.py")
|
|
}
|
|
self.assertEqual(set(route_files), expected_route_owners)
|
|
for path in route_files.values():
|
|
source = path.read_text(encoding="utf-8")
|
|
self.assertLessEqual(len(source.splitlines()), 120, path.name)
|
|
self.assertNotIn("backend.application", source)
|
|
self.assertNotRegex(source, r"\bSERVICE\b")
|
|
|
|
def test_application_service_methods_have_single_domain_owners(self) -> None:
|
|
owners = (
|
|
("features/system/service.py", "SystemServiceMixin", 300),
|
|
("features/accounts/application.py", "AccountApplicationMixin", 100),
|
|
("jobs/service.py", "JobServiceMixin", 80),
|
|
)
|
|
claimed: set[str] = set()
|
|
for relative, class_name, line_limit in owners:
|
|
path = APP_ROOT / "backend" / relative
|
|
methods = class_methods(path, class_name)
|
|
self.assertTrue(claimed.isdisjoint(methods))
|
|
claimed.update(methods)
|
|
self.assertLessEqual(len(path.read_text(encoding="utf-8").splitlines()), line_limit)
|
|
self.assertEqual(len(claimed), 29)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|