feat: sdk debug adapter - #2483
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
7841652 to
8d07d41
Compare
8d07d41 to
bff1fcf
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends Clarinet’s debugging stack to support SDK-driven, non-interactive debugging flows: a persistent clarinet dap TCP server (DAP + SDK ports), a Node SDK DebugClient to drive contract evaluation through that server, and VSCode UX (CodeLens + command/config) to attach and run a specific test under the debugger.
Changes:
- Add a persistent TCP “server mode” for
clarinet dapthat accepts an SDK JSON protocol connection and (optionally) a DAP attach client connection. - Introduce
DebugClient/startDebugServer()in the Node SDK and export it from the SDK entrypoint. - Add VSCode extension support: CodeLens “Debug with Clarinet”, a
clarity.debugTestcommand to orchestrate DAP + test run, and a minimal rspack build for the DAP adapter.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| components/clarity-vscode/rspack.dap.js | Adds a minimal rspack config to build only the debug adapter bundle. |
| components/clarity-vscode/package.json | Adds build:dap, a clarinetPath setting, a debug command, and an attach debug config option. |
| components/clarity-vscode/debug/debug.ts | Enhances the debug adapter relay to support both launch (spawn) and attach (TCP) modes. |
| components/clarity-vscode/client/src/clientNode.ts | Adds CodeLens + command to start clarinet dap, attach VSCode, and run a single vitest test. |
| components/clarity-repl/src/repl/debug/mod.rs | Adds a DebugState reset helper for successive calls while preserving breakpoints/watchpoints. |
| components/clarity-repl/src/repl/debug/dap/mod.rs | Refactors DAP I/O to support stdio, no-op mode, and TCP attach mode with handshake support. |
| components/clarinet-sdk/node/src/index.ts | Exports the new debug client APIs from the Node SDK entrypoint. |
| components/clarinet-sdk/node/src/debugClient.ts | Implements DebugClient + startDebugServer() for SDK-driven debug evaluation over TCP. |
| components/clarinet-cli/src/frontend/dap.rs | Adds run_dap_server implementing the dual-port TCP server and SDK request loop. |
| components/clarinet-cli/src/frontend/cli.rs | Extends clarinet dap CLI to accept server-mode options (--dap-port, --sdk-port, --manifest(-path)). |
| components/clarinet-cli/examples/pnpm-lock.yaml | Updates the examples lockfile (currently includes local-link artifacts). |
| components/clarinet-cli/examples/dap-debug-demo/vitest.config.ts | Adds a vitest config for the DAP debug demo project. |
| components/clarinet-cli/examples/dap-debug-demo/tests/counter.auto.test.ts | Adds demo tests showing how to use startDebugServer() (skipped in CI). |
| components/clarinet-cli/examples/dap-debug-demo/settings/Devnet.toml | Adds demo devnet settings (currently includes real mnemonic phrases). |
| components/clarinet-cli/examples/dap-debug-demo/package.json | Adds demo package.json wiring vitest + workspace SDK dependency. |
| components/clarinet-cli/examples/dap-debug-demo/contracts/counter.clar | Adds a small demo contract suitable for setting breakpoints. |
| components/clarinet-cli/examples/dap-debug-demo/Clarinet.toml | Adds demo Clarinet project manifest for the DAP debug demo. |
Files not reviewed (1)
- components/clarinet-cli/examples/pnpm-lock.yaml: Generated file
Suppressed comments (2)
components/clarinet-cli/src/frontend/cli.rs:142
- The
--manifest-pathhelp text says it's required when--dap-portis set, but the command handler falls back to auto-detecting the manifest (or "Clarinet.toml"). The CLI docs should reflect that defaulting behavior.
/// Path to Clarinet.toml. Required when `--dap-port` is set.
#[clap(long = "manifest-path", alias = "manifest", short = 'm')]
pub manifest_path: Option<String>,
components/clarinet-cli/examples/dap-debug-demo/settings/Devnet.toml:19
- This example Devnet.toml commits a full mnemonic phrase into the repo. Prefer a well-known public test mnemonic (or a placeholder) so it can't be mistaken for a real secret.
[accounts.wallet_2]
mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital"
balance = 100_000_000_000_000
# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
components/clarinet-sdk/node/src/debugClient.ts:203
CLARINET_DEBUG_PORT(oroptions.port) can parse toNaN(or an out-of-range value), andconnectPort != nullwill still be true forNaN, leading to an immediate connection failure with a confusing error. Consider validating that the port is a finite integer within[0, 65535]before entering connect mode, and fall back to auto-spawn (or throw a clear error) when invalid.
const envPort = process.env["CLARINET_DEBUG_PORT"]
? Number(process.env["CLARINET_DEBUG_PORT"])
: undefined;
const connectPort = options?.port ?? envPort;
// Connect mode: server is already running externally.
if (connectPort != null) {
const socket = await openSocket(connectPort);
return new DebugClient(socket);
}
components/clarity-vscode/client/src/clientNode.ts:133
- If the fallback server fails to bind (e.g., transient OS error), the promise never resolves and the debug command will hang. Add an
errorhandler for the fallback listener and ensure any server created is closed/cleaned up on failure; also consider rejecting the promise so the caller can surface a meaningful error.
function resolvePort(preferred: number): Promise<number> {
return new Promise((resolve) => {
const srv = net.createServer();
srv.listen(preferred, "127.0.0.1", () => {
srv.close(() => resolve(preferred));
});
srv.on("error", () => {
const fallback = net.createServer();
fallback.listen(0, "127.0.0.1", () => {
const addr = fallback.address() as net.AddressInfo;
fallback.close(() => resolve(addr.port));
});
});
});
}
components/clarity-vscode/debug/debug.ts:18
- The
Content-Lengthregex is unnecessarily strict ("Content-Length: (\d+)") and will fail on valid headers with extra whitespace (e.g.Content-Length: 123), causing the parser to skip sections and potentially desync the DAP stream. Consider usingContent-Length:\s*(\d+)and treating “missing Content-Length” as a hard parse error (or leaving bytes inremaining) rather than advancingpospast the header delimiter.
const header = buffer.subarray(pos, headerEnd).toString("ascii");
const match = /Content-Length: (\d+)/i.exec(header);
if (!match) {
pos = headerEnd + 4;
continue;
}
components/clarity-repl/src/repl/debug/dap/mod.rs:173
- In attach/server mode, successive calls reuse the same
DAPDebugger, butprepare_for_callonly resetsDebugState. Any per-call DAP caches onDAPDebuggeritself (e.g.self.current,self.stack_frames,self.scopes,self.variables) can remain stale across calls and may cause incorrect scope/variable responses or panics (e.g. indexing intoself.scopesfor a frame id from a prior call). Consider clearing/resetting those per-execution fields inprepare_for_call(or inreset_for_new_call) so each call starts from a clean execution view while preserving breakpoints/watchpoints.
pub fn prepare_for_call(&mut self, contract_id: &QualifiedContractIdentifier, snippet: &str) {
match &mut self.state {
Some(state) => state.reset_for_new_call(contract_id, snippet),
None => self.state = Some(DebugState::new(contract_id, snippet)),
}
}
cde54db to
eb6811d
Compare
d7b96ef to
9f0b002
Compare
9f0b002 to
5f8714d
Compare
feat: Automatic debug attachment via vitest global
| /// `setBreakpoints`, and `configurationDone`, then return. | ||
| /// After this returns the server is ready to accept SDK contract calls. | ||
| pub fn init_attach(&mut self) -> Result<(), ParseError> { | ||
| while !self.config_done { |
There was a problem hiding this comment.
This can occupy 100% of the CPU on a graceful disconnect. wait_for_command() will return Ok(true) indefinitely.
Should return an error to exit the loop in this case instead
Debugging works with any existing clarity test via the same vitest mechanism existing previously. The debug codelens allows running the debugger on specific
itblocks and the debugger can be set up manually in other editors via theclarinet dapcli commandScreen.Recording.2026-08-24.at.1.52.55.PM.mov
Followup work: #2485