Skip to content

mcpx testing: an in-process app entrypoint beside the binary suite, and a scriptable mock MCP server #1980

Open

Description

@dezren39

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

Summary

Today every e2e test runs mcpx as a subprocess. This issue proposes an in-process entrypoint alongside it: move the dispatch in cmd/mcpx/main.go into cli.Run(ctx, args, env, stdio) int. Then run most integration tests in-process, which is faster, gives coverage, and lets you set breakpoints. Keep a binary suite as a superset that also covers what only a real process has: signals, exit codes, argv parsing, the detached daemon, and the nix wrapper. Let the binary suite point at a prebuilt binary (the nix result, a container image), not only at a fresh go build. Separately, replace the fixed-behaviour fakemcp with a scriptable mock so each test states the server behaviour it needs.

Original notes

  • testing
    • a general goal is to ensure all features have one or more integration tests
      • the mcp servers can be specialized mocks but the app entrypoint or binary is used and config is passed in or config path set to appropriate test
      • for both binary && app entrypoint (consider binary a superset)
        • all tests should pass for nix flake & container

Current state (main d0b7e01)

Binary.

Entrypoint. main() (cmd/mcpx/main.go:52-204) does all of the following, and none of it is reachable from a test:

  • reads os.Args;
  • handles global flags inline (--json, --profile, --config, --tui, ...);
  • builds &cli.App{Version, Paths: daemon.ResolvePaths()};
  • sets the package global cli.SetPlumbingSource(app) (internal/cli/scripts.go:95);
  • dispatches through a local handlers map (main.go:119-162);
  • exits through os.Exit at five sites (main.go:74,103,179,188,202).

Process-global state that an in-process runner would share between tests:

  • daemon.ResolvePaths reads os.Getenv (internal/daemon/paths.go:34-52);
  • settings.Environ() is os.Environ() (internal/settings/bind.go:218);
  • internal/cli non-test files reference os.Stdout/Stderr/Stdin/Exit/Getenv 86 times.

Autostart re-executes itself. EnsureDaemon spawns autostart.bin or, if that is unset, os.Executable() (internal/cli/client.go:281-287). In-process, os.Executable() is the test binary, which would be started as a "daemon". An in-process runner must set autostart.bin to the built mcpx, or use daemon.inline=true (internal/settings/registry.go:379).

Mocks.

  • fakemcp (internal/testsupport/fakemcp/main.go, 321 lines) answers from compiled-in behaviour, with two knobs: FAKEMCP_FAIL_START and FAKEMCP_START_DELAY (main.go:35,39).
  • askmcp (272 lines) exists separately for elicitation (build_proto.go:19-39).
  • A new behaviour means editing a shared binary that every test uses.

Config. Each test writes .mcpx.json into its temp dir, with FAKE replaced by the fakemcp path, and sets MCPX_CONFIG (internal/e2e/e2e_test.go:41-60). That already matches "config path set to appropriate test". "Config passed in" (--config, main.go:88-94) is a separate path that nothing tests specifically.

Coverage. e2e contributes nothing to coverage, because the binary is built without -cover. Since Go 1.20, go build -cover produces a binary that writes coverage data to $GOCOVERDIR on exit. go tool covdata percent|textfmt -i=<dir> reads it, and go test -cover ./... -args -test.gocoverdir=<dir> writes unit coverage into the same directory so the two merge. Source: https://go.dev/doc/build-cover. A process killed by a signal writes nothing, so a detached daemon killed by stop must exit through its normal path for its coverage to count.

Approaches -- entrypoint

A -- in-process only (least recommended)

Replace the subprocess with cli.Run everywhere. This is the fastest option, and it loses exactly what an integration test is for:

  • real exit codes and signal handling (signal.NotifyContext, main.go:53);
  • argv parsing as the shell delivers it;
  • the detached daemon (Setsid, client.go:312);
  • the process-group kill of runtimes (runner.go:413-432);
  • the nix wrapper's PATH;
  • environment inheritance: Update dependency htmx.org to v1.9.12 #138 is a bug that exists only in the subprocess path.

Shared process globals would also make in-process tests interfere with each other. Not recommended.

B -- binary only, instrumented

Keep today's suite and add two things: a -cover build of the binary under $GOCOVERDIR, and an override (MCPX_TEST_BINARY=/path/to/mcpx, name to be decided) so the same suite runs against result/bin/mcpx or inside a container. It needs almost no code change, gives coverage, and makes "tests pass for nix flake & container" mean the shipped artifact (#134). It does not help speed (each call still pays a fork+exec plus daemon start) or debugging (a breakpoint in a subprocess needs dlv attach).

C -- both, with the binary suite as a superset (recommended)

  1. Refactor: func Run(ctx context.Context, args []string, env Env, stdio Stdio) int in internal/cli, where Env is an environ slice or lookup function and Stdio is {In io.Reader; Out, Err io.Writer}. main() shrinks to signal setup plus os.Exit(cli.Run(ctx, os.Args[1:], os.Environ(), cli.Stdio{...})). Global-flag parsing, looksRunnable and the handlers table move with it. The table becomes an exported cli.Commands(), which Update dependency @chenglou/pretext to ^0.0.9 [SECURITY] #132's guard needs.
  2. Thread the environment in stages. Stage 1: Run takes env but internals still read os.Getenv, so in-process tests use t.Setenv and cannot use t.Parallel. Stage 2: ResolvePaths, settings.Environ and plumbingApp take their inputs from Run, so in-process tests can run in parallel. That is the real speed gain, and it pairs with the CI time budget in Bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /sources/launcher #95.
  3. Harness: newEnv gets a runner interface with binary and inproc implementations. A test says what it needs, for example e := newEnv(t, cfg, needsProcess). The binary suite runs every test (the superset). The in-process suite runs the ones that do not need a process. The daemon stays a real process in both, via autostart.bin set to the built binary. An in-process daemon in a goroutine would leak goroutines and listeners between tests, which is the in-process version of chore(deps): bump build/buck/preludes/buck2-prelude from d956689 to e7457f6 #52 (fixed for the subprocess path in Phase 5: tag event bodies must travel with the event #182).
  4. Binary override and -cover from B, so the binary suite can target the nix result or a container image and produce coverage.

Recommended because it matches the notes (binary a superset) and makes the fast path the default for day-to-day agent runs (#99). It also avoids building a second harness. Its risk is the size of the refactor. Stage 1 is small; stage 2 touches every os.Getenv in internal/cli and internal/daemon.

The user's framing is right. One refinement: "app entrypoint" should mean cli.Run, not cli.App methods called directly. Calling app.CmdExec directly skips global-flag parsing and SettingsErr (main.go:183-189), so it would test a code path users never take.

Approaches -- mock MCP servers

A -- ad-hoc mocks per test (least recommended)

Each test that needs an unusual server writes its own Go program or httptest server. It is flexible, but it multiplies binaries to build (each is a go build, about 1 s) and duplicates the protocol plumbing each time.

B -- more knobs on fakemcp

Keep adding FAKEMCP_* environment variables. This is the path today. It stays cheap until the knobs interact, and then the fake becomes a second product with its own bugs.

C -- one scriptable mock (recommended)

fakemcp reads an optional behaviour file (--script <path> or FAKEMCP_SCRIPT), which declares:

  • tools and their schemas and results;
  • delays and failures, per call number;
  • server-initiated requests (elicitation, sampling, list_changed notifications);
  • the protocol revision to claim.

With no file it behaves exactly as it does today, so the existing e2e tests (199 at d0b7e01, 209 at 05c78b2) are unchanged. askmcp folds into it later, or stays if its opposite-behaviour rationale (build_proto.go:19-22) still holds. The script sits next to the test that uses it. The same file format can drive a remote mock (#134): the fake is baked into a container and gets its script over a mounted path. A behaviour that randomises (for example "fail 1 call in N") takes the run seed (#93).

Config: keep MCPX_CONFIG as the default and add one test each for --config <path> and for .mcpx.json found by the directory walk (internal/config/config.go:549-565), so all three ways config reaches the binary are covered once. When MCPX_CONFIG is unset, the walk also reads user-level files, so the walk test needs HOME/XDG_CONFIG_HOME isolated (#138).

Open questions

Ease · Impact · Related

Ease L (stage 1 plus override plus -cover: M; stage 2 env threading: L; scriptable fake: M) · Impact high
Depends on / related: #132 (exported command table), #134 (prebuilt-binary runs per environment), #138 (env isolation is a subprocess-path property), #79 (faster CLI/exec; the same main.go code), #178 (the ldflags version the installed-output rung checks), #52 (closed, fixed in #182), #93 (seeded mock behaviour), #95 (coverage artifacts, parallel shards), #99 (which runner agents use).

Security/auth deferred; see tracker.

Acceptance criteria

  • An in-process entrypoint exists beside the binary suite: the dispatch in cmd/mcpx/main.go is reachable as cli.Run(ctx, args, env, stdio) int (or the equivalent shape the implementer pins on this issue), and most integration tests can run in-process against it.
  • The binary suite is retained as the superset (signals, exit codes, argv parsing, detached daemon, nix wrapper) and can target a prebuilt binary (nix result, container image), not only a fresh go build.
  • The fixed-behaviour fakemcp is replaced or supplemented by a scriptable mock MCP server whose behaviour each test states.
  • 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, timings, or probe 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#133 (verbatim, oldest first):


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

    Overlaps #160, written concurrently by a second worker on the same slice. Both are kept until reconciled; see the tracker (#97) for the differences and which one to keep.


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

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


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

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

  3. added
    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
    queue/claimedQueue worker lifecycle state
    and removed on Oct 29, 2026
  6. 179 remaining items

  7. added
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    on Nov 3, 2026
  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  9. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 3, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: picked-up
    task: 3275
    worker: repo-worker-code
    lane: aea12c82-c5af-4e5a-9b66-f8e360f87177
    at: 2026-11-03T23:58:43Z
    next: Revalidate current issue #1980 labels, PR #2714 state/author/checks, and the recorded authorization/transfer condition before any worktree edits.

  12. added
    user/escalatedQueue or user escalation state
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 4, 2026
  13. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: blocked
    task: 3275
    worker: repo-worker-code
    lane: aea12c82-c5af-4e5a-9b66-f8e360f87177
    at: 2026-11-04T00:00:27Z
    blocker: Drew must explicitly authorize this worker to edit/merge PR #2714 or transfer ownership to a non-Drew owner.
    fresh evidence: #1980 is OPEN and assigned to dezren39; task-bound PR #2714 is OPEN, non-draft, and authored by dezren39. Current checks show production-line guard and CodeRabbit only; no fresh authorization or owner transfer is recorded. The existing checkpoint also records the remaining stages and stress follow-up.
    disposition: escalated to Drew; no PR, branch, or worktree edits were made.
    resume: after explicit authorization or ownership transfer, revalidate #1980, PR #2714, and checks; resume from the saved stage-1 checkpoint.

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/enhancementmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)queue/blockedQueue or user escalation statesource/creatorFiled by the new-issue-creator jobstatus/blockedQueue or user escalation statetask/triageQueue packet kind: verify, reproduce, and route this itemuser/escalatedQueue or user escalation state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions