fix(debug): accept directory programs for dlv and auto-select dlv mode

The debug tool ran validateLaunchProgram before adapter selection and
rejected any directory program with `launch program resolves to a
directory`, while dlv's default `mode=debug` requires a Go package
path (a directory or .go source file). Every Go-module launch failed:
passing the module dir was rejected outright, and passing the compiled
binary failed at dlv with `not a valid go module`.

- Add `acceptsDirectoryProgram` to DapAdapterConfig/DapResolvedAdapter
  and flag dlv in dap/defaults.json.
- In DebugTool.execute(launch), resolve the adapter first, then call
  validateLaunchProgram with the resolved adapter — the directory
  rejection only fires when the adapter does not advertise the flag.
- Add resolveLaunchOverrides in dap/config.ts: for dlv, derive `mode`
  from the program shape (directory or .go file → debug; other file
  → exec). Plumbed through DapLaunchSessionOptions.extraLaunchArguments
  and spread between adapter.launchDefaults and the hard-coded launch
  fields in session.ts.
- Refresh tests: cover the no-dir-support adapter rejection, dlv on a
  package directory keeping mode=debug, and dlv on a binary switching
  to mode=exec.

Fixes #2020
This commit is contained in:
roboomp
2026-06-07 02:59:02 +00:00
parent fe607cf22c
commit 1745529368
7 changed files with 175 additions and 22 deletions
+19 -7
View File
@@ -22,6 +22,7 @@ import {
type DapFunctionBreakpointRecord,
type DapInstructionBreakpointRecord,
type DapModule,
type DapResolvedAdapter,
type DapScope,
type DapSessionSummary,
type DapSource,
@@ -30,6 +31,8 @@ import {
type DapVariable,
dapSessionManager,
getAvailableAdapters,
type LaunchProgramKind,
resolveLaunchOverrides,
selectAttachAdapter,
selectLaunchAdapter,
} from "../dap";
@@ -489,16 +492,23 @@ function getConfiguredAdapters(cwd: string): string {
const adapters = getAvailableAdapters(cwd).map(adapter => adapter.name);
return adapters.length > 0 ? adapters.join(", ") : "none";
}
async function validateLaunchProgram(program: string, cwd: string): Promise<void> {
let isDirectory: boolean;
async function classifyLaunchProgram(program: string): Promise<LaunchProgramKind> {
try {
isDirectory = (await fs.stat(program)).isDirectory();
return (await fs.stat(program)).isDirectory() ? "directory" : "file";
} catch (error) {
if (isEnoent(error)) return;
if (isEnoent(error)) return "missing";
throw error;
}
if (!isDirectory) return;
}
function validateLaunchProgram(
program: string,
cwd: string,
programKind: LaunchProgramKind,
adapter: DapResolvedAdapter,
): void {
if (programKind !== "directory" || adapter.acceptsDirectoryProgram) return;
const displayPath = formatPathRelativeToCwd(program, cwd, { trailingSlash: true });
throw new ToolError(
`launch program resolves to a directory: ${displayPath}. Pass an executable file path, or for Python use adapter "debugpy" with program set to the .py file.`,
@@ -676,7 +686,7 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
}
const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
const program = resolveToCwd(params.program, commandCwd);
await validateLaunchProgram(program, commandCwd);
const programKind = await classifyLaunchProgram(program);
const adapter = selectLaunchAdapter(program, commandCwd, params.adapter);
if (!adapter) {
if (params.adapter === "debugpy") {
@@ -686,8 +696,10 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
`No debugger adapter available. Installed adapters: ${getConfiguredAdapters(commandCwd)}`,
);
}
validateLaunchProgram(program, commandCwd, programKind, adapter);
const extraLaunchArguments = resolveLaunchOverrides(adapter, program, programKind);
const snapshot = await dapSessionManager.launch(
{ adapter, program, args: params.args, cwd: commandCwd },
{ adapter, program, args: params.args, cwd: commandCwd, extraLaunchArguments },
combinedSignal,
timeoutSec * 1000,
);