Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (1307) — import check: `uv run python -c "from emrg.client.app import run_client"
Python: `uv run pytest tests/ -v` (1310) — import check: `uv run python -c "from emrg.client.app import run_client"
GUI: `cd emrg/gui && npm test` (100: 44 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 6 build-config + 4 boot-contract + 3 preload-api + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js`
Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (514: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 21 transcript + 11 TranscriptView + 15 history + 31 composer + 41 Composer + 6 LinkDialog + 16 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 18 daemonBridge + 7 DaemonBridgeProvider + 30 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Expand Down
14 changes: 12 additions & 2 deletions scripts/check-doc-count.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@

COLLECTED = re.compile(r"(\d+) tests? collected")

# The one spelling of "run this tool" that every hint in this repo prints: this
# module's two hints, the pytest guard's failure message, and Agent.md's doc
# line. Measured 2026-09-10 (cyc20260910-191242) in the main clone: this form
# exits 0, while the bare `python3` form the drift hint used to print exits 2
# without measuring anything - the host's `python3` has no pytest, so a hint
# spelled that way sends the reader straight into a second failure. A hint is
# only worth printing if it runs; keep the spelling here and let
# tests/test_check_doc_count.py prove the other sites agree with it.
INVOCATION = "uv run --no-sync python3 scripts/check-doc-count.py"


class DocCountError(Exception):
"""The doc or the tree is not in a shape this tool can act on."""
Expand All @@ -81,7 +91,7 @@ def measured_count() -> int:
f"pytest --collect-only failed (rc={proc.returncode}):\n"
+ (proc.stdout[-2000:] + proc.stderr[-2000:]).strip()
+ "\n\nhint: run this with the project interpreter, e.g."
" `uv run --no-sync python3 scripts/check-doc-count.py`"
f" `{INVOCATION}`"
)
match = COLLECTED.search(proc.stdout)
if not match:
Expand Down Expand Up @@ -165,7 +175,7 @@ def main(argv: list[str] | None = None) -> int:
f"but {measured} are collected"
)
if not (args.write or args.dry_run):
print("\nFix with: python3 scripts/check-doc-count.py --write")
print(f"\nFix with: {INVOCATION} --write")
return 1

if args.dry_run:
Expand Down
162 changes: 162 additions & 0 deletions tests/test_check_doc_count.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@
GUARD = REPO_ROOT / "tests" / "test_doc_counts.py"


def _load_guard():
"""Load the guard module so its function can be driven in both states.

By path, the same way `test_doc_counts.py` loads itself: pytest imports
these files as `tests.test_doc_counts`, and a plain top-level name does not
resolve.
"""
spec = importlib.util.spec_from_file_location("_doc_count_guard", GUARD)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


def _load_module():
spec = importlib.util.spec_from_file_location("check_doc_count", SCRIPT)
mod = importlib.util.module_from_spec(spec)
Expand Down Expand Up @@ -81,6 +94,96 @@ def _guard_pattern() -> str:
raise AssertionError("could not find the guard's count pattern")


def _guard_assert_messages() -> list[str]:
"""Every assertion message in the count guard, as static text.

Read from the AST, not by regex, so a reworded message is still read as the
message it is. Adjacent string constants inside an f-string are joined; the
`{doc}` / `{documented}` placeholders are `FormattedValue` nodes and drop
out, which is fine - the hint this test cares about is static text.
"""
tree = ast.parse(GUARD.read_text(encoding="utf-8"))
func = next(
node
for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef) and node.name == "test_python_count_matches_docs"
)
messages = []
for node in ast.walk(func):
if isinstance(node, ast.Assert) and node.msg is not None:
messages.append(
"".join(
part.value
for part in ast.walk(node.msg)
if isinstance(part, ast.Constant) and isinstance(part.value, str)
)
)
return messages


def test_guard_failure_names_a_runnable_repair_command(mod, capsys) -> None:
"""The guard must fail with a command the host can actually run.

Reporting drift without naming the repair path leaves the host to find the
tool - and the tool exists precisely for this failure. Both halves are
needed, so both are asserted: the path in the message must exist, and
`--write` must be a flag the tool really accepts. A hint spelled
consistently is not a repair path; a repair path that only exists in a
comment is not one either.

Necessary, not sufficient: this reads the hint out of the guard's source, and
a string that lives in an assert the drift path never reaches satisfies it.
`test_guard_drift_message_names_the_repair_command` is the half that drives
the guard and checks the message a host actually sees.
"""
messages = _guard_assert_messages()
hinted = [m for m in messages if "scripts/check-doc-count.py" in m]
assert hinted, (
"the guard's failure message no longer names the repair tool, so a host "
f"who hits it in CI has no next step; messages found: {messages}"
)
assert any("--write" in m for m in hinted), (
f"the hint does not offer the repair flag, only a path: {hinted[0]!r}"
)
assert SCRIPT.exists(), f"the guard points at {SCRIPT}, which does not exist"

with pytest.raises(SystemExit) as excinfo:
mod.main(["--help"])
assert excinfo.value.code == 0
assert "--write" in capsys.readouterr().out


def test_guard_drift_message_names_the_repair_command(mod, monkeypatch) -> None:
"""The hint must be in the message a host actually gets, in the drift state.

The static checks next door read the hint out of the guard's source, which
makes them necessary but not sufficient. Measured (cyc20260910-192726): moving
that same hint string onto the assert that fires when the *anchor* is missing
left both of them green, while a drifting host saw a message with no hint at
all - the exact regression this branch exists to prevent. So drive the guard
the way drift drives it: stub the collected count (no subprocess, ~0s) and
read the raised message.
"""
guard = _load_guard()
canonical = "uv run --no-sync python3 scripts/check-doc-count.py --write"
documented = mod.documented_count((REPO_ROOT / "Agent.md").read_text(encoding="utf-8"))

# Positive state: a consistent tree leaves the guard silent. Without this
# half, a guard that compared nothing would still pass the negative one.
monkeypatch.setattr(guard, "_collected_pytest_count", lambda: documented)
assert guard.test_python_count_matches_docs() is None

# Negative state: one test's worth of drift - the real incident shape.
monkeypatch.setattr(guard, "_collected_pytest_count", lambda: documented + 1)
with pytest.raises(AssertionError) as excinfo:
guard.test_python_count_matches_docs()
message = str(excinfo.value)
assert f"Fix with: {canonical}" in message, (
"the message a host sees on drift no longer names the repair command; "
f"it says: {message!r}"
)


def test_tool_pattern_agrees_with_the_guard(mod) -> None:
"""The guard and this tool must key on the same phrase in the real Agent.md.

Expand Down Expand Up @@ -235,3 +338,62 @@ def test_real_tree_is_consistent() -> None:
)
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "OK: Agent.md documents" in proc.stdout


# --- the repair hint itself must be a command that runs -----------------------


def test_every_repair_hint_prints_one_runnable_command(mod, monkeypatch, tmp_path, capsys) -> None:
"""Every site that tells someone how to repair the count must agree.

Measured (cycle cyc20260910-191242, main clone): the canonical form exits 0,
while the bare `python3` form the drift hint used to print exits 2 having
measured nothing - the host's `python3` cannot import pytest. The guard's
message, this tool's error hint and Agent.md already used the canonical form,
so the drift hint was the one site sending the reader into a second failure.
A hint that fails is worse than no hint: it looks like a next step.

All four sites are checked here (tool source, tool drift output, tool error
output, guard message, plus Agent.md), because the defect was precisely a
disagreement between them.
"""
canonical = "uv run --no-sync python3 scripts/check-doc-count.py"
assert mod.INVOCATION == canonical

source = SCRIPT.read_text(encoding="utf-8")
hits = list(re.finditer(r"python3 scripts/check-doc-count\.py", source))
assert hits, "the tool no longer mentions its own invocation at all"
for hit in hits:
prefix = source[max(0, hit.start() - len("uv run --no-sync ")) : hit.start()]
assert prefix == "uv run --no-sync ", (
"a hint in scripts/check-doc-count.py spells the invocation without the "
f"project runner: ...{source[max(0, hit.start() - 40) : hit.end() + 20]!r}"
)

doc = (REPO_ROOT / "Agent.md").read_text(encoding="utf-8")
assert canonical in doc, "Agent.md no longer documents the canonical invocation"
assert any(
f"Fix with: {canonical} --write" in message
for message in _guard_assert_messages()
), "the pytest guard's failure message no longer prints the canonical fix command"

# Drift state, for real: the printed line must be the runnable one.
mod.DOC = _doc(tmp_path, 1307)
monkeypatch.setattr(mod, "measured_count", lambda: 1308)
assert mod.main([]) == 1
assert f"Fix with: {canonical} --write" in capsys.readouterr().out

# Error state: the interpreter advice must be the same spelling. A fresh
# module, because `measured_count` on `mod` is stubbed above to reach the
# drift path, and this half needs the real function to run.
fresh = _load_module()

class _Proc:
returncode = 4
stdout = ""
stderr = ""

monkeypatch.setattr(fresh.subprocess, "run", lambda *a, **k: _Proc())
with pytest.raises(fresh.DocCountError) as excinfo:
fresh.measured_count()
assert f"`{canonical}`" in str(excinfo.value)
3 changes: 2 additions & 1 deletion tests/test_doc_counts.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ def test_python_count_matches_docs() -> None:
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."
f"(--collect-only). Sync the doc (and this guard) when adding/removing tests.\n"
"Fix with: uv run --no-sync python3 scripts/check-doc-count.py --write"
)


Expand Down
Loading