import { describe, expect, it } from "vitest"; import type { RunConfig } from "@/platform/ipc"; import { debugUnsupportedReason, launchConfigFor } from "../launch"; const rc = (p: Partial): RunConfig => ({ id: "x", name: "cfg", kind: "shell", command: "true", args: [], cwd: null, env: {}, detected: true, isTest: false, debugAdapter: null, ...p, }); const extra = (l: { extraJson: string | null } | null) => JSON.parse(l?.extraJson ?? "{}"); describe("run config → launch config", () => { it("returns null without an adapter", () => { expect(launchConfigFor(rc({}), "/p")).toBeNull(); }); it("php-debug: the script runs on the PHP from Settings with Xdebug's debug mode, on a free port", () => { const xdebug = ["-dxdebug.mode=debug", "-dxdebug.start_with_request=yes", "-dxdebug.client_port=${port}"]; // `artisan serve` with PHP at Default: the script is a file of the project. const serve = launchConfigFor( rc({ kind: "php", command: "php", args: ["artisan", "serve"], debugAdapter: "php-debug" }), "/app", "/opt/php@8.3/bin/php", )!; expect(serve).toMatchObject({ adapter: "php-debug", program: "/app/artisan", args: ["serve"], cwd: "/app" }); expect(extra(serve)).toEqual({ port: 0, runtimeArgs: xdebug, runtimeExecutable: "/opt/php@8.3/bin/php" }); // The PHP the command names wins, and its options stay with it. const file = launchConfigFor( rc({ kind: "php", command: "/usr/bin/php8.4", args: ["-dmemory_limit=1G", "src/a.php", "x"], debugAdapter: "php-debug", }), "/app", "/opt/php@8.3/bin/php", )!; expect(file).toMatchObject({ program: "/app/src/a.php", args: ["x"] }); expect(extra(file)).toEqual({ port: 0, runtimeArgs: [...xdebug, "-dmemory_limit=1G"], runtimeExecutable: "/usr/bin/php8.4", }); // PHPUnit runs its script directly; without a PHP in Settings, php-debug uses the `php` on PATH. const unit = launchConfigFor( rc({ kind: "phpunit", command: "vendor/bin/phpunit", args: ["--filter", "testA"], debugAdapter: "php-debug" }), "/app", )!; expect(unit).toMatchObject({ program: "/app/vendor/bin/phpunit", args: ["--filter", "testA"] }); expect(extra(unit)).toEqual({ port: 0, runtimeArgs: xdebug }); }); it("cargo run builds the binary through codelldb's cargo integration", () => { const l = launchConfigFor( rc({ kind: "cargo", command: "cargo", args: ["run", "-p", "app", "--bin", "app", "--", "--port", "1"], debugAdapter: "codelldb", }), "/p", )!; expect(l.adapter).toBe("codelldb"); expect(l.program).toBeNull(); expect(l.args).toEqual(["--port", "1"]); expect(l.cwd).toBe("/p"); expect(extra(l)).toEqual({ cargo: { args: ["build", "-p", "app", "--bin", "app"], filter: { name: "app", kind: "bin" } }, sourceLanguages: ["rust"], }); }); it("cargo test builds test binaries without running", () => { const l = launchConfigFor( rc({ kind: "cargo", command: "cargo", args: ["test", "--workspace"], debugAdapter: "codelldb" }), "/p", )!; expect(extra(l).cargo).toEqual({ args: ["test", "--no-run", "--workspace"] }); }); it("debugpy: script, module, console script, venv interpreter", () => { const script = launchConfigFor( rc({ kind: "python", command: ".venv/bin/python", args: ["manage.py", "runserver"], debugAdapter: "debugpy" }), "/p", )!; expect(script.program).toBe("/p/manage.py"); expect(script.args).toEqual(["runserver"]); expect(extra(script)).toMatchObject({ python: "/p/.venv/bin/python", django: true, console: "internalConsole" }); const mod = launchConfigFor( rc({ kind: "python", command: "python3", args: ["-m", "app", "-v"], debugAdapter: "debugpy" }), "/p", )!; expect(mod.program).toBeNull(); expect(mod.args).toEqual(["-v"]); expect(extra(mod)).toMatchObject({ module: "app", python: "python3" }); const pytest = launchConfigFor( rc({ kind: "pytest", command: ".venv/bin/pytest", args: ["-x"], debugAdapter: "debugpy" }), "/p", )!; expect(extra(pytest)).toMatchObject({ module: "pytest", python: "/p/.venv/bin/python" }); expect(pytest.args).toEqual(["-x"]); const globalPytest = launchConfigFor(rc({ kind: "pytest", command: "pytest", debugAdapter: "debugpy" }), "/p")!; expect(extra(globalPytest).python).toBe("python3"); }); it("debugpy: uv / poetry run the debuggee in the project's environment", () => { const debug = (command: string, args: string[]) => launchConfigFor(rc({ kind: "python", command, args, debugAdapter: "debugpy" }), "/p")!; const script = debug("uv", ["run", "python", "main.py", "-v"]); expect(script.program).toBe("/p/main.py"); expect(script.args).toEqual(["-v"]); expect(extra(script)).toMatchObject({ python: ["uv", "run", "python"], debugLauncherPython: "python3" }); expect(extra(script).module).toBeUndefined(); const mod = debug("poetry", ["run", "python3", "-m", "app"]); expect(extra(mod)).toMatchObject({ module: "app", python: ["poetry", "run", "python3"] }); const tool = debug("uv", ["run", "pytest", "-x"]); expect(extra(tool)).toMatchObject({ module: "pytest", python: ["uv", "run", "python"] }); expect(tool.args).toEqual(["-x"]); const django = debug("pdm", ["run", "python", "manage.py", "runserver"]); expect(extra(django)).toMatchObject({ django: true, python: ["pdm", "run", "python"] }); }); it("js-debug: npm scripts run through the package manager, node scripts directly", () => { const npm = launchConfigFor( rc({ kind: "npm", command: "pnpm", args: ["run", "dev"], cwd: "web", debugAdapter: "js-debug" }), "/p", )!; expect(npm.cwd).toBe("/p/web"); expect(npm.program).toBeNull(); expect(extra(npm)).toMatchObject({ type: "pwa-node", runtimeExecutable: "pnpm", runtimeArgs: ["run", "dev"] }); const node = launchConfigFor(rc({ command: "node", args: ["server.js", "--x"], debugAdapter: "js-debug" }), "/p")!; expect(node.program).toBe("/p/server.js"); expect(node.args).toEqual(["--x"]); expect(extra(node).runtimeExecutable).toBeUndefined(); // A located Node.js (e.g. 22.6+ for TypeScript) is used instead of PATH's. const ts = launchConfigFor( rc({ command: "/opt/node22/bin/node", args: ["a.ts"], debugAdapter: "js-debug" }), "/p", )!; expect(ts.program).toBe("/p/a.ts"); expect(extra(ts).runtimeExecutable).toBe("/opt/node22/bin/node"); }); it("js-debug: TypeScript through a runner or type-stripping flag runs as the runtime", () => { const flag = launchConfigFor( rc({ kind: "node", command: "/bin/node", args: ["--experimental-strip-types", "a.ts"], debugAdapter: "js-debug", }), "/p", )!; expect(flag.program).toBeNull(); expect(extra(flag)).toMatchObject({ runtimeExecutable: "/bin/node", runtimeArgs: ["--experimental-strip-types", "a.ts"], }); const tsx = launchConfigFor( rc({ kind: "node", command: "npx", args: ["tsx", "a.ts"], debugAdapter: "js-debug" }), "/p", )!; expect(extra(tsx)).toMatchObject({ runtimeExecutable: "npx", runtimeArgs: ["tsx", "a.ts"] }); }); it("deno: waits on its inspector at a free port that js-debug attaches to", () => { const l = launchConfigFor( rc({ kind: "deno", command: "/bin/deno", args: ["run", "-A", "--inspect", "main.ts", "x"], debugAdapter: "js-debug", }), "/p", )!; expect(l.adapter).toBe("js-debug"); expect(l.program).toBeNull(); expect(extra(l)).toMatchObject({ type: "pwa-node", runtimeExecutable: "/bin/deno", runtimeArgs: ["run", "--inspect-wait=127.0.0.1:${freePort}", "-A", "main.ts", "x"], attachSimplePort: "${freePort}", }); const test = launchConfigFor( rc({ kind: "deno", command: "deno", args: ["test"], debugAdapter: "js-debug" }), "/p", )!; expect(extra(test).runtimeArgs).toEqual(["test", "--inspect-wait=127.0.0.1:${freePort}"]); }); it("deno task and Bun can't be debugged, with a reason", () => { const task = rc({ kind: "deno", command: "deno", args: ["task", "dev"], debugAdapter: "js-debug" }); expect(launchConfigFor(task, "/p")).toBeNull(); expect(debugUnsupportedReason(task)).toContain("deno run"); const bun = rc({ kind: "bun", command: "/bin/bun", args: ["run", "a.ts"], debugAdapter: "js-debug" }); expect(launchConfigFor(bun, "/p")).toBeNull(); expect(debugUnsupportedReason(bun)).toContain("WebKit"); // `bun run