fix(HEL-412): 刷新降级不再整次失败,并补齐准备中提示
手动刷新与自动补跑共用可用数据判定:日线推算或上一交易日快照记为部分/准备中成功,避免前端误报刷新失败。HTTP JSON 解析错误不再把请求正文写入日志。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
0d13066386
commit
5085cacf0d
@@ -1,23 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import threading
|
||||
import unittest
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from backend.jobs.service import _verified_dashboard_result
|
||||
from backend.features.market.service import MarketServiceMixin
|
||||
from backend.jobs.refresh import (
|
||||
dashboard_has_usable_data,
|
||||
official_catchup_due,
|
||||
verified_dashboard_result,
|
||||
)
|
||||
from backend.data.providers.tushare_transport import TushareError
|
||||
|
||||
|
||||
class AdminRefreshStatusTests(unittest.TestCase):
|
||||
def test_carried_snapshot_is_reported_as_failed_job(self):
|
||||
result = _verified_dashboard_result(
|
||||
{"meta": {"carried_forward": True, "notice": "官方涨跌停数据尚未返回"}}
|
||||
def test_carried_snapshot_is_usable_not_failed_job(self):
|
||||
result = verified_dashboard_result(
|
||||
{
|
||||
"meta": {
|
||||
"trade_date": "2026-09-01",
|
||||
"requested_date": "2026-09-02",
|
||||
"carried_forward": True,
|
||||
"notice": "今日数据正在准备,当前展示 9 月 1 日",
|
||||
"data_status": "preparing",
|
||||
},
|
||||
"overview": {"limit_up_count": 12},
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertEqual(result["error"], "官方涨跌停数据尚未返回")
|
||||
self.assertNotEqual(result.get("status"), "failed")
|
||||
self.assertEqual(result["meta"]["data_status"], "preparing")
|
||||
self.assertTrue(dashboard_has_usable_data(result))
|
||||
|
||||
def test_derived_limit_snapshot_is_usable_not_failed_job(self):
|
||||
dashboard = {
|
||||
"meta": {
|
||||
"trade_date": "2026-09-02",
|
||||
"limit_data_source": "derived",
|
||||
"notice": "涨跌停高级接口当日数据尚未更新,已使用日线数据推算。",
|
||||
"data_status": "partial",
|
||||
},
|
||||
"overview": {"limit_up_count": 8},
|
||||
}
|
||||
|
||||
self.assertIs(verified_dashboard_result(dashboard), dashboard)
|
||||
|
||||
def test_current_snapshot_is_reported_as_successful_job(self):
|
||||
dashboard = {"meta": {"trade_date": "2026-08-28", "carried_forward": False}}
|
||||
|
||||
self.assertIs(_verified_dashboard_result(dashboard), dashboard)
|
||||
self.assertIs(verified_dashboard_result(dashboard), dashboard)
|
||||
|
||||
def test_empty_payload_is_still_failed(self):
|
||||
result = verified_dashboard_result({"meta": {}, "overview": {}})
|
||||
self.assertEqual(result["status"], "failed")
|
||||
|
||||
|
||||
class FakeSyncDatabase:
|
||||
def __init__(self, latest=None):
|
||||
self.latest = latest
|
||||
self.saved = []
|
||||
self.finished = []
|
||||
|
||||
def start_sync(self, *_args, **_kwargs):
|
||||
return 1
|
||||
|
||||
def save_snapshot(self, trade_date, source, payload):
|
||||
self.saved.append((trade_date, source, copy.deepcopy(payload)))
|
||||
|
||||
def save_data_snapshot(self, *_args, **_kwargs):
|
||||
return None
|
||||
|
||||
def finish_sync(self, *args, **kwargs):
|
||||
self.finished.append((args, kwargs))
|
||||
|
||||
def get_latest_real_snapshot(self, *_args, **_kwargs):
|
||||
return copy.deepcopy(self.latest)
|
||||
|
||||
def get_snapshot(self, *_args, **_kwargs):
|
||||
return None
|
||||
|
||||
def get_data_snapshot(self, *_args, **_kwargs):
|
||||
return None
|
||||
|
||||
def reason_overrides(self, *_args, **_kwargs):
|
||||
return {}
|
||||
|
||||
|
||||
class FakeDerivedClient:
|
||||
def dashboard(self, trade_date: str):
|
||||
return {
|
||||
"meta": {
|
||||
"trade_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}",
|
||||
"limit_data_source": "derived",
|
||||
"notice": "涨跌停高级接口当日数据尚未更新,已使用日线数据推算。",
|
||||
"updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
},
|
||||
"overview": {"limit_up_count": 3},
|
||||
"limits": [{"code": "000001"}],
|
||||
"broken": [],
|
||||
"down_limits": [],
|
||||
"yesterday_limits": [],
|
||||
}
|
||||
|
||||
|
||||
class FakeMissingDailyClient:
|
||||
def dashboard(self, trade_date: str):
|
||||
raise TushareError(f"No daily data returned for {trade_date}")
|
||||
|
||||
|
||||
class SyncHarness(MarketServiceMixin):
|
||||
def __init__(self, client, latest=None):
|
||||
self.configured = True
|
||||
self.sync_lock = threading.Lock()
|
||||
self.database = FakeSyncDatabase(latest)
|
||||
self._client = client
|
||||
self.current_user_id = 1
|
||||
|
||||
def _tushare_client(self):
|
||||
return self._client
|
||||
|
||||
def _enrich_dashboard_sentiment(self, dashboard, _trade_date):
|
||||
return dashboard
|
||||
|
||||
def _apply_reason_overrides(self, dashboard):
|
||||
return dashboard
|
||||
|
||||
|
||||
class DashboardFreshnessTests(unittest.TestCase):
|
||||
def test_derived_limits_are_kept_as_partial_success(self):
|
||||
today = date.today().strftime("%Y%m%d")
|
||||
harness = SyncHarness(FakeDerivedClient())
|
||||
payload = harness.sync_dashboard(today)
|
||||
meta = payload["meta"]
|
||||
|
||||
self.assertEqual(meta["limit_data_source"], "derived")
|
||||
self.assertEqual(meta["data_status"], "partial")
|
||||
self.assertFalse(meta.get("carried_forward"))
|
||||
self.assertIn("日线数据推算", meta["display_notice"])
|
||||
self.assertEqual(harness.database.finished[0][0][1], "success")
|
||||
self.assertEqual(verified_dashboard_result(payload), payload)
|
||||
|
||||
def test_missing_official_data_keeps_previous_day_with_preparing_notice(self):
|
||||
today = date.today()
|
||||
previous = (today - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
latest = {
|
||||
"meta": {"trade_date": previous, "source": "tushare"},
|
||||
"overview": {"limit_up_count": 20},
|
||||
}
|
||||
harness = SyncHarness(FakeMissingDailyClient(), latest)
|
||||
payload = harness.sync_dashboard(today.strftime("%Y%m%d"))
|
||||
meta = payload["meta"]
|
||||
|
||||
self.assertTrue(meta["carried_forward"])
|
||||
self.assertEqual(meta["data_status"], "preparing")
|
||||
self.assertIn("今日数据正在准备,当前展示", meta["display_notice"])
|
||||
self.assertIn("月", meta["display_notice"])
|
||||
self.assertNotIn("No daily data", meta["display_notice"])
|
||||
self.assertNotEqual(verified_dashboard_result(payload).get("status"), "failed")
|
||||
|
||||
def test_weekend_carry_is_not_labeled_as_preparing(self):
|
||||
snapshot = {
|
||||
"meta": {"trade_date": "2026-07-24", "source": "tushare", "updated_at": "2026-07-24T15:00:00+08:00"},
|
||||
"overview": {"limit_up_count": 1},
|
||||
}
|
||||
harness = SyncHarness(FakeMissingDailyClient())
|
||||
carried = harness._carry_dashboard(snapshot, "20260725", "非交易日沿用最近交易日收盘行情")
|
||||
self.assertEqual(carried["meta"]["data_status"], "carried")
|
||||
self.assertIn("非交易日", carried["meta"]["display_notice"])
|
||||
|
||||
def test_stale_derived_snapshot_is_retried(self):
|
||||
today = date.today().strftime("%Y%m%d")
|
||||
old = datetime.now(timezone.utc) - timedelta(minutes=5)
|
||||
snapshot = {
|
||||
"meta": {
|
||||
"source": "tushare",
|
||||
"trade_date": f"{today[:4]}-{today[4:6]}-{today[6:8]}",
|
||||
"limit_data_source": "derived",
|
||||
"updated_at": old.isoformat(),
|
||||
},
|
||||
"overview": {"limit_up_count": 1},
|
||||
}
|
||||
harness = SyncHarness(FakeDerivedClient())
|
||||
harness.database.get_snapshot = lambda *_args, **_kwargs: copy.deepcopy(snapshot)
|
||||
payload = harness.get_dashboard(today)
|
||||
self.assertEqual(payload["meta"]["data_status"], "partial")
|
||||
self.assertTrue(harness.database.saved)
|
||||
|
||||
def test_official_catchup_skips_complete_today_snapshot(self):
|
||||
today = date.today().strftime("%Y%m%d")
|
||||
iso = f"{today[:4]}-{today[4:6]}-{today[6:8]}"
|
||||
due = official_catchup_due(
|
||||
today,
|
||||
{"meta": {"trade_date": iso, "limit_data_source": "official"}},
|
||||
)
|
||||
derived_due = official_catchup_due(
|
||||
today,
|
||||
{"meta": {"trade_date": iso, "limit_data_source": "derived"}},
|
||||
)
|
||||
now = datetime.now().astimezone().time().replace(tzinfo=None)
|
||||
if datetime.strptime("15:05", "%H:%M").time() <= now < datetime.strptime("22:00", "%H:%M").time():
|
||||
self.assertFalse(due)
|
||||
self.assertTrue(derived_due)
|
||||
else:
|
||||
self.assertFalse(due)
|
||||
self.assertFalse(derived_due)
|
||||
|
||||
|
||||
class FrontendRefreshCopyTests(unittest.TestCase):
|
||||
def test_dashboard_script_distinguishes_partial_from_failure(self):
|
||||
script = (Path(__file__).resolve().parents[1] / "frontend" / "shared" / "dashboard.js").read_text(encoding="utf-8")
|
||||
self.assertIn("今日数据正在准备,当前展示", script)
|
||||
self.assertIn("部分正式数据尚未到齐", script)
|
||||
self.assertIn('job.status === "failed"', script)
|
||||
failed_block = script.split("if (job.status === \"failed\")", 1)[1].split("const query", 1)[0]
|
||||
self.assertIn("后台刷新失败", failed_block)
|
||||
success_block = script.split("const freshness = dashboardFreshnessMessage(meta);", 1)[1]
|
||||
self.assertNotIn("后台刷新失败", success_block.split("} else {", 1)[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -198,7 +198,8 @@ class DatahubBridgeTests(unittest.TestCase):
|
||||
self.assertEqual(canonical["vol"], 100000.0)
|
||||
self.assertEqual(canonical["amount"], 2000000.0)
|
||||
|
||||
def test_heaven_keeps_legacy_even_when_read_flag_is_on(self) -> None:
|
||||
def test_heaven_keeps_legacy_on_first_batch_even_when_read_flag_is_on(self) -> None:
|
||||
"""问天未永久冻结;首批只读接入仍走旧链路,后续迁移可以纳入。"""
|
||||
self.assertTrue(looks_like_heaven("backend.features.heaven.market_context", "backend/features/heaven/market_context.py"))
|
||||
self.assertFalse(looks_like_heaven("backend.features.market.service", "backend/features/market/service.py"))
|
||||
client = FakeClient()
|
||||
|
||||
Reference in New Issue
Block a user