Skip to content

Test fixture: scriptable adversarial adapter for e2e failure-path coverage (drop/delay/corrupt chosen DAP frames) #511

Description

@debugmcpdev

Motivation

The subtlest bugs in this codebase keep coming from adapter misbehavior at the DAP boundary, and each one has been diagnosed and verified with a hand-rolled one-off fake:

The #492/#493 work built exactly this as scratch tooling and it worked end-to-end against the real server: a ~60-line Ruby DAP impostor injected via RDBG_PATH (a .bat shim with the sibling extensionless script, so the ruby adapter's interpreter-wrapping picks it up — note #508) that speaks just enough DAP to complete a launch, but drops chosen frames. Details and transcript in docs/case-studies/rdbg-initialize-response-stall.md.

Right now that fixture lives in a session scratchpad and dies with it. The failure paths it exercises are pinned only by unit tests with stubbed connection managers — nothing in the suite drives the real worker + real TCP + real framing through an adapter that misbehaves on cue.

Proposal

Promote the pattern to a first-class fixture, e.g. tests/fixtures/adversarial-adapter/:

  1. A scriptable DAP impostor (Node, not Ruby — no toolchain dependency; TCP server speaking Content-Length framing) whose behavior is declared per-run, e.g. via env or a JSON scenario file:

    {
      "initialize": { "dropResponse": true, "sendEvent": "initialized", "delayMs": 34 },
      "launch":     { "dropResponse": true },        // wedged variant
      "*":          { "respond": "success" }         // default: happy path
    }

    Useful primitives: drop response, delay response, respond-then-die, prepend junk bytes (the Ruby/rdbg: dropped initialize response frame turns a transient framing hiccup into a hard 30s launch failure #470 shape), duplicate a response, send an event before/after its response, close the socket mid-frame.

  2. An injection seam per adapter where cheap. Ruby already works today via RDBG_PATH (once Ruby on Windows: RDBG_PATH error text advises pointing at the extensionless rdbg script, but that path is then spawned directly and cannot execute #508 makes the extensionless form work, injection gets simpler). A generic alternative that avoids per-adapter shims: an e2e helper that starts a session via the mock-adapter spawn path but pointed at the impostor's port — worth checking whether adapterCommand/spawn config can be overridden cleanly in test builds before inventing anything new.

  3. E2E tests that pin the behaviors shipped in fix(#492): don't park ruby launches on an initialize response rdbg never sends #507/fix(#493): make the proxy-init timeout say what actually stalled #509 against the real stack:

    • drop initialize response → ruby launch still completes, recovery warning logged (~4s, not 30s)
    • drop initialize + launch responses → failure at the deadline with the stage-aware message naming launch, the adapter PID, and data.initProgress/proxyLogPath in the result
    • junk-prefixed response → decoder resync, launch completes (Ruby/rdbg: dropped initialize response frame turns a transient framing hiccup into a hard 30s launch failure #470 regression, currently unit-level only)
    • never accept the TCP connection → the spawned-but-never-connected message variant

Why not just keep the unit tests

The unit tests encode the mechanisms and stay. What they can't catch is integration drift: framing edge cases in DapFrameDecoder against a real socket, status-IPC ordering between worker and parent, the interplay of the per-request 30s timeout with the parent deadline, and future refactors of the connection manager that invalidate stubbed assumptions. Each of #470/#492/#493 was exactly that kind of gap.

Scope guard

Not a general fuzzing framework — a deterministic scenario player with maybe six primitives, plus 3–5 e2e tests. The scratch version was ~60 lines of impostor + ~100 lines of driver; the fixture should stay in that weight class.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions