feat(v0.2): 功能门槛统一为数据源通用的能力标准

- 后端: 数据集→能力映射增广 (daily/adj_factor/minute/financial),
  非 tickflow provider 按声明数据集动态 grant; 数据源偏好更新与
  删除数据源后刷新 app.state.capabilities 快照; 新增 8 项测试
- 前端: 通用界面去档位词, 缺能力统一「{能力名} · 不可用」徽章
  (capability-labels 新增 MissingCapChip, 点击跳设置→数据源);
  StatCard/深度配置/分钟同步/历史扩展/财务页等 20+ 文件对齐;
  实时模式判定改用 quoteStatus.mode 与 realtime_allowed,
  替代前端 tierRank 推断; 监控设置页放开整页拦截
- 档位词仅保留 TickFlow 专属界面 (Key 配置/端点测速/引导页),
  docs/configuration.md 档位表加注解说明
This commit is contained in:
shy3130
2026-08-23 12:34:40 +08:00
parent 0f2c7ce9b0
commit da3ada28e1
25 changed files with 327 additions and 172 deletions
+120
View File
@@ -0,0 +1,120 @@
"""能力标准统一: 自定义/插件数据源能力增广回归测试。
对应 _augment_custom_sources 的数据集→能力映射 (daily/adj_factor/minute/financial):
某数据集的当前 provider 非 tickflow 且声明了该数据集 → grant 对应能力;
取数路由仍按 preferences 分流, 不会误调 TickFlow。
"""
from __future__ import annotations
from unittest.mock import MagicMock
from app.tickflow.capabilities import Cap, CapabilityLimits, CapabilitySet
from app.tickflow.policy import _augment_custom_sources
def _set_providers(monkeypatch, *, daily="tickflow", adj="same_as_daily",
minute="tickflow", financial="tickflow") -> None:
"""mock preferences 各数据集 provider getter。"""
from app.services import preferences
monkeypatch.setattr(preferences, "get_daily_data_provider", lambda: daily)
monkeypatch.setattr(preferences, "get_adj_factor_provider", lambda: adj)
monkeypatch.setattr(preferences, "get_minute_data_provider", lambda: minute)
monkeypatch.setattr(preferences, "get_financial_provider", lambda: financial)
def _set_datasets(monkeypatch, datasets: set[str]) -> None:
"""mock provider_has_dataset: 非 tickflow provider 对给定数据集返回 True。"""
monkeypatch.setattr(
"app.data_providers.custom.provider_has_dataset",
lambda name, ds: name != "tickflow" and ds in datasets,
)
def test_daily_custom_source_grants_daily_batch(monkeypatch):
_set_providers(monkeypatch, daily="mock_src")
_set_datasets(monkeypatch, {"daily"})
capset = CapabilitySet()
_augment_custom_sources(capset)
assert capset.has(Cap.KLINE_DAILY_BATCH)
# 未声明其他数据集 → 不补
assert not capset.has(Cap.ADJ_FACTOR)
assert not capset.has(Cap.KLINE_MINUTE_BATCH)
assert not capset.has(Cap.FINANCIAL)
def test_adj_same_as_daily_resolves_to_daily_provider(monkeypatch):
"""adj_factor_provider=same_as_daily → 跟随 daily provider 判定。"""
_set_providers(monkeypatch, daily="mock_src", adj="same_as_daily")
_set_datasets(monkeypatch, {"adj_factor"})
capset = CapabilitySet()
_augment_custom_sources(capset)
assert capset.has(Cap.ADJ_FACTOR)
def test_minute_custom_source_grants_minute_batch(monkeypatch):
"""原有 minute 增广行为保持。"""
_set_providers(monkeypatch, minute="mock_src")
_set_datasets(monkeypatch, {"minute"})
capset = CapabilitySet()
_augment_custom_sources(capset)
assert capset.has(Cap.KLINE_MINUTE_BATCH)
def test_financial_custom_source_grants_financial(monkeypatch):
_set_providers(monkeypatch, financial="mock_src")
_set_datasets(monkeypatch, {"financial"})
capset = CapabilitySet()
_augment_custom_sources(capset)
assert capset.has(Cap.FINANCIAL)
def test_provider_active_but_dataset_not_declared_no_grant(monkeypatch):
"""provider 被选为当前源但未声明该数据集 → 不 grant (回退 TickFlow 语义)。"""
_set_providers(monkeypatch, daily="mock_src", minute="mock_src",
adj="mock_src", financial="mock_src")
_set_datasets(monkeypatch, set()) # 什么都不声明
capset = CapabilitySet()
_augment_custom_sources(capset)
assert not capset.has(Cap.KLINE_DAILY_BATCH)
assert not capset.has(Cap.ADJ_FACTOR)
assert not capset.has(Cap.KLINE_MINUTE_BATCH)
assert not capset.has(Cap.FINANCIAL)
def test_tickflow_active_no_grant(monkeypatch):
"""全部数据集仍走 tickflow → 不补任何能力。"""
_set_providers(monkeypatch) # 默认全 tickflow
_set_datasets(monkeypatch, {"daily", "adj_factor", "minute", "financial"})
capset = CapabilitySet()
_augment_custom_sources(capset)
assert not capset.has(Cap.KLINE_DAILY_BATCH)
assert not capset.has(Cap.ADJ_FACTOR)
assert not capset.has(Cap.KLINE_MINUTE_BATCH)
assert not capset.has(Cap.FINANCIAL)
def test_grant_does_not_override_tickflow_limits(monkeypatch):
"""grant 不覆盖 TickFlow 已有能力及其限制。"""
_set_providers(monkeypatch, minute="mock_src")
_set_datasets(monkeypatch, {"minute"})
capset = CapabilitySet()
capset.grant(Cap.KLINE_MINUTE_BATCH, CapabilityLimits(rpm=30, batch=100))
_augment_custom_sources(capset)
lim = capset.limits(Cap.KLINE_MINUTE_BATCH)
assert lim is not None and lim.rpm == 30 and lim.batch == 100
def test_update_data_providers_refreshes_capability_snapshot(monkeypatch):
"""切换数据源后 app.state.capabilities 快照应刷新 (读缓存+增广, 无网络)。"""
from app.api import settings as settings_api
monkeypatch.setattr("app.services.preferences.save", lambda upd: None)
sentinel = CapabilitySet()
monkeypatch.setattr(settings_api, "detect_capabilities", lambda: sentinel)
mock_request = MagicMock()
settings_api.update_data_providers(
MagicMock(model_dump=lambda exclude_none: {"daily_data_provider": "mock_src"}),
mock_request,
)
assert mock_request.app.state.capabilities is sentinel