from __future__ import annotations import copy import threading import unittest from datetime import date, datetime, timedelta, timezone, time as dt_time from unittest.mock import patch from pathlib import Path 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_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.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) 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": [], } SHANGHAI = timezone(timedelta(hours=8)) TRADE_DAY = date(2026, 9, 8) def at_clock(hour: int, minute: int, day: date = TRADE_DAY) -> datetime: return datetime(day.year, day.month, day.day, hour, minute, tzinfo=SHANGHAI) class FakeMissingDailyClient: def __init__(self, open_today: bool = True): self.open_today = open_today def dashboard(self, trade_date: str): raise TushareError(f"No daily data returned for {trade_date}") def resolve_trade_context(self, requested: str): if self.open_today: return requested, "20260907" return "20260907", "20260904" class FakeRealtimeTodayClient: def dashboard(self, trade_date: str): return { "meta": { "trade_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}", "requested_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}", "realtime": True, "mode": "realtime", "market_status": "trading", "notice": "盘中行情由 Tushare rt_k 实时计算;涨停原因、封板时间和开板次数以盘后榜单校正为准。", "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), }, "overview": {"limit_up_count": 15}, "limits": [{"code": "000001"}], "broken": [], "down_limits": [], "yesterday_limits": [], } def resolve_trade_context(self, requested: str): return requested, "20260907" class FakeFreeRealtimeTodayClient: def dashboard(self, trade_date: str): return { "meta": { "trade_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}", "requested_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}", "realtime": True, "mode": "realtime", "quote_source": "eastmoney_clist", "source": "eastmoney", "market_status": "trading", "notice": "盘中行情由东财免费实时快照计算;涨停原因、封板时间和开板次数以盘后榜单校正为准。", "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "indices": [{"code": "000001", "price": 3800.1, "change": 0.5}], }, "overview": {"limit_up_count": 18, "up_count": 2100, "amount_billion": 12345.6}, "limits": [{"code": "000001"}], "broken": [], "down_limits": [], "yesterday_limits": [], } def resolve_trade_context(self, requested: str): return requested, "20260907" class SyncHarness(MarketServiceMixin): def __init__(self, client, latest=None, clock=None): self.configured = True self.sync_lock = threading.Lock() self.database = FakeSyncDatabase(latest) self._client = client self.current_user_id = 1 self.clock = clock 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_intraday_refresh_keeps_today_and_does_not_fall_back_to_yesterday(self): today = TRADE_DAY.strftime("%Y%m%d") latest = { "meta": {"trade_date": "2026-09-07", "source": "tushare"}, "overview": {"limit_up_count": 20}, } harness = SyncHarness( FakeRealtimeTodayClient(), latest, clock=lambda: at_clock(10, 5), ) payload = harness.sync_dashboard(today) meta = payload["meta"] self.assertFalse(meta.get("carried_forward")) self.assertTrue(meta["realtime"]) self.assertEqual(meta["data_status"], "intraday") self.assertEqual(str(meta["trade_date"]).replace("-", ""), today) self.assertNotIn("今日数据正在准备", meta.get("display_notice") or "") self.assertEqual(harness.database.saved[0][0], today) def test_intraday_free_source_keeps_today_and_indices(self): today = TRADE_DAY.strftime("%Y%m%d") latest = { "meta": {"trade_date": "2026-09-07", "source": "tushare"}, "overview": {"limit_up_count": 20}, } harness = SyncHarness( FakeFreeRealtimeTodayClient(), latest, clock=lambda: at_clock(10, 5), ) payload = harness.sync_dashboard(today) meta = payload["meta"] self.assertFalse(meta.get("carried_forward")) self.assertTrue(meta["realtime"]) self.assertEqual(meta["data_status"], "intraday") self.assertEqual(str(meta["trade_date"]).replace("-", ""), today) self.assertEqual(meta["quote_source"], "eastmoney_clist") self.assertEqual(payload["overview"]["amount_billion"], 12345.6) self.assertEqual(meta["indices"][0]["price"], 3800.1) self.assertEqual(harness.database.saved[0][0], today) def test_intraday_missing_quotes_do_not_carry_yesterday(self): today = TRADE_DAY.strftime("%Y%m%d") latest = { "meta": {"trade_date": "2026-09-07", "source": "tushare"}, "overview": {"limit_up_count": 20}, } harness = SyncHarness( FakeMissingDailyClient(), latest, clock=lambda: at_clock(10, 5), ) with self.assertRaises(ValueError) as ctx: harness.sync_dashboard(today) self.assertIn("当天盘中行情", str(ctx.exception)) self.assertFalse(harness.database.saved) def test_intraday_keeps_existing_today_snapshot_when_refresh_fails(self): today = TRADE_DAY.strftime("%Y%m%d") existing = { "meta": { "trade_date": "2026-09-08", "realtime": True, "mode": "realtime", "source": "tushare", }, "overview": {"limit_up_count": 11}, "limits": [{"code": "600000"}], "broken": [], "down_limits": [], "yesterday_limits": [], } harness = SyncHarness( FakeMissingDailyClient(), clock=lambda: at_clock(10, 5), ) harness.database.get_snapshot = lambda *_args, **_kwargs: copy.deepcopy(existing) payload = harness.sync_dashboard(today) meta = payload["meta"] self.assertEqual(str(meta["trade_date"]).replace("-", ""), today) self.assertTrue(meta["realtime"]) self.assertEqual(meta["data_status"], "intraday") self.assertFalse(meta.get("carried_forward")) def test_lunch_and_after_hours_keep_today_until_official_arrives(self): today = TRADE_DAY.strftime("%Y%m%d") for clock in (lambda: at_clock(12, 0), lambda: at_clock(16, 10)): harness = SyncHarness( FakeRealtimeTodayClient(), clock=clock, ) payload = harness.sync_dashboard(today) self.assertEqual(str(payload["meta"]["trade_date"]).replace("-", ""), today) self.assertFalse(payload["meta"].get("carried_forward")) def test_preopen_and_weekend_still_carry_last_session(self): latest = { "meta": {"trade_date": "2026-09-07", "source": "tushare"}, "overview": {"limit_up_count": 20}, } preopen = SyncHarness( FakeMissingDailyClient(), latest, clock=lambda: at_clock(8, 30), ) preopen_payload = preopen.sync_dashboard(TRADE_DAY.strftime("%Y%m%d")) self.assertTrue(preopen_payload["meta"]["carried_forward"]) self.assertEqual(preopen_payload["meta"]["data_status"], "preparing") self.assertIn("今日数据正在准备,当前展示", preopen_payload["meta"]["display_notice"]) weekend = SyncHarness( FakeMissingDailyClient(open_today=False), latest, clock=lambda: at_clock(10, 5, date(2026, 9, 5)), ) weekend_payload = weekend.sync_dashboard("20260905") self.assertTrue(weekend_payload["meta"]["carried_forward"]) def test_history_date_still_uses_official_or_preparing_notice(self): latest = { "meta": {"trade_date": "2026-09-01", "source": "tushare"}, "overview": {"limit_up_count": 8}, } harness = SyncHarness( FakeMissingDailyClient(), latest, clock=lambda: at_clock(10, 5), ) payload = harness.sync_dashboard("20260902") self.assertTrue(payload["meta"]["carried_forward"]) self.assertIn("所选日期数据尚未到齐", payload["meta"]["display_notice"]) def test_carried_today_snapshot_is_retried_immediately_in_session(self): today = TRADE_DAY.strftime("%Y%m%d") snapshot = { "meta": { "source": "tushare", "trade_date": "2026-09-07", "carried_forward": True, "requested_date": "2026-09-08", "updated_at": at_clock(10, 0).isoformat(), }, "overview": {"limit_up_count": 1}, } harness = SyncHarness( FakeRealtimeTodayClient(), clock=lambda: at_clock(10, 5), ) harness.database.get_snapshot = lambda *_args, **_kwargs: copy.deepcopy(snapshot) payload = harness.get_dashboard(today) self.assertEqual(str(payload["meta"]["trade_date"]).replace("-", ""), today) self.assertEqual(payload["meta"]["data_status"], "intraday") self.assertTrue(harness.database.saved) 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 dt_time(15, 5) <= now < dt_time(22, 0): self.assertFalse(due) self.assertTrue(derived_due) else: self.assertFalse(due) self.assertFalse(derived_due) def test_official_catchup_is_due_for_intraday_snapshot_after_close(self): today = TRADE_DAY.strftime("%Y%m%d") snapshot = { "meta": { "trade_date": "2026-09-08", "realtime": True, "mode": "realtime", } } with patch("backend.jobs.refresh.datetime") as mocked: mocked.now.return_value = at_clock(16, 10) mocked.strptime = datetime.strptime self.assertTrue(official_catchup_due(today, snapshot)) official = { "meta": { "trade_date": "2026-09-08", "limit_data_source": "official", "realtime": False, } } self.assertFalse(official_catchup_due(today, official)) 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("盘中行情", script) self.assertIn("meta.realtime && actualCompact === requestedCompact", 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__": unittest.main()