A Tests area that does not cover the file beside it

Audit finding, 2026-09-21 (#144).

What was wrong

Crm/Counterparty/Test/CounterpartyViewTests.cs defined 36 cases. CounterpartyTestsArea.cs named 33. The three it missed:

NewestActivity_ExcludesTheRoot_AndTakesTheMaxNotTheFirst
Provenance_ReportsTheNodesRow_AndAContainersActivityInstead
Provenance_SaysNothingWhileActivityIsStillUnknown

The area still renders, still runs the 33 it does name, and still reports tests=ok. Nothing anywhere said 33 was not 36.

The gap survived a rename. It was measured on Crm/Client/Test/ClientTests.cs; Phase C consolidated Client into Counterparty and moved the suite to CounterpartyViewTests.cs. The three unregistered cases travelled with the type, still unregistered — a rename carries the omission, because nothing compares the two files.

Why these three and not any three

Two of them pin the provenance line's container behaviour: that a counterparty page reports the newest activity under the counterparty rather than the root's own row (on a retyped account the root's LastModified is the migration, stamped system-security, while the deals beneath moved days later), and that the line says nothing while the activity read is still unanswered rather than falling back to "no activity". That second one is the not read yet is not empty rule in its provenance form. Both failure modes render a plausible-looking WRONG sentence rather than an error, which is the only class of bug a reader cannot catch for you.

What check-test-suites.py could not see

The existing ratchet reads the mesh gate's own log:

ok  Crm/Board:    compile=Ok render=ok tests=ok
ok  Crm/Question: compile=Ok render=ok tests=skipped

It fails a NodeType that exposes no Tests area. It cannot see a case that exists in the file and is wired into nothing, because that type reports tests=ok. The ratchet measures the area's EXISTENCE; this gate measures its COVERAGE of the file beside it. They are different invariants and the second one had no reader.

Two instruments, two different answers about "has it ever run?"

Worth stating plainly, because the honest answer is not the same for both:

instrument sees the three cases? runs in this repo's CI?
the mesh gate (test-repos, the Tests layout area on a real instance) no — the area does not name them yes, and it is what gates the PR
gate 5, core's run-node-tests.py (reflection over every static class *Tests) yes — registration is irrelevant to it no lane invokes it; it is the local loop

So "never executed" is precise about the thing that gates a merge and about CI as a whole, and imprecise about a developer's laptop: run locally, all three PASS (python3 scripts/run-node-tests.py Crm --type Counterparty → 103/103 before the registration and after it). Registering them therefore adds no red — but until they are named by the area they had never met a real mesh, which compiles them live, against the real framework, under a real viewer's locale and time zone. A console-app pass is not that.

The gate

scripts/validate-repos.py gained an arm — it runs in the shared validate lane, which is a required context, so it needs no new job and renames no check:

every public static void X() on a static class whose name ends in Tests, in a *Tests.cs file, must be NAMED as <Class>.X somewhere in the repo's C# with comments stripped.

The rule is deliberately the harness's own rule. Core's .github/scripts/run-node-tests.py selects t.IsClass && t.IsAbstract && t.IsSealed && t.Name.EndsWith("Tests") and then its public static parameterless void methods. Mirroring that filter is what makes the gate's claim meaningful: the mesh area covers exactly what the reflection harness runs. The two must not diverge, because the harness compiles into a throwaway console app and the area runs on a real instance — a case only the harness runs has never met the framework it ships against.

Three details the rule needs, each of which was a way to pass while checking nothing:

There is no allow file, for the reason plugin-tests.allow stays empty: the measurement after this change is 177 cases defined, 177 registered, 0 excused. An allow file seeded with today's state makes today's debt permanent, and here there is none to excuse.

The control, both ways

origin/main's area  →  3 error(s), naming exactly the three cases
this change's area  →  0 error(s)
--self-test         →  refuses 6 unregistered shapes, accepts 9 legitimate ones

The self-test carries one case per way of not registering (missing from a self-hosted area, missing from a split area, named only in a doc comment, behind a //, behind a /* */) and one per legitimate shape that must not red (both hosting shapes, a // inside a string literal, a parameterised method, a private helper, a non-*Tests helper class, a public static void on the area class, an area in another module's file, a non-*Tests.cs file).

The other repos

The same comparison over every other *Tests.cs in this repo finds no further unregistered case, so this was an omission in one file rather than a pattern. The pattern risk is the gate's absence, not the omission's spread: nothing in the fleet compares an area against the file beside it, and the natural home for a fleet-wide version is the canonical harness in core, which already knows both sides. That is worth doing and is not done here.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.