Files
daily-robots/tests/test_generate_golden.py
yumao ea8de9fe61 test: 新增 generate_report 黄金/确定性回归
冻结 _now_cst、mock 网络/文件/LLM(featured_pick 与 push_gate
有独立测试,此处冻结),验证同一输入下完整版与企微版产物逐字节
一致——拆分 _collect/_select/_render 不得改变行为(T1+T4)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 16:35:42 +08:00

104 lines
4.5 KiB
Python

# tests/test_generate_golden.py
"""黄金文件/确定性回归: 冻结时间、mock 网络与 LLM, 验证 generate_report
在同一输入下产出逐字节一致(拆分 _collect/_select/_render 不得改变行为)。"""
from __future__ import annotations
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest import mock
import daily.generate as g
FIXED_NOW = datetime(2026, 7, 18, 8, 50, tzinfo=timezone(timedelta(hours=8)))
def _feed() -> dict:
return {
"updatedAt": "2026-07-18T08:00:00",
"topTrending": [
{"id": "a", "source": "src", "title": "alpha", "description": "desc a",
"installs": 100, "link": "https://x/a"},
{"id": "b", "source": "src", "title": "beta", "description": "desc b",
"installs": 90, "link": "https://x/b"},
],
"topHot": [
{"id": "c", "source": "src", "title": "gamma", "description": "desc c",
"installs": 80, "link": "https://x/c"},
],
}
def _patches(tmp: Path):
"""集中 mock 所有外部边界: 网络/文件/时间/LLM。返回 patcher 列表。"""
return [
mock.patch.object(g, "_now_cst", return_value=FIXED_NOW),
mock.patch.object(g, "load_feed", return_value=_feed()),
mock.patch.object(g, "_load_snapshot", return_value=set()),
mock.patch.object(g, "_save_snapshot", lambda *a, **k: None),
mock.patch.object(g, "load_boards", return_value=(
_feed()["topTrending"], _feed()["topHot"])),
mock.patch.object(g, "fetch_github_trending", return_value=[]),
mock.patch.object(g, "fetch_emerging_repos", return_value=[]),
mock.patch.object(g, "fetch_topic_hot_repos", return_value=("topic", [])),
mock.patch.object(g, "fetch_ai_news", return_value={
"enabled": False, "categories": [], "flat": [], "stats": {}}),
mock.patch.object(g, "fetch_cn_ai_news", return_value={
"enabled": False, "categories": [], "flat": [], "stats": {}}),
mock.patch.object(g, "is_research_mode", return_value=False),
mock.patch.object(g, "is_agent_mode", return_value=False),
mock.patch.object(g, "run_editorial", return_value=None),
mock.patch.object(g, "cursor_editor_enabled", return_value=False),
mock.patch.object(g, "record_pushed_links", lambda *a, **k: None),
mock.patch.object(g, "save_json", lambda *a, **k: None),
mock.patch.object(g, "load_recent_shown_keys", return_value={
"skills_trending": set(), "skills_hot": set(),
"github_trending": set(), "github_emerging": set(),
"github_topic": set()}),
mock.patch.object(g, "OUTPUT_DIR", tmp),
mock.patch.object(g, "_localize_descriptions_in_place", lambda *a, **k: None),
# featured_pick / push_gate 有独立测试;此处冻结以免触网(LLM)与读盘。
mock.patch.object(g, "apply_featured_pick", return_value={
"title": "alpha", "url": "https://x/a",
"command": "npx skills add src/alpha", "why": "昨日 star 增速第一"}),
mock.patch.object(g, "evaluate_push_gate", return_value=mock.Mock(
should_push=False, silent=True, reasons=["golden-mock"])),
]
class GenerateGoldenTests(unittest.TestCase):
def _run(self, tmp: Path) -> tuple[str, str]:
patches = _patches(tmp)
for p in patches:
p.start()
try:
markdown, wecom_md, _md, _we = g.generate_report()
return markdown, wecom_md
finally:
for p in patches:
try:
p.stop()
except Exception:
pass
def test_deterministic_same_input_same_output(self):
import tempfile
with tempfile.TemporaryDirectory() as d1, tempfile.TemporaryDirectory() as d2:
md1, we1 = self._run(Path(d1))
md2, we2 = self._run(Path(d2))
self.assertEqual(md1, md2, "完整版 markdown 在相同输入下必须逐字节一致")
self.assertEqual(we1, we2, "企微 wecom_md 在相同输入下必须逐字节一致")
def test_output_contains_core_sections(self):
import tempfile
with tempfile.TemporaryDirectory() as d:
markdown, wecom_md = self._run(Path(d))
self.assertIn("# 早报 · 2026-07-18", markdown)
self.assertIn("主题聚类", markdown)
self.assertIsInstance(wecom_md, str)
self.assertTrue(len(wecom_md) > 0)
if __name__ == "__main__":
unittest.main()