Skip to content

mcpx testing: guard tests that close a whole bug class #1971

Open

Description

@dezren39

Relocated from dezren39/darwin-nix#129 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #99

Summary

When a bug is one instance of a class -- a declaration and its use live apart, and the same shape has shipped before -- the fix adds a guard test that fails for every member of the class, present and future, not just the instance found. mcpx already has more of these than the strategy doc lists; this issue inventories them, finds where the existing ones leak, picks how guards are written, and lists the guards worth adding next. It also sets the rule for when a bug "is a class". It owns #181 collision 15: a setting must be able to change behaviour, not merely be read. That guard is due before or with #172.

Original notes

  • testing
    • agents should make at least 1 simple, new test for every bug observed from any source, when cheap to do so-- aggresively test many permutations.

(Guards are the per-class extension of "at least 1 simple, new test for every bug": the per-instance policy is #128.)

Current state: guards that exist at 05c78b2

There are about twenty-five, not three. The ones that matter here, by mechanism:

Guard Class it closes Mechanism Tier
TestEveryExecFlagChangesSomething (internal/e2e/e2e_test.go:2877, from #74/#75) a flag accepted and ignored hand table + runtime probe (with vs without) e2e
TestEveryDeclaredRouteAnswersOverTheSocket (internal/e2e/v1ops_test.go:287) a declared /v1 op that 404s registry enumeration (api.Ops()) + runtime probe e2e
TestEveryDaemonRouteIsDeclared, TestEveryDeclaredOpHasARoute, TestEveryNonStreamingOpIsReachableAsAnMCPTool (internal/api/parity_test.go:54,73,87) route/op/MCP-tool drift source scan of internal/daemon/*.go + registry enumeration unit
TestNoPackageRedeclaresADefault (internal/defaults/guard_test.go:92) a constant outside defaults.json regex source scan with a reasoned allowlist and an outOfScope list unit
TestEverySettingIsReachableFromAllThreeSurfaces, TestEveryDefaultSurvivesItsOwnValidator, TestEverySettingSaysWhoReadsIt (internal/settings/registry_test.go:27,41,287) settings registry consistency registry enumeration unit
TestEverySettingIsReadSomewhere, TestNoHandRolledSettingEnv, TestThePluginReadsNoUndeclaredSetting (internal/settings/consumed_test.go:73,145,209, from #171) a setting declared and never read; a registry-owned variable read by hand; the plugin honouring an undeclared variable registry enumeration + regex source scan for a Set accessor call outside the settings package (accessorRead/readViaHelper, :54,60), one-entry allowlist with a reason (paths.config, :66) unit
TestTheDocumentationListsEverySetting (internal/settings/docs_test.go:19) docs drift registry vs docs/configuration.md unit
TestEverySettingIsReachableOverV1 (internal/e2e/settings_test.go:173), TestEverySettingTheSchemaAdvertisesActuallyWorks (e2e_test.go:1879) registry vs served surfaces runtime probe e2e
Nine effect tests in internal/e2e/unread_test.go:48-184 (from #171), e.g. TestPoolDefaultsAreReachableFromTheEnvironment, TestScriptEnvIsReachableFromTheEnvironment a wired setting that changes nothing hand-picked runtime probes: set the value as a user would, assert an observable change e2e
TestNoOrphanProcessesAfterStop (e2e_test.go:534) MCP children surviving stop runtime probe e2e
TestEnvCleanupStopsTheDaemonItStarted (e2e_test.go:2630, from #182) a test env's daemon surviving its cleanup (#52) runtime probe; falsified by reverting the fix e2e
TestEveryTuiViewProducesRowsFromRealData, TestEveryConsoleMethodIsCoveredAndNameIsKept, TestEveryModernRequestCarriesVersionAndCapabilities, ... various mixed mixed

The "declared but never read" audit landed as #171. Its inverse ("read but never declared", 44 MCPX_* variables) is #172 and is in flight. TestEverySettingSaysWhoReadsIt checks only that a setting declares a scope; TestEverySettingIsReadSomewhere checks that something reads it. Nothing checks that reading it can change anything (leak 5 below).

Where the existing guards leak

  1. TestEveryExecFlagChangesSomething does not enumerate the flags. It has four rows (--export, --launcher, --prefix, --typecheck) and a comment "Add a row when you add a flag". The exec/run flag set has 23 flags (internal/cli/commands.go:435-481), including three that Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75 fixed (--no-launcher, --allow-repeat, --no-capture-console) with no row. A new flag with no row passes. The class is not closed; it relies on memory, the thing the guard exists to replace. Its probe half is the model to copy: each row asserts the outcome with the flag and its absence without, and that half caught Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75's own first --typecheck assertion, which matched output present either way.
  2. Regex source scanning misses spellings. PR chore(deps): bump rustix from 0.37.19 to 0.37.25 in /sources/pavex/examples/realworld #56: TestNoPackageRedeclaresADefault matched "5 * time.Minute" while gofmt writes 5*time.Minute in call arguments, so nine live constants were invisible to it. A guard that silently passes is worse than none; it was found by accident.
  3. Allowlists are one-way doors. outOfScope in guard_test.go still exempts seven whole packages and one file (guard_test.go:86-90) "owned by work happening in parallel"; nothing makes it shrink.
  4. Nothing fails when a test reaches the network (Update Rust crate axum to 0.6.20 #65 fixed the one instance via MCPX_REGISTRY_URL, e2e_test.go:57). chore(deps): bump build/buck/preludes/buck2-prelude from d956689 to e7457f6 #52 is fixed (Phase 5: tag event bodies must travel with the event #182): cleanup is stop --all with the error logged, not discarded (e2e_test.go:71-85), and TestEnvCleanupStopsTheDaemonItStarted checks one env's daemon dies. Still unguarded: a daemon started outside newEnv, and testsupport's mcpx-bin-*/mcpx-fake-* dirs, which are never removed (internal/testsupport/build.go:27,57).
  5. consumed_test.go proves a setting is read, not that reading it matters. registry.pageSize is read (internal/cli/registry.go:265, internal/daemon/ops.go:285) but consulted only inside if limit <= 0 (internal/registry/registry.go:154-155). Every ordinary caller passes a positive limit: --limit defaults to 20 (internal/cli/registry.go:26), and the daemon falls back to registry.limit (internal/daemon/ops.go:275-276). So it passed Phase 2: delegated Manage cannot add members #171's sweep while inert (Phase 3: /id-sync/1 is used by both range and LogSync handshakes #179). The scan sees a read; it cannot see that the read is unreachable. unread_test.go has the right shape, a with/without probe, but covers nine hand-picked settings out of 149. This is Phase 3: sync, blob gate, and relays #181 collision 15, owned here (next section).
  6. A guard's name can overclaim. TestEverySettingIsReachableFromAllThreeSurfaces (internal/settings/registry_test.go:27-39) checks that a flag name exists, that the variable is MCPX_-prefixed and that the setting is documented. That is name shape, not reachability. The e2e TestEverySettingIsReachableOverV1 is the one that probes.
  7. An allowlist entry's reason is prose nothing checks, and one is false. consumed_test.go:67-70 exempts paths.config as "read by config.SearchPath from MCPX_PATHS_CONFIG directly" (repeated in docs/declared-but-unread.md:93-96). Nothing reads MCPX_PATHS_CONFIG; the search path reads MCPX_CONFIG (internal/config/config.go:549). The exemption hid a setting that is dead on all three channels (Bump build/buck/preludes/buck2-prelude from 3a02ea3 to 370cd4d #5). An allowlist reason that names a mechanism should be a mechanism a test can check (the variable is read somewhere, the function exists), or say "declared, not read" plainly.

Owns #181 collision 15: a setting must be able to change behaviour

Owns #181 collision 15: the stronger settings guard. Every registry setting must be able to change an observable outcome through an ordinary caller, not merely be read. #179 (registry.pageSize) is the instance; this issue owns the class.

When a bug "is a class"

Add a guard (in the fix PR, or file testing + implement and link it) when any of these holds:

  • the same shape has shipped before (Update Rust crate petgraph to 0.6.4 - autoclosed #74 was the third "accepted and ignored");
  • the bug is drift between a declaration and its use (registry vs reader, op table vs route, flag vs options struct, docs vs code);
  • the fix edits a table, registry or allowlist;
  • the bug depended on the environment the test ran in (network, OS, leftover processes).

Approaches: how guards are written

A -- source scanning with regexes (least recommended)

Walk .go files and match text, as TestNoPackageRedeclaresADefault, daemonRoutes and #171's consumed_test.go do. Quick to write and needs no registry. Risks: formatting changes the text (the gofmt miss above), comments and strings produce false hits, renames produce false passes, a match proves a call exists but not that it is reachable (leak 5), and failures read as "line 212 matched a pattern" rather than "setting X is never read". Keep only where nothing better exists, and if so parse with go/parser/go/ast (or golang.org/x/tools/go/packages for type info) rather than regexes.

B -- enumerate the registry (reflection over data)

Iterate settings.Registry(), api.Ops(), flag.FlagSet.VisitAll, the command table, and assert properties per entry. Unit-speed, exact, cannot be fooled by spelling -- most of the table above is this. Limitation: it proves declarations are consistent, not that they work; a registry entry nothing reads passes every B guard, and so does one that is read but cannot matter (registry.pageSize).

C -- enumerate, require a probe per entry, prove the guard can fail (recommended)

  1. Enumerate the registry (B) as the source of truth.
  2. Completeness: a unit test fails when an entry has neither a probe row nor an exemption with a reason (the allowed map style in guard_test.go). Fixes leak 1: a new flag without a row fails in milliseconds, locally, at T1/T3 of Update Rust crate tracing to v0.1.40 [SECURITY] #127.
  3. Probe: each row proves an observable effect in both directions (the Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75 with/without pattern); probes that need the binary live in e2e and run in CI.
  4. Ratchet: exemption lists may only shrink -- a test pins their length, or CI compares it to main.
  5. Negative test: every guard's checker is a function with its own test that feeds it a planted violation and expects a failure (fixes leak 2's lesson: a guard that cannot fail is not tested).
  6. Use AST scanning (not regex) only for classes with no registry. (Phase 2: delegated Manage cannot add members #171's "declared but never read" guard used a regex scan, which is enough to find a read but, as leak 5 shows, not to prove it matters; step 3's probe is what closes that.)

Recommended because it closes the gap the current exec guard has (enumeration), keeps the cheap half (completeness) in unit time so agents actually run it, and makes the guards themselves falsifiable.

Candidate new guards

Guard Class Mechanism (per C) Owner / coordination
Every setting can change an observable outcome read but inert (#179, registry.pageSize) settings.Registry() vs a probe table; completeness at unit speed; with/without probes in e2e; ratcheted exemptions; planted-inert negative test here -- #181 collision 15; first implement task, before or with #172
Every exec/run flag has a row or an exemption accepted and ignored (#74) VisitAll over the exec flag set vs the table here
Every command's flags, not just exec same, other commands command table enumeration; rows per command here
Every setting in the registry is read by something declared, never read done in #171 (consumed_test.go, regex scan for a Set accessor read) superseded by the row above for "read but inert"
No two concurrent runs share a generated path #69, #70, #73 (three instances in three PRs) property over the runner's naming function (a content hash of every input that changes the program), plus a concurrent e2e probe per artifact kind here; candidate
Every flag mcpx passes to a runtime has the effect it is passed for an inert subprocess flag (#108) An acceptance check ("the runtime exits 0 with this argv") would not have caught #108: Node 24 accepts --experimental-strip-types and it is simply inert. Use a pinned capability matrix instead: real argv against a fixture per TypeScript construct per runtime, failing when the matrix changes, so an inert flag shows up as the Node column losing its enum rows. Keep the cheap exit-0 check as a first half, since it still catches a typo or an upgrade that drops a flag. Deno's permission flags (--allow-*, --no-check) are the same exposure: no test file mentions them here; #108 (in flight) owns the fix, #100 the runtime matrix
Every documented capability exists a documented capability that does not exist (#102, #106) Split the ask. Mechanical where a registry is behind it: #102's tool list vs the generated client, #106's CLI vs /v1/exec resolution; those should not wait. Prose gets a marker table, which is probably #132's feature inventory with one more column (build it once). Prose-linting is rejected outright: there is no honest mechanism for it here + #132 (#97)
Every CLI flag has non-empty usage and appears in mcpx --help/mcpx man undocumented knob enumerate flag sets vs generated man (package.nix:88-89 builds it from the binary) docs tree for the book; here for the test
Every /v1 op has a CLI command, or a listed exemption CLI parity (#76) api.Ops() vs command table; exemption list starts as #76's table and ratchets down #76
Every feature has an integration test untested feature reads #97's inventory file, checks each named test exists (go test -list) #97 owns the inventory; this issue owns only the rule that a guard reads it
No test reaches the network #65 see sub-choice below here + #95
No test leaves a process (or temp dir) behind #52 (fixed in #182 per env) TestMain in internal/e2e: after m.Run(), find processes whose argv contains this run's temp prefix, kill them and fail; remove mcpx-bin-*/mcpx-fake-*. #182 already stopped discarding the stop error and added the per-env test here (suite-wide backstop)
Quarantined tests have an issue link and an unexpired date permanent quarantine scan for the quarantine helper #130

Sub-choice: enforcing "no network"

  • Env only (weakest): set HTTP_PROXY/HTTPS_PROXY to a dead port (http://127.0.0.1:9) in newEnv and a unit TestMain. Catches Go HTTP clients that honour proxy env (including the spawned mcpx binary); misses raw dials and anything in NO_PROXY.
  • In-process dialer guard: replace http.DefaultTransport's DialContext in TestMain to refuse non-loopback. Precise for unit tests; cannot reach the mcpx subprocess or the JS runtimes.
  • No network in the CI test step (recommended, plus the env layer locally): run tests in a network namespace (unshare -rn / systemd-run --property=PrivateNetwork=yes / a container with --network none) after go mod download. Catches everything, including deno/bun/node. Linux-only, so it lives in CI (Bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /sources/launcher #95); macOS keeps the env layer.

Tasks

Open questions

  • Should the completeness half of an e2e guard be a separate unit test (runs at T1), or stay inside the e2e test (runs only in CI)? Proposed: separate.
  • Does the "declared but never read" audit produce a registry-level guard (B/C) or an AST scan (A with go/ast)? Answered by Phase 2: delegated Manage cannot add members #171: a regex scan (A's mechanism, consumed_test.go:54,60) over registry enumeration. The effect guard above is the C half it lacks.
  • Which settings cannot be probed from Go at all (plugin-scoped ones read only by TypeScript, paths.config)? Do they get a TypeScript-side probe in the plugin's bun test, which CI does not run yet (Bump dezren39/nix flake input to 3499ed3 — opencode now defaults to v2 #141)?
  • Where do exemption lists live: in the test file (current style) or in a data file several guards share?

Ease · Impact · Related

Ease M (policy S; each guard S-M) · Impact high
Depends on / related: #74, #75, #76, #65, #52 (closed, fixed in #182), #56 (the defaults-guard miss), #69/#70/#73 (the shared-path class), #97 (integration inventory), #95 (no-network CI step), #93 (generators for probe inputs); #171 (merged: the read guard), #172 (in flight: its inverse; lands with or after the effect guard), #179 (in flight: the registry.pageSize instance), #181 (collision 15, owned here); siblings #127 (T3 guards list), #128, #130, #131.

Security/auth deferred; see tracker.

Acceptance criteria

  • The existing guard tests are inventoried on this issue, with where each leaks (class members it cannot catch) named.
  • The rule for when a bug "is a class" is recorded, and the house style for writing guards is pinned.
  • The Phase 3: sync, blob gate, and relays #181 collision-15 guard — a setting must be able to change behaviour, not merely be read — is landed, or scheduled before/with Phase 2: membership, delegation, groups, and server config #172 as the body requires.
  • The guards worth adding next are listed, with follow-up issues filed for any not landed here.
  • The work described in the Summary/Scope above is delivered via a branch + PR (no direct push to main); verification runs in GitHub Actions CI, not on the sandbox.
  • Evidence (CI links or guard output as applicable) is recorded on this issue before it is closed.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#129 (verbatim, oldest first):


    @dezren39 · 2026-09-30T06:51:57Z

    The in-flight "declared but never read" audit named here is now #171; its inverse ("read but never declared") is #172.


    @dezren39 · 2026-09-30T07:21:19Z

    Integration pass. Placed in the DAG rooted at #181.


    @dezren39 · 2026-09-30T11:06:11Z

    Collision ownership (issues pass, 2026-09-30, at 05c78b2). #181 listed these cross-theme collisions with no owner. Each now has one:

    #181 row Collision Owner What the owner decides
    15 the newest guard test proves a setting is read, not that reading it changes behaviour #129 #129 owns the stronger guard (a setting must be able to change an observable outcome through an ordinary caller, as #75's flag table does for exec flags). It should be applied before or with #172's 44 new declarations, not after.

    Where this issue is the owner, the decision is made here; where it is not, it adopts the owner's decision rather than making its own.


    @dezren39 · 2026-09-30T12:03:34Z

    Issues pass (2026-09-30, at 05c78b2).


    @dezren39 · 2026-09-30T12:16:50Z

    Issues pass (2026-09-30, architect follow-up). New leak 7: an allowlist entry's reason is unchecked prose, and consumed_test.go:67-70's reason for paths.config is false (nothing reads MCPX_PATHS_CONFIG; see #5, #159 item 19). Recommend reasons that name a checkable mechanism.

  3. added
    kind/testTest coverage, test infrastructure, or test-only fixes
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    Intake review

    🤖 Agent note · state: filed

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-10-28 19:19 CDT
    • Found via: monitor reconciliation (intake enrichment, protocol §5)
    • Task: intake enrichment — acceptance criteria, normalized labels, this review
    • Expectation: effort/l; done when the AC section, labels, and this note are on the issue
    • Links: n/a
  5. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  6. 64 remaining items

  7. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 4, 2026
  9. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: queue worker (repo-worker-code), lane code-z
    • Session / ID: task 8158
    • Found via: queue claim
    • Task: execution lane is opencode — handed to the opencode claimant, which launches a session bound to task 8158 for implementation of the remainder on issue mcpx testing: guard tests that close a whole bug class #1971
    • Expectation: claimant session drives task 8158 to its terminal state
    • Links: task 8158
  11. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    Additional concrete setting-effect case found in current mcpx audit

    Please include the artifacts.enabled daemon-exec path in the guard remainder. docs/audit/surface.md:58 records a reproduction with mcpx daemon --artifacts-enabled=false, but internal/daemon/routes_exec.go execSettings() rebuilds exec settings and appears to drop the daemon setting. internal/settings/effect.go exempts the artifacts setting from the effect probe, so the class guard will not catch this instance unless the exemption is removed or paired with an explicit effect test.

    Suggested regression: start the daemon with artifacts disabled, invoke daemon exec with a script that would otherwise produce an artifact, and assert the disabled mode creates/publishes no artifact while enabled mode still does. This fits #1971's existing “declared setting must change behavior” scope; no new issue is needed.

  12. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 5, 2026
  13. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor
    • Session / ID: task 8158 (repo-fleet-monitor run 2026-11-05T15:45:48Z)
    • Found via: monitor reconciliation (Duty 1 claim/label divergence repair)
    • Task: Claim evaporated: task 8158 is queued and unclaimed in repo-health-code-pilot, but this issue still carried claimed labels. Labels reset to the truth (queue/queued + status/needs-triage). Checkpoint stage at evaporation: handed_off.
    • Expectation: The next lane claim picks task 8158 up from the queued state and posts a fresh pickup note.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-insenhancementNew feature or requestkind/testTest coverage, test infrastructure, or test-only fixesmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions