dap-mcp-server 0.1.9 → 0.1.11

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.
@@ -46,14 +46,17 @@ describe("Debug Lifecycle Integration", () => {
46
46
  expect(caps.supportsConfigurationDoneRequest).toBe(true);
47
47
  expect(session.state).toBe("configuring");
48
48
 
49
- // Set breakpoints
49
+ // Send launch first (don't await — real adapters block until configurationDone)
50
+ const launchPromise = session.launch({ program: "/test/app.py", stopOnEntry: true });
51
+
52
+ // Set breakpoints while launch is pending
50
53
  const bps = await session.setBreakpoints("/test/app.py", [{ line: 5 }, { line: 10 }]);
51
54
  expect(bps).toHaveLength(2);
52
55
  expect(bps[0].verified).toBe(true);
53
56
 
54
- // Configuration done + launch
57
+ // Configuration done + await launch
55
58
  await session.configurationDone();
56
- await session.launch({ program: "/test/app.py", stopOnEntry: true });
59
+ await launchPromise;
57
60
 
58
61
  // Wait for stopped on entry
59
62
  const stoppedEvent = await session.waitForStopped(5000);
@@ -98,6 +101,27 @@ describe("Debug Lifecycle Integration", () => {
98
101
  expect(session.state).toBe("terminated");
99
102
  });
100
103
 
104
+ it("returns nested children when collectState uses variableDepth > 1", async () => {
105
+ session = new DebugSession("integ-deep", { type: "mock", runtime: "node", program: MOCK_ADAPTER });
106
+ await session.initialize();
107
+ const launchPromise = session.launch({ program: "/test/app.py", stopOnEntry: true });
108
+ await session.configurationDone();
109
+ await launchPromise;
110
+ await session.waitForStopped(5000);
111
+
112
+ const state = await session.collectState(1, 10, 2);
113
+ const locals = state.scopes.find(s => s.name === "Locals");
114
+ const data = locals!.variables.find(v => v.name === "data");
115
+ expect(data).toBeDefined();
116
+ expect(data!.hasChildren).toBe(true);
117
+ expect(data!.children).toBeDefined();
118
+ expect(data!.children!.map(c => c.value)).toEqual(["1", "2", "3"]);
119
+
120
+ // Leaf variables should not get a children property.
121
+ const x = locals!.variables.find(v => v.name === "x");
122
+ expect(x!.children).toBeUndefined();
123
+ });
124
+
101
125
  it("handles adapter crash gracefully", async () => {
102
126
  const crashAdapter = path.join(__dirname, "../test-fixtures/crash-adapter.js");
103
127
  session = new DebugSession("integ-crash", { type: "mock", runtime: "node", program: crashAdapter });
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.",
55
77
  "",
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.",
78
+ "## Interactive Debugging Workflow",
79
+ "",
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({
@@ -203,7 +238,9 @@ export function createServer(): McpServer {
203
238
  exceptionBreakpoints,
204
239
  adapterConfig,
205
240
  }) => {
206
- const shouldStop = stopOnEntry !== false;
241
+ // Default: if breakpoints are provided, run to the first one; otherwise stop at entry.
242
+ const shouldStop =
243
+ stopOnEntry ?? !(breakpoints !== undefined && breakpoints.length > 0);
207
244
  let session: DebugSession | undefined;
208
245
  try {
209
246
  const config =
@@ -229,6 +266,20 @@ export function createServer(): McpServer {
229
266
  // Initialize (waits for the initialized event internally)
230
267
  await session.initialize();
231
268
 
269
+ // Build launch request
270
+ const launchArgs: Record<string, unknown> = {
271
+ ...config.launchDefaults,
272
+ program,
273
+ stopOnEntry: shouldStop,
274
+ };
275
+ if (args !== undefined) launchArgs.args = args;
276
+ if (cwd !== undefined) launchArgs.cwd = cwd;
277
+ if (env !== undefined) launchArgs.env = env;
278
+
279
+ // Send launch first (don't await — real adapters like debugpy block
280
+ // the launch response until configurationDone arrives)
281
+ const launchPromise = session.launch(launchArgs);
282
+
232
283
  // Set breakpoints grouped by file
233
284
  if (breakpoints && breakpoints.length > 0) {
234
285
  const byFile = new Map<string, typeof breakpoints>();
@@ -255,20 +306,11 @@ export function createServer(): McpServer {
255
306
  await session.setExceptionBreakpoints(exceptionBreakpoints);
256
307
  }
257
308
 
258
- // Configuration done
309
+ // Configuration done — signals the adapter to proceed with launch
259
310
  await session.configurationDone();
260
311
 
261
- // Build launch request
262
- const launchArgs: Record<string, unknown> = {
263
- ...config.launchDefaults,
264
- program,
265
- stopOnEntry: shouldStop,
266
- };
267
- if (args !== undefined) launchArgs.args = args;
268
- if (cwd !== undefined) launchArgs.cwd = cwd;
269
- if (env !== undefined) launchArgs.env = env;
270
-
271
- await session.launch(launchArgs);
312
+ // Now await the launch response
313
+ await launchPromise;
272
314
 
273
315
  let stateData: Record<string, unknown>;
274
316
  if (shouldStop) {
@@ -348,7 +390,6 @@ export function createServer(): McpServer {
348
390
  sessions.add(session);
349
391
 
350
392
  await session.initialize();
351
- await session.configurationDone();
352
393
 
353
394
  // Merge order: adapter defaults (lowest priority) < user attachConfig < explicit params (highest)
354
395
  const attachArgs: Record<string, unknown> = {
@@ -359,7 +400,10 @@ export function createServer(): McpServer {
359
400
  if (host !== undefined) attachArgs.host = host;
360
401
  if (processId !== undefined) attachArgs.processId = processId;
361
402
 
362
- await session.attach(attachArgs);
403
+ // Send attach first (don't await — real adapters block until configurationDone)
404
+ const attachPromise = session.attach(attachArgs);
405
+ await session.configurationDone();
406
+ await attachPromise;
363
407
 
364
408
  return {
365
409
  content: [
@@ -539,7 +583,7 @@ export function createServer(): McpServer {
539
583
  content: [
540
584
  {
541
585
  type: "text" as const,
542
- text: `Error: Session is not stopped (current state: ${session.state}). Use 'step' with action 'pause' to stop first.`,
586
+ 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.`,
543
587
  },
544
588
  ],
545
589
  isError: true,
@@ -671,7 +715,7 @@ export function createServer(): McpServer {
671
715
  // ────────────────────────────────────────────────────────────────────────────
672
716
  server.tool(
673
717
  "get_output",
674
- "Get buffered stdout/stderr and debug adapter output for a session.",
718
+ "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.",
675
719
  {
676
720
  sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
677
721
  category: z
@@ -679,11 +723,17 @@ export function createServer(): McpServer {
679
723
  .optional()
680
724
  .describe("Filter by output category: 'stdout', 'stderr', 'console', etc."),
681
725
  clear: z.boolean().optional().describe("Clear the output buffer after reading (default: false)"),
726
+ since: z
727
+ .number()
728
+ .optional()
729
+ .describe(
730
+ "Return only entries with seq > since. Pass the `nextSince` value from a prior call to fetch incremental output."
731
+ ),
682
732
  },
683
- async ({ sessionId, category, clear }) => {
733
+ async ({ sessionId, category, clear, since }) => {
684
734
  try {
685
735
  const session = sessions.resolve(sessionId);
686
- const result = session.getOutput(category, clear ?? false);
736
+ const result = session.getOutput(category, clear ?? false, since);
687
737
  return {
688
738
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
689
739
  };
@@ -787,13 +837,16 @@ export function createServer(): McpServer {
787
837
  // Initialize (waits for the initialized event internally)
788
838
  await newSession.initialize();
789
839
 
840
+ // Send launch first (don't await — real adapters block until configurationDone)
841
+ const launchPromise = newSession.launch(launchConfig);
842
+
790
843
  // Re-apply saved breakpoints
791
844
  for (const [file, bps] of savedBreakpoints) {
792
845
  await newSession.setBreakpoints(file, bps);
793
846
  }
794
847
 
795
848
  await newSession.configurationDone();
796
- await newSession.launch(launchConfig);
849
+ await launchPromise;
797
850
 
798
851
  const stopOnEntry = launchConfig.stopOnEntry !== false;
799
852
  let stateData: Record<string, unknown>;
@@ -833,6 +886,149 @@ export function createServer(): McpServer {
833
886
  }
834
887
  );
835
888
 
889
+ // ────────────────────────────────────────────────────────────────────────────
890
+ // 13. debug_inspect
891
+ // ────────────────────────────────────────────────────────────────────────────
892
+ server.tool(
893
+ "debug_inspect",
894
+ "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.",
895
+ {
896
+ adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
897
+ program: z.string().describe("Path to the program to debug"),
898
+ args: z.array(z.string()).optional().describe("Program arguments"),
899
+ cwd: z.string().optional().describe("Working directory for the program"),
900
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
901
+ at: z
902
+ .object({
903
+ file: z.string().describe("Absolute path to the source file"),
904
+ line: z.number().describe("Line number (1-based)"),
905
+ condition: z.string().optional().describe("Conditional breakpoint expression"),
906
+ hitCondition: z.string().optional().describe("Hit-count condition (e.g. '5' for 5th hit)"),
907
+ })
908
+ .describe("Breakpoint location to stop at"),
909
+ expressions: z
910
+ .array(z.string())
911
+ .optional()
912
+ .describe("Expressions to evaluate when stopped at the breakpoint. Omit to just verify the line is reached."),
913
+ timeout: z
914
+ .number()
915
+ .optional()
916
+ .describe("Milliseconds to wait for the breakpoint to be hit (default: 30000)"),
917
+ adapterConfig: z
918
+ .object({
919
+ type: z.string(),
920
+ program: z.string(),
921
+ runtime: z.string().optional(),
922
+ args: z.array(z.string()).optional(),
923
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
924
+ installHint: z.string().optional(),
925
+ })
926
+ .optional()
927
+ .describe("Override adapter config inline"),
928
+ },
929
+ async (opts) => {
930
+ try {
931
+ const result = await runDebugInspect(registry, sessions, opts);
932
+ return {
933
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
934
+ };
935
+ } catch (err) {
936
+ return {
937
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
938
+ isError: true,
939
+ };
940
+ }
941
+ }
942
+ );
943
+
944
+ // ────────────────────────────────────────────────────────────────────────────
945
+ // 14. list_breakpoints
946
+ // ────────────────────────────────────────────────────────────────────────────
947
+ server.tool(
948
+ "list_breakpoints",
949
+ "List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.",
950
+ {
951
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
952
+ },
953
+ async ({ sessionId }) => {
954
+ try {
955
+ const session = sessions.resolve(sessionId);
956
+ const bps = session.getBreakpoints();
957
+ const files = Array.from(bps.entries()).map(([file, breakpoints]) => ({
958
+ file,
959
+ breakpoints,
960
+ }));
961
+ const totalCount = files.reduce((n, f) => n + f.breakpoints.length, 0);
962
+ return {
963
+ content: [
964
+ {
965
+ type: "text" as const,
966
+ text: JSON.stringify({ count: totalCount, files }, null, 2),
967
+ },
968
+ ],
969
+ };
970
+ } catch (err) {
971
+ return {
972
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
973
+ isError: true,
974
+ };
975
+ }
976
+ }
977
+ );
978
+
979
+ // ────────────────────────────────────────────────────────────────────────────
980
+ // 15. debug_run
981
+ // ────────────────────────────────────────────────────────────────────────────
982
+ server.tool(
983
+ "debug_run",
984
+ "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.",
985
+ {
986
+ adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
987
+ program: z.string().describe("Path to the program to debug"),
988
+ args: z.array(z.string()).optional().describe("Program arguments"),
989
+ cwd: z.string().optional().describe("Working directory for the program"),
990
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
991
+ logpoints: z
992
+ .array(
993
+ z.object({
994
+ file: z.string().describe("Absolute path to the source file"),
995
+ line: z.number().describe("Line number (1-based)"),
996
+ logMessage: z.string().describe("Message template; expressions in {braces} are evaluated in scope"),
997
+ })
998
+ )
999
+ .optional()
1000
+ .describe("Logpoints to emit per-hit messages without pausing execution"),
1001
+ timeout: z
1002
+ .number()
1003
+ .optional()
1004
+ .describe("Total wall-clock timeout for the whole run in milliseconds (default: 60000)"),
1005
+ adapterConfig: z
1006
+ .object({
1007
+ type: z.string(),
1008
+ program: z.string(),
1009
+ runtime: z.string().optional(),
1010
+ args: z.array(z.string()).optional(),
1011
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
1012
+ installHint: z.string().optional(),
1013
+ })
1014
+ .optional()
1015
+ .describe("Override adapter config inline"),
1016
+ },
1017
+ async (opts) => {
1018
+ try {
1019
+ const result = await runDebugRun(registry, sessions, opts);
1020
+ return {
1021
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
1022
+ };
1023
+ } catch (err) {
1024
+ return {
1025
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
1026
+ isError: true,
1027
+ };
1028
+ }
1029
+ }
1030
+ );
1031
+
836
1032
  // ────────────────────────────────────────────────────────────────────────────
837
1033
  // Resource: dap://sessions
838
1034
  // ────────────────────────────────────────────────────────────────────────────
@@ -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
+ });