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
@@ -20,6 +20,7 @@ const TEST_ADAPTER: DapResolvedAdapter = {
launchDefaults: {},
attachDefaults: {},
connectMode: "stdio",
acceptsDirectoryProgram: false,
};
const DELAYED_UNIX_SOCKET_ADAPTER = `
@@ -338,24 +339,116 @@ describe("DAP launch failure handling", () => {
});
describe("DebugTool launch validation", () => {
it("rejects directory-valued launch programs before adapter selection", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "omp-debug-program-"));
it("rejects directory programs when the selected adapter cannot debug a directory", async () => {
const dapModule = await import("../../src/dap");
const launchSpy = spyOn(dapModule, "selectLaunchAdapter").mockReturnValue(TEST_ADAPTER);
try {
await fs.mkdir(path.join(cwd, "python"));
const session: ToolSession = {
cwd,
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => "*",
settings: Settings.isolated({ "debug.enabled": true }),
};
const tool = new DebugTool(session);
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "omp-debug-program-"));
try {
await fs.mkdir(path.join(cwd, "python"));
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: "python" })).rejects.toThrow(
/launch program resolves to a directory.*python/,
);
await expect(tool.execute("call", { action: "launch", program: "python" })).rejects.toThrow(
/launch program resolves to a directory.*python/,
);
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
} finally {
await fs.rm(cwd, { recursive: true, force: true });
launchSpy.mockRestore();
}
});
it("allows directory programs when the selected adapter accepts them (dlv on a Go package)", async () => {
const dapModule = await import("../../src/dap");
const dlvAdapter: DapResolvedAdapter = {
...TEST_ADAPTER,
name: "dlv",
command: "dlv",
resolvedCommand: "dlv",
launchDefaults: { request: "launch", mode: "debug", stopOnEntry: true },
acceptsDirectoryProgram: true,
};
const launchSpy = spyOn(dapModule, "selectLaunchAdapter").mockReturnValue(dlvAdapter);
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-dir-"));
try {
await fs.mkdir(path.join(cwd, "cmd"));
const session: ToolSession = {
cwd,
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => "*",
settings: Settings.isolated({ "debug.enabled": true }),
};
const tool = new DebugTool(session);
// Validation must pass and propagate to dapSessionManager.launch; we
// stop the actual spawn there and inspect the launch arguments.
await expect(tool.execute("call", { action: "launch", program: "cmd", adapter: "dlv" })).rejects.toThrow(
/captured launch/,
);
expect(sessionLaunchSpy).toHaveBeenCalledTimes(1);
const [opts] = sessionLaunchSpy.mock.calls[0]!;
expect(opts.extraLaunchArguments).toEqual({ mode: "debug" });
expect(opts.program).toBe(path.join(cwd, "cmd"));
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
} finally {
sessionLaunchSpy.mockRestore();
launchSpy.mockRestore();
}
});
it("dlv launch with a compiled binary switches mode from debug to exec", async () => {
const dapModule = await import("../../src/dap");
const dlvAdapter: DapResolvedAdapter = {
...TEST_ADAPTER,
name: "dlv",
command: "dlv",
resolvedCommand: "dlv",
launchDefaults: { request: "launch", mode: "debug", stopOnEntry: true },
acceptsDirectoryProgram: true,
};
const launchSpy = spyOn(dapModule, "selectLaunchAdapter").mockReturnValue(dlvAdapter);
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-exec-"));
try {
await fs.writeFile(path.join(cwd, "hello"), "#!/usr/bin/env sh\necho hi\n");
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: "hello", adapter: "dlv" })).rejects.toThrow(
/captured launch/,
);
const [opts] = sessionLaunchSpy.mock.calls[0]!;
expect(opts.extraLaunchArguments).toEqual({ mode: "exec" });
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
} finally {
sessionLaunchSpy.mockRestore();
launchSpy.mockRestore();
}
});