import { afterEach, describe, expect, it, vi } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { getAdapterConfigs, resolveAdapter, selectLaunchAdapter } from "../../src/dap/config"; import { injectPluginDirRoots } from "../../src/discovery/helpers"; const tempDirs: string[] = []; const ORIGINAL_OMP_PLUGIN_DIR = process.env.OMP_PLUGIN_DIR; const ORIGINAL_OMP_MARKETPLACE_DIR = process.env.OMP_MARKETPLACE_DIR; async function makeTempDir(prefix: string): Promise { const cwd = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); tempDirs.push(cwd); return cwd; } afterEach(async () => { vi.restoreAllMocks(); if (ORIGINAL_OMP_PLUGIN_DIR === undefined) { delete process.env.OMP_PLUGIN_DIR; } else { process.env.OMP_PLUGIN_DIR = ORIGINAL_OMP_PLUGIN_DIR; } if (ORIGINAL_OMP_MARKETPLACE_DIR === undefined) { delete process.env.OMP_MARKETPLACE_DIR; } else { process.env.OMP_MARKETPLACE_DIR = ORIGINAL_OMP_MARKETPLACE_DIR; } await injectPluginDirRoots(os.homedir(), []); await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true }))); }); describe("DAP adapter configuration", () => { it("loads a custom adapter from dap.json and selects it by file extension", async () => { const cwd = await makeTempDir("omp-dap-config-json-"); await fs.writeFile(path.join(cwd, "pom.xml"), "\n"); await fs.mkdir(path.join(cwd, "src"), { recursive: true }); await fs.writeFile(path.join(cwd, "src", "Main.java"), "class Main {}\n"); await fs.writeFile( path.join(cwd, "dap.json"), JSON.stringify({ adapters: { "custom-jvm": { command: "bun", args: ["run", "debug-adapter"], languages: ["java", "kotlin"], fileTypes: [".java", ".kt"], rootMarkers: ["pom.xml", "build.gradle.kts"], launchDefaults: { request: "launch", mainClass: "" }, attachDefaults: { request: "attach", host: "127.0.0.1" }, }, }, }), ); const adapter = resolveAdapter("custom-jvm", cwd); expect(adapter?.name).toBe("custom-jvm"); expect(adapter?.command).toBe("bun"); expect(adapter?.args).toEqual(["run", "debug-adapter"]); expect(adapter?.languages).toEqual(["java", "kotlin"]); expect(adapter?.fileTypes).toEqual([".java", ".kt"]); expect(adapter?.launchDefaults).toEqual({ request: "launch", mainClass: "" }); expect(adapter?.attachDefaults).toEqual({ request: "attach", host: "127.0.0.1" }); const selected = selectLaunchAdapter(path.join("src", "Main.java"), cwd); expect(selected?.name).toBe("custom-jvm"); }); it("merges partial user overrides over built-in adapters", async () => { const cwd = await makeTempDir("omp-dap-config-override-"); await fs.writeFile(path.join(cwd, "script.py"), "print('hi')\n"); await fs.writeFile( path.join(cwd, "dap.json"), JSON.stringify({ adapters: { debugpy: { args: ["-m", "debugpy.adapter", "--log-dir", ".debugpy-logs"], launchDefaults: { justMyCode: false }, }, }, }), ); const config = getAdapterConfigs(cwd).debugpy; expect(config.command).toBe("python"); expect(config.args).toEqual(["-m", "debugpy.adapter", "--log-dir", ".debugpy-logs"]); expect(config.fileTypes).toContain(".py"); expect(config.launchDefaults).toMatchObject({ request: "launch", justMyCode: false }); }); it("loads adapter config from project config directories and YAML", async () => { const cwd = await makeTempDir("omp-dap-config-yaml-"); await fs.mkdir(path.join(cwd, ".omp"), { recursive: true }); await fs.writeFile(path.join(cwd, "build.gradle.kts"), "plugins {}\n"); await fs.writeFile(path.join(cwd, "Main.kt"), "fun main() {}\n"); await fs.writeFile( path.join(cwd, ".omp", "dap.yaml"), [ "adapters:", " yaml-kotlin:", " command: bun", " args:", " - run", " - kotlin-debug-adapter", " languages:", " - kotlin", " fileTypes:", " - .kt", " rootMarkers:", " - build.gradle.kts", " launchDefaults:", " request: launch", " projectRoot: .", "", ].join("\n"), ); const selected = selectLaunchAdapter("Main.kt", cwd); expect(selected?.name).toBe("yaml-kotlin"); expect(selected?.launchDefaults).toEqual({ request: "launch", projectRoot: "." }); }); it("resolves relative adapter commands from the debug cwd", async () => { const cwd = await makeTempDir("omp-dap-config-relative-command-"); const command = path.join(cwd, "tools", process.platform === "win32" ? "debug-adapter.cmd" : "debug-adapter"); await fs.mkdir(path.dirname(command), { recursive: true }); await fs.writeFile(command, ""); await fs.chmod(command, 0o755); await fs.writeFile( path.join(cwd, "dap.json"), JSON.stringify({ adapters: { relative: { command: process.platform === "win32" ? ".\\tools\\debug-adapter.cmd" : "./tools/debug-adapter", fileTypes: [".rel"], }, }, }), ); const adapter = resolveAdapter("relative", cwd); expect(adapter?.command).toBe( process.platform === "win32" ? ".\\tools\\debug-adapter.cmd" : "./tools/debug-adapter", ); expect(adapter?.resolvedCommand).toBe(command); }); it("loads plugin DAP adapters from plugin config files", async () => { const cwd = await makeTempDir("omp-dap-config-plugin-"); const pluginRoot = path.join(cwd, "plugins", "acme-debug"); await fs.mkdir(path.join(pluginRoot, ".claude-plugin"), { recursive: true }); await fs.writeFile(path.join(cwd, "app.rb"), "puts 'hi'\n"); await fs.writeFile( path.join(pluginRoot, ".claude-plugin", "plugin.json"), JSON.stringify({ name: "acme-debug" }), ); await fs.writeFile( path.join(pluginRoot, ".dap.json"), JSON.stringify({ adapters: { "acme-ruby": { command: "ruby-debug-adapter", fileTypes: [".rb"], }, }, }), ); process.env.OMP_PLUGIN_DIR = path.join(cwd, "plugins"); process.env.OMP_MARKETPLACE_DIR = path.join(cwd, "marketplaces"); await injectPluginDirRoots(cwd, [pluginRoot], cwd); expect(getAdapterConfigs(cwd)["acme-ruby"]?.command).toBe("ruby-debug-adapter"); }); it("ignores invalid custom adapters without discarding valid configs", async () => { const cwd = await makeTempDir("omp-dap-config-invalid-"); await fs.writeFile( path.join(cwd, "dap.json"), JSON.stringify({ adapters: { "missing-command": { fileTypes: [".bad"], }, valid: { command: "bun", fileTypes: [".ok"], rootMarkers: ["."], }, }, }), ); const config = getAdapterConfigs(cwd); expect(config["missing-command"]).toBeUndefined(); expect(config.valid?.command).toBe("bun"); }); });