Appendix A - 12. Test-serializer (N=1 flock on `dotnet test`)
The Structure of Test-serializer (N=1 flock on dotnet test) — its shape at a glance:
The serializer holds one exclusive host lock around the run, and an enforcer inside the test process makes the un-mediated path impossible — the ban is what makes the single-writer real.
flowchart LR
W1[Worktree A] --> Lock{{Host lock, N=1}}
W2[Worktree B] --> Lock
Lock -->|acquired| Run([Test runner runs])
Lock -->|held| Queue([Wait, fail loud at cap])
Raw[/Raw un-mediated call/] -.->|enforcer refuses| Blocked([Refused])
Accessible description: worktrees contend for one exclusive host lock around the test runner; the holder runs while others queue and fail loud at a wait cap, and an in-process enforcer refuses any raw un-mediated invocation so serialization is structural, not a convention.
Projected from the catalogue entry mediators-and-resource-locks / Test-serializer (N=1 flock on dotnet test).
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — A host-level wrapper that serializes dotnet test to a single writer via an exclusive flock, so concurrent agent worktrees on one machine don't saturate I/O and interfere with each other's test runs.
Motivation
Several worktrees each running dotnet test on one host saturate CPU and disk and interfere (port contention, shared build artifacts, I/O thrash), so tests flake or hang for reasons that have nothing to do with the code under test. The failure is false-flaky tests plus wall-clock blowup, and it recurs whenever two or more agents test at once, which under a fleet is most of the time. Worse, a false flake sends an agent chasing a non-bug.
Applicability
- A host-global lock file every worktree contends on (the single point of serialization).
- An enforcer that can intercept the raw tool (here a
[ModuleInitializer]that runs before any test), so the mediated path is the only path. - A wait cap that fails loud, so a stuck lock surfaces instead of hanging forever.
Structure
The Structure diagram appears at the top of this page.
Sample Code
An exclusive flock gives N=1, and — decisively — an enforcer that runs before any test refuses a raw invocation from an agent worktree, so the mediated path is the only path. The ban makes the serialization real rather than a convention agents forget under time pressure.
import fcntl, sys
def serialized_test(lock_path: str, run_tests, wait_cap_s=1800):
with open(lock_path, "w") as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX) # N=1: exclusive, one writer per host
except OSError:
sys.exit("test lock stuck past the wait cap — failing loud instead of hanging")
return run_tests()
def enforce_no_raw_test(cwd: str, mediated: bool) -> None:
# runs before any test executes; a raw runner from an agent worktree is refused
if is_agent_worktree(cwd) and not mediated:
raise SystemExit("raw test runner is banned from an agent worktree — route through the serializer")
Consequences
- Serialization is wall-clock cost. N=1 means tests queue; a long run blocks every other worktree behind it. This is the deliberate trade — correctness of results over raw parallelism.
- A stuck lock stalls everyone. The 30-minute fail-loud cap bounds the damage but does not eliminate it.
- The bypass is a hole.
ADA_TOOL_TEST_BYPASS_MEDIATOR=1exists for humans; misused, it reintroduces the contention. - Single-host assumption. The lock coordinates one machine; it does nothing across hosts.
Example use within DocAble
- The test serializer: the N=1 flock wrapper (+ auto-coverage for fuzz filters).
TestMediatorEnforcer, a[ModuleInitializer]that refuses rawdotnet testfrom agent CWDs.ADA_TOOL_TEST_BYPASS_MEDIATOR=1, the audited human escape.
Related Patterns
- Counterpart — the
[ModuleInitializer]enforcer (hard) holds the serializer's discipline in place: without the ban, the flock is just an unenforced convention. - See also (sibling) — build-serializer: the same mediator pattern at M=8 instead of N=1. The pair illustrates the lock-cardinality choice (below).
- Layer — with aggregate-compute-protection, the three form the host-compute rationing tier.