mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* feat: add report image sharing * fix(review-feedback-2146): 评审结论 - 代码检查 :当前整个 PR 仍有 2 个未关闭的高置信度代码 blocker。最新复核摘要:基于 merge-base * fix(review-feedback-2146): preserve the Markdown fallback for unavailable snapshots and preserve * fix(review-feedback-2146): 补一组通知回归测试,至少覆盖 * fix(review-feedback-2146): 在源码树执行 npm ci 和 npx playwright install chromium,这与预编译 desktop 发行包环境不一致 * fix(review-feedback-2146): 评审结论 - 代码检查 :当前整个 PR 仍有 1 个未关闭的高置信度代码 blocker。最新复核摘要:基于 merge-base * fix(review-feedback-2146): 补上明确的 CLI/Chromium 安装提示,避免当前日志和 503 文案继续把用户引向 * fix(review-feedback-2146): 补到与 docs/share-images.md 一致,直接提示 cd apps/dsa-web && npm ci 和 npx
931 lines
41 KiB
Python
931 lines
41 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for localized market review wrappers."""
|
|
|
|
import importlib
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from types import ModuleType, SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from tests.litellm_stub import ensure_litellm_stub
|
|
|
|
ensure_litellm_stub()
|
|
|
|
def _build_optional_module_stubs() -> dict[str, ModuleType]:
|
|
stubs: dict[str, ModuleType] = {}
|
|
google_module: ModuleType | None = None
|
|
|
|
for module_name in ("google.generativeai", "google.genai", "anthropic"):
|
|
try:
|
|
importlib.import_module(module_name)
|
|
continue
|
|
except ImportError:
|
|
stub = ModuleType(module_name)
|
|
stubs[module_name] = stub
|
|
if not module_name.startswith("google."):
|
|
continue
|
|
if google_module is None:
|
|
try:
|
|
google_module = importlib.import_module("google")
|
|
except ImportError:
|
|
google_module = ModuleType("google")
|
|
stubs["google"] = google_module
|
|
setattr(google_module, module_name.split(".", 1)[1], stub)
|
|
|
|
return stubs
|
|
|
|
|
|
sys.modules.update(_build_optional_module_stubs())
|
|
import src.core.market_review as market_review_module
|
|
from src.config import Config
|
|
from src.llm.generation_backend import GenerationError, GenerationErrorCode
|
|
from src.services.run_diagnostics import activate_run_diagnostic_context, reset_run_diagnostic_context
|
|
from src.storage import AnalysisHistory, DatabaseManager
|
|
|
|
run_market_review = market_review_module.run_market_review
|
|
|
|
|
|
class MarketReviewLocalizationTestCase(unittest.TestCase):
|
|
def _make_notifier(self) -> MagicMock:
|
|
notifier = MagicMock()
|
|
notifier.save_report_to_file.return_value = "/tmp/market_review.md"
|
|
notifier.is_available.return_value = True
|
|
notifier.send.return_value = True
|
|
return notifier
|
|
|
|
def test_resolve_market_review_regions_returns_ordered_non_empty_list(self) -> None:
|
|
cases = [
|
|
(None, ["cn"]),
|
|
("", ["cn"]),
|
|
("both", ["cn", "hk", "us", "jp", "kr"]),
|
|
(" CN,US,cn ", ["cn", "us"]),
|
|
("us,cn,us", ["cn", "us"]),
|
|
("jp", ["jp"]),
|
|
("KR", ["kr"]),
|
|
("kr,jp,us", ["us", "jp", "kr"]),
|
|
("eu,apac", ["cn"]),
|
|
(",,", ["cn"]),
|
|
("HK", ["hk"]),
|
|
("invalid", ["cn"]),
|
|
]
|
|
|
|
for raw_region, expected in cases:
|
|
with self.subTest(raw_region=raw_region):
|
|
self.assertEqual(
|
|
market_review_module._resolve_market_review_regions(raw_region),
|
|
expected,
|
|
)
|
|
|
|
def test_run_market_review_uses_english_notification_title(self) -> None:
|
|
notifier = self._make_notifier()
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="## 2026-04-10 A-share Market Recap\n\nBody",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-04-10", "score": 60},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="en", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
return_value=market_analyzer,
|
|
), patch.object(market_review_module, "_persist_market_review_history") as persist_history:
|
|
result = run_market_review(notifier, send_notification=True)
|
|
|
|
self.assertEqual(result, "## 2026-04-10 A-share Market Recap\n\nBody")
|
|
saved_content = notifier.save_report_to_file.call_args.args[0]
|
|
self.assertTrue(saved_content.startswith("[dsa-market-region]: # (cn)\n\n# 🎯 Market Review\n\n"))
|
|
sent_content = notifier.send.call_args.args[0]
|
|
self.assertTrue(sent_content.startswith("[dsa-market-region]: # (cn)\n\n🎯 Market Review\n\n"))
|
|
self.assertTrue(notifier.send.call_args.kwargs["email_send_to_all"])
|
|
self.assertEqual(notifier.send.call_args.kwargs["route_type"], "report")
|
|
persist_history.assert_called_once()
|
|
self.assertTrue(persist_history.call_args.kwargs["query_id"].startswith("market_review_"))
|
|
|
|
def test_run_market_review_can_skip_report_file_for_context_generation(self) -> None:
|
|
notifier = self._make_notifier()
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="CN body",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-03-06", "score": 60},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
return_value=market_analyzer,
|
|
), patch.object(market_review_module, "_persist_market_review_history") as persist_history:
|
|
result = run_market_review(
|
|
notifier,
|
|
send_notification=False,
|
|
return_structured=True,
|
|
save_report_file=False,
|
|
)
|
|
|
|
self.assertIsInstance(result, market_review_module.MarketReviewRunResult)
|
|
self.assertEqual(result.report, "CN body")
|
|
notifier.save_report_to_file.assert_not_called()
|
|
persist_history.assert_called_once()
|
|
|
|
def test_run_market_review_passes_request_config_to_generation(self) -> None:
|
|
notifier = self._make_notifier()
|
|
request_config = SimpleNamespace(report_language="en", market_review_region="cn")
|
|
global_config = SimpleNamespace(report_language="zh", market_review_region="cn")
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="English market review body",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-04-10", "score": 60},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=global_config,
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
return_value=market_analyzer,
|
|
) as analyzer_cls, patch.object(market_review_module, "_persist_market_review_history") as persist_history:
|
|
result = run_market_review(
|
|
notifier,
|
|
config=request_config,
|
|
send_notification=False,
|
|
return_structured=True,
|
|
)
|
|
|
|
self.assertEqual(analyzer_cls.call_args.kwargs["config"], request_config)
|
|
self.assertEqual(persist_history.call_args.kwargs["config"], request_config)
|
|
self.assertTrue(
|
|
notifier.save_report_to_file.call_args.args[0].startswith(
|
|
"[dsa-market-region]: # (cn)\n\n# 🎯 Market Review\n\n"
|
|
)
|
|
)
|
|
self.assertEqual(result.market_review_payload["language"], "en")
|
|
self.assertEqual(result.report, "English market review body")
|
|
|
|
def test_run_market_review_returns_sector_fallback_for_merged_notification(self) -> None:
|
|
notifier = self._make_notifier()
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="## 今日大盘\n\n盘面正文。",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-06-03", "score": 60},
|
|
structured_payload={
|
|
"kind": "market_review",
|
|
"region": "cn",
|
|
"language": "zh",
|
|
"title": "今日大盘",
|
|
"sections": [
|
|
{
|
|
"key": "overview",
|
|
"title": "概览",
|
|
"markdown": "盘面正文。",
|
|
}
|
|
],
|
|
"sectors": {
|
|
"top": [{"name": "AI算力", "change_pct": 3.25}],
|
|
"bottom": [{"name": "煤炭", "change_pct": -1.12}],
|
|
},
|
|
},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
return_value=market_analyzer,
|
|
), patch.object(market_review_module, "_persist_market_review_history"):
|
|
result = run_market_review(
|
|
notifier,
|
|
send_notification=True,
|
|
merge_notification=True,
|
|
)
|
|
|
|
self.assertIn("## 今日大盘", result)
|
|
self.assertIn("### 板块主线", result)
|
|
self.assertIn("| 1 | AI算力 | +3.25% |", result)
|
|
self.assertIn("| 1 | 煤炭 | -1.12% |", result)
|
|
notifier.send.assert_not_called()
|
|
|
|
def test_run_market_review_reraises_generation_backend_config_error(self) -> None:
|
|
notifier = self._make_notifier()
|
|
backend_error = GenerationError(
|
|
error_code=GenerationErrorCode.BACKEND_NOT_CONFIGURED,
|
|
stage="generation",
|
|
retryable=False,
|
|
fallbackable=False,
|
|
backend="codex",
|
|
details={
|
|
"field": "GENERATION_BACKEND",
|
|
"requested_backend": "codex",
|
|
},
|
|
)
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.side_effect = backend_error
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
return_value=market_analyzer,
|
|
):
|
|
with self.assertRaises(GenerationError) as exc_info:
|
|
run_market_review(
|
|
notifier,
|
|
config=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
send_notification=False,
|
|
save_report_file=False,
|
|
persist_history=False,
|
|
)
|
|
|
|
self.assertIs(exc_info.exception, backend_error)
|
|
notifier.save_report_to_file.assert_not_called()
|
|
notifier.send.assert_not_called()
|
|
|
|
def test_run_market_review_merges_both_regions_with_english_wrappers(self) -> None:
|
|
notifier = self._make_notifier()
|
|
cn_analyzer = MagicMock()
|
|
cn_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="CN body",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-03-06", "score": 60},
|
|
)
|
|
hk_analyzer = MagicMock()
|
|
hk_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="HK body",
|
|
market_light_snapshot={"region": "hk", "trade_date": "2026-03-06", "score": 58},
|
|
)
|
|
us_analyzer = MagicMock()
|
|
us_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="US body",
|
|
market_light_snapshot={"region": "us", "trade_date": "2026-03-06", "score": 55},
|
|
)
|
|
jp_analyzer = MagicMock()
|
|
jp_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="JP body",
|
|
market_light_snapshot={"region": "jp", "trade_date": "2026-03-06", "score": 54},
|
|
)
|
|
kr_analyzer = MagicMock()
|
|
kr_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="KR body",
|
|
market_light_snapshot={"region": "kr", "trade_date": "2026-03-06", "score": 53},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="en", market_review_region="both"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
side_effect=[cn_analyzer, hk_analyzer, us_analyzer, jp_analyzer, kr_analyzer],
|
|
), patch.object(market_review_module, "_persist_market_review_history") as persist_history:
|
|
result = run_market_review(notifier, send_notification=True)
|
|
|
|
self.assertIn("# A-share Market Recap\n\nCN body", result)
|
|
self.assertIn("# HK Market Recap\n\nHK body", result)
|
|
self.assertIn("> Next market recap follows", result)
|
|
self.assertIn("# US Market Recap\n\nUS body", result)
|
|
self.assertIn("# Japan Market Recap\n\nJP body", result)
|
|
self.assertIn("# Korea Market Recap\n\nKR body", result)
|
|
saved_content = notifier.save_report_to_file.call_args.args[0]
|
|
self.assertTrue(saved_content.startswith("# 🎯 Market Review\n\n"))
|
|
self.assertIn("# A-share Market Recap\n\nCN body", saved_content)
|
|
self.assertIn("> Next market recap follows", saved_content)
|
|
self.assertIn("# HK Market Recap\n\nHK body", saved_content)
|
|
self.assertIn("# US Market Recap\n\nUS body", saved_content)
|
|
self.assertIn("# Japan Market Recap\n\nJP body", saved_content)
|
|
self.assertIn("# Korea Market Recap\n\nKR body", saved_content)
|
|
self.assertIn(
|
|
"# A-share Market Recap\n\nCN body",
|
|
persist_history.call_args.kwargs["markdown_report"],
|
|
)
|
|
self.assertEqual(
|
|
set(persist_history.call_args.kwargs["market_light_snapshots"]),
|
|
{"cn", "hk", "us"},
|
|
)
|
|
sent_content = notifier.send.call_args.args[0]
|
|
self.assertTrue(sent_content.startswith("🎯 Market Review\n\n"))
|
|
self.assertIn("# US Market Recap\n\nUS body", sent_content)
|
|
self.assertIn("# Japan Market Recap\n\nJP body", sent_content)
|
|
self.assertIn("# Korea Market Recap\n\nKR body", sent_content)
|
|
|
|
def test_run_market_review_comma_joined_subset_jp_kr(self) -> None:
|
|
notifier = self._make_notifier()
|
|
jp_analyzer = MagicMock()
|
|
jp_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="JP body",
|
|
market_light_snapshot={"region": "jp", "trade_date": "2026-03-06", "score": 54},
|
|
)
|
|
kr_analyzer = MagicMock()
|
|
kr_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="KR body",
|
|
market_light_snapshot={"region": "kr", "trade_date": "2026-03-06", "score": 53},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
side_effect=[jp_analyzer, kr_analyzer],
|
|
), patch.object(market_review_module, "_persist_market_review_history"):
|
|
result = run_market_review(
|
|
notifier, send_notification=False, override_region="jp,kr"
|
|
)
|
|
|
|
self.assertIn("# 日股大盘复盘\n\nJP body", result)
|
|
self.assertIn("# 韩股大盘复盘\n\nKR body", result)
|
|
self.assertNotIn("A股大盘复盘", result)
|
|
self.assertNotIn("美股大盘复盘", result)
|
|
|
|
def test_run_market_review_comma_joined_subset_cn_us(self) -> None:
|
|
"""Regression: compute_effective_region("both", {"cn","us"}) -> "cn,us"
|
|
must produce A-share + US report without HK."""
|
|
notifier = self._make_notifier()
|
|
cn_analyzer = MagicMock()
|
|
cn_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="CN body",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-03-06", "score": 60},
|
|
)
|
|
us_analyzer = MagicMock()
|
|
us_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="US body",
|
|
market_light_snapshot={"region": "us", "trade_date": "2026-03-06", "score": 55},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
side_effect=[cn_analyzer, us_analyzer],
|
|
), patch.object(market_review_module, "_persist_market_review_history"):
|
|
result = run_market_review(
|
|
notifier, send_notification=False, override_region="cn,us"
|
|
)
|
|
|
|
self.assertIn("# A股大盘复盘\n\nCN body", result)
|
|
self.assertIn("# 美股大盘复盘\n\nUS body", result)
|
|
self.assertNotIn("港股", result)
|
|
self.assertNotIn("HK", result)
|
|
|
|
def test_run_market_review_comma_joined_subset_cn_hk(self) -> None:
|
|
"""Regression: compute_effective_region("both", {"cn","hk"}) -> "cn,hk"
|
|
must produce A-share + HK report without US."""
|
|
notifier = self._make_notifier()
|
|
cn_analyzer = MagicMock()
|
|
cn_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="CN body",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-03-06", "score": 60},
|
|
)
|
|
hk_analyzer = MagicMock()
|
|
hk_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="HK body",
|
|
market_light_snapshot={"region": "hk", "trade_date": "2026-03-06", "score": 58},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
side_effect=[cn_analyzer, hk_analyzer],
|
|
), patch.object(market_review_module, "_persist_market_review_history"):
|
|
result = run_market_review(
|
|
notifier, send_notification=False, override_region="cn,hk"
|
|
)
|
|
|
|
self.assertIn("# A股大盘复盘\n\nCN body", result)
|
|
self.assertIn("# 港股大盘复盘\n\nHK body", result)
|
|
self.assertNotIn("美股", result)
|
|
self.assertNotIn("US Market", result)
|
|
|
|
def test_run_market_review_persists_only_current_run_market_light_snapshots(self) -> None:
|
|
notifier = self._make_notifier()
|
|
cn_analyzer = MagicMock()
|
|
cn_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="CN body",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-03-06", "score": 60},
|
|
)
|
|
us_analyzer = MagicMock()
|
|
us_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="US body",
|
|
market_light_snapshot={"region": "us", "trade_date": "2026-03-06", "score": 55},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
side_effect=[cn_analyzer, us_analyzer],
|
|
), patch.object(market_review_module, "_persist_market_review_history") as persist_history:
|
|
run_market_review(notifier, send_notification=False, override_region="cn,us")
|
|
|
|
snapshots = persist_history.call_args.kwargs["market_light_snapshots"]
|
|
self.assertEqual(set(snapshots), {"cn", "us"})
|
|
self.assertEqual(snapshots["cn"]["score"], 60)
|
|
self.assertEqual(snapshots["us"]["score"], 55)
|
|
|
|
def test_run_market_review_jp_kr_skips_market_light_snapshot_schema(self) -> None:
|
|
notifier = self._make_notifier()
|
|
|
|
from src.market_analyzer import MarketOverview
|
|
|
|
with patch.object(
|
|
market_review_module.MarketAnalyzer,
|
|
"get_market_overview",
|
|
side_effect=[
|
|
MarketOverview(date="2026-03-06"),
|
|
MarketOverview(date="2026-03-06"),
|
|
],
|
|
), patch.object(
|
|
market_review_module.MarketAnalyzer,
|
|
"search_market_news",
|
|
return_value=[],
|
|
), patch.object(
|
|
market_review_module.MarketAnalyzer,
|
|
"generate_market_review",
|
|
side_effect=["JP body", "KR body"],
|
|
), patch.object(market_review_module, "_persist_market_review_history") as persist_history:
|
|
result = run_market_review(
|
|
notifier,
|
|
config=SimpleNamespace(report_language="zh", market_review_region="jp,kr"),
|
|
send_notification=False,
|
|
)
|
|
|
|
self.assertIn("# 日股大盘复盘\n\nJP body", result)
|
|
self.assertIn("# 韩股大盘复盘\n\nKR body", result)
|
|
self.assertEqual(persist_history.call_args.kwargs["market_light_snapshots"], {})
|
|
payload = persist_history.call_args.kwargs["market_review_payload"]
|
|
self.assertNotIn("market_light", payload["markets"]["jp"])
|
|
self.assertNotIn("market_light", payload["markets"]["kr"])
|
|
|
|
def test_run_market_review_normalizes_single_region_snapshot_key(self) -> None:
|
|
notifier = self._make_notifier()
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="CN body",
|
|
market_light_snapshot={
|
|
"region": "cn",
|
|
"trade_date": "2026-03-06",
|
|
"score": 60,
|
|
},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
return_value=market_analyzer,
|
|
) as analyzer_cls, patch.object(
|
|
market_review_module, "_persist_market_review_history"
|
|
) as persist_history:
|
|
run_market_review(notifier, send_notification=False, override_region="CN")
|
|
|
|
self.assertEqual(analyzer_cls.call_args.kwargs["region"], "cn")
|
|
persist_history.assert_called_once()
|
|
self.assertEqual(persist_history.call_args.kwargs["region"], "cn")
|
|
snapshots = persist_history.call_args.kwargs["market_light_snapshots"]
|
|
self.assertEqual(set(snapshots), {"cn"})
|
|
self.assertEqual(snapshots["cn"]["trade_date"], "2026-03-06")
|
|
|
|
def test_run_market_review_invalid_comma_subset_falls_back_to_cn(self) -> None:
|
|
notifier = self._make_notifier()
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="CN body",
|
|
market_light_snapshot={
|
|
"region": "cn",
|
|
"trade_date": "2026-03-06",
|
|
"score": 60,
|
|
},
|
|
)
|
|
|
|
with patch.object(
|
|
market_review_module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
), patch.object(
|
|
market_review_module,
|
|
"MarketAnalyzer",
|
|
return_value=market_analyzer,
|
|
) as analyzer_cls, patch.object(
|
|
market_review_module, "_persist_market_review_history"
|
|
) as persist_history:
|
|
result = run_market_review(
|
|
notifier, send_notification=False, override_region="eu,apac"
|
|
)
|
|
|
|
self.assertEqual(result, "CN body")
|
|
self.assertEqual(analyzer_cls.call_args.kwargs["region"], "cn")
|
|
persist_history.assert_called_once()
|
|
self.assertEqual(persist_history.call_args.kwargs["region"], "cn")
|
|
snapshots = persist_history.call_args.kwargs["market_light_snapshots"]
|
|
self.assertEqual(set(snapshots), {"cn"})
|
|
|
|
def test_render_market_review_payload_markdown_does_not_repeat_title(self) -> None:
|
|
markdown = market_review_module._render_market_review_payload_markdown(
|
|
{
|
|
"title": "2026-06-03 大盘复盘",
|
|
"sections": [
|
|
{
|
|
"key": "daily_review",
|
|
"title": "2026-06-03 大盘复盘",
|
|
"markdown": "> 今日指数强弱分化。\n\n### 一、盘面总览\n正文",
|
|
}
|
|
],
|
|
},
|
|
wrapper_title="🎯 大盘复盘",
|
|
)
|
|
|
|
self.assertEqual(markdown.count("2026-06-03 大盘复盘"), 1)
|
|
self.assertTrue(markdown.startswith("🎯 大盘复盘\n\n## 2026-06-03 大盘复盘"))
|
|
|
|
def test_render_market_review_payload_markdown_prefixes_single_region_metadata(self) -> None:
|
|
markdown = market_review_module._render_market_review_payload_markdown(
|
|
{
|
|
"region": "us",
|
|
"title": "2026-06-03 大盘复盘",
|
|
"sections": [
|
|
{
|
|
"key": "overview",
|
|
"title": "2026-06-03 大盘复盘",
|
|
"markdown": "> 今晚重点观察科技股承接。",
|
|
}
|
|
],
|
|
},
|
|
wrapper_title="🎯 大盘复盘",
|
|
)
|
|
|
|
self.assertTrue(markdown.startswith("[dsa-market-region]: # (us)\n\n🎯 大盘复盘"))
|
|
self.assertEqual(markdown.count("[dsa-market-region]: # (us)"), 1)
|
|
|
|
def test_render_market_review_payload_markdown_appends_structured_sector_fallback(self) -> None:
|
|
markdown = market_review_module._render_market_review_payload_markdown(
|
|
{
|
|
"title": "2026-06-03 大盘复盘",
|
|
"language": "zh",
|
|
"sections": [
|
|
{
|
|
"key": "overview",
|
|
"title": "Overview",
|
|
"markdown": "> 今日指数强弱分化。",
|
|
}
|
|
],
|
|
"sectors": {
|
|
"top": [{"name": "AI算力", "change_pct": 3.25}],
|
|
"bottom": [{"name": "煤炭", "change_pct": -1.12}],
|
|
},
|
|
},
|
|
wrapper_title="🎯 大盘复盘",
|
|
)
|
|
|
|
self.assertIn("### 板块主线", markdown)
|
|
self.assertIn("#### 领涨板块 Top 5", markdown)
|
|
self.assertIn("| 1 | AI算力 | +3.25% |", markdown)
|
|
self.assertIn("#### 领跌板块 Top 5", markdown)
|
|
self.assertIn("| 1 | 煤炭 | -1.12% |", markdown)
|
|
|
|
def test_render_market_review_payload_markdown_keeps_injected_chinese_sector_block_once(self) -> None:
|
|
markdown = market_review_module._render_market_review_payload_markdown(
|
|
{
|
|
"title": "2026-06-03 大盘复盘",
|
|
"language": "zh",
|
|
"markdown_report": (
|
|
"## 2026-06-03 大盘复盘\n\n"
|
|
"### 板块表现\n\n"
|
|
"#### 行业板块领涨 Top 5\n"
|
|
"| 排名 | 行业板块 | 涨跌幅 |\n"
|
|
"|------|------|--------|\n"
|
|
"| 1 | AI算力 | +3.25% |"
|
|
),
|
|
"sectors": {
|
|
"top": [{"name": "AI算力", "change_pct": 3.25}],
|
|
"bottom": [{"name": "煤炭", "change_pct": -1.12}],
|
|
},
|
|
}
|
|
)
|
|
|
|
self.assertEqual(markdown.count("#### 行业板块领涨 Top 5"), 1)
|
|
self.assertNotIn("### 板块主线", markdown)
|
|
self.assertNotIn("#### 领涨板块 Top 5", markdown)
|
|
self.assertNotIn("#### 领跌板块 Top 5", markdown)
|
|
|
|
def test_render_market_review_payload_markdown_appends_each_market_sector_fallback(self) -> None:
|
|
markdown = market_review_module._render_market_review_payload_markdown(
|
|
{
|
|
"language": "zh",
|
|
"markdown_report": (
|
|
"## A 股大盘\n\n今日震荡。\n\n"
|
|
"---\n\n"
|
|
"## 港股大盘\n\n今日反弹。\n\n"
|
|
"---\n\n"
|
|
"## 美股大盘\n\n科技走强。"
|
|
),
|
|
"markets": {
|
|
"cn": {
|
|
"title": "A 股大盘",
|
|
"language": "zh",
|
|
"sectors": {"top": [{"name": "AI算力", "change_pct": 3.25}]},
|
|
},
|
|
"hk": {
|
|
"title": "港股大盘",
|
|
"language": "zh",
|
|
"sectors": {"top": [{"name": "科技", "change_pct": 2.18}]},
|
|
},
|
|
"us": {
|
|
"title": "美股大盘",
|
|
"language": "zh",
|
|
"sectors": {"top": [{"name": "半导体", "change_pct": 1.86}]},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
self.assertIn("### A 股大盘 / 板块主线", markdown)
|
|
self.assertIn("| 1 | AI算力 | +3.25% |", markdown)
|
|
self.assertIn("### 港股大盘 / 板块主线", markdown)
|
|
self.assertIn("| 1 | 科技 | +2.18% |", markdown)
|
|
self.assertIn("### 美股大盘 / 板块主线", markdown)
|
|
self.assertIn("| 1 | 半导体 | +1.86% |", markdown)
|
|
self.assertLess(markdown.index("### A 股大盘 / 板块主线"), markdown.index("## 港股大盘"))
|
|
self.assertLess(markdown.index("### 港股大盘 / 板块主线"), markdown.index("## 美股大盘"))
|
|
|
|
def test_render_market_review_payload_markdown_checks_duplicate_titles_by_market_wrapper(self) -> None:
|
|
duplicate_title = "2026-06-03 大盘复盘"
|
|
markdown = market_review_module._render_market_review_payload_markdown(
|
|
{
|
|
"language": "zh",
|
|
"markdown_report": (
|
|
"# A股大盘复盘\n\n"
|
|
f"## {duplicate_title}\n\n"
|
|
"### 板块表现\n\n"
|
|
"#### 行业板块领涨 Top 5\n"
|
|
"| 排名 | 行业板块 | 涨跌幅 |\n"
|
|
"|------|------|--------|\n"
|
|
"| 1 | AI算力 | +3.25% |\n\n"
|
|
"---\n\n"
|
|
"> 以下为下一市场大盘复盘\n\n"
|
|
"# 港股大盘复盘\n\n"
|
|
f"## {duplicate_title}\n\n"
|
|
"港股正文。\n\n"
|
|
"---\n\n"
|
|
"> 以下为下一市场大盘复盘\n\n"
|
|
"# 美股大盘复盘\n\n"
|
|
f"## {duplicate_title}\n\n"
|
|
"美股正文。"
|
|
),
|
|
"markets": {
|
|
"cn": {
|
|
"title": duplicate_title,
|
|
"language": "zh",
|
|
"sectors": {"top": [{"name": "AI算力", "change_pct": 3.25}]},
|
|
},
|
|
"hk": {
|
|
"title": duplicate_title,
|
|
"language": "zh",
|
|
"sectors": {"top": [{"name": "科技", "change_pct": 2.18}]},
|
|
},
|
|
"us": {
|
|
"title": duplicate_title,
|
|
"language": "zh",
|
|
"sectors": {"top": [{"name": "半导体", "change_pct": 1.86}]},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
self.assertEqual(markdown.count("#### 行业板块领涨 Top 5"), 1)
|
|
self.assertEqual(markdown.count(f"### {duplicate_title} / 板块主线"), 2)
|
|
self.assertIn("| 1 | 科技 | +2.18% |", markdown)
|
|
self.assertIn("| 1 | 半导体 | +1.86% |", markdown)
|
|
|
|
def test_render_market_review_payload_markdown_preserves_segment_boundaries_after_fallback(self) -> None:
|
|
markdown = market_review_module._render_market_review_payload_markdown(
|
|
{
|
|
"language": "en",
|
|
"markdown_report": (
|
|
"## CN Market\n\n"
|
|
"CN overview.\n\n"
|
|
"## HK Market\n\n"
|
|
"HK overview.\n\n"
|
|
"---\n\n"
|
|
"## US Market\n\n"
|
|
"US overview."
|
|
),
|
|
"markets": {
|
|
"cn": {
|
|
"title": "CN Market",
|
|
"language": "en",
|
|
"sectors": {"top": [{"name": "AI", "change_pct": 3.25}]},
|
|
},
|
|
"hk": {
|
|
"title": "HK Market",
|
|
"language": "en",
|
|
"sectors": {"top": [{"name": "Tech", "change_pct": 2.18}]},
|
|
},
|
|
"us": {
|
|
"title": "US Market",
|
|
"language": "en",
|
|
"sectors": {},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
self.assertIn("| 1 | AI | +3.25% |\n\n## HK Market", markdown)
|
|
self.assertIn("| 1 | Tech | +2.18% |\n\n---\n\n## US Market", markdown)
|
|
self.assertNotIn("+3.25% |## HK Market", markdown)
|
|
self.assertNotIn("+2.18% |---", markdown)
|
|
|
|
def test_persist_market_review_history_saves_markdown_report(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
old_db_path = os.environ.get("DATABASE_PATH")
|
|
os.environ["DATABASE_PATH"] = os.path.join(temp_dir, "market_review_history.db")
|
|
Config._instance = None
|
|
DatabaseManager.reset_instance()
|
|
try:
|
|
saved = market_review_module._persist_market_review_history(
|
|
review_report="## 今日大盘\n\n复盘正文",
|
|
markdown_report="# 🎯 大盘复盘\n\n## 今日大盘\n\n复盘正文",
|
|
region="cn",
|
|
config=SimpleNamespace(report_language="zh"),
|
|
query_id="market-task-001",
|
|
market_light_snapshots={
|
|
"cn": {
|
|
"region": "cn",
|
|
"trade_date": "2026-03-06",
|
|
"status": "red",
|
|
"score": 30,
|
|
"label": "偏防守",
|
|
"temperature_label": "偏弱",
|
|
"reasons": ["test"],
|
|
"guidance": "test",
|
|
"dimensions": {
|
|
"breadth": {"score": 20, "available": True},
|
|
"index": {"score": 30, "available": True},
|
|
"limit": {"score": 10, "available": True},
|
|
},
|
|
"data_quality": "ok",
|
|
}
|
|
},
|
|
market_review_payload={
|
|
"version": 1,
|
|
"kind": "market_review",
|
|
"region": "cn",
|
|
"sections": [{"title": "今日大盘", "markdown": "复盘正文"}],
|
|
},
|
|
)
|
|
|
|
self.assertGreater(saved, 0)
|
|
db = DatabaseManager.get_instance()
|
|
with db.get_session() as session:
|
|
row = session.query(AnalysisHistory).filter(
|
|
AnalysisHistory.query_id == "market-task-001"
|
|
).first()
|
|
self.assertIsNotNone(row)
|
|
self.assertEqual(row.id, saved)
|
|
self.assertEqual(row.code, market_review_module.MARKET_REVIEW_HISTORY_CODE)
|
|
self.assertEqual(row.name, "大盘复盘")
|
|
self.assertEqual(row.report_type, market_review_module.MARKET_REVIEW_REPORT_TYPE)
|
|
self.assertEqual(row.news_content, "## 今日大盘\n\n复盘正文")
|
|
self.assertIn("# 🎯 大盘复盘", row.raw_result)
|
|
self.assertIn('"market_light_snapshots"', row.context_snapshot)
|
|
self.assertIn('"market_review_payload"', row.context_snapshot)
|
|
self.assertIn('"trade_date": "2026-03-06"', row.context_snapshot)
|
|
snapshot = json.loads(row.context_snapshot or "{}")
|
|
self.assertEqual(snapshot["market_review_region"], "cn")
|
|
self.assertEqual(snapshot["market_review_payload"]["region"], "cn")
|
|
self.assertIn("analysis_context_pack_overview", snapshot)
|
|
finally:
|
|
DatabaseManager.reset_instance()
|
|
Config._instance = None
|
|
if old_db_path is None:
|
|
os.environ.pop("DATABASE_PATH", None)
|
|
else:
|
|
os.environ["DATABASE_PATH"] = old_db_path
|
|
|
|
def test_run_market_review_persists_notification_diagnostics_after_history_save(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
old_db_path = os.environ.get("DATABASE_PATH")
|
|
os.environ["DATABASE_PATH"] = os.path.join(temp_dir, "market_review_notification.db")
|
|
Config._instance = None
|
|
DatabaseManager.reset_instance()
|
|
query_id = "market-task-notification"
|
|
notifier = self._make_notifier()
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="## 今日大盘\n\n复盘正文",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-03-06", "score": 60},
|
|
)
|
|
token = activate_run_diagnostic_context(
|
|
trace_id="trace-market-notification",
|
|
task_id=query_id,
|
|
query_id=query_id,
|
|
stock_code=market_review_module.MARKET_REVIEW_HISTORY_CODE,
|
|
trigger_source="api",
|
|
)
|
|
try:
|
|
with patch.object(market_review_module, "MarketAnalyzer", return_value=market_analyzer):
|
|
result = run_market_review(
|
|
notifier,
|
|
config=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
send_notification=True,
|
|
query_id=query_id,
|
|
trigger_source="api",
|
|
)
|
|
|
|
self.assertEqual(result, "## 今日大盘\n\n复盘正文")
|
|
db = DatabaseManager.get_instance()
|
|
with db.get_session() as session:
|
|
row = session.query(AnalysisHistory).filter(
|
|
AnalysisHistory.query_id == query_id
|
|
).first()
|
|
self.assertIsNotNone(row)
|
|
context_snapshot = json.loads(row.context_snapshot)
|
|
notification_runs = context_snapshot["diagnostics"]["notification_runs"]
|
|
self.assertEqual(notification_runs[-1]["status"], "success")
|
|
self.assertTrue(notification_runs[-1]["success"])
|
|
finally:
|
|
reset_run_diagnostic_context(token)
|
|
DatabaseManager.reset_instance()
|
|
Config._instance = None
|
|
if old_db_path is None:
|
|
os.environ.pop("DATABASE_PATH", None)
|
|
else:
|
|
os.environ["DATABASE_PATH"] = old_db_path
|
|
|
|
def test_run_market_review_reuses_generated_query_id_for_notification_diagnostics(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
old_db_path = os.environ.get("DATABASE_PATH")
|
|
os.environ["DATABASE_PATH"] = os.path.join(temp_dir, "market_review_generated_query.db")
|
|
Config._instance = None
|
|
DatabaseManager.reset_instance()
|
|
notifier = self._make_notifier()
|
|
market_analyzer = MagicMock()
|
|
market_analyzer.run_daily_review_with_snapshot.return_value = SimpleNamespace(
|
|
report="## 今日大盘\n\n复盘正文",
|
|
market_light_snapshot={"region": "cn", "trade_date": "2026-03-06", "score": 60},
|
|
)
|
|
token = activate_run_diagnostic_context(
|
|
trace_id="trace-market-generated",
|
|
task_id="task-market-generated",
|
|
stock_code=market_review_module.MARKET_REVIEW_HISTORY_CODE,
|
|
trigger_source="cli",
|
|
)
|
|
try:
|
|
with patch.object(market_review_module, "MarketAnalyzer", return_value=market_analyzer):
|
|
result = run_market_review(
|
|
notifier,
|
|
config=SimpleNamespace(report_language="zh", market_review_region="cn"),
|
|
send_notification=True,
|
|
trigger_source="cli",
|
|
)
|
|
|
|
self.assertEqual(result, "## 今日大盘\n\n复盘正文")
|
|
db = DatabaseManager.get_instance()
|
|
with db.get_session() as session:
|
|
rows = session.query(AnalysisHistory).all()
|
|
self.assertEqual(len(rows), 1)
|
|
row = rows[0]
|
|
self.assertTrue(row.query_id.startswith("market_review_"))
|
|
context_snapshot = json.loads(row.context_snapshot)
|
|
notification_runs = context_snapshot["diagnostics"]["notification_runs"]
|
|
self.assertEqual(notification_runs[-1]["status"], "success")
|
|
self.assertTrue(notification_runs[-1]["success"])
|
|
finally:
|
|
reset_run_diagnostic_context(token)
|
|
DatabaseManager.reset_instance()
|
|
Config._instance = None
|
|
if old_db_path is None:
|
|
os.environ.pop("DATABASE_PATH", None)
|
|
else:
|
|
os.environ["DATABASE_PATH"] = old_db_path
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|