AGENTS.md
August 25, 2026 · View on GitHub
Resource/fault/lifecycle verifier for nginx and angie modules. NOT a second
request/response suite: ordinary status/header/body behaviour belongs in
nginx.pm. What this harness uniquely proves is pool, FD, slab, worker,
upstream-journal and allocation-failure evidence across signals, reloads and
faults.
MISSION — this is an ADVERSARIAL tool. Its purpose is to break the module
under test, by any means available. Hostile and malformed input, allocation-
failure injection (fault_slab=/fault_palloc=/fault_tempfile=/
fault_accept=), resource exhaustion, lifecycle attacks (reload, binary
upgrade, worker death, signal storms), environmental hostility (clock jumps,
locale). If you think of another way to break a module, it belongs here — see
README "Ideas and opportunities" for the ones already identified and not yet
built. Coverage goal is 100% of the code under test, understood as a
direction of travel, not a promise: chase the uncovered lines, never the
percentage, and there is deliberately NO coverage-% merge gate (moving that
number with non-asserting tests is the vacuous-gate failure this repo hunts).
ci/prober/coverage-director.sh is the reachability generator — use it to find
what is NOT reached.
SCOPE (a narrower question — what THIS repo's own CI spends budget on). The
mission above is what the tool does to a consumer's module; the rule below is
what our CI proves about our own code. We develop a probe TOOL for nginx, not an
nginx MODULE. The thing
under test is our own code: the prober binary, its rule parser, the probe's JSON
emitter, the shell plumbing. nginx is the fixture, never the subject. So: the
parser runs under ASan/UBSan deterministically on every PR (fuzz-replay job);
nginx and the main prober binary do not (decided 2026-07-28, implemented 2026-07-30).
SAN=1 ci/prober/test.sh remains available for one-off local runs when a parser bug
is suspected. No valgrind on nginx, no whole-server fuzzing, no nginx benchmarking.
Fuzzing our OWN parser stays in scope. Do not add a sanitizer job back without the
user asking for it. Full rationale + the known cost → README "What this repo is — and
what it is not".
DO NOT ADD A TEST HERE THAT THE PERL SUITE CAN ALREADY DO. A consuming
module has Test::Nginx::Socket and its own .t files, and that is the right
home for ordinary request/response behaviour — status, headers, body, rewrites,
error_page, config permutations. This harness is for what the Perl suite
structurally CANNOT assert: the worker's insides before and after, i.e.
cycle-pool bytes, descriptor counts, slab pages, allocation neutrality across a
request or a reload. Test to apply to any proposed scenario: could it be
written as a .t file with no probe snapshot? Then write it as a .t file and
reject it here. Duplicating the Perl suite is not free — a second suite
asserting the same thing is another place to update on every behaviour change,
and it dilutes the signal this one exists to give.
Superrepo root is /opt/myguard — see /opt/myguard/AGENTS.md.
Working notes for this repo live OUTSIDE it, at
/opt/myguard/memory/labs/nginx-module-testkit/ (read HANDOFF.md first, then
TODO.md, issues.md, lessons.md).
Layout
ci/prober/— the C prober + its shell library.lib.shis the shared harness (boot, render, probe, slope, teardown);rules.cparses the.ruleDSL;http.cowns wire timing and framing. All three are single-writer files — never two branches open against one of them.ci/prober/scenarios/<name>/— one scenario per directory:nginx.conftemplate,*.rule, optionalenv,driver.sh,requires(a gate that SKIPs rather than fails when a prerequisite is absent), optionalbackendscript.src/— the reference probe module (ngx_test_probe*.{c,h}) compiled into a.sothe scenarios load.t/module— the minimal consumer module. Deliberately tiny.ci/prober/impact.map— the reverse-impact DB: which targets a changed file must run. A changed executable source that maps to NO target fails closed.
Build and test
cd prober && ./build.sh # the prober + its unit tests
ci/prober/test.sh # full local gate, 28 suites
# Optional local tools (not wired into CI):
ci/prober/verify-impact --explain # what a diff selects (omit --base for the working tree)
ci/prober/pr-impact --budget 90 # actually run the selected fast lane
Repo-root shellcheck, exactly as CI runs it — must exit 0:
shellcheck -x --source-path=prober ci/prober/*.sh ci/prober/verify-impact \
ci/prober/pr-impact ci/prober/pr-memcheck ci/prober/scenarios/*/driver.sh
A bare shellcheck without -x cannot follow . ./lib.sh and reports spurious
SC2034 on variables the library consumes. Use the form above.
House rules that bite
- Every gate must be mutation-proven. A new test guards nothing until its control fails BY THAT ASSERTION. If a defect has no honest failing test, say so in the test's own comment rather than shipping a green assertion that proves nothing — a vacuous gate is worse than no gate, and this repo has shipped several by accident.
set -euo pipefaileverywhere. An unguarded$(cmd)whose non-match is legitimate will kill a TAP suite mid-stream, which reads as a pass to anything countingnot ok. Guard with|| true.- Scenario port allocation:
test-scenarios.shruns at core count by default (PROBER_SCENARIO_JOBS), assigning each slot a unique port offset from the base (PROBER_SCENARIO_PORT_BASE, default 18120):PROBER_PORT=$((PORT_BASE + slot)). Single-scenario runs use a hardcoded default of 18099 (PROBER_PORT); to avoid collisions in concurrent CI on a shared box, either setPROBER_PORT=<free>for single runs or setPROBER_SCENARIO_PORT_BASE=<free_base>andPROBER_SCENARIO_JOBS=<count>for multi-scenario runs. - A stale reference
.sofakes flavor-specific failures. Check its mtime againstsrc/ngx_test_probe.cbefore believing any local scenario red. - Own nginx modules are BSD-2-Clause.
Reviewing a diff here
Read the changed hunks against lib.sh's existing idiom, not in isolation. The
recurring defect classes, in rough order of how often they have actually bitten:
vacuous gates (an assertion that cannot fail), fail-open guards (a check whose
removal breaks no test), arithmetic that truncates or wraps at a boundary, and
oracles that overshoot the boundary they claim to pin by orders of magnitude.
Do not run the deterministic lint pre-pass from this directory: it lives at
/opt/myguard/.claude/skills/_shared/review-lint.sh and a relative path fails.