diff --git a/Agent.md b/Agent.md index c0ebdc8..871c8b7 100644 --- a/Agent.md +++ b/Agent.md @@ -93,7 +93,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (480) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (484) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (86: 22 daemon_client + 22 app-commands + 17 renderer smoke + 15 i18n + 7 integration + 3 commands) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) diff --git a/README.md b/README.md index 16802a7..1043c6f 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ EMRG doesn't just keep up — it catches up on its own. git clone https://github.com/argszero/emrg.git cd emrg uv sync # install deps -uv run pytest tests/ -v # run tests (currently 480 items) +uv run pytest tests/ -v # run tests (currently 484 items) uv run python -m emrg # launch TUI # CI includes actionlint workflow gate (#444): workflow parse errors fail PR CI diff --git a/tests/test_doc_counts.py b/tests/test_doc_counts.py new file mode 100644 index 0000000..df4584a --- /dev/null +++ b/tests/test_doc_counts.py @@ -0,0 +1,74 @@ +"""Guard against the recurring README/Agent.md test-count drift. + +Pattern history: #426 -> #430 -> #510 -> #511. Every time tests are added or +removed, the documented counts drift and require a follow-up doc PR. This +module asserts the documented Python count matches the real collection, and +that the documented GUI breakdown sums to its headline number. +""" + +import re +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _collected_pytest_count() -> int: + """Run pytest in collect-only mode and parse the total.""" + out = subprocess.check_output( + [__import__("sys").executable, "-m", "pytest", "--collect-only", "-q"], + cwd=str(REPO_ROOT), + text=True, + stderr=subprocess.STDOUT, + ) + m = re.search(r"(\d+) tests? collected", out) + assert m, f"could not parse collected count from pytest output:\n{out[-2000:]}" + return int(m.group(1)) + + +def _gui_breakdowns() -> list[tuple[str, int, list[int]]]: + """Extract (label, headline, parts) for every documented GUI count.""" + found = [] + for doc in ("README.md", "Agent.md"): + text = (REPO_ROOT / doc).read_text(encoding="utf-8") + for line in text.splitlines(): + if "npm test" not in line: + continue + m = re.search(r"\((\d+): ([^)]+)\)", line) + if not m: + continue + headline = int(m.group(1)) + # each breakdown part starts with its count ("22 daemon_client + ..."); + # take the first number per part (avoids false digits inside names like i18n) + parts = [ + int(re.match(r"\s*(\d+)", part).group(1)) + for part in m.group(2).split("+") + if re.match(r"\s*\d+", part) + ] + found.append((f"{doc}: {line.strip()[:70]}", headline, parts)) + return found + + +def test_python_count_matches_docs() -> None: + collected = _collected_pytest_count() + for doc in ("README.md", "Agent.md"): + text = (REPO_ROOT / doc).read_text(encoding="utf-8") + # README: "run tests (currently N items)" | Agent.md: "pytest tests/ -v` (N)" + m = re.search(r"currently (\d+) items", text) or re.search( + r"uv run pytest tests/ -v` \((\d+)\)", text + ) + assert m, f"no documented Python count found in {doc}" + documented = int(m.group(1)) + assert documented == collected, ( + f"{doc} documents {documented} Python tests but {collected} are collected " + f"(--collect-only). Sync the doc (and this guard) when adding/removing tests." + ) + + +def test_gui_breakdown_sums_to_headline() -> None: + breakdowns = _gui_breakdowns() + assert breakdowns, "no GUI test breakdowns found in README.md/Agent.md" + for label, headline, parts in breakdowns: + assert sum(parts) == headline, ( + f"{label}: breakdown {parts} sums to {sum(parts)} but headline says {headline}" + )