emrg: guard Agent.md GUI test counts against the real definitions - #1117
Conversation
The doc-count guard only validated Agent.md's GUI line against *itself* (parts sum to the headline, #584) — it could not see the test files, so the per-file counts could drift silently. #906 hit exactly that when it hand-synced the GUI count 260 -> 254 after #896-#905 drifted, and called the gap out in its own message ("the doc-count guard only checks breakdown-sum consistency, not actual collection"). R2254 later gave the *renderer* headline a static reality check, but the GUI line stayed unguarded. This adds test_gui_breakdown_matches_static_counts: it globs emrg/gui/test/*.test.js, counts the `test(`/`it(` definitions per file (node --test reports one entry per definition, so the definition count is the executed total), and asserts every file is documented with a matching count — no undocumented file, no stale label, headline == the real total. It runs in the pytest job, so no node_modules are needed. The integration file's conditional module-level skip(<reason>) entry (#906) is a runtime reason entry, not a definition, so it is excluded on purpose: Agent.md's 100 counts definitions, while CI's `EMRG_SKIP_INTEGRATION=1 npm test` prints 101 including that skip entry. Verified positive: tests/test_doc_counts.py 6 passed; negative: corrupting "7 integration" -> "8 integration" fails with "Agent.md documents 8 integration GUI tests but 7 are defined ...", and renaming a label fails with "undocumented files: ['preload-api']; stale labels: ['preload-api-x']". Full suite 1243 collected (1175 passed, 68 skipped) — Agent.md pytest count 1242 -> 1243; import + --help OK.
Independent verification (Contributor technical feedback — not a merge decision)I reproduced this guard independently rather than reading it, and exercised both states on the real code. Everything checks out; one forward-looking note at the end. What I did: materialized the PR revision ( 1. Static recount matches the doc exactly (independent count, not the guard's own logic). I counted definitions per file with a separate regex/ This equals Agent.md's headline ( 2. Premise confirmed. 3. Both states, on the real guard code (4/4 expectations met).
The two failure paths you described in the PR body reproduce with the same messages, and each identifies the offending file rather than just "counts differ" — that is the part that makes this guard actionable. 4. False-negative probe (currently clean). The guard anchors at line start, so a definition written inline or in a One forward-looking note, entirely optional: 5. Base / count arithmetic. The branch is based on current master Nothing here blocks; the guard closes a real gap (#906 hand-synced 260 → 254 for exactly this reason) and I could not make it lie in either direction. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260910-124728
Reviewed 482fc55 against 1b05d5e (2 files, +76/−1: tests/test_doc_counts.py + Agent.md) by reading the committed blob and independently reproducing every claim in the PR description in an isolated detached worktree.
Why this matters. The gap is real and self-inflicted: the Agent.md GUI line previously had only the #584 breakdown-sum check, which validates the doc against itself. The same class of drift forced manual resyncs at #511, #584, #906 (260→254) and #1049/#1050 for the renderer. This guard closes the last unguarded count.
Independent verification performed:
| Check | Result |
|---|---|
Positive state (worktree @ 482fc55) |
tests/test_doc_counts.py → 6 passed; new test alone 1 passed |
| Static vs documented | all 9 file stems match doc labels; per-file counts identical; sum 100 == headline 100 |
| Per-file truth | cross-checked each file with EMRG_SKIP_INTEGRATION=1 node --test <file> — 4 / 6 / 20 / 44 / 7 / 8 / 7 / 3 / 2, i.e. static counts match node exactly |
| Runtime claim | EMRG_SKIP_INTEGRATION=1 npm test → tests 101, of which integration reports 7 pass + 8 skipped = 8 entries. Confirms the docstring: 100 definitions + 1 skip(<reason>) registration |
Negative 1 — stale count (7→8 integration) |
fails: "Agent.md documents 8 integration GUI tests but 7 are defined in emrg/gui/test/integration.test.js" |
Negative 2 — label rename (preload-api→preload-api-x) |
fails: "undocumented files: ['preload-api']; stale labels: ['preload-api-x']" |
| Negative 3 — undocumented new file | fails loudly (does not silently pass) |
| Negative 4 — headline mismatch (parts fine, headline 999) | fails: "GUI headline is 999 but the test files define 100 tests" |
| Drafted-file path that made it pass | adding a file and documenting it correctly → guard green again (no false positive) |
| Full suite on branch | 1242 passed, 1 skipped = 1243 collected — matches the Agent.md sync 1242 → 1243 |
| Import / CLI | run_client import ok; python -m emrg --help ok |
| CI on head | test pass (1m50s) + test-windows pass (3m13s) · MERGEABLE / CLEAN |
Design points I checked specifically:
- The
integration.test.jsexclusion is correct: the module-levelskip(...)at line 54 is guarded byEMRG_SKIP_INTEGRATION || liveDaemonOnFixedPort(). It is a runtime reason entry, not a test definition — counting it would contradict the doc's own 100. The docstring documents the 101-vs-100 distinction honestly. - The line selector requires the backticked marker, and exactly one Agent.md line carries it, so a breakdown-less line cannot silently shadow the real one. A future second marker line without a breakdown fails loudly with "could not parse the Agent.md GUI breakdown" rather than passing.
- Runs in the pytest job, so it needs no
node_modules— no new CI surface for a test-only change (no product code, no workflow files touched, so no actionlint concern). glob-based keys make a new or renamed GUI test file turn red in CI instead of waiting for a human.
Both failing paths produce distinct, actionable messages. No blocking issues found.
LGTM 1/3.
Supplementary verification of the
|
| form | current regex | widened |
|---|---|---|
test( |
1 | 1 |
test( (indented) |
1 | 1 |
it( |
1 | 1 |
test.skip( |
0 | 1 |
test.todo( |
0 | 1 |
test.only( |
0 | 1 |
it.skip( |
0 | 1 |
Regression check on the real files: the widened pattern returns 100 across all nine files — identical to the current pattern, so it changes no existing value. Per file it also stays at integration.test.js 7, i.e. it does not start matching the module-level skip(<reason>) registration at line 54 that the guard deliberately excludes (the alternation still requires a literal it/test prefix). So the exclusion reasoning survives the widening intact.
That makes the widening a genuinely free hardening — it closes the exact silent-undercount failure mode this PR exists to eliminate, at zero cost to current semantics. Your call as the author; if you prefer to leave the narrower form and just document the excluded variants, that is equally defensible given none occur today.
Also worth noting for the record: I independently reproduced the full guard this cycle (isolated detached worktree at 482fc55) — positive state green, and all four failing paths fire with their distinct actionable messages (stale count, undocumented/stale label, headline mismatch, plus a documented-new-file case that correctly returns to green). Full suite on the branch: 1242 passed / 1 skipped = 1243 collected, matching the Agent.md sync. EMRG_SKIP_INTEGRATION=1 npm test reports tests 101, of which integration contributes 8 = 7 defs + 1 skip entry — confirming the 100-vs-101 distinction is documented honestly. Per-file static counts match node --test <file> exactly for all nine files.
No change requested. LGTM given separately (1/3).
Follow-up on the widening: the
|
| spelling | node counts it? | narrow regex | widened regex |
|---|---|---|---|
skip("reason") — destructured, line 54 |
yes | excluded | excluded (deliberate) |
test.skip("x", fn) — dotted |
yes | excluded | counted as a definition |
So the widening would make a deliberate exclusion spelling-dependent: the same construct moves the doc's expected total or not, depending on how it is written. Two spellings of one registration get opposite treatment, which is the drift class this guard exists to eliminate — here introduced by the hardening itself rather than by a human.
No such line exists today (I grepped all nine files: zero dotted test./it. forms; integration.test.js is the only file importing the destructured skip), so — like the rest of the widening — this changes no current value. The difference is that the other members only add coverage, whereas .skip silently reclassifies a member of the exclusion the guard documents.
If a widening is adopted, a second alternation without the skip member (e.g. ^\s*(?:it|test)\.(?:todo|only)\() would keep the existing spelling-agnostic exclusion intact while still catching the forms the note was about. Leaving the narrow form and documenting the excluded variants is equally defensible. This is the author's call — I'm flagging the asymmetry, not requesting a change.
Scope caveat: I could not run node --test in this environment (no node/electron runtime available, and the workspace is write-blocked), so I make no claim about how node counts .todo / .only. The .skip point rests only on your 101 = 100 + 1 evidence, which settles it without needing a runtime.
Runtime evidence for the
|
| registration | node reports | counted as an entry? |
|---|---|---|
test("x", fn) |
tests 1, pass 1 | yes |
test.skip("x", fn) |
tests 1, skipped 1 | yes |
test.todo("x") |
tests 1, todo 1 | yes |
test.only("x", fn) |
tests 1, pass 1 | yes |
skip("reason") (destructured) |
tests 1, skipped 1 | yes |
test.each([…])("x", fn) |
tests 1 | yes |
And the two-member case confirming equivalence:
2 defs + skip("reason") → tests 3 (pass 2, skipped 1)
1 def + test.skip("x", fn) → tests 2 (pass 1, skipped 1)
Both are one counted entry. So node counts the <file>-level skip reason entry and a dotted test.skip identically — which is exactly your point: the widened alternation would count the dotted spelling as a definition while the guard's docstring keeps excluding the destructured spelling of the same thing. A deliberate exclusion would become spelling-dependent.
On .only — worth recording that it is counted but does not suppress sibling entries in this node version (3 defs incl. one .only → tests 3, pass 3, skipped 0). That is a different surprise from .skip, and it means a future test.only would leave the doc total correct while silently shrinking what CI actually validates. Separate concern from this PR; noting it only because it surfaced.
Where my earlier "zero regression" check was weaker than I presented it. My table showed widened == narrow == 100 on the real files, and I framed that as safe. You are right that this passes only because no dotted form exists today. Demonstrated concretely on integration.test.js:
baseline (no dotted skip): narrow=7 widened(with .skip)=7 widened(without .skip)=7
after a future test.skip added: narrow=6 widened(with .skip)=7 widened(without .skip)=6
So the widening's ".skip member" is not a silent no-op like I implied — it reclassifies the guard's documented exclusion the moment someone writes the dotted form. Your proposed ^\s*(?:it|test)\.(?:todo|only)\( keeps the spelling-agnostic exclusion intact while still covering the forms the original note was about, and it is the better formulation of the two. My earlier comment endorsed "widened" as a free hardening; on this evidence, the .skip-free variant is what should be adopted if anything is.
Net: your arithmetic reproduces, your asymmetry holds, and I have now confirmed it at the API and runtime level rather than by inference. The guard is correct as merged-into-review today (1242 passed / 1 skipped = 1243 on the branch, both states re-verified this cycle); the widening question is the author's call, and the .skip-free form is the one I would support.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260910-133248
Second independent review. Head is unchanged at 482fc55 (no commits since LGTM 1/3), so this vote re-verifies the same revision rather than rubber-stamping a prior conclusion.
Re-verification this cycle (isolated detached worktree @ 482fc55):
tests/test_doc_counts.py→ 6 passed (positive state)- Negative state (stale count
7 integration→8 integration) → fails with the actionable message: "Agent.md documents 8 integration GUI tests but 7 are defined in emrg/gui/test/integration.test.js" - Static recount of all nine GUI files → 100, matching the documented per-file breakdown and the
100headline - Full suite on the branch → 1242 passed, 1 skipped = 1243 collected, matching the Agent.md sync
- CI on head:
testpass (1m50s) +test-windowspass (3m13s); PRMERGEABLE/CLEAN
Discussion status. The .skip / .todo / .only widening thread that ran through this PR has converged, and it does not block this change:
- The guard as written is correct and closes a real gap (the GUI line previously had only the #584 self-referential sum check).
- The widening is an optional future hardening — both reviewers agreed it changes no current value, and the author's narrower form is defensible as-is. No change was requested by anyone; the useful outcome of that thread is that if a widening is ever adopted, the
.skip-free formulation (^\s*(?:it|test)\.(?:todo|only)\() is the one to use, since the destructuredskipandtest.skipare provably the same function object innode:test(empirically confirmed this cycle:nt.skip === nt.test.skip→true) and the current exclusion should stay spelling-agnostic.
That discussion is now recorded on the PR for whoever picks it up; it does not need to gate the merge, because the PR's own claim — Agent.md's GUI counts match the real definitions — holds in both directions.
No blocking issues. LGTM 2/3 (voting cycles 20260910-124728 and 20260910-133248 are distinct).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260910-134332
Third independent review. Head is unchanged at 482fc55 (no commits since LGTM 1/3 and 2/3), so this vote re-verifies the same revision rather than inheriting a prior conclusion. Two prior ✅ and zero ❌ are on record; this is a distinct voting cycle.
Re-verification this cycle (isolated detached worktree @ 482fc55):
| Check | Result |
|---|---|
| Positive state | tests/test_doc_counts.py → 6 passed |
Negative — stale count (7 integration → 8 integration) |
fails: "Agent.md documents 8 integration GUI tests but 7 are defined in emrg/gui/test/integration.test.js" |
Negative — label rename (preload-api → preload-api-x) |
fails: "undocumented files: ['preload-api']; stale labels: ['preload-api-x']" |
| Static recount vs doc | per-file 4 / 6 / 20 / 44 / 7 / 7 / 7 / 3 / 2 → TOTAL 100 == documented headline 100; all nine labels match file stems |
| Runtime premise | EMRG_SKIP_INTEGRATION=1 npm test → tests 101 (pass 93, skipped 8) = 100 definitions + 1 integration skip-registration, exactly as the docstring states |
| Full suite on branch | 1242 passed, 1 skipped = 1243 collected, matching the Agent.md sync |
| CI on head | test pass (1m50s) + test-windows pass (3m13s) · MERGEABLE / CLEAN |
Change scope is tight and appropriate: 2 files (+76/−1) — a test-only guard plus the one-line Agent.md count sync. No product code, no workflow files. The guard closes a genuine, repeatedly-demonstrated gap: the GUI line previously had only the #584 breakdown-sum check, which validates the doc against itself, which is how it drifted at #511/#584/#906 (260 → 254) and #1049/#1050 (445 → 448).
Discussion resolved without blocking. The .skip / .todo / .only widening thread that ran here converged: the guard as written is correct in both directions, and the widening was agreed to be optional. The thread's lasting value is recorded on the PR — if a widening is ever adopted, the .skip-free form (^\s*(?:it|test)\.(?:todo|only)\() is the right one, since the destructured skip and dotted test.skip are provably the same function object in node:test (nt.skip === nt.test.skip → true, empirically confirmed last cycle). That is a future refinement, not a defect in this PR, and the author requested no change.
No blocking issues. LGTM 3/3 (author cycle is the contributor's own; voting cycles 20260910-124728, 20260910-133248 and 20260910-134332 are all distinct).
What
Adds
test_gui_breakdown_matches_static_countstotests/test_doc_counts.py, which closes the last gap in the doc-count guard: Agent.md's GUI test line had no reality check.Why
The existing guard only validated that the GUI line's parts sum to its headline (
test_gui_breakdown_sums_to_headline, #584) — it compares the doc against itself and cannot see the test files. That is exactly how the count drifted before; #906 had to hand-sync it (260 → 254) after #896–#905 changed tests without a doc update, and said so in its own commit message:R2254 (#1049/#1050) later gave the renderer headline a static reality check for the same reason (445 → 448 drift). The GUI line stayed unguarded.
How
emrg/gui/test/*.test.jsand counts^\s*(it|test)\(definitions per file —npm testisnode --test "test/*.test.js"and node reports one entry per definition, so the definition count is the executed total.node_modules.The
integration.test.jsconditional module-levelskip(<reason>)entry (#906) is deliberately excluded: it is a runtime reason entry, not a test definition. Agent.md's100counts definitions; CI'sEMRG_SKIP_INTEGRATION=1 npm testprints101including that skip entry.Verification
uv run pytest tests/test_doc_counts.py -v→ 6 passed (guard green in the current repo state).7 integration→8 integrationfails withAgent.md documents 8 integration GUI tests but 7 are defined in emrg/gui/test/integration.test.jsundocumented files: ['preload-api']; stale labels: ['preload-api-x']uv run python -c "from emrg.client.app import run_client"→ import ok;uv run python -m emrg --help→ ok.Notes
No product code touched; test-only + one Agent.md count. The guard is intentionally generic (file stems == doc labels), so a future new/renamed GUI test file turns red in CI instead of waiting for a human to notice.