dap-mcp-server 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.ts CHANGED
@@ -24,12 +24,28 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
24
  import { z } from "zod";
25
25
  import os from "node:os";
26
26
  import path from "node:path";
27
+ import { readFileSync } from "node:fs";
28
+ import { fileURLToPath } from "node:url";
27
29
 
28
30
  import { AdapterRegistry } from "./adapters/registry.js";
29
31
  import { SessionManager } from "./session/session-manager.js";
30
32
  import { DebugSession } from "./session/session.js";
33
+ import { runDebugInspect } from "./session/inspect.js";
34
+ import { runDebugRun } from "./session/run.js";
31
35
  import { checkAdapterAvailable } from "./adapters/detect.js";
32
36
 
37
+ function readPackageVersion(): string {
38
+ try {
39
+ const here = path.dirname(fileURLToPath(import.meta.url));
40
+ const pkg = JSON.parse(
41
+ readFileSync(path.join(here, "..", "package.json"), "utf-8")
42
+ ) as { version?: string };
43
+ return pkg.version ?? "0.0.0";
44
+ } catch {
45
+ return "0.0.0";
46
+ }
47
+ }
48
+
33
49
  export function createServer(): McpServer {
34
50
  const globalDir = path.join(os.homedir(), ".dap-mcp");
35
51
  const projectDir = path.join(process.cwd(), ".dap-mcp");
@@ -45,41 +61,44 @@ export function createServer(): McpServer {
45
61
  const server = new McpServer(
46
62
  {
47
63
  name: "dap-mcp-server",
48
- version: "0.1.0",
64
+ version: readPackageVersion(),
49
65
  },
50
66
  {
51
67
  instructions: [
52
68
  "This server provides AI-driven debugging via the Debug Adapter Protocol (DAP).",
53
69
  "",
54
- "## Debugging Workflow",
70
+ "## When to use which tool",
71
+ "",
72
+ "- **`debug_inspect`** — One-shot 'what is X at line N?'. Launches, hits a breakpoint, evaluates expressions,",
73
+ " disconnects. Use this first; it collapses a multi-call workflow into a single call.",
74
+ "- **`debug_run`** — Run a program with no stops and collect all output. Combine with `logpoints` (breakpoints",
75
+ " with `logMessage`) for printf-style debugging without modifying source.",
76
+ "- **`debug_launch` + step/evaluate** — Interactive multi-step session when you need to explore.",
77
+ "",
78
+ "## Interactive Debugging Workflow",
55
79
  "",
56
- "A typical session follows this sequence:",
57
- "1. `list_adapters` — discover available debug adapters and check which are installed",
58
- "2. `debug_launch` (or `debug_attach`) — start a debug session. This performs the full DAP handshake,",
59
- " sets initial breakpoints, and launches the program. Returns a sessionId and initial stopped state.",
60
- "3. Inspect state with `get_state` — returns threads, call stack, scopes, and variables in one call.",
61
- " This is a coarse-grained tool designed to minimize round-trips; prefer it over multiple fine-grained queries.",
62
- "4. `step` — control execution (continue, next, stepIn, stepOut, pause). Each step action blocks until",
63
- " the program stops again and returns the new state, so you do NOT need to call get_state after stepping.",
64
- "5. `evaluate` — evaluate expressions in the context of a stopped frame (REPL, watch, or hover context).",
65
- "6. `set_breakpoints` / `set_exception_breakpoints` — modify breakpoints during the session.",
66
- "7. `get_output` — retrieve buffered stdout/stderr/console output (ring buffer, max 10K lines).",
67
- "8. `debug_disconnect` — end the session. `debug_restart` replays the same launch config with preserved breakpoints.",
80
+ "1. `list_adapters` — discover available adapters; the response includes a summary and install hints.",
81
+ "2. `debug_launch` (or `debug_attach`) — start a session. With breakpoints provided, `stopOnEntry` defaults",
82
+ " to false so the program runs to the first breakpoint. Returns sessionId + initial stopped state.",
83
+ "3. Inspect state with `get_state` — threads + call stack + scopes + variables in one call.",
84
+ "4. `step` — continue, next, stepIn, stepOut, pause. Blocks until stopped; returns new state directly.",
85
+ "5. `evaluate` — expressions in the context of a stopped frame.",
86
+ "6. `set_breakpoints` / `set_exception_breakpoints` — modify breakpoints mid-session. Use `logMessage`",
87
+ " on a breakpoint to make it a logpoint (emits formatted text without pausing).",
88
+ "7. `list_breakpoints` — see what's currently set.",
89
+ "8. `get_output` — retrieve stdout/stderr/console output (10K-line ring buffer).",
90
+ "9. `debug_disconnect` — end. `debug_restart` replays the launch config with preserved breakpoints.",
68
91
  "",
69
92
  "## Key Conventions",
70
93
  "",
71
- "- **Auto-session resolution**: All tools accept an optional `sessionId`. When only one session is active,",
72
- " you can omit it — the server resolves it automatically.",
73
- "- **Synchronous stepping**: `step` returns the new stopped state directly. Do not follow a step with get_state",
74
- " unless you need to change parameters like `variableDepth` or `threadId`.",
75
- "- **Coarse get_state**: Fetches threads + stack frames + scopes + variables in a single call.",
76
- " Use `depth` (default 10) and `variableDepth` (default 1) to control how much data is returned.",
77
- "- **Output buffering**: Program output is buffered in a 10K-line ring buffer. Oldest lines are dropped",
78
- " when the buffer is full. Use `get_output` with `clear: true` to consume and free buffer space.",
79
- "- **Adapter registration**: Use `configure_adapter` to register adapters. Configs are persisted to",
80
- " `~/.dap-mcp/adapters.json` (global) or `./.dap-mcp/adapters.json` (project), with project overriding global.",
81
- "- **Error handling**: All tool errors are returned as `isError: true` results with an error message.",
82
- " The session remains usable after most errors.",
94
+ "- **Auto-session resolution**: tools accept an optional `sessionId`; with one active session, omit it.",
95
+ "- **Synchronous stepping**: `step` returns the new stopped state. Don't follow it with `get_state`.",
96
+ "- **Variable depth**: `get_state` accepts `variableDepth` (default 1). With `> 1`, container variables",
97
+ " expose their entries as a structured `children` array.",
98
+ "- **stopOnEntry default**: false when `debug_launch` is given breakpoints, true otherwise.",
99
+ "- **Adapter registration**: `configure_adapter` persists to `~/.dap-mcp/adapters.json` (global) or",
100
+ " `./.dap-mcp/adapters.json` (project); project overrides global.",
101
+ "- **Error handling**: tool errors return `isError: true`. The session usually remains usable.",
83
102
  ].join("\n"),
84
103
  }
85
104
  );
@@ -89,7 +108,7 @@ export function createServer(): McpServer {
89
108
  // ────────────────────────────────────────────────────────────────────────────
90
109
  server.tool(
91
110
  "list_adapters",
92
- "List all known debug adapters with their availability status on this machine.",
111
+ "List all known debug adapters with their availability status on this machine. Returns a summary line, the full adapter list, and a separate 'missing' list with installation hints for adapters that aren't installed.",
93
112
  {},
94
113
  async () => {
95
114
  try {
@@ -101,8 +120,21 @@ export function createServer(): McpServer {
101
120
  return { ...a, available };
102
121
  })
103
122
  );
123
+ const available = results.filter((r) => r.available);
124
+ const missing = results
125
+ .filter((r) => !r.available)
126
+ .map((r) => ({ type: r.type, installHint: r.installHint }));
127
+ const summary =
128
+ missing.length === 0
129
+ ? `${available.length}/${results.length} adapters available`
130
+ : `${available.length}/${results.length} adapters available; missing: ${missing.map((m) => m.type).join(", ")}`;
104
131
  return {
105
- content: [{ type: "text" as const, text: JSON.stringify(results, null, 2) }],
132
+ content: [
133
+ {
134
+ type: "text" as const,
135
+ text: JSON.stringify({ summary, adapters: results, missing }, null, 2),
136
+ },
137
+ ],
106
138
  };
107
139
  } catch (err) {
108
140
  return {
@@ -163,7 +195,10 @@ export function createServer(): McpServer {
163
195
  args: z.array(z.string()).optional().describe("Program arguments"),
164
196
  cwd: z.string().optional().describe("Working directory for the program"),
165
197
  env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
166
- stopOnEntry: z.boolean().optional().describe("Stop on program entry (default: true)"),
198
+ stopOnEntry: z
199
+ .boolean()
200
+ .optional()
201
+ .describe("Stop on program entry. Defaults to false when breakpoints are provided (run to first breakpoint), true otherwise."),
167
202
  breakpoints: z
168
203
  .array(
169
204
  z.object({
@@ -188,6 +223,8 @@ export function createServer(): McpServer {
188
223
  args: z.array(z.string()).optional(),
189
224
  launchDefaults: z.record(z.string(), z.unknown()).optional(),
190
225
  installHint: z.string().optional(),
226
+ transport: z.enum(["stdio", "tcp"]).optional(),
227
+ portMatcher: z.string().optional(),
191
228
  })
192
229
  .optional()
193
230
  .describe("Override adapter config inline instead of looking up from registry"),
@@ -203,7 +240,9 @@ export function createServer(): McpServer {
203
240
  exceptionBreakpoints,
204
241
  adapterConfig,
205
242
  }) => {
206
- const shouldStop = stopOnEntry !== false;
243
+ // Default: if breakpoints are provided, run to the first one; otherwise stop at entry.
244
+ const shouldStop =
245
+ stopOnEntry ?? !(breakpoints !== undefined && breakpoints.length > 0);
207
246
  let session: DebugSession | undefined;
208
247
  try {
209
248
  const config =
@@ -239,9 +278,27 @@ export function createServer(): McpServer {
239
278
  if (cwd !== undefined) launchArgs.cwd = cwd;
240
279
  if (env !== undefined) launchArgs.env = env;
241
280
 
281
+ // Adapter-specific arg normalization. delve's `cwd` is the debuggee's
282
+ // working directory; the *build* (go build) runs from delve's own
283
+ // process cwd, controlled by `dlvCwd`. Mirror cwd → dlvCwd so a
284
+ // user-supplied cwd is found by `go build` looking for go.mod.
285
+ if (config.type === "go" && cwd !== undefined && launchArgs.dlvCwd === undefined) {
286
+ launchArgs.dlvCwd = cwd;
287
+ }
288
+
242
289
  // Send launch first (don't await — real adapters like debugpy block
243
290
  // the launch response until configurationDone arrives)
244
291
  const launchPromise = session.launch(launchArgs);
292
+ // Suppress unhandled rejection: the real `await launchPromise` below
293
+ // will still throw if launch fails. Without this, an early launch
294
+ // rejection (e.g. adapter rejects the request) crashes the MCP server.
295
+ launchPromise.catch(() => { /* handled at await */ });
296
+
297
+ // Wait for the adapter to emit the "initialized" event before sending
298
+ // configuration requests. Some adapters (delve) emit it right after the
299
+ // initialize response; others (debugpy) only emit it after receiving
300
+ // launch — that's why we fire launch first and wait for initialized here.
301
+ await session.waitForInitialized(10000);
245
302
 
246
303
  // Set breakpoints grouped by file
247
304
  if (breakpoints && breakpoints.length > 0) {
@@ -328,6 +385,8 @@ export function createServer(): McpServer {
328
385
  args: z.array(z.string()).optional(),
329
386
  launchDefaults: z.record(z.string(), z.unknown()).optional(),
330
387
  installHint: z.string().optional(),
388
+ transport: z.enum(["stdio", "tcp"]).optional(),
389
+ portMatcher: z.string().optional(),
331
390
  })
332
391
  .optional()
333
392
  .describe("Override adapter config inline"),
@@ -365,6 +424,10 @@ export function createServer(): McpServer {
365
424
 
366
425
  // Send attach first (don't await — real adapters block until configurationDone)
367
426
  const attachPromise = session.attach(attachArgs);
427
+ // Suppress unhandled rejection: the real `await attachPromise` below
428
+ // will still throw if attach fails.
429
+ attachPromise.catch(() => { /* handled at await */ });
430
+ await session.waitForInitialized(10000);
368
431
  await session.configurationDone();
369
432
  await attachPromise;
370
433
 
@@ -546,7 +609,7 @@ export function createServer(): McpServer {
546
609
  content: [
547
610
  {
548
611
  type: "text" as const,
549
- text: `Error: Session is not stopped (current state: ${session.state}). Use 'step' with action 'pause' to stop first.`,
612
+ text: `Error: Session is not stopped (current state: ${session.state}). Expressions can only be evaluated at a stopped frame. Set a breakpoint where you want to inspect (set_breakpoints) and continue to it (step with action 'continue'), then retry.`,
550
613
  },
551
614
  ],
552
615
  isError: true,
@@ -678,7 +741,7 @@ export function createServer(): McpServer {
678
741
  // ────────────────────────────────────────────────────────────────────────────
679
742
  server.tool(
680
743
  "get_output",
681
- "Get buffered stdout/stderr and debug adapter output for a session.",
744
+ "Get buffered stdout/stderr and debug adapter output for a session. Pass `since` (the `nextSince` from a previous call) to get only newly-emitted entries — useful for polling logpoint output without re-reading the full buffer.",
682
745
  {
683
746
  sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
684
747
  category: z
@@ -686,11 +749,17 @@ export function createServer(): McpServer {
686
749
  .optional()
687
750
  .describe("Filter by output category: 'stdout', 'stderr', 'console', etc."),
688
751
  clear: z.boolean().optional().describe("Clear the output buffer after reading (default: false)"),
752
+ since: z
753
+ .number()
754
+ .optional()
755
+ .describe(
756
+ "Return only entries with seq > since. Pass the `nextSince` value from a prior call to fetch incremental output."
757
+ ),
689
758
  },
690
- async ({ sessionId, category, clear }) => {
759
+ async ({ sessionId, category, clear, since }) => {
691
760
  try {
692
761
  const session = sessions.resolve(sessionId);
693
- const result = session.getOutput(category, clear ?? false);
762
+ const result = session.getOutput(category, clear ?? false, since);
694
763
  return {
695
764
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
696
765
  };
@@ -796,6 +865,10 @@ export function createServer(): McpServer {
796
865
 
797
866
  // Send launch first (don't await — real adapters block until configurationDone)
798
867
  const launchPromise = newSession.launch(launchConfig);
868
+ // Suppress unhandled rejection: the real `await launchPromise` below
869
+ // will still throw if launch fails.
870
+ launchPromise.catch(() => { /* handled at await */ });
871
+ await newSession.waitForInitialized(10000);
799
872
 
800
873
  // Re-apply saved breakpoints
801
874
  for (const [file, bps] of savedBreakpoints) {
@@ -843,6 +916,153 @@ export function createServer(): McpServer {
843
916
  }
844
917
  );
845
918
 
919
+ // ────────────────────────────────────────────────────────────────────────────
920
+ // 13. debug_inspect
921
+ // ────────────────────────────────────────────────────────────────────────────
922
+ server.tool(
923
+ "debug_inspect",
924
+ "One-shot inspection: launch a program, run to a breakpoint, evaluate a list of expressions in that frame, then disconnect. Collapses launch + wait + evaluate + disconnect into a single call. Use for 'what is X at line N?' debugging.",
925
+ {
926
+ adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
927
+ program: z.string().describe("Path to the program to debug"),
928
+ args: z.array(z.string()).optional().describe("Program arguments"),
929
+ cwd: z.string().optional().describe("Working directory for the program"),
930
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
931
+ at: z
932
+ .object({
933
+ file: z.string().describe("Absolute path to the source file"),
934
+ line: z.number().describe("Line number (1-based)"),
935
+ condition: z.string().optional().describe("Conditional breakpoint expression"),
936
+ hitCondition: z.string().optional().describe("Hit-count condition (e.g. '5' for 5th hit)"),
937
+ })
938
+ .describe("Breakpoint location to stop at"),
939
+ expressions: z
940
+ .array(z.string())
941
+ .optional()
942
+ .describe("Expressions to evaluate when stopped at the breakpoint. Omit to just verify the line is reached."),
943
+ timeout: z
944
+ .number()
945
+ .optional()
946
+ .describe("Milliseconds to wait for the breakpoint to be hit (default: 30000)"),
947
+ adapterConfig: z
948
+ .object({
949
+ type: z.string(),
950
+ program: z.string(),
951
+ runtime: z.string().optional(),
952
+ args: z.array(z.string()).optional(),
953
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
954
+ installHint: z.string().optional(),
955
+ transport: z.enum(["stdio", "tcp"]).optional(),
956
+ portMatcher: z.string().optional(),
957
+ })
958
+ .optional()
959
+ .describe("Override adapter config inline"),
960
+ },
961
+ async (opts) => {
962
+ try {
963
+ const result = await runDebugInspect(registry, sessions, opts);
964
+ return {
965
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
966
+ };
967
+ } catch (err) {
968
+ return {
969
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
970
+ isError: true,
971
+ };
972
+ }
973
+ }
974
+ );
975
+
976
+ // ────────────────────────────────────────────────────────────────────────────
977
+ // 14. list_breakpoints
978
+ // ────────────────────────────────────────────────────────────────────────────
979
+ server.tool(
980
+ "list_breakpoints",
981
+ "List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.",
982
+ {
983
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
984
+ },
985
+ async ({ sessionId }) => {
986
+ try {
987
+ const session = sessions.resolve(sessionId);
988
+ const bps = session.getBreakpoints();
989
+ const files = Array.from(bps.entries()).map(([file, breakpoints]) => ({
990
+ file,
991
+ breakpoints,
992
+ }));
993
+ const totalCount = files.reduce((n, f) => n + f.breakpoints.length, 0);
994
+ return {
995
+ content: [
996
+ {
997
+ type: "text" as const,
998
+ text: JSON.stringify({ count: totalCount, files }, null, 2),
999
+ },
1000
+ ],
1001
+ };
1002
+ } catch (err) {
1003
+ return {
1004
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
1005
+ isError: true,
1006
+ };
1007
+ }
1008
+ }
1009
+ );
1010
+
1011
+ // ────────────────────────────────────────────────────────────────────────────
1012
+ // 15. debug_run
1013
+ // ────────────────────────────────────────────────────────────────────────────
1014
+ server.tool(
1015
+ "debug_run",
1016
+ "Launch a program with no stops, let it run to completion, and return all stdout/stderr/console output. Optionally inject logpoints (breakpoints with logMessage) for printf-style debugging without modifying source. Use for 'what does this script actually do?' cases where you don't need to inspect intermediate state.",
1017
+ {
1018
+ adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
1019
+ program: z.string().describe("Path to the program to debug"),
1020
+ args: z.array(z.string()).optional().describe("Program arguments"),
1021
+ cwd: z.string().optional().describe("Working directory for the program"),
1022
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
1023
+ logpoints: z
1024
+ .array(
1025
+ z.object({
1026
+ file: z.string().describe("Absolute path to the source file"),
1027
+ line: z.number().describe("Line number (1-based)"),
1028
+ logMessage: z.string().describe("Message template; expressions in {braces} are evaluated in scope"),
1029
+ })
1030
+ )
1031
+ .optional()
1032
+ .describe("Logpoints to emit per-hit messages without pausing execution"),
1033
+ timeout: z
1034
+ .number()
1035
+ .optional()
1036
+ .describe("Total wall-clock timeout for the whole run in milliseconds (default: 60000)"),
1037
+ adapterConfig: z
1038
+ .object({
1039
+ type: z.string(),
1040
+ program: z.string(),
1041
+ runtime: z.string().optional(),
1042
+ args: z.array(z.string()).optional(),
1043
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
1044
+ installHint: z.string().optional(),
1045
+ transport: z.enum(["stdio", "tcp"]).optional(),
1046
+ portMatcher: z.string().optional(),
1047
+ })
1048
+ .optional()
1049
+ .describe("Override adapter config inline"),
1050
+ },
1051
+ async (opts) => {
1052
+ try {
1053
+ const result = await runDebugRun(registry, sessions, opts);
1054
+ return {
1055
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
1056
+ };
1057
+ } catch (err) {
1058
+ return {
1059
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
1060
+ isError: true,
1061
+ };
1062
+ }
1063
+ }
1064
+ );
1065
+
846
1066
  // ────────────────────────────────────────────────────────────────────────────
847
1067
  // Resource: dap://sessions
848
1068
  // ────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Copyright (c) 2026 Ivan Iraci <ivan.iraci@professioneit.com>
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in
12
+ * all copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+
23
+ import { describe, it, expect } from "vitest";
24
+ import { runDebugInspect } from "./inspect.js";
25
+ import { AdapterRegistry } from "../adapters/registry.js";
26
+ import { SessionManager } from "./session-manager.js";
27
+ import path from "node:path";
28
+ import os from "node:os";
29
+ import { fileURLToPath } from "node:url";
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const MOCK_ADAPTER = path.join(__dirname, "../test-fixtures/mock-adapter.js");
33
+
34
+ describe("runDebugInspect", () => {
35
+ it("launches, hits a breakpoint, evaluates expressions, and disconnects", async () => {
36
+ const registry = new AdapterRegistry(
37
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
38
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
39
+ );
40
+ const sessions = new SessionManager();
41
+
42
+ const result = await runDebugInspect(registry, sessions, {
43
+ adapter: "mock",
44
+ program: "/test/app.py",
45
+ at: { file: "/test/app.py", line: 5 },
46
+ expressions: ["x", "name"],
47
+ timeout: 5000,
48
+ adapterConfig: {
49
+ type: "mock",
50
+ runtime: "node",
51
+ program: MOCK_ADAPTER,
52
+ },
53
+ });
54
+
55
+ expect(result.hit).toBe(true);
56
+ expect(result.reason).toBe("breakpoint");
57
+ expect(result.location?.file).toBe("/test/app.py");
58
+ expect(result.values).toBeDefined();
59
+ expect(result.values!["x"].result).toBe("42");
60
+ expect(result.values!["name"].result).toBe("42");
61
+ expect(sessions.listAll()).toHaveLength(0);
62
+ });
63
+
64
+ it("returns hit=true with no values when expressions omitted", async () => {
65
+ const registry = new AdapterRegistry(
66
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
67
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
68
+ );
69
+ const sessions = new SessionManager();
70
+
71
+ const result = await runDebugInspect(registry, sessions, {
72
+ adapter: "mock",
73
+ program: "/test/app.py",
74
+ at: { file: "/test/app.py", line: 5 },
75
+ timeout: 5000,
76
+ adapterConfig: {
77
+ type: "mock",
78
+ runtime: "node",
79
+ program: MOCK_ADAPTER,
80
+ },
81
+ });
82
+
83
+ expect(result.hit).toBe(true);
84
+ expect(result.values).toBeUndefined();
85
+ });
86
+
87
+ it("rejects when adapter is unknown and no inline config provided", async () => {
88
+ const registry = new AdapterRegistry(
89
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
90
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
91
+ );
92
+ const sessions = new SessionManager();
93
+
94
+ await expect(
95
+ runDebugInspect(registry, sessions, {
96
+ adapter: "nonexistent",
97
+ program: "/test/app.py",
98
+ at: { file: "/test/app.py", line: 5 },
99
+ })
100
+ ).rejects.toThrow(/not found/);
101
+ });
102
+
103
+ it("cleans up the session even when the breakpoint never hits", async () => {
104
+ const registry = new AdapterRegistry(
105
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
106
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
107
+ );
108
+ const sessions = new SessionManager();
109
+
110
+ const noStopAdapter = path.join(__dirname, "../test-fixtures/no-stop-adapter.js");
111
+ const result = await runDebugInspect(registry, sessions, {
112
+ adapter: "mock",
113
+ program: "/test/app.py",
114
+ at: { file: "/test/app.py", line: 5 },
115
+ timeout: 200,
116
+ adapterConfig: {
117
+ type: "mock",
118
+ runtime: "node",
119
+ program: noStopAdapter,
120
+ },
121
+ });
122
+
123
+ expect(result.hit).toBe(false);
124
+ expect(sessions.listAll()).toHaveLength(0);
125
+ });
126
+ });