fix(debug): prefer dlv for go directory launches

Prefer directory-capable adapters when selecting a launch adapter for a
resolved directory program. This keeps Go package directories on dlv in
mixed projects that also expose native-debugger root markers such as a
Makefile, instead of selecting gdb/lldb-dap first and rejecting the
directory during validation.

Added a regression test covering a Go module with both go.mod and
Makefile plus local dlv/gdb adapter shims.

Fixes #2020
This commit is contained in:
roboomp
2026-06-07 03:03:57 +00:00
parent 1745529368
commit 8c25f7d4aa
4 changed files with 48 additions and 4 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
### Fixed
- Fixed `debug` tool refusing every `dlv` launch on Go modules. The launch handler 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). Adapter resolution now precedes validation, the rejection only fires when the resolved adapter does not advertise `acceptsDirectoryProgram` (set on `dlv` in `dap/defaults.json`), and dlv's `mode` is derived from the program shape — directories and `.go` files launch as `mode=debug`, other files as `mode=exec` — so `omp` can debug both Go packages and pre-built binaries ([#2020](https://github.com/can1357/oh-my-pi/issues/2020)).
- Fixed `debug` tool refusing every `dlv` launch on Go modules. The launch handler 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). Adapter resolution now precedes validation, directory programs prefer adapters that advertise `acceptsDirectoryProgram` before falling back to native extensionless debuggers, the rejection only fires when the resolved adapter does not advertise that flag (set on `dlv` in `dap/defaults.json`), and dlv's `mode` is derived from the program shape — directories and `.go` files launch as `mode=debug`, other files as `mode=exec` — so `omp` can debug both Go packages and pre-built binaries ([#2020](https://github.com/can1357/oh-my-pi/issues/2020)).
## [15.10.0] - 2026-06-06
+9 -2
View File
@@ -126,12 +126,19 @@ function sortAdaptersForLaunch(program: string, cwd: string, adapters: DapResolv
return rootAware.map(entry => entry.adapter);
}
export function selectLaunchAdapter(program: string, cwd: string, adapterName?: string): DapResolvedAdapter | null {
export function selectLaunchAdapter(
program: string,
cwd: string,
adapterName?: string,
programKind: LaunchProgramKind = "file",
): DapResolvedAdapter | null {
if (adapterName) {
return resolveAdapter(adapterName, cwd);
}
const matches = getMatchingAdapters(program, cwd);
const sorted = sortAdaptersForLaunch(program, cwd, matches);
const candidates =
programKind === "directory" ? matches.filter(adapter => adapter.acceptsDirectoryProgram) : matches;
const sorted = sortAdaptersForLaunch(program, cwd, candidates.length > 0 ? candidates : matches);
return sorted[0] ?? null;
}
+1 -1
View File
@@ -687,7 +687,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);
const programKind = await classifyLaunchProgram(program);
const adapter = selectLaunchAdapter(program, commandCwd, params.adapter);
const adapter = selectLaunchAdapter(program, commandCwd, params.adapter, programKind);
if (!adapter) {
if (params.adapter === "debugpy") {
throw new ToolError("adapter 'debugpy' is not available: python not found in PATH");
@@ -411,6 +411,43 @@ describe("DebugTool launch validation", () => {
}
});
it("prefers directory-capable dlv over native adapters for extensionless Go package directories", async () => {
const dapModule = await import("../../src/dap");
const sessionLaunchSpy = spyOn(dapModule.dapSessionManager, "launch").mockImplementation(async opts => {
throw Object.assign(new Error("captured launch"), { capturedOptions: opts });
});
try {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "omp-debug-dlv-mixed-roots-"));
try {
await fs.writeFile(path.join(cwd, "go.mod"), "module hello\n\ngo 1.22\n");
await fs.writeFile(path.join(cwd, "Makefile"), "all:\n\tgo build ./...\n");
await fs.mkdir(path.join(cwd, "bin"));
await fs.writeFile(path.join(cwd, "bin", "dlv"), "");
await fs.writeFile(path.join(cwd, "bin", "gdb"), "");
await fs.mkdir(path.join(cwd, "cmd", "hello"), { recursive: true });
const session: ToolSession = {
cwd,
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => "*",
settings: Settings.isolated({ "debug.enabled": true }),
};
const tool = new DebugTool(session);
await expect(tool.execute("call", { action: "launch", program: "cmd/hello" })).rejects.toThrow(
/captured launch/,
);
const [opts] = sessionLaunchSpy.mock.calls[0]!;
expect(opts.adapter.name).toBe("dlv");
expect(opts.extraLaunchArguments).toEqual({ mode: "debug" });
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
} finally {
sessionLaunchSpy.mockRestore();
}
});
it("dlv launch with a compiled binary switches mode from debug to exec", async () => {
const dapModule = await import("../../src/dap");
const dlvAdapter: DapResolvedAdapter = {