From d5b4491db13d3f69542e314d0ea073f46f41d21f Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:52:58 -0800 Subject: [PATCH] feat: add tests and helper for using run.executable for interpreter not activatedRun --- .github/copilot-instructions.md | 5 +++ .../unittest/adapter/factory.unit.test.ts | 24 +++++++++++ src/test/unittest/common/helpers.ts | 42 +++++++++++++++++++ .../unittest/common/pythonTrue.unit.test.ts | 40 +++++++++++++++++- 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..8f03f25c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,5 @@ +# Copilot Instructions for vscode-python-debugger + +## Learnings + +- Always use `run.executable` (the actual Python binary path) instead of `activatedRun.executable` for interpreter identification in `getInterpreterDetails`, `getSettingsPythonPath`, and `getExecutableCommand`. `activatedRun.executable` may be a wrapper command (e.g. `pixi run python`) set by environment managers like pixi or conda, which breaks the debugger if used as a replacement for the binary. (1) diff --git a/src/test/unittest/adapter/factory.unit.test.ts b/src/test/unittest/adapter/factory.unit.test.ts index df17088d..029ac48a 100644 --- a/src/test/unittest/adapter/factory.unit.test.ts +++ b/src/test/unittest/adapter/factory.unit.test.ts @@ -32,6 +32,7 @@ import * as telemetryReporter from '../../../extension/telemetry/reporter'; import * as vscodeApi from '../../../extension/common/vscodeapi'; import { DebugConfigStrings } from '../../../extension/common/utils/localize'; import { PythonEnvironment } from '../../../extension/envExtApi'; +import { buildPythonEnvironmentWithActivatedRun } from '../common/helpers'; use(chaiAsPromised); @@ -340,4 +341,27 @@ suite('Debugging - Adapter Factory', () => { assert.deepStrictEqual(descriptor, debugExecutable); }); + + test('Use run.executable rather than activatedRun.executable for interpreter identification', async () => { + // Simulates environment managers like pixi/conda that set activatedRun to a wrapper + // command (e.g. "pixi run python") while run.executable is the actual Python binary. + const actualPythonPath = 'path/to/actual/python3'; + const wrapperCommand = 'pixi'; + const interpreterWithWrapper = buildPythonEnvironmentWithActivatedRun( + actualPythonPath, + wrapperCommand, + '3.10.0', + ['run', 'python'], + ); + const session = createSession({}); + // The debug adapter should use the actual Python binary, not the wrapper + const debugExecutable = new DebugAdapterExecutable(interpreterWithWrapper.execInfo.run.executable, [ + debugAdapterPath, + ]); + getInterpreterDetailsStub.resolves({ path: [interpreterWithWrapper.execInfo.run.executable] }); + resolveEnvironmentStub.resolves(interpreterWithWrapper); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); }); diff --git a/src/test/unittest/common/helpers.ts b/src/test/unittest/common/helpers.ts index 6ce8d848..e7ae446c 100644 --- a/src/test/unittest/common/helpers.ts +++ b/src/test/unittest/common/helpers.ts @@ -28,3 +28,45 @@ export function buildPythonEnvironment(execPath: string, version: string, sysPre sysPrefix, } as PythonEnvironment; } + +/** + * Helper to build a PythonEnvironment where activatedRun differs from run. + * This simulates environment managers like pixi or conda that set activatedRun + * to a wrapper command (e.g. 'pixi run python') while run.executable points to + * the actual Python binary. + * + * @param execPath string - path to the actual python executable (run.executable) + * @param activatedRunExecutable string - path/command for the wrapper (activatedRun.executable) + * @param version string - python version string (e.g. '3.9.0') + * @param activatedRunArgs string[] - optional args for activatedRun + */ +export function buildPythonEnvironmentWithActivatedRun( + execPath: string, + activatedRunExecutable: string, + version: string, + activatedRunArgs: string[] = [], +): PythonEnvironment { + const execUri = Uri.file(execPath); + return { + envId: { + id: execUri.fsPath, + managerId: 'Venv', + }, + name: `Python ${version}`, + displayName: `Python ${version}`, + displayPath: execUri.fsPath, + version: version, + environmentPath: execUri, + execInfo: { + run: { + executable: execUri.fsPath, + args: [], + }, + activatedRun: { + executable: activatedRunExecutable, + args: activatedRunArgs, + }, + }, + sysPrefix: '', + } as PythonEnvironment; +} diff --git a/src/test/unittest/common/pythonTrue.unit.test.ts b/src/test/unittest/common/pythonTrue.unit.test.ts index cccb587e..68ec69aa 100644 --- a/src/test/unittest/common/pythonTrue.unit.test.ts +++ b/src/test/unittest/common/pythonTrue.unit.test.ts @@ -9,7 +9,7 @@ import { Uri, Disposable, Extension, extensions } from 'vscode'; import * as path from 'path'; import * as pythonApi from '../../../extension/common/python'; import * as utilities from '../../../extension/common/utilities'; -import { buildPythonEnvironment } from './helpers'; +import { buildPythonEnvironment, buildPythonEnvironmentWithActivatedRun } from './helpers'; // Platform-specific path constants using path.join so tests assert using native separators. // Leading root '/' preserved; on Windows this yields a leading backslash (e.g. '\\usr\\bin'). @@ -166,6 +166,24 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { expect(result).to.be.undefined; }); + + test('Should use run.executable instead of activatedRun.executable when they differ', async () => { + // Simulates environment managers like pixi/conda that set activatedRun to a wrapper command + const actualPythonPath = PYTHON_PATH; + const wrapperCommand = path.join('/', 'usr', 'local', 'bin', 'pixi'); + const mockPythonEnv = buildPythonEnvironmentWithActivatedRun(actualPythonPath, wrapperCommand, '3.9.0', [ + 'run', + 'python', + ]); + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv); + mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv); + + const result = await pythonApi.getSettingsPythonPath(); + + // Should return the actual Python binary path, not the wrapper command + expect(result).to.deep.equal([actualPythonPath]); + }); }); suite('getEnvironmentVariables', () => { @@ -374,6 +392,26 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => { expect(result.path).to.be.undefined; }); + + test('Should use run.executable instead of activatedRun.executable when they differ', async () => { + // Simulates environment managers like pixi/conda that set activatedRun to a wrapper command + const actualPythonPath = PYTHON_PATH; + const wrapperCommand = path.join('/', 'usr', 'local', 'bin', 'pixi'); + const mockEnv = buildPythonEnvironmentWithActivatedRun(actualPythonPath, wrapperCommand, '3.9.0', [ + 'run', + 'python', + ]); + + (mockEnvsExtension as any).exports = mockPythonEnvApi; + mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(actualPythonPath) }); + mockPythonEnvApi.resolveEnvironment.resolves(mockEnv); + + const result = await pythonApi.getInterpreterDetails(); + + // Should return the actual Python binary path, not the wrapper command + expect(result.path).to.deep.equal([actualPythonPath]); + expect(result.resource).to.be.undefined; + }); }); suite('onDidChangePythonInterpreter event', () => {