feat: 早报系统重构与功能增强

- 新增常驻调度器 daily/scheduler.py + run-scheduler.ps1(定时生成/推送)
- 新增 daily/bridge_manager.py:Windows 兼容的 Cursor SDK 桥接
- 新增 skills/daily-featured-pick 首推 Skill 与叙事轴/去重逻辑
- 新闻抓取窗口、GitHub 搜索、企微 delta 模式等多项改进
- 补充设计文档与 superpowers 计划/规范
- 新增对应测试(scheduler、featured_pick、github_search、news_fetch_window 等)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:12:00 +08:00
parent 6192dd4e2a
commit 6ea2a4e4c6
35 changed files with 4389 additions and 359 deletions

View File

@@ -0,0 +1,117 @@
"""AI 时讯 deep-research 解析与企微格式。"""
from __future__ import annotations
import unittest
from daily.format_wecom import _ai_news_lines, replace_wecom_news_sections
from daily.news.research import parse_research_response
class TestAiNewsResearchParse(unittest.TestCase):
def test_parse_items(self):
raw = """
{
"items": [
{
"title": "Apple sues OpenAI",
"link": "https://techcrunch.com/2026/07/10/apple/",
"source_name": "TechCrunch",
"desc_short": "苹果起诉 OpenAI 涉嫌窃取商业机密",
"published_fmt": "07-11 05:00"
}
]
}
"""
items, tech = parse_research_response(raw, limit=10)
self.assertEqual(len(items), 1)
self.assertEqual(items[0]["source_name"], "TechCrunch")
self.assertIn("苹果", items[0]["desc_short"])
self.assertEqual(tech, [])
def test_dedupe_links(self):
raw = """{"items": [
{"title": "A", "link": "https://example.com/a", "source_name": "Ex", "desc_short": ""},
{"title": "B", "link": "https://example.com/a", "source_name": "Ex", "desc_short": ""}
]}"""
items, _ = parse_research_response(raw, limit=10)
self.assertEqual(len(items), 1)
def test_parse_tech_items(self):
raw = """{"items": [
{"title": "A", "link": "https://example.com/a", "source_name": "Ex", "desc_short": ""}
], "tech_items": [
{"title": "B", "link": "https://example.com/b", "source_name": "Ex", "desc_short": ""}
]}"""
items, tech = parse_research_response(raw, limit=10, tech_limit=5)
self.assertEqual(len(items), 1)
self.assertEqual(len(tech), 1)
self.assertEqual(tech[0]["title"], "B")
class TestMergedWecomNews(unittest.TestCase):
def test_merged_block_format(self):
items = [
{
"title": "Apple sues OpenAI",
"link": "https://techcrunch.com/x",
"source_name": "TechCrunch",
"desc_short": "苹果起诉 OpenAI",
"published_fmt": "07-11 05:00",
}
]
lines = _ai_news_lines(items, merged=True)
self.assertIn("TechCrunch - Apple sues OpenAI", lines[0])
self.assertIn("— 苹果起诉 OpenAI", lines[0])
self.assertNotIn("07-11", lines[0])
def test_merged_with_tech_block(self):
md = """📰 **早报**
📈 **Skills Trending Top 1**
1. skill
"""
main = [
{"title": "Main", "link": "https://example.com/m", "source_name": "Src", "desc_short": "主条", "published_fmt": "07-11"}
]
tech = [
{"title": "Tech", "link": "https://example.com/t", "source_name": "Src2", "desc_short": "技术条", "published_fmt": "07-12"}
]
out = replace_wecom_news_sections(md, ai_news=main, tech_ai_news=tech, merged=True)
self.assertIn("📰 **AI 时讯精选 Top 2**", out)
self.assertNotIn("技术类时讯", out)
self.assertNotIn("🔧", out)
self.assertNotIn("07-11", out)
self.assertNotIn("07-12", out)
self.assertIn("2. [Src2 - Tech]", out)
def test_replace_merged_removes_split_blocks(self):
md = """📰 **早报**
🌍 **国际 AI 时讯 Top 1**
1. [old](https://example.com/old) · `X`
🇨🇳 **国内 AI 时讯 Top 1**
1. [old2](https://example.com/old2) · `Y`
📈 **Skills Trending Top 1**
1. skill
"""
items = [
{
"title": "New story",
"link": "https://example.com/new",
"source_name": "Fortune",
"desc_short": "新故事",
"published_fmt": "",
}
]
out = replace_wecom_news_sections(md, ai_news=items, merged=True)
self.assertIn("📰 **AI 时讯精选 Top 1**", out)
self.assertNotIn("国际 AI 时讯", out)
self.assertNotIn("国内 AI 时讯", out)
self.assertIn("📈 **Skills Trending Top 1**", out)
if __name__ == "__main__":
unittest.main()

View File

@@ -27,6 +27,19 @@ class ShownKeysTests(unittest.TestCase):
items = [{"repo": "a/b"}, {"repo": "c/d"}]
self.assertEqual(extract_shown_keys("github_trending", items), ["a/b", "c/d"])
def test_extract_skill_keys_include_source(self):
items = [
{
"id": "open.feishu.cn/lark-drive",
"source": "open.feishu.cn",
"title": "lark-drive",
}
]
self.assertEqual(
extract_shown_keys("skills_trending", items),
["open.feishu.cn/lark-drive", "open.feishu.cn"],
)
def test_load_recent_reads_wecom_shown_not_baseline(self):
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp)
@@ -48,6 +61,41 @@ class ShownKeysTests(unittest.TestCase):
self.assertEqual(keys["github_trending"], {"x/y"})
self.assertNotIn("a/b", keys["github_trending"])
def test_load_recent_falls_back_to_wecom_md_when_shown_missing(self):
"""旧日 data 无 wecom_shown_keys 时,从同日 wecom.md 解析实际展示 keys。"""
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp)
payload = {
"data": {
"date": "2026-07-13",
"github_trending": [{"repo": "other/top"}],
}
}
(out / "2026-07-13.data.json").write_text(
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
)
(out / "2026-07-13.wecom.md").write_text(
"\n".join(
[
"🐙 **GitHub Trending Top 2**",
"1. [vinta/awesome-python](https://github.com/vinta/awesome-python)",
"2. [react/react](https://github.com/react/react)",
"",
"🌱 **GitHub 新兴 Top 1**",
"1. [elder-plinius/T3MP3ST](https://github.com/elder-plinius/T3MP3ST)",
]
),
encoding="utf-8",
)
with patch("daily.board_history.OUTPUT_DIR", out):
keys = load_recent_shown_keys("2026-07-14", lookback_days=7)
self.assertEqual(
keys["github_trending"],
{"vinta/awesome-python", "react/react"},
)
self.assertEqual(keys["github_emerging"], {"elder-plinius/T3MP3ST"})
self.assertNotIn("other/top", keys["github_trending"])
def test_merge_shown_does_not_touch_baseline(self):
data = {
"movement_baseline": {"github_trending": [{"repo": "raw/one"}]},

View File

@@ -53,6 +53,39 @@ class BoardSelectTests(unittest.TestCase):
)
self.assertEqual([x["id"] for x in out], ["c/d/s2"])
def test_skill_filters_recent_by_source(self):
items = [
{"id": "a/b/s-new", "source": "a/b", "title": "s-new", "installs": 10},
{"id": "c/d/s2", "source": "c/d", "title": "s2", "installs": 9},
]
out = board_select(
board="skills_hot",
items=items,
recent_keys={"a/b"}, # source-level history
limit=10,
pool_size=50,
kind="skill",
)
self.assertEqual([x["id"] for x in out], ["c/d/s2"])
def test_skill_filters_recent_skill_id_as_same_source(self):
from daily.format_wecom import expand_skill_recent_keys
items = [
{"id": "open.feishu.cn/lark-drive", "source": "open.feishu.cn", "title": "lark-drive", "installs": 10},
{"id": "fresh/src/s", "source": "fresh/src", "title": "s", "installs": 9},
]
recent = expand_skill_recent_keys({"open.feishu.cn/lark-doc"})
out = board_select(
board="skills_trending",
items=items,
recent_keys=recent,
limit=10,
pool_size=50,
kind="skill",
)
self.assertEqual([x["id"] for x in out], ["fresh/src/s"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,95 @@
"""Tests for daily.featured_pick."""
from __future__ import annotations
import os
import unittest
from unittest.mock import patch
from daily.featured_pick import (
apply_featured_pick,
match_in_data,
parse_featured_pick,
pick_command_from_featured,
pick_why_from_featured,
research_featured_pick,
)
SAMPLE_INPUT = {
"skills_trending": [
{
"id": "foo/bar/gstack-cli",
"title": "gstack-cli",
"source": "foo/bar",
"installs": 1200,
"installs_fmt": "1.2K",
"link": "https://skills.sh/foo/bar/gstack-cli",
"description": "Agent workflow CLI",
}
],
"skills_hot": [],
"github_trending": [
{
"repo": "acme/gstack",
"url": "https://github.com/acme/gstack",
"total_stars_fmt": "3.2K",
"description": "GStack toolkit",
}
],
"github_emerging": [],
"github_topic": {"topic": "llm", "repos": []},
}
class ParseFeaturedPickTests(unittest.TestCase):
def test_empty(self):
with patch.dict(os.environ, {}, clear=True):
self.assertIsNone(parse_featured_pick())
def test_query_only(self):
with patch.dict(os.environ, {"DAILY_FEATURED_PICK": "gstack"}, clear=True):
self.assertEqual(parse_featured_pick(), {"query": "gstack"})
def test_query_with_url(self):
with patch.dict(os.environ, {"DAILY_FEATURED_PICK": "gstack|https://example.com"}, clear=True):
self.assertEqual(
parse_featured_pick(),
{"query": "gstack", "url_hint": "https://example.com"},
)
class MatchInDataTests(unittest.TestCase):
def test_matches_skill_and_github(self):
matches = match_in_data(SAMPLE_INPUT, "gstack")
self.assertEqual(len(matches["skills"]), 1)
self.assertEqual(matches["skills"][0]["title"], "gstack-cli")
self.assertEqual(len(matches["github"]), 1)
self.assertEqual(matches["github"][0]["repo"], "acme/gstack")
class FeaturedPickWorkflowTests(unittest.TestCase):
def test_fallback_without_llm(self):
llm_input = dict(SAMPLE_INPUT)
with patch.dict(os.environ, {"DAILY_FEATURED_PICK": "gstack"}, clear=True):
with patch("daily.featured_pick.has_llm_configured", return_value=False):
featured = research_featured_pick(llm_input, date_str="2026-07-03")
self.assertIsNotNone(featured)
assert featured is not None
self.assertEqual(featured["type"], "skill")
self.assertIn("npx skills add foo/bar/gstack-cli", featured["command"])
self.assertTrue(featured["why_today"])
def test_apply_featured_pick_mutates_input(self):
llm_input = dict(SAMPLE_INPUT)
with patch.dict(os.environ, {"DAILY_FEATURED_PICK": "gstack"}, clear=True):
with patch("daily.featured_pick.has_llm_configured", return_value=False):
featured = apply_featured_pick(llm_input, date_str="2026-07-03")
self.assertIsNotNone(featured)
self.assertIn("featured_pick", llm_input)
self.assertEqual(pick_command_from_featured(featured), llm_input["featured_pick"]["command"])
self.assertEqual(pick_why_from_featured(featured), llm_input["featured_pick"]["why_today"])
if __name__ == "__main__":
unittest.main()

View File

@@ -1,10 +1,19 @@
# tests/test_featured_resolve.py
from __future__ import annotations
import json
import random
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from daily.featured_pick import featured_identity_key, featured_resolve
from daily.featured_pick import (
featured_identity_key,
featured_resolve,
load_recent_featured_keys,
load_yesterday_featured_key,
)
class FeaturedResolveTests(unittest.TestCase):
@@ -78,6 +87,46 @@ class FeaturedResolveTests(unittest.TestCase):
"foo/bar",
)
def test_load_yesterday_falls_back_to_featured_pick(self):
"""缺 featured_pick_key 时从 featured_pick.url 推导身份,避免连日重复首推。"""
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp)
payload = {
"data": {
"date": "2026-07-13",
"featured_pick": {
"type": "github",
"title": "headroom",
"url": "https://github.com/headroomlabs-ai/headroom",
},
}
}
(out / "2026-07-13.data.json").write_text(
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
)
with patch("daily.featured_pick.OUTPUT_DIR", out):
key = load_yesterday_featured_key("2026-07-14")
self.assertEqual(key, "headroomlabs-ai/headroom")
def test_load_recent_falls_back_to_featured_pick(self):
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp)
payload = {
"data": {
"date": "2026-07-13",
"featured_pick": {
"type": "github",
"url": "https://github.com/headroomlabs-ai/headroom",
},
}
}
(out / "2026-07-13.data.json").write_text(
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
)
with patch("daily.featured_pick.OUTPUT_DIR", out):
keys = load_recent_featured_keys("2026-07-14", days=7)
self.assertIn("headroomlabs-ai/headroom", keys)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,71 @@
"""Tests for GitHub Search pagination / deep pool."""
from __future__ import annotations
import os
import unittest
from unittest.mock import MagicMock, patch
class SearchGithubReposPaginationTests(unittest.TestCase):
def test_search_paginates_beyond_first_page_of_30(self):
from daily.github.search import search_github_repos
def make_items(start: int, n: int) -> list[dict]:
return [
{
"full_name": f"org/repo{i}",
"html_url": f"https://github.com/org/repo{i}",
"description": f"desc {i}",
"language": "Python",
"stargazers_count": 1000 - i,
"created_at": "2026-01-01T00:00:00Z",
}
for i in range(start, start + n)
]
responses = [
MagicMock(status_code=200, json=lambda: {"items": make_items(1, 100)}),
MagicMock(status_code=200, json=lambda: {"items": make_items(101, 50)}),
]
client = MagicMock()
client.__enter__.return_value = client
client.__exit__.return_value = False
client.get.side_effect = responses
with patch.dict(os.environ, {"GITHUB_TOKEN": "test-token"}, clear=False):
with patch("daily.github.search.httpx.Client", return_value=client):
with patch("daily.github.search.github_token", return_value="test-token"):
repos = search_github_repos("stars:>50", 120, require_token=True)
self.assertEqual(len(repos), 120)
self.assertEqual(repos[0]["repo"], "org/repo1")
self.assertEqual(repos[119]["repo"], "org/repo120")
self.assertEqual(client.get.call_count, 2)
first_params = client.get.call_args_list[0].kwargs["params"]
self.assertEqual(first_params["per_page"], 100)
self.assertEqual(first_params["page"], 1)
class GithubBoardDeepPoolTests(unittest.TestCase):
def test_board_select_fills_ten_when_deep_pool_has_fresh_repos(self):
from daily.board_select import board_select
recent = {f"old/r{i}" for i in range(1, 33)}
items = [{"repo": f"old/r{i}"} for i in range(1, 31)] + [
{"repo": f"fresh/r{i}"} for i in range(1, 20)
]
selected = board_select(
board="github_trending",
items=items,
recent_keys=recent,
limit=10,
pool_size=100,
kind="github",
)
self.assertEqual(len(selected), 10)
self.assertTrue(all(r["repo"].startswith("fresh/") for r in selected))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,156 @@
"""Tests for news time window filtering."""
from __future__ import annotations
import os
import unittest
from datetime import datetime, timezone, timedelta
from unittest.mock import patch
from zoneinfo import ZoneInfo
from daily.news.fetch import _cutoff_datetime, _parse_datetime, _within_window
class NewsWindowTests(unittest.TestCase):
def test_cutoff_floor_today_excludes_yesterday_even_within_24h(self):
tz = ZoneInfo("Asia/Shanghai")
# 2026-07-09 09:00 CST = 2026-07-09 01:00 UTC
fixed = datetime(2026, 7, 9, 1, 0, tzinfo=timezone.utc)
with patch("daily.news.fetch._now_utc", return_value=fixed):
with patch.dict(os.environ, {"DAILY_AI_NEWS_HOURS": "24"}, clear=False):
cutoff = _cutoff_datetime(floor_today=True)
start_today_cst = datetime(2026, 7, 9, 0, 0, tzinfo=tz).astimezone(timezone.utc)
self.assertEqual(cutoff, start_today_cst)
yesterday = datetime(2026, 7, 8, 20, 0, tzinfo=tz).astimezone(timezone.utc)
self.assertFalse(_within_window({"published": yesterday.isoformat()}, cutoff))
def test_cutoff_rolling_only_includes_last_24h(self):
fixed = datetime(2026, 7, 9, 12, 0, tzinfo=timezone.utc)
with patch("daily.news.fetch._now_utc", return_value=fixed):
with patch.dict(os.environ, {"DAILY_AI_NEWS_HOURS": "24"}, clear=False):
cutoff = _cutoff_datetime(floor_today=False)
self.assertEqual(cutoff, fixed - timedelta(hours=24))
def test_within_window_rejects_missing_datetime(self):
cutoff = datetime(2026, 7, 9, 0, 0, tzinfo=timezone.utc)
self.assertFalse(_within_window({"title": "x", "link": "https://a.com"}, cutoff))
def test_parse_date_only_uses_local_noon(self):
with patch.dict(os.environ, {"DAILY_AI_NEWS_TZ": "Asia/Shanghai"}, clear=False):
dt = _parse_datetime("2026-07-09")
self.assertIsNotNone(dt)
assert dt is not None
local = dt.astimezone(ZoneInfo("Asia/Shanghai"))
self.assertEqual(local.hour, 12)
class NewsFormatTests(unittest.TestCase):
def test_ai_news_lines_use_desc_as_link_text(self):
from daily.format_wecom import _ai_news_lines
lines = _ai_news_lines(
[
{
"title": "English Title",
"link": "https://example.com/a",
"source_name": "Src",
"published_fmt": "07-11",
"desc_short": "中文摘要一句",
}
]
)
self.assertEqual(len(lines), 1)
self.assertIn("[中文摘要一句](https://example.com/a)", lines[0])
self.assertNotIn("English Title", lines[0])
self.assertNotIn("> ", lines[0])
def test_ai_news_lines_fallback_to_title(self):
from daily.format_wecom import _ai_news_lines
lines = _ai_news_lines(
[
{
"title": "仅标题",
"link": "https://example.com/b",
"source_name": "Src",
"published_fmt": "",
"desc_short": "",
}
]
)
self.assertIn("[仅标题](https://example.com/b)", lines[0])
class NewsSummaryTests(unittest.TestCase):
def test_brief_news_summary_no_ellipsis(self):
from daily.news.fetch import brief_news_summary
text = (
"Meta told Dylan Byers, of Puck News, that the company removed "
"the controversial AI feature after user backlash on Instagram."
)
out = brief_news_summary(text, limit=72)
self.assertNotIn("...", out)
self.assertLessEqual(len(out), 72)
self.assertTrue(out.startswith("Meta told"))
def test_brief_news_summary_filters_junk(self):
from daily.news.fetch import brief_news_summary
self.assertEqual(brief_news_summary("点击查看原文>"), "")
self.assertEqual(brief_news_summary("Article URL: https://example.com"), "")
def test_sync_wecom_news_rows_after_localize(self):
from daily.news.fetch import _to_wecom_news_row, sync_wecom_news_rows
row = _to_wecom_news_row(
{
"title": "t",
"link": "https://a.com/x",
"source_name": "s",
"published_fmt": "07-11",
"summary": "Short english stub that was truncated early...",
}
)
flat = [
{
"link": "https://a.com/x",
"summary": "苹果指控 OpenAI 窃取硬件商业机密,诉讼称 misconduct 涉及多名前员工。",
}
]
sync_wecom_news_rows([row], flat)
self.assertNotIn("...", row["desc_short"])
self.assertIn("苹果", row["desc_short"])
def test_finalize_wecom_news_forces_chinese(self):
from daily.news.fetch import finalize_wecom_news_items
items = [
{
"link": "https://a.com/1",
"desc_short": "Meta removed the feature after backlash.",
"summary_plain": "Meta removed the feature after backlash.",
}
]
with patch(
"daily.localize.localize_brief_descriptions",
return_value={"wecom-news:https://a.com/1": "Meta 在舆论压力下移除了该功能"},
):
finalize_wecom_news_items(items, force_chinese=True)
self.assertIn("Meta", items[0]["desc_short"])
self.assertNotIn("backlash", items[0]["desc_short"])
class NewsPickTests(unittest.TestCase):
def test_pick_and_backfill_to_limit(self):
from daily.news.fetch import _fill_picked_to_limit, _pick_news_items
flat = [
{"link": f"https://a.com/{i}", "title": f"t{i}", "category_id": "media", "summary": "s"}
for i in range(12)
]
picked = _pick_news_items(flat, 10, ("media",))
self.assertEqual(len(picked), 10)
picked = _fill_picked_to_limit(picked[:3], [flat], 10)
self.assertEqual(len(picked), 10)

118
tests/test_scheduler.py Normal file
View File

@@ -0,0 +1,118 @@
"""Tests for daily.scheduler."""
from __future__ import annotations
import unittest
from datetime import datetime
from zoneinfo import ZoneInfo
from daily.scheduler import (
ClockTime,
SchedulerState,
next_occurrence_after,
parse_hhmm,
plan_next_action,
)
class ParseHhmmTests(unittest.TestCase):
def test_parse(self):
t = parse_hhmm("08:50")
self.assertEqual((t.hour, t.minute), (8, 50))
def test_invalid(self):
with self.assertRaises(ValueError):
parse_hhmm("25:00")
class PlanNextActionTests(unittest.TestCase):
def setUp(self) -> None:
self.tz = ZoneInfo("Asia/Shanghai")
self.gen = ClockTime(8, 50)
self.push = ClockTime(9, 0)
def test_before_generate_waits_for_generate(self):
now = datetime(2026, 7, 9, 8, 30, tzinfo=self.tz)
run_at, action = plan_next_action(
now=now,
tz=self.tz,
state=SchedulerState(),
generate_at=self.gen,
push_at=self.push,
)
self.assertEqual(action, "generate")
self.assertEqual(run_at.hour, 8)
self.assertEqual(run_at.minute, 50)
def test_after_generate_before_push_waits_for_push(self):
now = datetime(2026, 7, 9, 8, 55, tzinfo=self.tz)
state = SchedulerState(last_generate_date="2026-07-09")
run_at, action = plan_next_action(
now=now,
tz=self.tz,
state=state,
generate_at=self.gen,
push_at=self.push,
)
self.assertEqual(action, "push")
self.assertEqual(run_at.hour, 9)
def test_catch_up_generate_when_started_late(self):
now = datetime(2026, 7, 9, 8, 55, tzinfo=self.tz)
run_at, action = plan_next_action(
now=now,
tz=self.tz,
state=SchedulerState(),
generate_at=self.gen,
push_at=self.push,
)
self.assertEqual(action, "generate")
self.assertEqual(run_at, now)
def test_next_day_after_both_done(self):
now = datetime(2026, 7, 9, 10, 0, tzinfo=self.tz)
state = SchedulerState(last_generate_date="2026-07-09", last_push_date="2026-07-09")
run_at, action = plan_next_action(
now=now,
tz=self.tz,
state=state,
generate_at=self.gen,
push_at=self.push,
)
self.assertEqual(action, "generate")
self.assertEqual(run_at.date().isoformat(), "2026-07-10")
def test_evening_start_waits_for_tomorrow_generate(self):
now = datetime(2026, 7, 9, 20, 35, tzinfo=self.tz)
run_at, action = plan_next_action(
now=now,
tz=self.tz,
state=SchedulerState(),
generate_at=self.gen,
push_at=self.push,
)
self.assertEqual(action, "generate")
self.assertEqual(run_at.date().isoformat(), "2026-07-10")
self.assertEqual((run_at.hour, run_at.minute), (8, 50))
def test_catch_up_push_when_generate_done(self):
now = datetime(2026, 7, 9, 20, 35, tzinfo=self.tz)
state = SchedulerState(last_generate_date="2026-07-09")
run_at, action = plan_next_action(
now=now,
tz=self.tz,
state=state,
generate_at=self.gen,
push_at=self.push,
)
self.assertEqual(action, "push")
self.assertEqual(run_at, now)
class NextOccurrenceTests(unittest.TestCase):
def test_tomorrow_when_past(self):
tz = ZoneInfo("Asia/Shanghai")
now = datetime(2026, 7, 9, 10, 0, tzinfo=tz)
nxt = next_occurrence_after(ClockTime(8, 50), tz, now)
self.assertEqual(nxt.date().isoformat(), "2026-07-10")
self.assertEqual((nxt.hour, nxt.minute), (8, 50))

View File

@@ -229,6 +229,192 @@ class DeltaFormatTests(unittest.TestCase):
self.assertIn("Skills Trending Top 10", text)
self.assertNotIn("Skills Trending 变化", text)
def test_delta_pad_keeps_large_clusters_merged_and_fills_limit(self):
"""补榜按 source 合并态取条,大 cluster 不得撑爆 flat 预算导致短榜。"""
from daily.format_wecom import build_skills_delta_sections
def cluster(source: str, n: int, installs: int) -> dict:
titles = [f"t{i}" for i in range(n)]
return {
"id": f"{source}/{titles[0]}",
"title": titles[0],
"source": source,
"installs": installs,
"installs_fmt": str(installs),
"cluster": True,
"cluster_count": n,
"cluster_skills": titles,
"cluster_titles": ", ".join(titles[:4]) + "",
"link": f"https://skills.sh/{source}/{titles[0]}",
"description": f"{source} cluster",
}
full = [cluster(f"big{i}/pkg", 20, 1000 - i) for i in range(1, 5)] + [
{
"id": f"other{n}/pkg/skill",
"title": "skill",
"source": f"other{n}/pkg",
"installs": 50 - n,
"installs_fmt": str(50 - n),
"link": f"https://skills.sh/other{n}/pkg/skill",
"description": f"other {n}",
}
for n in range(1, 12)
]
with patch("daily.format_wecom.localize_brief_descriptions", return_value={}):
with patch("daily.format_wecom.needs_chinese", return_value=False):
text = build_skills_delta_sections(
[],
[],
trending_full=full,
hot_full=[],
trending_limit=10,
pad=True,
)
self.assertIn("Skills Trending Top 10", text)
self.assertIn("20 skills", text)
self.assertIn("other6/pkg", text)
def test_delta_pad_recent_blocks_same_source_not_just_primary_id(self):
"""周去重按 source换同仓另一个 skill id 不得再上榜。"""
from daily.format_wecom import build_skills_delta_sections
titles = [f"t{i}" for i in range(20)]
full = [
{
"id": f"big/pkg/{titles[0]}",
"title": titles[0],
"source": "big/pkg",
"installs": 999,
"installs_fmt": "999",
"cluster": True,
"cluster_count": 20,
"cluster_skills": titles,
"cluster_titles": ", ".join(titles[:4]) + "",
"link": f"https://skills.sh/big/pkg/{titles[0]}",
"description": "big cluster",
},
*[
{
"id": f"other{n}/pkg/skill",
"title": "skill",
"source": f"other{n}/pkg",
"installs": 50 - n,
"installs_fmt": str(50 - n),
"link": f"https://skills.sh/other{n}/pkg/skill",
"description": f"other {n}",
}
for n in range(1, 12)
],
]
# 昨日展示的是同 source 另一 skill id非今日 primary
recent = {f"big/pkg/{titles[5]}"}
with patch("daily.format_wecom.localize_brief_descriptions", return_value={}):
with patch("daily.format_wecom.needs_chinese", return_value=False):
text = build_skills_delta_sections(
[],
[],
trending_full=full,
hot_full=[],
trending_limit=10,
pad=True,
recent_trending=recent,
)
self.assertIn("Skills Trending Top 10", text)
self.assertNotIn("big/pkg", text)
self.assertIn("other1/pkg", text)
def test_delta_pad_hot_recent_unions_trending_history_by_source(self):
"""Skills Hot 周去重合并 Trending 历史:隔日换榜也不能同 source 再出现。"""
from daily.format_wecom import build_skills_delta_sections
hot_full = [
{
"id": "101-skills/skills/ai-music",
"title": "ai-music",
"source": "101-skills/skills",
"installs": 200,
"installs_fmt": "200",
"link": "https://skills.sh/101-skills/skills/ai-music",
"description": "hot candidate",
},
{
"id": "fresh/src/skill",
"title": "skill",
"source": "fresh/src",
"installs": 100,
"installs_fmt": "100",
"link": "https://skills.sh/fresh/src/skill",
"description": "fresh",
},
]
with patch("daily.format_wecom.localize_brief_descriptions", return_value={}):
with patch("daily.format_wecom.needs_chinese", return_value=False):
text = build_skills_delta_sections(
[],
[],
trending_full=[],
hot_full=hot_full,
trending_limit=10,
hot_limit=10,
pad=True,
recent_trending={"101-skills/skills/ai-video-generation"},
recent_hot=set(),
)
self.assertIn("Skills Hot Top 1", text)
self.assertIn("fresh/src", text)
self.assertNotIn("101-skills", text)
def test_delta_pad_hot_excludes_trending_by_source(self):
"""同日 Hot 补榜按 source 避开 Trending而非展开全部 cluster skill id。"""
from daily.format_wecom import build_skills_delta_sections
trending_full = [
{
"id": "same/src/a",
"title": "a",
"source": "same/src",
"installs": 100,
"installs_fmt": "100",
"link": "https://skills.sh/same/src/a",
"description": "trending item",
}
]
hot_full = [
{
"id": "same/src/b",
"title": "b",
"source": "same/src",
"installs": 90,
"installs_fmt": "90",
"link": "https://skills.sh/same/src/b",
"description": "hot twin",
},
{
"id": "fresh/src/skill",
"title": "skill",
"source": "fresh/src",
"installs": 80,
"installs_fmt": "80",
"link": "https://skills.sh/fresh/src/skill",
"description": "fresh hot",
},
]
with patch("daily.format_wecom.localize_brief_descriptions", return_value={}):
with patch("daily.format_wecom.needs_chinese", return_value=False):
text = build_skills_delta_sections(
[],
[],
trending_full=trending_full,
hot_full=hot_full,
trending_limit=10,
hot_limit=10,
pad=True,
)
self.assertIn("Skills Hot Top 1", text)
self.assertIn("fresh/src", text)
self.assertNotIn("same/src/b", text)
def test_delta_pad_uses_large_pool_when_recent_excludes_top(self):
from daily.format_wecom import build_skills_delta_sections
@@ -306,6 +492,46 @@ class DeltaFormatTests(unittest.TestCase):
self.assertIn("GitHub Trending Top 10", text)
self.assertNotIn("GitHub Trending 变化", text)
def test_delta_pad_github_unions_recent_across_boards(self):
"""GitHub 三榜共用周去重Trending 出过的 repo新兴/Topic 不得再出。"""
from daily.format_wecom import build_github_delta_sections
movement = {"github_trending_moves": [], "github_emerging_moves": [], "github_topic_moves": []}
shared = {
"repo": "seen/repo",
"url": "https://github.com/seen/repo",
"language": "Go",
"stars_today_fmt": "100",
"total_stars_fmt": "1K",
"created_at": "2026-07-01",
"description": "already shown",
"desc_short": "already shown",
}
fresh = {
"repo": "fresh/repo",
"url": "https://github.com/fresh/repo",
"language": "Go",
"stars_today_fmt": "90",
"total_stars_fmt": "900",
"created_at": "2026-07-02",
"description": "fresh",
"desc_short": "fresh",
}
with patch("daily.format_wecom.localize_brief_descriptions", return_value={}):
text = build_github_delta_sections(
movement,
topic_name="llm",
github_trending=[],
github_emerging=[shared, fresh],
github_topic=[shared],
emerging_limit=5,
topic_limit=5,
pad=True,
recent_board_keys={"github_trending": {"seen/repo"}},
)
self.assertIn("fresh/repo", text)
self.assertNotIn("seen/repo", text)
def test_delta_pad_skips_recent_skills(self):
from daily.format_wecom import build_skills_delta_sections
@@ -320,6 +546,16 @@ class DeltaFormatTests(unittest.TestCase):
"description": f"skill {i}",
}
for i in range(4, 6)
] + [
{
"id": "fresh/src/skill",
"title": "skill",
"source": "fresh/src",
"installs": 50,
"installs_fmt": "50",
"link": "https://skills.sh/fresh/src/skill",
"description": "fresh skill",
}
]
with patch("daily.format_wecom.localize_brief_descriptions", return_value={}):
with patch("daily.format_wecom.needs_chinese", return_value=False):
@@ -330,10 +566,12 @@ class DeltaFormatTests(unittest.TestCase):
hot_full=[],
trending_limit=10,
pad=True,
# 同仓历史 skill id → 整仓 source 去重;仅保留其它 source
recent_trending={f"x/y/s{i}" for i in range(1, 4)},
)
self.assertIn("Skills Trending Top 1", text)
self.assertIn("2 skills", text)
self.assertIn("fresh/src", text)
self.assertNotIn("x/y", text)
def test_delta_pad_skips_recent_github(self):
from daily.format_wecom import build_github_delta_sections
@@ -363,6 +601,45 @@ class DeltaFormatTests(unittest.TestCase):
self.assertIn("org/r4", text)
self.assertNotIn("org/r1", text)
def test_delta_pad_skips_recent_github_moves(self):
"""异动新入榜若昨日企微已展示pad 时仍应排除(不只滤补榜)。"""
from daily.format_wecom import build_github_delta_sections
movement = {
"github_trending_moves": [
{
"repo": "vinta/awesome-python",
"url": "https://github.com/vinta/awesome-python",
"language": "Python",
"total_stars_fmt": "308K",
"description": "list",
}
],
"github_emerging_moves": [],
"github_topic_moves": [],
}
full = [
{
"repo": "fresh/repo",
"url": "https://github.com/fresh/repo",
"language": "Go",
"total_stars_fmt": "1K",
"description": "fresh",
"desc_short": "fresh",
}
]
with patch("daily.format_wecom.localize_brief_descriptions", return_value={}):
text = build_github_delta_sections(
movement,
topic_name="llm",
github_trending=full,
trending_limit=10,
pad=True,
recent_board_keys={"github_trending": {"vinta/awesome-python"}},
)
self.assertIn("fresh/repo", text)
self.assertNotIn("vinta/awesome-python", text)
def test_load_recent_board_keys_from_data_json(self):
import json
import tempfile