Skip to content

test(guards): make the guards prove what they claim about themselves - #101

Merged
donislawdev merged 4 commits into
masterfrom
test/guards-prove-they-guard
Aug 2, 2026
Merged

test(guards): make the guards prove what they claim about themselves#101
donislawdev merged 4 commits into
masterfrom
test/guards-prove-they-guard

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Four commits, one theme: the guards had claims about themselves that nothing checked.

1. The repository scanners now prove they scanned something

A guard built on a glob or a walk fails in a way that looks identical to success: the collection comes back empty and every assertion over it holds vacuously. Renaming beantester/ would have silenced four guards at once, on green.

  • test_the_repository_scanners_actually_read_files names one file each of the seven collectors must return, plus a floor on the count. The anchor catches a collector pointed at the wrong root, the floor catches one that quietly narrowed.
  • test_the_repository_scanners_stay_out_of_what_is_not_in_the_repository keeps the whole-tree scans off internal_tools/, .claude/, crashes/ and the maintainer's private notes.

Measured: the dash scan covered 183 files, 12 of them git-ignored - so the same test measured a different set locally than in CI, and a red CI cannot reproduce teaches that red is local noise. crashes/latest-crash.txt was in that set, carrying arbitrary text from OS exceptions.

2. "Verified by mutation" became data instead of prose

That phrase is this project's strongest claim about its own tests. It appears over twenty times in the internal notes and in a dozen docstrings, and nothing checked a single instance.

tests/test_mutation_registry.py holds it in three states that mean three different things: MUTATIONS (re-runnable now), PROVEN_BY_HAND (a dated session did it, wrote no patch, so nothing repeats it) and NOT_PROVEN (no mutation, said out loud). The suite checks the bookkeeping - every named test exists, none is filed twice, and every search pattern still occurs exactly once, so an entry rots the day the code moves rather than the day someone runs the rig.

The rig itself is a maintainer script (outside the repo, one subprocess suite run per entry) and carries a mandatory canary: a tree that fails to compile also exits non-zero, so without it a run can report "all caught" and mean nothing. Current state: 9 caught, 0 survived, canary BROKEN.

3. Size ceilings, as a ratchet

Function at 167 logic lines, module at 1299 - today's maxima, so nothing had to be rewritten to make it pass. Down is routine work, up is the owner's decision.

Comments and docstrings do not count, and that was measured before the metric was chosen: 9 776 of the package's 17 770 lines are logic, so 45% of this package is explanation. A raw-line cap would have been a cap on explaining, making deletion of the paragraph that says why a constant is 0.30 the cheapest way back under the limit. A second test pins that property directly.

4. The hot path is gated against retaining anything per packet

"Zero allocations" sat in the performance section with nothing checking it. Ceilings are 64 blocks and 4096 bytes over 5000 calls, against a measured floor of 13 and 608 that does not move with duplication, latency, the NAT flow table or four times the ports.

🔴 It reads two meters, and that is the useful part. The first version counted allocated blocks only, and the mutation that makes decide() append to an ever-growing list survived it - the appended value was a cached small int, so no object was created and blocks went from 6 to 7. Bytes see the same case as 42 032 against 208. The meter canary now proves both meters can rise, including the references-only case that defeated the first one.

Transient garbage is still not caught and the file says so: both meters are net, and CPython has no cheap deterministic counter of total allocations.

Testing

python -m pytest tests: 918 passed, exit 0 on an elevated shell. python smoke_gui.py: OK. Full mutation registry: 9 caught, 0 survived, canary BROKEN.

Every new guard in this PR is itself mutation-checked - emptying a collector, moving the walk to a missing root, letting internal_tools back into the scanned set, making comments count as logic, and making decide() retain per packet each turn the matching test red.

🤖 Generated with Claude Code

donislawdev and others added 4 commits August 2, 2026 15:07
A guard built on a glob or a walk fails in a way that looks identical to
success: the collection comes back empty and every assertion over it holds
vacuously. Nothing noticed - renaming beantester/ would have silenced four
guards at once, on green.

Two halves, both mutation-checked:

- test_the_repository_scanners_actually_read_files names one file each of the
  seven collectors must return, plus a floor on the count. The anchor catches a
  collector pointed at the wrong root, the floor catches one that narrowed.
- test_the_repository_scanners_stay_out_of_what_is_not_in_the_repository keeps
  the whole-tree scans off internal_tools/, .claude/, crashes/ and the private
  notes. Measured: the dash scan covered 183 files, 12 of them git-ignored, so
  it measured a different set here than in CI - and a red CI cannot reproduce
  teaches that red is local noise. crashes/latest-crash.txt was in that set,
  carrying arbitrary text from OS exceptions.

Convention 33 keeps its coverage of the private notes: the check moves to the
Stop hook, which runs exactly where those files exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…in prose

"Verified by mutation" is this project's strongest claim about its own tests. It
appears over twenty times in the notes and in a dozen docstrings, and nothing
checked a single one - convention 5's own evidence was exactly the unguarded
prose convention 5 exists to catch.

The claim is now data, in three states that mean three different things:

- MUTATIONS: re-runnable today, five entries, proven by internal_tools/mutate.py
- PROVEN_BY_HAND: a dated session did it and wrote no patch, so nothing repeats it
- NOT_PROVEN: no mutation at all, named out loud

The suite checks the bookkeeping, which is the half that rots: every named test
exists, none is filed twice, and every search pattern still occurs exactly once,
so an entry goes red the day the code moves rather than the day someone runs the
rig. The rig itself lives outside git and carries a mandatory canary - a tree
that fails to compile also exits non-zero, so without it a run can report
"all caught" and prove nothing. First full run: 5 caught, 0 survived, canary
BROKEN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nobody reads this code line by line, so nothing notices a function reaching
three hundred lines over four sessions that each added twenty. A ceiling is the
only mechanism that notices, and it costs nothing while nothing grows.

Set at today's maxima - 167 logic lines for a function (theme.py::init_style),
1299 for a module (gui/app.py) - so nothing needed rewriting to make it pass.
Down is routine work, up is the owner's decision: a threshold bent to fit the
code has stopped being a threshold.

Comments and docstrings are free, and that was measured before the metric was
chosen: 9 776 of the package's 17 770 lines are logic, so 45% of this package is
explanation. A raw-line cap would have been a cap on explaining, making deletion
of the paragraph that says why a constant is 0.30 the cheapest way back under
the limit. A second test pins that property directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Zero allocations" has sat in the hot-path section for a long time with nothing
checking it. test_hot_path.py guards the neighbouring rule - no syscalls on the
packet threads - and this catches the other class: something starting to be kept
once per packet. At ~14k packets a second that is tens of megabytes a minute,
and the symptom is not slowness but a session that dies after an hour.

Ceilings are 64 blocks and 4096 bytes over 5000 calls, against a measured floor
of 13 and 608 that does not move with duplication, latency, the NAT flow table
or four times the ports.

It reads TWO meters, and the reason is the useful part of this commit: the
first version counted allocated blocks only, and the mutation that makes
decide() append to an ever-growing list SURVIVED it. The appended value was a
cached small int, so no object was created and the block count went from 6 to 7.
Bytes see the same case as 42 032 against 208. The meter canary now proves both
meters can rise, including the references-only case that defeated the first one.

Transient garbage is still not caught, and the file says so: both meters are
net, and CPython has no cheap deterministic counter of total allocations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 47d6187 into master Aug 2, 2026
6 checks passed
@donislawdev
donislawdev deleted the test/guards-prove-they-guard branch August 2, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant