Files
daily_stock_analysis/tests/test_pipeline_prefetch_dry_run.py
mumu 0db0c78da8 feat: 个股分析接入当日大盘上下文 (#1623)
* feat: add daily market context for stock analysis

* fix(review-feedback-1623): Share the market context service across worker threads and Use the

* fix(review-feedback-1623): Avoid overwriting per-region context reports and Expand history lookup

* fix(review-feedback-1623): add position even though the guardrail supposedly softened the

* fix(review-feedback-1623): Avoid generating the CLI market review twice

* fix(review-feedback-1623): 修正

* fix(review-feedback-1623): 修复普通 CLI/调度运行中大盘复盘被上下文预热摘要替代的问题,并收敛 PR 描述与当前实现范围

* fix(review-feedback-1623): 修正保守护栏的误判风险,并同步收敛 PR 描述范围

* fix(review-feedback-1623): 处理并发生成大盘上下文的锁语义风险,并澄清/补充外部模型或运行时配置风险检测的依据

* fix(review-feedback-1623): 收敛大盘上下文预热对用户可见大盘复盘的行为影响,并补齐外部模型/API 与运行时配置检测风险的澄清或验证证据

* fix(review-feedback-1623): Do not skip multi-market reviews after one cached context and Do not

* fix(review-feedback-1623): Prime context with the effective trading date

* fix(review-feedback-1623): 收敛 PR 描述与实际 diff 的范围不一致,并澄清结构化检测提示的 provider/model/base

* fix(review-feedback-1623): 补一句“本 PR 不修改 LLM provider/model/base url、默认模型、配置清理或迁移逻辑”,避免后续审查继续误判

* fix(review-feedback-1623): 收敛 PR 描述与实际 diff 的范围矛盾

* fix(review-feedback-1623): 补齐这些承诺的实现

* fix(review-feedback-1623): 收敛 PR 描述与实际 diff 范围,避免以未实现能力作为验收项

* fix(review-feedback-1623): 收敛 PR 描述与实际范围,并澄清外部模型/API 与运行时配置检测风险

* fix(review-feedback-1623): Match freshly saved reviews to the target trade date

* fix(review-feedback-1623): preserve structured market-light risk signals

* fix(review-feedback-1623): Escape market-summary sentinels before prompting

* fix(review-feedback-1623): 修复同日缓存复用时把低敏摘要当完整大盘复盘输出的行为风险,并收敛 PR 描述与实际 diff

* fix(review-feedback-1623): 收敛

* fix(review-feedback-1623): 收敛 PR 描述与实际 scope

* fix(review-feedback-1623): 修正大盘复盘复用语义,并让 PR 描述与实际 scope 对齐

* fix(review-feedback-1623): 修正同日大盘上下文缓存复用语义,并让 PR 描述与实际 scope 对齐

* fix(review-feedback-1623): Disable cached market context when skipping market review

* fix(review-feedback-1623): 收敛 PR 描述与实际 scope,并补充外部模型/API 与运行时配置语义未变化的可核验证据

* fix(review-feedback-1623): 修正 PR 描述与实际 scope 的实质性不一致,并补齐或澄清 LLM/provider/model/base URL

* fix(review-feedback-1623): 修正 operation advice 契约漂移,并让 PR 描述/验收范围与当前实际 diff 对齐

* fix(review-feedback-1623): 收敛 main.py 中大盘复盘复用导致的通知/执行语义变化,并补对应回归测试

* fix(review-feedback-1623): 修复生成型大盘上下文未绑定当前 query id 的行为风险,并收敛 PR body 与实际 diff 范围

* fix(review-feedback-1623): 修复缓存语言匹配问题,并收敛 PR 描述与实际 diff/验收范围的一致性

* fix(review-feedback-1623): 收敛当前实现与描述/文档中的验收边界,并澄清 schedule/CLI 首次运行无历史大盘复盘时是否应生成并注入上下文

* fix(review-feedback-1623): 收敛 PR 描述与 docs/CHANGELOG.md 中关于验收边界的表述,避免与实际 diff 实质性矛盾

* fix(review-feedback-1623): 修正 PR 描述与实际 diff 的范围漂移

* fix(review-feedback-1623): Reuse the cached runtime market context on fallback

* fix(review-feedback-1623): PR 描述:当前完整改动文件列表没有 api/ 、apps/dsa-web/ 、日报状态表或四阶段日报持久化相关实现,但 PR

* fix(review-feedback-1623): 收敛描述范围

* fix(review-feedback-1623): 收敛并发生成复用路径的正确性风险,并修正 PR 描述的实际范围与验收项

* fix(review-feedback-1623): 收敛描述和验收范围

* fix(review-feedback-1623): PR 描述的 Background / Acceptance Criteria 仍包含独立 API、Web

* fix(review-feedback-1623): 收敛 PR 描述与实际实现边界,并补齐 LLM/provider/base url 相关兼容性说明或验证证据

* fix(review-feedback-1623): PR 描述与实际 diff 范围不一致:描述和验收项仍写有独立 API、Web

* fix(review-feedback-1623): Wait long enough for in-flight market reviews

* fix(review-feedback-1623): 修复共享 market review lock 释放后仍不生成匹配上下文的行为风险,并同步收敛 PR description 的实际范围

* fix(review-feedback-1623): 修复锁竞争 fallback 下丢失运行时服务的问题,并同步收敛 PR description 与实际 diff 范围

* fix(review-feedback-1623): 收敛 PR description 与实际改动范围

* fix(review-feedback-1623): PR 描述与实际 diff 存在实质性矛盾:描述声称提供独立 API、Web

* fix(review-feedback-1623): reuse runtime market context and handle English negations

* fix: honor daily market context config

* docs: remove unrelated changelog entries

* fix(review-feedback-1623): preserve the skipped market-review artifact

* fix(review-feedback-1623): add low position cap and are not in CONSERVATIVE TEXT MARKERS , so an
2026-06-13 10:33:04 +08:00

128 lines
4.5 KiB
Python

# -*- coding: utf-8 -*-
"""
Regression tests for prefetch behavior in StockAnalysisPipeline.run().
"""
import os
import sys
import unittest
from datetime import date, datetime, timezone
from types import SimpleNamespace
from unittest.mock import MagicMock, call
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from tests.litellm_stub import ensure_litellm_stub
ensure_litellm_stub()
from src.core.pipeline import StockAnalysisPipeline
class TestPipelinePrefetchBehavior(unittest.TestCase):
@staticmethod
def _build_pipeline(process_result):
pipeline = StockAnalysisPipeline.__new__(StockAnalysisPipeline)
pipeline.max_workers = 1
pipeline.fetcher_manager = MagicMock()
pipeline.db = MagicMock()
pipeline.db.has_today_data.return_value = False
pipeline.process_single_stock = MagicMock(return_value=process_result)
pipeline.config = SimpleNamespace(
stock_list=["000001"],
refresh_stock_list=lambda: None,
single_stock_notify=False,
report_type="simple",
analysis_delay=0,
)
return pipeline
def test_run_dry_run_skips_stock_name_prefetch(self):
pipeline = self._build_pipeline(process_result=None)
pipeline.run(stock_codes=["000001"], dry_run=True, send_notification=False)
pipeline.fetcher_manager.prefetch_stock_names.assert_not_called()
def test_run_non_dry_run_prefetches_stock_names(self):
pipeline = self._build_pipeline(process_result=SimpleNamespace(code="000001"))
pipeline.run(stock_codes=["000001"], dry_run=False, send_notification=False)
pipeline.fetcher_manager.prefetch_stock_names.assert_called_once_with(
["000001"], use_bulk=False
)
def test_run_dry_run_counts_existing_data_by_effective_trading_date(self):
pipeline = self._build_pipeline(process_result=None)
pipeline._resolve_resume_target_date = MagicMock(
side_effect=[date(2026, 3, 27), date(2026, 3, 26)]
)
pipeline.db.has_today_data.side_effect = [True, False]
pipeline.run(
stock_codes=["600519", "AAPL"],
dry_run=True,
send_notification=False,
)
self.assertEqual(
pipeline.db.has_today_data.call_args_list,
[
call("600519", date(2026, 3, 27)),
call("AAPL", date(2026, 3, 26)),
],
)
def test_run_uses_one_frozen_reference_time_for_tasks_and_dry_run_stats(self):
pipeline = self._build_pipeline(process_result=None)
pipeline._resolve_resume_target_date = MagicMock(
side_effect=[date(2026, 3, 27), date(2026, 3, 26)]
)
pipeline.db.has_today_data.side_effect = [True, False]
pipeline.run(
stock_codes=["600519", "AAPL"],
dry_run=True,
send_notification=False,
)
task_reference_times = [
call.kwargs["current_time"]
for call in pipeline.process_single_stock.call_args_list
]
stats_reference_times = [
call.kwargs["current_time"]
for call in pipeline._resolve_resume_target_date.call_args_list
]
self.assertEqual(len(task_reference_times), 2)
self.assertEqual(len(stats_reference_times), 2)
self.assertEqual(len({id(value) for value in task_reference_times}), 1)
self.assertEqual(len({id(value) for value in stats_reference_times}), 1)
self.assertIs(task_reference_times[0], stats_reference_times[0])
def test_run_uses_supplied_reference_time_for_tasks_and_dry_run_stats(self):
pipeline = self._build_pipeline(process_result=None)
reference_time = datetime(2026, 3, 27, 1, 30, tzinfo=timezone.utc)
pipeline._resolve_resume_target_date = MagicMock(
side_effect=[date(2026, 3, 27), date(2026, 3, 26)]
)
pipeline.db.has_today_data.side_effect = [True, False]
pipeline.run(
stock_codes=["600519", "AAPL"],
dry_run=True,
send_notification=False,
current_time=reference_time,
)
for process_call in pipeline.process_single_stock.call_args_list:
self.assertIs(process_call.kwargs["current_time"], reference_time)
for resolve_call in pipeline._resolve_resume_target_date.call_args_list:
self.assertIs(resolve_call.kwargs["current_time"], reference_time)
if __name__ == "__main__":
unittest.main()