Skip to content

Commit f550816

Browse files
authored
Merge pull request #60 from debugmcp/fix/go-adapter-auto-detect-mode
fix(adapter-go): auto-infer dlv mode from program file extension
2 parents e4ff999 + 084ade3 commit f550816

3 files changed

Lines changed: 134 additions & 4 deletions

File tree

‎packages/adapter-go/src/go-debug-adapter.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,25 @@ export class GoDebugAdapter extends EventEmitter implements IDebugAdapter {
304304
// ===== Debug Configuration =====
305305

306306
async transformLaunchConfig(config: GenericLaunchConfig): Promise<GoLaunchConfig> {
307+
// Inference rule: a .go source file means dlv should compile-and-run
308+
// (mode 'debug'); anything else (a pre-built binary) means dlv should
309+
// run it directly (mode 'exec'). dlv exits with no useful error when
310+
// given a binary in 'debug' mode, so this auto-detection is required
311+
// for usable UX. An explicit user-supplied mode always wins (e.g.
312+
// 'test', 'replay', 'core').
313+
const rawConfig = config as Record<string, unknown>;
314+
const userMode = rawConfig.mode as GoLaunchConfig['mode'] | undefined;
315+
const program = rawConfig.program;
316+
const isGoSource = typeof program === 'string'
317+
&& program.toLowerCase().endsWith('.go');
318+
const inferredMode: GoLaunchConfig['mode'] = isGoSource ? 'debug' : 'exec';
319+
const mode = userMode ?? inferredMode;
320+
307321
const goConfig: GoLaunchConfig = {
308322
...config,
309323
type: 'go',
310324
request: 'launch',
311-
// Preserve mode if specified (e.g., 'test'), otherwise default to 'debug'
312-
mode: (config as Record<string, unknown>).mode as GoLaunchConfig['mode'] || 'debug',
325+
mode,
313326
// Default stopOnEntry to false: Delve returns "unknown goroutine" when
314327
// stack traces are requested immediately after stopping on entry.
315328
stopOnEntry: config.stopOnEntry ?? false,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { GoDebugAdapter } from '../../src/index.js';
3+
import type { AdapterDependencies, GenericLaunchConfig } from '@debugmcp/shared';
4+
5+
const deps = {
6+
logger: {
7+
info: () => {},
8+
error: () => {},
9+
debug: () => {},
10+
warn: () => {}
11+
}
12+
} as unknown as AdapterDependencies;
13+
14+
describe('GoDebugAdapter.transformLaunchConfig — mode inference', () => {
15+
let adapter: GoDebugAdapter;
16+
17+
beforeEach(() => {
18+
adapter = new GoDebugAdapter(deps);
19+
});
20+
21+
it('infers mode "debug" for a .go source file', async () => {
22+
const cfg = await adapter.transformLaunchConfig({
23+
program: '/proj/main.go'
24+
} as unknown as GenericLaunchConfig);
25+
26+
expect(cfg.mode).toBe('debug');
27+
expect(cfg.type).toBe('go');
28+
expect(cfg.request).toBe('launch');
29+
});
30+
31+
it('infers mode "exec" for a Windows .exe binary', async () => {
32+
const cfg = await adapter.transformLaunchConfig({
33+
program: 'C:\\proj\\hello.exe'
34+
} as unknown as GenericLaunchConfig);
35+
36+
expect(cfg.mode).toBe('exec');
37+
});
38+
39+
it('infers mode "exec" for a bare Linux binary path with no extension', async () => {
40+
const cfg = await adapter.transformLaunchConfig({
41+
program: '/tmp/myapp'
42+
} as unknown as GenericLaunchConfig);
43+
44+
expect(cfg.mode).toBe('exec');
45+
});
46+
47+
it('treats uppercase .GO extension as source (case-insensitive)', async () => {
48+
const cfg = await adapter.transformLaunchConfig({
49+
program: 'C:\\proj\\HELLO.GO'
50+
} as unknown as GenericLaunchConfig);
51+
52+
expect(cfg.mode).toBe('debug');
53+
});
54+
55+
it('preserves explicit mode "test" against a _test.go program', async () => {
56+
const cfg = await adapter.transformLaunchConfig({
57+
program: '/proj/foo_test.go',
58+
mode: 'test'
59+
} as unknown as GenericLaunchConfig);
60+
61+
expect(cfg.mode).toBe('test');
62+
});
63+
64+
it('preserves explicit mode "exec" even when program is .go (user override wins)', async () => {
65+
const cfg = await adapter.transformLaunchConfig({
66+
program: '/proj/main.go',
67+
mode: 'exec'
68+
} as unknown as GenericLaunchConfig);
69+
70+
expect(cfg.mode).toBe('exec');
71+
});
72+
73+
it('preserves explicit mode "replay" with no program', async () => {
74+
const cfg = await adapter.transformLaunchConfig({
75+
mode: 'replay'
76+
} as unknown as GenericLaunchConfig);
77+
78+
expect(cfg.mode).toBe('replay');
79+
});
80+
81+
it('falls through to "exec" when program is undefined and no explicit mode is set', async () => {
82+
const cfg = await adapter.transformLaunchConfig(
83+
{} as unknown as GenericLaunchConfig
84+
);
85+
86+
expect(cfg.mode).toBe('exec');
87+
});
88+
89+
it('preserves the existing transform behavior alongside mode inference', async () => {
90+
const cfg = await adapter.transformLaunchConfig({
91+
program: '/proj/main.go',
92+
cwd: '/proj',
93+
args: ['--verbose'],
94+
env: { FOO: 'bar' },
95+
stopOnEntry: true
96+
} as unknown as GenericLaunchConfig);
97+
98+
expect(cfg.mode).toBe('debug');
99+
expect(cfg.dlvCwd).toBe('/proj');
100+
expect(cfg.args).toEqual(['--verbose']);
101+
expect(cfg.env).toEqual({ FOO: 'bar' });
102+
expect(cfg.stopOnEntry).toBe(true);
103+
expect(cfg.stackTraceDepth).toBe(50);
104+
expect(cfg.showGlobalVariables).toBe(false);
105+
expect(cfg.hideSystemGoroutines).toBe(true);
106+
});
107+
108+
it('defaults stopOnEntry to false when not provided', async () => {
109+
const cfg = await adapter.transformLaunchConfig({
110+
program: '/proj/main.go'
111+
} as unknown as GenericLaunchConfig);
112+
113+
expect(cfg.stopOnEntry).toBe(false);
114+
});
115+
});

‎tests/e2e/mcp-server-smoke-go.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,10 @@ describe('MCP Server Go Debugging Smoke Test @requires-go', () => {
207207
const bpResponse = parseSdkToolResult(bpResult);
208208
console.log('[Go Smoke Test] Breakpoint response:', bpResponse);
209209

210-
// 3. Start debugging (exec mode for pre-compiled binary)
210+
// 3. Start debugging — scriptPath is a pre-compiled binary, and the
211+
// adapter must auto-infer mode 'exec' from the absence of a .go
212+
// extension. Do NOT pass an explicit mode here; that's the property
213+
// under test.
211214
console.log('[Go Smoke Test] Starting debugging...');
212215
const startResult = await mcpClient!.callTool({
213216
name: 'start_debugging',
@@ -216,7 +219,6 @@ describe('MCP Server Go Debugging Smoke Test @requires-go', () => {
216219
scriptPath: testBinary, // Pre-compiled binary
217220
args: [],
218221
dapLaunchArgs: {
219-
mode: 'exec',
220222
stopOnEntry: false
221223
}
222224
}

0 commit comments

Comments
 (0)