From 8075ab78e8ea1cbbac09856b8252e9e07a8c1315 Mon Sep 17 00:00:00 2001 From: yumao Date: Fri, 3 Jul 2026 14:45:58 +0800 Subject: [PATCH] =?UTF-8?q?test:=20Phase=203=20=E6=B7=BB=E5=8A=A0=20smoke?= =?UTF-8?q?=20test=E3=80=81CI=20=E4=B8=8E=20run-daily=20=E5=8E=BB=E9=87=8D?= =?UTF-8?q?=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 离线 mock 测试 generate 产出,GitHub Actions 跑 pytest,30 分钟内重复调度自动 skip。 Co-authored-by: Cursor --- .github/workflows/test.yml | 20 +++++++++ pytest.ini | 3 ++ requirements-dev.txt | 4 ++ run-daily.ps1 | 78 ++++++++++++++++++++++++--------- tests/conftest.py | 50 +++++++++++++++++++++ tests/fixtures/sample-rss.xml | 12 +++++ tests/fixtures/skills-feed.json | 32 ++++++++++++++ tests/test_smoke.py | 66 ++++++++++++++++++++++++++++ 8 files changed, 244 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/sample-rss.xml create mode 100644 tests/fixtures/skills-feed.json create mode 100644 tests/test_smoke.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..45e9e4f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: test + +on: + push: + branches: [main] + pull_request: + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + - name: Install dependencies + run: pip install -r requirements-dev.txt + - name: Run tests + run: pytest -q diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..4584de7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests +pythonpath = . diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..e08bb14 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +python-dotenv>=1.0.0 +httpx>=0.27.0 +certifi>=2024.0.0 +pytest>=8.0 diff --git a/run-daily.ps1 b/run-daily.ps1 index aab641e..c02d7df 100644 --- a/run-daily.ps1 +++ b/run-daily.ps1 @@ -3,16 +3,21 @@ # .\run-daily.ps1 # .\run-daily.ps1 -SkipPush # .\run-daily.ps1 -SkipGenerate +# .\run-daily.ps1 -Force # bypass duplicate-run lock param( [switch]$SkipPush, - [switch]$SkipGenerate + [switch]$SkipGenerate, + [switch]$Force ) $ErrorActionPreference = "Stop" $Root = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $Root +$LockFile = Join-Path $Root ".cache\run-daily.lock" +$LockMaxMinutes = 30 + function Import-DotEnvFile { param([string]$Path) if (-not (Test-Path $Path)) { return } @@ -29,30 +34,61 @@ function Import-DotEnvFile { } } -Import-DotEnvFile (Join-Path $Root ".env") -Import-DotEnvFile (Join-Path $Root ".env.local") +function Test-RunDailyLock { + if (-not (Test-Path $LockFile)) { return $false } + $age = (Get-Date) - (Get-Item $LockFile).LastWriteTime + return $age.TotalMinutes -lt $LockMaxMinutes +} -$python = "python" -$date = Get-Date -Format "yyyy-MM-dd" -$reportWecom = Join-Path $Root "output\$date.wecom.md" +function Set-RunDailyLock { + $dir = Split-Path $LockFile -Parent + if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + Set-Content -Path $LockFile -Value (Get-Date -Format "o") -Encoding UTF8 +} -if (-not $SkipGenerate) { - Write-Host "Generating daily report: $date" - & $python -m daily generate - if ($LASTEXITCODE -ne 0) { - throw "daily generate failed with exit code $LASTEXITCODE" +function Clear-RunDailyLock { + if (Test-Path $LockFile) { + Remove-Item $LockFile -Force -ErrorAction SilentlyContinue } } -if (-not $SkipPush) { - if (-not (Test-Path $reportWecom)) { - throw "Report not found: $reportWecom" - } - Write-Host "Pushing to WeCom webhook..." - & $python -m daily push $reportWecom - if ($LASTEXITCODE -ne 0) { - throw "daily push failed with exit code $LASTEXITCODE" - } +if (-not $Force -and (Test-RunDailyLock)) { + Write-Host "Skip: run-daily already ran within ${LockMaxMinutes} minutes (lock: $LockFile). Use -Force to override." + exit 0 } -Write-Host "Done: $date" +Set-RunDailyLock +try { + Import-DotEnvFile (Join-Path $Root ".env") + Import-DotEnvFile (Join-Path $Root ".env.local") + + $python = "python" + $date = Get-Date -Format "yyyy-MM-dd" + $reportWecom = Join-Path $Root "output\$date.wecom.md" + + if (-not $SkipGenerate) { + Write-Host "Generating daily report: $date" + & $python -m daily generate + if ($LASTEXITCODE -ne 0) { + throw "daily generate failed with exit code $LASTEXITCODE" + } + } + + if (-not $SkipPush) { + if (-not (Test-Path $reportWecom)) { + throw "Report not found: $reportWecom" + } + Write-Host "Pushing to WeCom webhook..." + & $python -m daily push $reportWecom + if ($LASTEXITCODE -ne 0) { + throw "daily push failed with exit code $LASTEXITCODE" + } + } + + Write-Host "Done: $date" +} +finally { + Clear-RunDailyLock +} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..76fc899 --- /dev/null +++ b/tests/conftest.py @@ -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") diff --git a/tests/fixtures/sample-rss.xml b/tests/fixtures/sample-rss.xml new file mode 100644 index 0000000..43d4e9c --- /dev/null +++ b/tests/fixtures/sample-rss.xml @@ -0,0 +1,12 @@ + + + + Sample AI Feed + + Sample AI headline for smoke tests + https://example.com/ai-news/1 + Wed, 02 Jul 2026 08:00:00 GMT + A minimal RSS item used by offline tests. + + + diff --git a/tests/fixtures/skills-feed.json b/tests/fixtures/skills-feed.json new file mode 100644 index 0000000..e54c19c --- /dev/null +++ b/tests/fixtures/skills-feed.json @@ -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": [] +} diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..bf81ba8 --- /dev/null +++ b/tests/test_smoke.py @@ -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 "" in xml