Skip to content

mcpx testing: choose the generator engine for one run-wide seed (native fuzz, testing/quick, rapid, hand-rolled) #1956

Closed

Description

@dezren39

Relocated from dezren39/darwin-nix#123 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: #93

Summary

Pick the engine that turns the run seed (#121) into test inputs. Candidates: Go native fuzzing (go test -fuzz), testing/quick, pgregory.net/rapid, and hand-rolled generators on math/rand/v2. The deciding constraint is the notes' hard rule -- one seed per run for all generated inputs -- plus per-test substreams and "avoid changing how seed generates data". Findings below are from docs and source; recommendation at the end, including how native fuzz corpora fit.

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.
      • you can absolutely rely on fuzzing anywhere you can fit it and you should whenever you can-- but you must use a single seed per-run to generate all inputs. (this could eventually be discussed with careful consideration.) it should be appended into a test tracking structured file in the repo and it can be committed. ideally main branch commit's seed or pr merge seed should make it back into file eventually. to prevent churn, maybe ci appends the seed into a gha var (race/overwrite risk?), or attached as artifact to each workflow in init job before tests begin-- a collector job can scoop them up sometime later. once a day/week/on-release/every-100-commits. im thinking touch tests/seeds/ or uv has a seed generate job. anyways ci it should end up in that dir, then as an artifact if gha and main branch, then passed into the test matrix. if uv test without arg- warn, generate and write it yourself pass it to all tests, if some other testing method there should not be a static seed, throw for empty or setup should make the seed. avoid changing how seed generates data. im not sure if a convenient uuid with time, v7?, is valid, but as long as its enough entropy whatever works. either put nothing in their or compact json, consider seeds/v1/id/<first 2 char>/<--symlink: seeds/yyyy/mm/dd/ avoiding conflicts is important.

Current state

No fuzz, property, or random tests exist (735 tests at 05c78b2, none use testing.F, testing/quick, rapid or math/rand). Go 1.26.7. Zero test-only dependencies today; go.mod deps are bubbletea/bubbles/lipgloss, yaml.v3, modernc.org/sqlite (chosen, per package.nix, to avoid a C toolchain -- the project leans toward few, pure-Go deps).

Findings

Native fuzz (testing.F) testing/quick rapid v1.3.0 Hand-rolled on math/rand/v2
Seedable from our run seed No. The mutator's randomness is internal; there is no seed flag. Yes: Config.Rand *rand.Rand (math/rand v1; wrap a ChaCha8 in a v1 Source adapter). Partly: -rapid.seed / RAPID_SEED (uint64, 0 = random from maphash), process-global (data.go:21 baseSeed()); settable in-process via flag.Set("rapid.seed", ...) because it is read at check time. Yes, fully.
Per-test substream keyed by test name n/a Yes (we build the *rand.Rand per test) No. Every rapid.Check in the process starts from the same base seed and uses base + iter per case (engine.go:420). Only achievable by flag.Set before each Check, which breaks under t.Parallel. Yes
Runs under plain go test ./... Only the seed corpus (f.Add + testdata/fuzz/FuzzX/) runs, as regular tests. Actual fuzzing needs -fuzz, which must match exactly one package and one target per invocation (go help testflag). Yes Yes Yes
Structured inputs Args limited to []byte, string, bool, ints, floats; structure must be decoded from bytes reflection over Go types; poor for "valid config document" shapes Excellent (Custom, OneOf, SliceOfN, state machines via T.Repeat) Whatever we write
Shrinking Yes (-fuzzminimizetime) No Yes, automatic No (cheap substitute: draw sizes small-first)
Coverage guidance Yes No No No
Output stability across upgrades n/a Frozen package ("not accepting new features", go doc testing/quick) Tied to rapid's version: fail files record rapid's internal format version (persist.go:21) and are ignored when it differs (engine.go:376) -- rapid itself treats an upgrade as possibly changing what a bitstream generates Ours, guarded by the golden test in #121 section 3
Writes into the repo failing inputs to testdata/fuzz/FuzzX/ (wanted: they are the regression corpus) no fail files to testdata/rapid/<Test>/ unless -rapid.nofailfile no
Timing sensitivity -fuzztime fixed count cases cut short by a deadline heuristic (engine.go:413), and -short divides checks by 5 -- same seed, variable number of cases (a prefix of the same sequence) fixed count per budget
Dependency stdlib stdlib 1 module, MPL-2.0, no transitive deps stdlib

Also: rapid's failure message prints its own -rapid.seed=N, not our run seed, so the reproduce line would need translating; and rapid.MakeFuzz(prop) turns a rapid property into a native fuzz target (bytes become rapid's bitstream).

Approaches

A -- testing/quick (least recommended)

Seedable and stdlib, but frozen, reflection-driven (hard to express "a valid layered config" or "a JSON Schema"), no shrinking, and on math/rand v1. Nothing it offers is not better in B or C.

B -- rapid as the primary engine

The best off-the-shelf generator and shrinker in Go; property code is pleasant (rapid.Check(t, func(t *rapid.T){ ... Draw(t, "x") })), and MakeFuzz bridges to native fuzzing for free. Seeding works via RAPID_SEED derived from the run seed (#121 section 2), so the "one seed per run" rule is satisfiable. The costs: no per-test substream (all tests share base + iter; adding a case to one property does not shift another, but the scheme is rapid's, not ours), generation output is coupled to rapid's version (an upgrade is an implicit generator-version bump for every rapid-backed generator -- in tension with "avoid changing how seed generates data"), its deadline heuristic makes the case count timing-dependent, and it writes fail files into testdata/rapid/ unless disabled. It would also be mcpx's sixth direct Go dependency. package.nix:36-53 names and defends each of the five, and a test-only module still lands in go.mod's direct block and changes vendorHash (package.nix:54). Adopting rapid therefore means amending that comment in the same PR (addendum below). A defensible choice if shrinking turns out to matter more than stream control.

C -- hand-rolled generators on ChaCha8 behind a tiny Source interface, native fuzz for parsers (recommended)

  • internal/testsupport/gen: generators are func(src Source, size int) T where Source is just Uint64() uint64. In property mode, src is ChaCha8 keyed by stream(t, gen@version) from Update Rust crate sqlx to 0.8.0 [SECURITY] #121.
  • The same generator also serves native fuzzing: a bytesSource that reads Uint64s from the fuzzer's []byte (zeros when exhausted) lets any generator become a Fuzz* target -- the pattern rapid.MakeFuzz uses, in ~20 lines. So coverage-guided fuzzing of structured shapes is available without a second generator.
  • Native Fuzz* targets for byte-oriented parsers where coverage guidance pays: config.StripJSONC (internal/config/config.go:718), codegen.TypeFor over raw schema bytes (internal/codegen/ts.go:70), launcher.Expand templates (internal/launcher/template.go:80), settings.ParseBytes (internal/settings/value.go:415). Their seed corpora (f.Add + testdata/fuzz/FuzzX/) run in every go test as deterministic regression tests; the minimized failing input from a seeded failure is committed there too. Long -fuzz sessions are a scheduled/soak job (one target per invocation) owned by Bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /sources/launcher #95's budget, not part of the per-run seed rule -- they are explicitly outside "one seed per run", which the notes' "(this could eventually be discussed with careful consideration.)" leaves room for.
  • Shrinking substitute: generators take a size that the runner grows from small to large across a test's budget, so the first failure is usually small; revisit rapid if that is not enough.
  • Why C: it is the only option that satisfies all three hard requirements (one run seed, per-test substreams, versioned stable output) without fighting the library; it is stdlib-only; and it keeps a clean upgrade path to B (rapid can later be driven by RAPID_SEED from the same root).

Open questions

Ease · Impact · Related

Ease S (research is done here; the choice gates #124/#125) · Impact high (fixes the shape of every generator)
Depends on / related: #121 (seed, derivation, golden guard), #124 (generator registry), #125 (first generator), #126; #95 (soak -fuzz job and budget), #99 (policy on committing fuzz corpora).

Security/auth deferred; see tracker.

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#123 (verbatim, oldest first):


    @dezren39 · 2026-09-30T06:44:04Z

    Addendum from a second pass over the same slice (a duplicate fuzz worker ran concurrently; this issue got there first and is the one to keep). Three facts that are not in the body and that strengthen its recommendation C.

    rapid would be mcpx's sixth direct Go dependency, and this repo has a written policy about that

    The Findings table costs rapid as "1 module, MPL-2.0, no transitive deps". That undercounts it. pkgs/mcpx/package.nix:36-53 opens with a comment block headed:

    # Five direct Go dependencies.

    and then spends seventeen lines defending each one individually — modernc.org/sqlite is the pure-Go translation specifically so the derivation needs no C toolchain and cross-compilation still works; bubbletea/bubbles/lipgloss are costed at 1.2 MB in the stripped binary and justified as replacing terminal-handling code "that is never finished and never correct on every terminal"; yaml.v3 exists because OpenAPI documents are published as YAML as often as JSON. It closes:

    # Everything else -- the MCP client, the process pools, the JSON Schema to
    # TypeScript compiler, the CLI -- is standard library.

    go.mod matches: exactly five entries in the direct require block.

    "Test-only dependency" is not an escape hatch from that policy:

    • Test files are part of the package, so rapid lands in go.mod's direct require block, not the indirect one. The comment's count becomes wrong the day it is added.
    • It lands in the vendored module tree, so it changes vendorHash (package.nix:54).
    • It therefore becomes one more thing Renovate bumps — and the vendorHash-repair step in .github/workflows/mcpx.yml exists precisely because dependency bumps break the nix build and merged silently until someone rebuilt.

    So adopting rapid means editing that comment block from "Five" to "Six" and writing rapid a paragraph as good as sqlite's. That is a fair price for a genuine need; it is not free, and it should be stated in Approach B rather than discovered in review.

    None of this changes the recommendation — it makes C stronger, and it sharpens the Open question "Is shrinking worth a dependency?" into a concrete deliverable: if rapid is adopted, the PR must also amend package.nix:36-53. Worth adding that as an acceptance criterion on whichever issue eventually adopts it.

    F.Fuzz's accepted argument types, for the record

    The Findings table says "args limited to []byte, string, bool, ints, floats". The exact list, since it bounds which parsers can be native fuzz targets without a byte-decoder:

    The following types are allowed: []byte, string, bool, byte, rune, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64. More types may be supported in the future.
    — go doc testing.F.Fuzz

    This is why the bytesSource bridge in Approach C is load-bearing rather than a convenience: it is the only way a structured generator reaches coverage-guided fuzzing without a second generator.

    The free half of native fuzzing, stated explicitly

    Approach C mentions it; it is worth making unmissable because it is the cheapest thing in this tracker. With -fuzz unset, a Fuzz* target still runs against its seed corpus as an ordinary test — F.Add entries plus every file in testdata/fuzz/<Name>/ (go doc testing, lines 261-264). And go test -fuzz writes failing inputs into that same directory automatically (lines 254-258).

    So a committed corpus is a regression suite that runs on every CI run with zero bookkeeping, and the soak job files its own regressions. That is #99's "one test per bug" delivered mechanically for the byte-shaped targets, which is a much stronger argument for native fuzz targets than coverage guidance alone.


    @dezren39 · 2026-09-30T07:20:50Z

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


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

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

    • Re-counted: 735 tests, still no testing.F, testing/quick, rapid or math/rand. Re-cited settings.ParseBytes to internal/settings/value.go:415 (it was :375).
    • Folded the addendum's cost into Approach B: rapid would be the sixth direct Go dependency, against the "Five direct Go dependencies" comment at package.nix:36-53, and it changes vendorHash (:54). The addendum asked for this to be "stated in Approach B rather than discovered in review".
    • The recommendation (C) is unchanged.
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off (input from #1896 triage, task 2790)

    One fact for this issue's engine decision, found while triaging the parent tracker #1896 against current main (8124cc0b7005, 2026-10-29):

    The "sixth direct dependency" framing in #1896's body is stale. pkgs/mcpx/package.nix (lines 36–53) still opens with "Five direct Go dependencies" and narrates five (modernc.org/sqlite, bubbletea, bubbles, lipgloss, yaml.v3), but pkgs/mcpx/go.mod now lists seven direct requires — the five plus github.com/santhosh-tekuri/jsonschema/v6 and github.com/tetratelabs/wazero. So adopting pgregory.net/rapid would be direct dependency eight, and the package.nix comment needs a recount paragraph whichever engine is chosen. Verification for any dependency change runs in CI (go test ./... in pkgs/mcpx via mcpx.yml + the nix build, which pins vendorHash).

    Full triage verdict: #1896 (triage comment by repo-worker-code, task 2790).

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

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code), lane C shift
    • Session / ID: queue task 2856
    • Found via: queue claim
    • Task: Triage relocated mcpx generator-engine choice issue: dedupe vs existing code issues, confirm scope against current pkgs/mcpx main, enqueue implementation follow-up. No implementation in this task.
    • Expectation: s — done = triage verdict posted, follow-up packet enqueued, labels truthful.
    • Links: queue task 2856
  6. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Triage verdict: CONFIRMED — recommendation C stands; implementation follow-up enqueued

    Dedupe (checked 2026-10-29): No duplicate. Searches for "generator engine", "rapid", and "testsupport gen ChaCha8" surface no other issue covering the engine choice or a testsupport/gen package. #1958 (bug-shape generator registry) consumes this engine; #1949 supplies the seed/derivation it keys from; #1896 is the parent. This issue is the sole owner of the engine decision.

    Scope confirmed against current pkgs/mcpx main (fresh checks 2026-10-29):

    • go.mod carries exactly the five direct deps the issue names (bubbles, bubbletea, lipgloss, yaml.v3, modernc.org/sqlite) — the "rapid would be a sixth direct dep + package.nix amendment" cost is real.
    • internal/testsupport does not exist (404); code search finds no testing.F and no generator use of math/rand in pkgs/mcpx. The only math/rand/v2 uses are time.Sleep(rand.N(40ms)) jitter at internal/e2e/keepalive_takeover_test.go:253 and internal/daemon/handoff_conns_test.go:289 — not input generation, so the issue's "no fuzz/property/random generation" claim holds in substance.
    • All four native-fuzz target sites named in the issue exist on main: internal/config/config.go, internal/codegen/ts.go, internal/launcher/template.go, internal/settings/value.go.

    Decision carried forward: Approach C (hand-rolled generators on ChaCha8 behind a Source interface + native fuzz for parsers) is the only candidate meeting all three hard requirements — one run seed, per-test substreams, versioned stable output — and it is stdlib-only. The follow-up (task 3251) is deliberately scoped to the core: internal/testsupport/gen (Source, ChaCha8 stream keyed per #1949 derivation, bytesSource fuzz adapter, size-growing runner), the golden stability guard, and ONE proof target (FuzzStripJSONC). The remaining three parser targets, the registry (#1958), and the -fuzz soak job stay as named follow-on work on this issue; rapid adoption stays deferred per the issue's own open question (revisit after ~5 generators exist).

    This issue stays open as the tracking issue for task 3251 and the follow-on targets.

  7. 23 remaining items

  8. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    work/assignedQueue worker lifecycle state
    on Oct 30, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code), OpenChamber scheduled lane
    • Session / ID: queue task 7804 (triage), lane 9aca43b4-5439-4939-9f71-323f5d32521b
    • Found via: queue claim
    • Task: Coverage-pass triage of the engine-choice decision issue against current main: confirm whether the decision is still open, and either scope the remaining fix or close with evidence.
    • Expectation: s — done = verdict posted, residual work owned by a live task/issue, labels truthful.
    • Links: queue task 7804 · issue mcpx testing: choose the generator engine for one run-wide seed (native fuzz, testing/quick, rapid, hand-rolled) #1956
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage verdict)

    Triage verdict: DECISION LANDED — close; residuals owned by #1949, #1985, #1960, #1896

    Checked against origin/main 1e3763faf4e3 (2026-11-03) with fresh reads, no cached state:

    1. The engine choice is implemented, not merely recorded. PR feat(mcpx): testsupport/gen core (ChaCha8 streams) + FuzzStripJSONC proof target (#1956) #2636 (merged 2026-10-30T09:12:57Z, merge sha c63e5af54796033dc25e236be391fea0a100e936) is an ancestor of main, and pkgs/mcpx/internal/testsupport/gen/ exists on main: gen.go, generators.go, registry.go, plus gen_test.go and registry_test.go. gen.go's package doc names Approach C and states the derivation. feat(mcpx): testsupport/gen core (ChaCha8 streams) + FuzzStripJSONC proof target (#1956) #2636 touched only internal/testsupport/gen/{gen.go,gen_test.go} and the proof target — no go.mod/go.sum change, i.e. the choice cost zero new direct dependencies, as Approach C claimed.
    2. The derivation in code is the mcpx testing: one seed per run -- format (UUIDv7), derivation, and how it reaches every test #1949 §2 v1 one. Root() = SHA-256("mcpx-test-seed/v1\x00" || seedID); StreamKey() = SHA-256(root || 0x00 || "g@v" || 0x00 || testName) → ChaCha8 key; Stream/ForTest hand out per-test substreams. So "one run-wide seed, per-test substreams keyed by test name, version-stable output" holds by construction, not by convention.
    3. The registry follow-on landed separately: mcpx testing: bug-shape generators -- registry, versioning, budget, first shapes #1958 is closed and registry.go/registry_test.go are on main.
    4. The proof target landed and its corpus is on main: internal/config/stripjsonc_fuzz_test.go:19 (FuzzStripJSONC, drawing through testsupport/gen) with five committed seeds under internal/config/testdata/fuzz/FuzzStripJSONC/; the corpus runs as ordinary tests under plain go test ./....
    5. The three further targets named in Approach C do not exist yet — grep "func Fuzz" pkgs/mcpx returns only FuzzStripJSONC. Targets: codegen.TypeFor (internal/codegen/ts.go:88), launcher.Expand (internal/launcher/template.go:82), settings.ParseBytes (internal/settings/value.go:457). They have no issue of their own; mcpx testing: review all tests, set up the fuzz framework and its policies #1960 (fuzz framework and policies, open) is their natural owner, and I recorded them there.
    6. Everything else in this issue's blast radius is owned elsewhere: seed format/plumbing mcpx testing: one seed per run -- format (UUIDv7), derivation, and how it reaches every test #1949 (open), CI workflow skeleton mcpx testing: CI workflow skeleton -- init, matrix, final job; triggers, concurrency, caching, checkout #1985 (open), parent tracker mcpx testing: fuzzing and property tests with one seed per run #1896 (open, queued). Nothing in this issue is unowned.

    Correction to the dependency-policy framing (this body and the 2026-10-29 note)

    pkgs/mcpx/go.mod today has eleven direct requires — bubbles, bubbletea, lipgloss, mattn/go-runewidth, santhosh-tekuri/jsonschema/v6, tetratelabs/wazero, golang.org/x/net, golang.org/x/sys, yaml.v3, modernc.org/sqlite, tailscale.com — on Go 1.27.2. So rapid would have been direct dependency #12, not #6/#7/#8; the "sixth dependency" cost argument is stale in the same direction it has been stale twice already. pkgs/mcpx/package.nix:50 still opens # Five direct Go dependencies. — a standing doc defect independent of this decision (whichever PR next touches package.nix should recount, or the count should be derived). It did not need fixing here: Approach C shipped stdlib-only.

    Disposition

    Closing as completed: the question this issue asks — which engine turns the run seed into test inputs — is answered (Approach C: hand-rolled generators on ChaCha8 behind a Source interface, stdlib only, native testing.F for byte-oriented parsers) and the answer is merged. Keeping it open only re-feeds coverage passes, which is why task 7804 exists.

    Residual work, by owner: #1949 (seed id/plumbing) · #1985 (CI workflow skeleton, soak -fuzz budget) · #1960 (fuzz framework, policies, the three remaining targets) · #1896 (parent tracker).

  11. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  12. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/none). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

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