test: Phase 3 添加 smoke test、CI 与 run-daily 去重锁

离线 mock 测试 generate 产出,GitHub Actions 跑 pytest,30 分钟内重复调度自动 skip。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-03 14:45:58 +08:00
parent ebc25c9e9b
commit 8075ab78e8
8 changed files with 244 additions and 21 deletions

50
tests/conftest.py Normal file
View File

@@ -0,0 +1,50 @@
from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
FIXTURES = Path(__file__).parent / "fixtures"
@pytest.fixture
def skills_feed() -> dict:
return json.loads((FIXTURES / "skills-feed.json").read_text(encoding="utf-8"))
@pytest.fixture
def fixed_cst():
return datetime(2026, 7, 3, 9, 30, tzinfo=timezone(timedelta(hours=8)))
@pytest.fixture
def isolated_output(tmp_path, monkeypatch):
import daily.config as config
import daily.report_data as report_data
out = tmp_path / "output"
cache = tmp_path / "cache"
logs = tmp_path / "logs"
out.mkdir()
cache.mkdir()
logs.mkdir()
monkeypatch.setattr(config, "OUTPUT_DIR", out)
monkeypatch.setattr(report_data, "OUTPUT_DIR", out)
monkeypatch.setattr(config, "CACHE_DIR", cache)
monkeypatch.setattr(config, "LOG_DIR", logs)
monkeypatch.setattr(config, "SNAPSHOT_FILE", cache / "last-report.json")
return out
@pytest.fixture
def offline_generate_env(monkeypatch):
monkeypatch.setenv("DAILY_AI_NEWS", "0")
monkeypatch.setenv("DAILY_CN_AI_NEWS", "0")
monkeypatch.setenv("DAILY_ZH_DESC", "0")
monkeypatch.setenv("SKILLS_BOARD_SOURCE", "feed")
monkeypatch.setenv("DAILY_REPORT_MODE", "classic")
monkeypatch.setenv("DAILY_CURSOR_EDITOR", "0")
monkeypatch.setenv("DAILY_TRENDING_LIMIT", "10")
monkeypatch.setenv("DAILY_HOT_LIMIT", "10")

12
tests/fixtures/sample-rss.xml vendored Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Sample AI Feed</title>
<item>
<title>Sample AI headline for smoke tests</title>
<link>https://example.com/ai-news/1</link>
<pubDate>Wed, 02 Jul 2026 08:00:00 GMT</pubDate>
<description>A minimal RSS item used by offline tests.</description>
</item>
</channel>
</rss>

32
tests/fixtures/skills-feed.json vendored Normal file
View File

@@ -0,0 +1,32 @@
{
"updatedAt": "2026-07-02T12:00:00Z",
"topTrending": [
{
"id": "vercel-labs/skills/find-skills",
"title": "find-skills",
"source": "vercel-labs/skills",
"installs": 18000,
"link": "https://www.skills.sh/vercel-labs/skills/find-skills",
"description": "Help users discover and install Agent Skills."
},
{
"id": "halt-catch-fire/skills/remotion-render",
"title": "remotion-render",
"source": "halt-catch-fire/skills",
"installs": 21549,
"link": "https://www.skills.sh/halt-catch-fire/skills/remotion-render",
"description": "Render videos from React/Remotion component code."
}
],
"topHot": [
{
"id": "vercel-labs/skills/find-skills",
"title": "find-skills",
"source": "vercel-labs/skills",
"installs": 243,
"link": "https://www.skills.sh/vercel-labs/skills/find-skills",
"description": "Help users discover and install Agent Skills."
}
],
"topAllTime": []
}

66
tests/test_smoke.py Normal file
View File

@@ -0,0 +1,66 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
FIXTURES = Path(__file__).parent / "fixtures"
def _sample_github_repo() -> dict:
return {
"repo": "owner/sample",
"url": "https://github.com/owner/sample",
"description": "Sample repository for smoke tests.",
"language": "Python",
"stars_today_fmt": "120",
"total_stars_fmt": "1.2K",
"created_at": "2026-06-01",
}
def test_format_installs():
from shared.skills_data import format_installs
assert format_installs(999) == "999"
assert format_installs(18000) == "18.0K"
assert format_installs(2_500_000) == "2.5M"
def test_generate_report_offline(
skills_feed,
fixed_cst,
isolated_output,
offline_generate_env,
monkeypatch,
):
import daily.generate as generate
monkeypatch.setattr(generate, "load_feed", lambda force=False: skills_feed)
monkeypatch.setattr(generate, "fetch_github_trending", lambda n: [_sample_github_repo()])
monkeypatch.setattr(generate, "fetch_emerging_repos", lambda n, exclude=None: [])
monkeypatch.setattr(generate, "fetch_topic_hot_repos", lambda n, exclude=None: ("llm", []))
monkeypatch.setattr(generate, "_now_cst", lambda: fixed_cst)
_, wecom_md, out_md, out_wecom = generate.generate_report()
assert out_md.exists()
assert out_wecom.exists()
assert "早报" in out_md.read_text(encoding="utf-8")
assert wecom_md.strip()
assert "📰" in wecom_md
data_path = isolated_output / "2026-07-03.data.json"
assert data_path.exists()
payload = json.loads(data_path.read_text(encoding="utf-8"))
assert payload["meta"]["report_mode"] == "classic"
assert payload["data"]["date"] == "2026-07-03"
assert len(payload["data"]["skills_trending"]) >= 1
assert len(payload["data"]["skills_hot"]) >= 1
def test_rss_fixture_is_well_formed():
xml = (FIXTURES / "sample-rss.xml").read_text(encoding="utf-8")
assert "Sample AI headline" in xml
assert "<item>" in xml