Repository navigation
Description
Problem
The most common debugging entry point is a failing test, and translating a test invocation into a debug launch takes runner-specific knowledge an agent (or human) shouldn't need: worker flags (--runInBand for Jest, single-thread flags for Vitest), module launch (python -m pytest), or locating hashed test binaries (cargo test --no-run). Today that translation is manual and error-prone; done wrong, breakpoints silently don't bind (worker pools) or the launch is refused.
Direction
A preset layer that maps (runner, target) → the adapter's launch config, so the intent is one call:
{ "testRunner": "pytest", "testTarget": "tests/test_billing.py::test_proration_rounding" }With #982's snapshot, the launch answer is the autopsy: the one call ends paused at the failure with exception + frames + locals in the response.
In-repo precedent: packages/adapter-dart/src/runner.ts already auto-detects dart/dart-test/flutter/flutter-test from the pubspec and program path — this issue generalizes that idea. Prior art: .github/actions/debug-failing-test (#241, experimental).
Shape decision up front: new debug_test tool vs. preset args on start_debugging. A 30th tool touches the tool-count pins (server-required-arguments.test.ts, 24 tool-list snapshots, ~11 docs stating "29 tools") and must be decided before the 1.0 tool-schema freeze (ROADMAP). Preset args on start_debugging add no tool and keep the schema surface smaller — current lean, to be confirmed in M1.
Milestones (#759-style)
- M0 — debugpy module launch. Blocked today: the launch path always sends
program(proxy-launcher.tssets it fromscriptPath;dap-proxy-connection-manager.tsputs it on the wire), and debugpy rejectsprogram+module("mutually exclusive"). Allow a module-mode launch config through withoutprogram. (Workaround exists — launching pytest's__main__.pyas the program — but carries asys.path[0]difference vspython -m pytest.) - M1 — pytest.
-m pytest <target> -x+ the assertion-stop question: theuncaughtdefault never pauses on an assert pytest catches; debugpy'suserUnhandledfilter exists but is deliberately unexposed ("noisy in practice",adapter-policy-python.ts). Spike: scopeduserUnhandledfor test-preset sessions only, measured against real pytest suites before any claim. - M2 — jest / vitest.
ChildSessionManageradopts exactly ONE child (child-session-manager.tsreleases the rest undebugged) → presets must force single-worker execution (--runInBand; vitest single-thread/no-file-parallelism). Ship with measured per-runner caveats, not promises. - M3 — cargo test / go test.
cargo test --no-run+ hashed-binary resolution (the deadcargo.testbranch inrust-debug-adapter.ts:776-798and the uncalledrunCargoTestincargo-utils.tsshould be revived or deleted as part of this);go test -c. - M4 — rspec / dotnet test / minitest.
Each milestone lands with spike-notes-style measured facts (what binds, what doesn't, which flags are mandatory).
Activity
fix(python): a module launch derives its cwd from the package, names …
feat(python): a module launch goes out without program (#985 M0) (#992)
debugmcpdev commentedon Oct 26, 2026
Status 2026-10-25: M0 merged, M1 in review
c803ab31): a Python module launch goes through.AdapterPolicy.launchTargetKeys(python:program/module/code), the connection manager fillsprogramonly when no target key is set, the Python transform drops the stand-inprogram, derivescwdas the directory above the top-most package containing the file, and says what it did; the dry run namesmodule: <name>.start_debugging { scriptPath: <test file>, testRunner: "pytest", testTarget }. The answer is paused at the failing assert with the Paused stop answers carry a crash snapshot (exception, top frames, bounded locals); attach answers gain landing parity #982 snapshot; a passing test runs to exit 0.Measured (docs/python/spike-notes-pytest.md in #994)
uncaughtalone never pauses on a failing test (pytest catches it). debugpy'suserUnhandleddoes: at the assert in the test frame with locals; for a raise in a helper, the raise-site frames sit above the live test frame (locals readable); never insidepytest.raises; once onpytest.skip(); in the fixture for a fixture error. Whole fixture file: 13 tests → 8 stops (6 failures + 1 fixture error + 1 skip), 0 on the 5 passing tests."all"(debugpyraised) is not a usable default: three false stops across two passing tests, one per user frame an expected exception crosses.(note: full exception trace is shown but execution is paused at: …)ontext/exceptionId/typeName; the snapshot strips it. The frame name carries(Current frame).WinError 6: the debuggee runs underpythonw.exe, python (Windows): the debuggee runs under pythonw.exe, so a subprocess that inherits stderr fails with WinError 6 (pytest-xdist workers) #993) and would run the test undebugged anyway; cov coexists on 3.13 (unmeasured on ≤3.11).-p no:xdist -p no:covis silent when absent, clean when present; the preset passes both unless the caller's args ask for the plugin.printuntil the report;-sis not forced (args: ["-s"]opts in).Shape M1 fixes for M2–M4
testRunner/testTargetas optionalstart_debuggingarguments, folded intoadapterLaunchConfigat intake (no new tool; the facade andrestart_debuggingunchanged).AdapterModes.testRunnerson the factory metadata (mirrored in the loader's list, shown bylist_supported_languages, pinned inadapter-attach-contract.test.ts), andclassifyTestRunner(src/session/launch/test-runner-gate.ts) refusing an undeclared runner by name before the session changes state.transformLaunchConfigowns the expansion (packages/adapter-python/src/test-runners.tsis the template: node id, the runner's single-process flags, a rootdir walk for cwd, one launch-scoped note), with the keys inconsumedLaunchKeys.AdapterPolicy.exceptionFiltersForLaunch(mode, launchConfig)for runner-specific filters; the launcher records what the launch armed beyond the mode, and the pending answer names it.Open after M1
unittestas a second Python runner (sameuserUnhandledshape, unmeasured).cargo test --no-run/go test -c(and the deadcargo.testbranch); M4 rspec / dotnet test / minitest.pythonw.exedebuggee.feat(python): one-call failing-test debugging — start_debugging { tes…
feat(python): one-call failing-test debugging — start_debugging { tes…
debugmcpdev commentedon Nov 2, 2026
Status note (2026-11-02): the "shape decision up front" in the body is made — M1 (#994) landed
testRunner/testTargetasstart_debuggingarguments,AdapterModes.testRunnerson the factory metadata, the policy-owned expansion, no new tool. Two things for the remaining milestones:cargo.testbranch inpackages/adapter-rust/src/rust-debug-adapter.ts:789and the uncalledrunCargoTestinpackages/adapter-rust/src/utils/cargo-utils.ts:133get revived or deleted in the same PR, not left as a third state.debugmcpdev commentedon Nov 5, 2026
The session tree's last step (#1091) lifts the gate this issue names: pytest-xdist workers are child debug sessions by default now —
python -m pytest -n 2yields two, and the worker running the test stops on a breakpoint set before the run (tests/adapters/python/integration/python-pytest-xdist.test.ts;pytest-xdistis inrequirements/pytest.txt). The preset keeps-p no:xdistfor now: a worker's stop is reported bywait_for_stop, not by the launch answer the one-call autopsy promises, so turning xdist back on in the preset is this issue's call, not the tree's.