From ea8de9fe61e016b29fecc301f1dff04315914e68 Mon Sep 17 00:00:00 2001 From: yumao Date: Sat, 18 Jul 2026 16:35:42 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=96=B0=E5=A2=9E=20generate=5Freport?= =?UTF-8?q?=20=E9=BB=84=E9=87=91/=E7=A1=AE=E5=AE=9A=E6=80=A7=E5=9B=9E?= =?UTF-8?q?=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 冻结 _now_cst、mock 网络/文件/LLM(featured_pick 与 push_gate 有独立测试,此处冻结),验证同一输入下完整版与企微版产物逐字节 一致——拆分 _collect/_select/_render 不得改变行为(T1+T4)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_generate_golden.py | 103 ++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/test_generate_golden.py diff --git a/tests/test_generate_golden.py b/tests/test_generate_golden.py new file mode 100644 index 0000000..18e496e --- /dev/null +++ b/tests/test_generate_golden.py @@ -0,0 +1,103 @@ +# 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()