From 7297012c2a74ffa40d43747ec781486b1985c356 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:25:01 -0500 Subject: [PATCH 01/12] DH-21980_python-managed-servers/task-01: Add TypeScript types for Python Environments API to extensionApiUtils.ts. Currently only has MsPythonExtensionApi interface for old ms-python.python API. Need to add PythonEnvironmentApi interface matching the new ms-python.vscode-python-envs extension API including: PythonEnvironment, PythonEnvironmentId, PythonEnvironmentExecutionInfo, Package, PackageId interfaces and getEnvironment, getPackages, onDidChangePackages methods. Added PythonEnvironmentApi interface with getEnvironment, getPackages, onDidChangePackages methods; added supporting interfaces PythonEnvironment, PythonEnvironmentExecutionInfo, PythonEnvironmentId, Package, PackageId, DidChangePackagesEventArgs, PackageChangeKind enum, GetEnvironmentScope type, and getPythonEnvironmentApi() getter function to extensionApiUtils.ts. TypeScript compiles with no errors. (#DH-21980) --- src/util/extensionApiUtils.ts | 79 +++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/util/extensionApiUtils.ts b/src/util/extensionApiUtils.ts index f7bdc675b..02679aac1 100644 --- a/src/util/extensionApiUtils.ts +++ b/src/util/extensionApiUtils.ts @@ -3,6 +3,7 @@ import type { ExtensionInfo, ExtensionVersion, McpVersion } from '../types'; import { uniqueId } from './idUtils'; const MS_PYTHON_EXTENSION_ID = 'ms-python.python'; +const MS_PYTHON_ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; /** Microsoft Python extension api */ interface MsPythonExtensionApi { @@ -11,6 +12,75 @@ interface MsPythonExtensionApi { }; } +/** Options for executing a Python executable */ +interface PythonCommandRunConfiguration { + executable: string; + args?: string[]; +} + +/** Execution details for a Python environment */ +interface PythonEnvironmentExecutionInfo { + run: PythonCommandRunConfiguration; + activatedRun?: PythonCommandRunConfiguration; + activation?: PythonCommandRunConfiguration[]; + deactivation?: PythonCommandRunConfiguration[]; +} + +/** Unique identifier for a Python environment */ +interface PythonEnvironmentId { + id: string; + managerId: string; +} + +/** A Python environment from the ms-python.vscode-python-envs extension */ +interface PythonEnvironment { + readonly envId: PythonEnvironmentId; + readonly name: string; + readonly displayName: string; + readonly displayPath: string; + readonly version: string; + readonly environmentPath: vscode.Uri; + readonly execInfo: PythonEnvironmentExecutionInfo; + readonly sysPrefix: string; + readonly description?: string; +} + +/** Unique identifier for a Python package */ +interface PackageId { + id: string; + managerId: string; + environmentId: string; +} + +/** A Python package from the ms-python.vscode-python-envs extension */ +interface Package { + readonly pkgId: PackageId; + readonly name: string; + readonly displayName: string; + readonly version?: string; + readonly description?: string; +} + +export enum PackageChangeKind { + add = 'add', + remove = 'remove', +} + +/** Arguments for the onDidChangePackages event */ +interface DidChangePackagesEventArgs { + environment: PythonEnvironment; + changes: { kind: PackageChangeKind; pkg: Package }[]; +} + +export type GetEnvironmentScope = undefined | vscode.Uri; + +/** Python Environments extension API (ms-python.vscode-python-envs) */ +interface PythonEnvironmentApi { + getEnvironment(scope: GetEnvironmentScope): Promise; + getPackages(environment: PythonEnvironment): Promise; + onDidChangePackages: vscode.Event; +} + /** Create ExtensionInfo from the ExtensionContext */ export function createExtensionInfo( context: vscode.ExtensionContext @@ -54,3 +124,12 @@ export function getMsPythonExtensionApi(): MS_PYTHON_EXTENSION_ID ); } + +/** Get the Python Environments extension api (ms-python.vscode-python-envs) */ +export function getPythonEnvironmentApi(): + | vscode.Extension + | undefined { + return vscode.extensions.getExtension( + MS_PYTHON_ENVS_EXTENSION_ID + ); +} From 56f223236845edaff8a54dfca8e10315a342f6ef Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:25:38 -0500 Subject: [PATCH 02/12] DH-21980_python-managed-servers/task-02: Add getPythonEnvsExtensionApi function to extensionApiUtils.ts. Currently exports getMsPythonExtensionApi for ms-python.python extension. Add similar function for ms-python.vscode-python-envs extension that returns vscode.Extension. Extension ID is 'ms-python.vscode-python-envs'. Renamed getPythonEnvironmentApi to getPythonEnvsExtensionApi in extensionApiUtils.ts to match required function name. Returns vscode.Extension | undefined using MS_PYTHON_ENVS_EXTENSION_ID. (#DH-21980) --- src/util/extensionApiUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/extensionApiUtils.ts b/src/util/extensionApiUtils.ts index 02679aac1..2e8708f10 100644 --- a/src/util/extensionApiUtils.ts +++ b/src/util/extensionApiUtils.ts @@ -126,7 +126,7 @@ export function getMsPythonExtensionApi(): } /** Get the Python Environments extension api (ms-python.vscode-python-envs) */ -export function getPythonEnvironmentApi(): +export function getPythonEnvsExtensionApi(): | vscode.Extension | undefined { return vscode.extensions.getExtension( From 37169e1eb5d873beee165639a96398b77249270c Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:26:18 -0500 Subject: [PATCH 03/12] DH-21980_python-managed-servers/task-03: Update PipServerController.getPythonInterpreterPath to use new Python Environments API. Currently uses old ms-python.python API getActiveEnvironmentPath method (line 178). Replace with new API: call getEnvironment(undefined) for global scope, then return env.execInfo.run.executable. If new extension not available, disable managed servers by returning null (no fallback to old API needed). Updated getPythonInterpreterPath to use getPythonEnvsExtensionApi and new api.getEnvironment(undefined) returning env.execInfo.run.executable (#DH-21980) --- src/controllers/PipServerController.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/PipServerController.ts b/src/controllers/PipServerController.ts index 7c0b9d5df..331900475 100644 --- a/src/controllers/PipServerController.ts +++ b/src/controllers/PipServerController.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { - getMsPythonExtensionApi, + getPythonEnvsExtensionApi, getPipServerUrl, Logger, parsePort, @@ -163,7 +163,7 @@ export class PipServerController implements IDisposable { * @returns The Python interpreter path or `null` if not found. */ getPythonInterpreterPath = async (): Promise => { - const pythonExtension = getMsPythonExtensionApi(); + const pythonExtension = getPythonEnvsExtensionApi(); if (pythonExtension == null) { logger.debug('Python extension not found'); @@ -174,11 +174,11 @@ export class PipServerController implements IDisposable { await pythonExtension.activate(); } - const pythonApi = pythonExtension.exports; - const interpreter = await pythonApi.environments.getActiveEnvironmentPath(); - logger.debug('Python interpreter:', interpreter); + const api = pythonExtension.exports; + const env = await api.getEnvironment(undefined); + logger.debug('Python interpreter:', env?.execInfo.run.executable); - return interpreter?.path ?? null; + return env?.execInfo.run.executable ?? null; }; /** From 207c572bf447ee3940895a7e3d4c7985c79b66b1 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:27:05 -0500 Subject: [PATCH 04/12] DH-21980_python-managed-servers/task-04: Replace execFileSync in checkPipInstall with getPackages API. Currently spawns subprocess at line 107. Use getEnvironment then getPackages to check for deephaven-server package. Replaced execFileSync subprocess call with getPackages API. Now calls getEnvironment(undefined) then getPackages(env) and checks for deephaven-server package by name. Removed execFileSync import. (#DH-21980) --- src/controllers/PipServerController.ts | 28 +++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/controllers/PipServerController.ts b/src/controllers/PipServerController.ts index 331900475..2f8cf5fc4 100644 --- a/src/controllers/PipServerController.ts +++ b/src/controllers/PipServerController.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { getPythonEnvsExtensionApi, @@ -102,12 +101,31 @@ export class PipServerController implements IDisposable { logger.debug('Using Python interpreter:', pythonInterpreterPath); - try { - execFileSync(pythonInterpreterPath, ['-c', 'import deephaven_server']); - return { isAvailable: true, interpreterPath: pythonInterpreterPath }; - } catch (err) { + const pythonExtension = getPythonEnvsExtensionApi(); + if (pythonExtension == null) { return { isAvailable: false }; } + + if (!pythonExtension.isActive) { + await pythonExtension.activate(); + } + + const api = pythonExtension.exports; + const env = await api.getEnvironment(undefined); + if (env == null) { + return { isAvailable: false }; + } + + const packages = await api.getPackages(env); + const hasDeephavenServer = packages?.some( + pkg => pkg.name === 'deephaven-server' + ); + + if (!hasDeephavenServer) { + return { isAvailable: false }; + } + + return { isAvailable: true, interpreterPath: pythonInterpreterPath }; }; /** From 5d012988dba72cc42379c7da210864e79d9e0eaa Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:29:24 -0500 Subject: [PATCH 05/12] DH-21980_python-managed-servers/task-06: Update startServer to use env.sysPrefix for VIRTUAL_ENV. Line 264 manually derives path with path.dirname. Use environment.sysPrefix directly from PythonEnvironment object. Works for venv, uv, conda, all manager types. Updated checkPipInstall to return environment (PythonEnvironment), exported PythonEnvironment interface, and updated startServer to use environment.sysPrefix for VIRTUAL_ENV instead of manual path.dirname derivation (#DH-21980) --- src/controllers/PipServerController.ts | 12 ++++++------ src/util/extensionApiUtils.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/PipServerController.ts b/src/controllers/PipServerController.ts index 2f8cf5fc4..58090e207 100644 --- a/src/controllers/PipServerController.ts +++ b/src/controllers/PipServerController.ts @@ -5,6 +5,7 @@ import { getPipServerUrl, Logger, parsePort, + type PythonEnvironment, } from '../util'; import type { IDisposable, @@ -83,8 +84,8 @@ export class PipServerController implements IDisposable { * servers can be managed from the extension. */ checkPipInstall = async (): Promise< - | { isAvailable: true; interpreterPath: string } - | { isAvailable: false; interpreterPath?: never } + | { isAvailable: true; interpreterPath: string; environment: PythonEnvironment } + | { isAvailable: false; interpreterPath?: never; environment?: never } > => { if (!PIP_SERVER_SUPPORTED_PLATFORMS.has(process.platform)) { logger.debug(`Pip server not supported on platform: ${process.platform}`); @@ -125,7 +126,7 @@ export class PipServerController implements IDisposable { return { isAvailable: false }; } - return { isAvailable: true, interpreterPath: pythonInterpreterPath }; + return { isAvailable: true, interpreterPath: pythonInterpreterPath, environment: env }; }; /** @@ -266,7 +267,7 @@ export class PipServerController implements IDisposable { } // In case pip env has changed since last server check - const { isAvailable, interpreterPath } = await this.checkPipInstall(); + const { isAvailable, interpreterPath, environment } = await this.checkPipInstall(); this._isPipServerInstalled = isAvailable; if (!isAvailable) { @@ -275,7 +276,6 @@ export class PipServerController implements IDisposable { } const interpreterBinDirPath = path.dirname(interpreterPath); - const interpreterEnvPath = path.dirname(interpreterBinDirPath); const terminal = vscode.window.createTerminal({ name: `Deephaven (${port})`, @@ -288,7 +288,7 @@ export class PipServerController implements IDisposable { // the workspace. PYTHONPATH: './', // Venv activation typically sets this, so mimic that here. - VIRTUAL_ENV: interpreterEnvPath, + VIRTUAL_ENV: environment.sysPrefix, /* eslint-enable @typescript-eslint/naming-convention */ }, isTransient: true, diff --git a/src/util/extensionApiUtils.ts b/src/util/extensionApiUtils.ts index 2e8708f10..1233e4ccb 100644 --- a/src/util/extensionApiUtils.ts +++ b/src/util/extensionApiUtils.ts @@ -33,7 +33,7 @@ interface PythonEnvironmentId { } /** A Python environment from the ms-python.vscode-python-envs extension */ -interface PythonEnvironment { +export interface PythonEnvironment { readonly envId: PythonEnvironmentId; readonly name: string; readonly displayName: string; From 0483061840bb7a6914e6d5830f94b7cb523d038a Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:30:21 -0500 Subject: [PATCH 06/12] DH-21980_python-managed-servers/task-07: Add onDidChangePackages subscription in PipServerController constructor. Subscribe to API onDidChangePackages event. When deephaven-server installed or removed, call syncManagedServers with forceCheck true to update canStartServer status. Added onDidChangePackages subscription in PipServerController constructor. Subscribes to the API event after activation, detects deephaven-server install/remove changes, and calls syncManagedServers with forceCheck true. (#DH-21980) --- src/controllers/PipServerController.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/controllers/PipServerController.ts b/src/controllers/PipServerController.ts index 58090e207..5323fc7f7 100644 --- a/src/controllers/PipServerController.ts +++ b/src/controllers/PipServerController.ts @@ -4,6 +4,7 @@ import { getPythonEnvsExtensionApi, getPipServerUrl, Logger, + PackageChangeKind, parsePort, type PythonEnvironment, } from '../util'; @@ -58,6 +59,27 @@ export class PipServerController implements IDisposable { ); this._serverManager.onDidLoadConfig(this.onDidLoadConfig); + + const pythonExtension = getPythonEnvsExtensionApi(); + if (pythonExtension != null) { + void pythonExtension.activate().then(() => { + pythonExtension.exports.onDidChangePackages( + ({ changes }) => { + const deephavenServerChanged = changes.some( + ({ pkg, kind }) => + pkg.name === 'deephaven-server' && + (kind === PackageChangeKind.add || + kind === PackageChangeKind.remove) + ); + if (deephavenServerChanged) { + void this.syncManagedServers({ forceCheck: true }); + } + }, + undefined, + this._context.subscriptions + ); + }); + } } private readonly _context: vscode.ExtensionContext; From 9e5b08e2cd4e5045ad2fde61ba3ada5657ad2911 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:33:35 -0500 Subject: [PATCH 07/12] DH-21980_python-managed-servers/task-08: Update or create unit tests for new Python Environments API integration. Test getPythonInterpreterPath with mocked new API. Test checkPipInstall with mocked getPackages. Follow existing test patterns in codebase. Created PipServerController.spec.ts with 12 tests covering getPythonInterpreterPath (null when extension absent/env absent, returns path when active/inactive) and checkPipInstall (unsupported platform, no interpreter, no extension, no env, missing package, no packages, deephaven-server present). All mocking getPythonEnvsExtensionApi from ../util. All 12 tests pass. (#DH-21980) --- src/controllers/PipServerController.spec.ts | 299 ++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 src/controllers/PipServerController.spec.ts diff --git a/src/controllers/PipServerController.spec.ts b/src/controllers/PipServerController.spec.ts new file mode 100644 index 000000000..fce8bbb11 --- /dev/null +++ b/src/controllers/PipServerController.spec.ts @@ -0,0 +1,299 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as vscode from 'vscode'; +import { PipServerController } from './PipServerController'; +import type { PythonEnvironment } from '../util'; +import type { IServerManager, IToastService } from '../types'; + +// See __mocks__/vscode.ts for the mock implementation +vi.mock('vscode'); + +vi.mock('../util', async () => { + const actual = await vi.importActual('../util'); + return { + ...actual, + getPythonEnvsExtensionApi: vi.fn(), + }; +}); + +vi.mock('../services', async () => { + const actual = + await vi.importActual('../services'); + return { + ...actual, + pollUntilTrue: vi.fn().mockReturnValue({ promise: Promise.resolve(), cancel: vi.fn() }), + }; +}); + +vi.mock('../dh/dhc', () => ({ + isDhcServerRunning: vi.fn().mockResolvedValue(true), +})); + +// Import after mocks are set up +const { getPythonEnvsExtensionApi } = await import('../util'); + +const mockEnvironment: PythonEnvironment = { + envId: { id: 'env1', managerId: 'venv' }, + name: 'myenv', + displayName: 'My Env', + displayPath: '/path/to/env', + version: '3.11.0', + environmentPath: {} as vscode.Uri, + execInfo: { + run: { executable: '/path/to/env/bin/python' }, + }, + sysPrefix: '/path/to/env', +}; + +const mockPackages = [ + { + pkgId: { id: 'dh', managerId: 'pip', environmentId: 'env1' }, + name: 'deephaven-server', + displayName: 'Deephaven Server', + version: '0.36.0', + }, +]; + +function createMockExtension( + isActive: boolean, + envResult: PythonEnvironment | undefined, + packagesResult: typeof mockPackages | undefined +) { + const api = { + getEnvironment: vi.fn().mockResolvedValue(envResult), + getPackages: vi.fn().mockResolvedValue(packagesResult), + onDidChangePackages: vi.fn().mockReturnValue({ dispose: vi.fn() }), + }; + return { + isActive, + activate: vi.fn().mockResolvedValue(undefined), + exports: api, + }; +} + +function createController(): PipServerController { + const context = { + subscriptions: [], + extension: { packageJSON: { version: '1.0.0' } }, + } as unknown as vscode.ExtensionContext; + + const serverManager = { + onDidLoadConfig: vi.fn(), + syncManagedServers: vi.fn().mockResolvedValue(undefined), + canStartServer: false, + getServers: vi.fn().mockReturnValue([]), + getServer: vi.fn(), + disconnectFromServer: vi.fn(), + updateStatus: vi.fn(), + } as unknown as IServerManager; + + const outputChannel = { + appendLine: vi.fn(), + } as unknown as vscode.OutputChannel; + + const toastService = { + error: vi.fn(), + info: vi.fn(), + } as unknown as IToastService; + + return new PipServerController( + context, + serverManager, + outputChannel, + toastService + ); +} + +beforeEach(() => { + vi.clearAllMocks(); + + // Add missing properties to the vscode.window mock + Object.assign(vscode.window, { + onDidCloseTerminal: vi + .fn() + .mockName('onDidCloseTerminal') + .mockReturnValue({ dispose: vi.fn() }), + terminals: [], + createTerminal: vi.fn().mockReturnValue({ + sendText: vi.fn(), + exitStatus: undefined, + dispose: vi.fn(), + }), + }); +}); + +describe('getPythonInterpreterPath', () => { + it('returns null when Python Environments extension is not found', async () => { + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue(undefined); + + const controller = createController(); + const result = await controller.getPythonInterpreterPath(); + + expect(result).toBeNull(); + }); + + it('returns null when getEnvironment returns undefined', async () => { + const mockExt = createMockExtension(true, undefined, undefined); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const controller = createController(); + const result = await controller.getPythonInterpreterPath(); + + expect(result).toBeNull(); + }); + + it('returns executable path when environment is found (extension already active)', async () => { + const mockExt = createMockExtension(true, mockEnvironment, undefined); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const controller = createController(); + // Clear mocks after constructor to only track calls from getPythonInterpreterPath + vi.clearAllMocks(); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const result = await controller.getPythonInterpreterPath(); + + expect(result).toBe('/path/to/env/bin/python'); + // When extension is already active, activate should not be called + expect(mockExt.activate).not.toHaveBeenCalled(); + expect(mockExt.exports.getEnvironment).toHaveBeenCalledWith(undefined); + }); + + it('activates extension and returns executable path when extension is not yet active', async () => { + const mockExt = createMockExtension(false, mockEnvironment, undefined); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const controller = createController(); + const result = await controller.getPythonInterpreterPath(); + + expect(mockExt.activate).toHaveBeenCalled(); + expect(result).toBe('/path/to/env/bin/python'); + }); +}); + +describe('checkPipInstall', () => { + it('returns isAvailable false on unsupported platform', async () => { + vi.stubEnv('PLATFORM', 'win32'); + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + + const controller = createController(); + const result = await controller.checkPipInstall(); + + expect(result.isAvailable).toBe(false); + + Object.defineProperty(process, 'platform', { + value: originalPlatform, + configurable: true, + }); + }); + + it('returns isAvailable false when Python interpreter not found', async () => { + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue(undefined); + + const controller = createController(); + const result = await controller.checkPipInstall(); + + expect(result.isAvailable).toBe(false); + }); + + it('returns isAvailable false when Python extension not found for package check', async () => { + // First call (getPythonInterpreterPath) returns extension, second call (checkPipInstall body) returns undefined + const mockExt = createMockExtension(true, mockEnvironment, undefined); + vi.mocked(getPythonEnvsExtensionApi) + .mockReturnValueOnce( + mockExt as unknown as ReturnType + ) + .mockReturnValueOnce(undefined); + + const controller = createController(); + const result = await controller.checkPipInstall(); + + expect(result.isAvailable).toBe(false); + }); + + it('returns isAvailable false when getEnvironment returns null during package check', async () => { + const mockExtWithEnv = createMockExtension(true, mockEnvironment, undefined); + const mockExtNoEnv = createMockExtension(true, undefined, undefined); + + vi.mocked(getPythonEnvsExtensionApi) + .mockReturnValueOnce( + mockExtWithEnv as unknown as ReturnType + ) + .mockReturnValueOnce( + mockExtNoEnv as unknown as ReturnType + ); + + const controller = createController(); + const result = await controller.checkPipInstall(); + + expect(result.isAvailable).toBe(false); + }); + + it('returns isAvailable false when deephaven-server is not in packages', async () => { + const packagesWithoutDh = [ + { + pkgId: { id: 'np', managerId: 'pip', environmentId: 'env1' }, + name: 'numpy', + displayName: 'NumPy', + version: '1.26.0', + }, + ]; + const mockExt = createMockExtension(true, mockEnvironment, packagesWithoutDh); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const controller = createController(); + const result = await controller.checkPipInstall(); + + expect(result.isAvailable).toBe(false); + }); + + it('returns isAvailable false when getPackages returns undefined', async () => { + const mockExt = createMockExtension(true, mockEnvironment, undefined); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const controller = createController(); + const result = await controller.checkPipInstall(); + + expect(result.isAvailable).toBe(false); + }); + + it('returns isAvailable true with interpreter path and environment when deephaven-server is installed', async () => { + const mockExt = createMockExtension(true, mockEnvironment, mockPackages); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const controller = createController(); + const result = await controller.checkPipInstall(); + + expect(result.isAvailable).toBe(true); + if (result.isAvailable) { + expect(result.interpreterPath).toBe('/path/to/env/bin/python'); + expect(result.environment).toBe(mockEnvironment); + } + }); + + it('calls getPackages with the environment returned by getEnvironment', async () => { + const mockExt = createMockExtension(true, mockEnvironment, mockPackages); + vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( + mockExt as unknown as ReturnType + ); + + const controller = createController(); + await controller.checkPipInstall(); + + expect(mockExt.exports.getPackages).toHaveBeenCalledWith(mockEnvironment); + }); +}); From 495046d0fefcc65bc1ffe6be6c7ba4491d5d00ad Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Mon, 16 Mar 2026 11:34:39 -0500 Subject: [PATCH 08/12] DH-21980_python-managed-servers/task-09: Document Python Environments extension requirement and update related docs. Check if getMsPythonExtensionApi is still used anywhere. Update comments mentioning ms-python.python API. Consider adding note about new extension requirement to README or workspace-setup docs. Removed unused getMsPythonExtensionApi function, MsPythonExtensionApi interface, and MS_PYTHON_EXTENSION_ID constant from extensionApiUtils.ts. Updated JSDoc comment in PipServerController.ts to reference Python Environments extension. Added Python Environments extension requirement note to docs/workspace-setup.md. (#DH-21980) --- docs/workspace-setup.md | 2 ++ src/controllers/PipServerController.ts | 2 +- src/util/extensionApiUtils.ts | 17 ----------------- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/docs/workspace-setup.md b/docs/workspace-setup.md index 59b7c60b8..5a6670803 100644 --- a/docs/workspace-setup.md +++ b/docs/workspace-setup.md @@ -14,6 +14,8 @@ A `requirements.txt` file can be generated containing all of the packages instal ## Managed pip Servers (Community only) +> **Requirement:** Managed pip servers require the [Python Environments](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs) extension (`ms-python.vscode-python-envs`) to be installed in VS Code. + If you want to manage Deephaven servers from within the extension, include `deephaven-server` in the venv pip installation. Once installed, clicking the `refresh` button in the server tree panel should reveal a `Managed` servers node. diff --git a/src/controllers/PipServerController.ts b/src/controllers/PipServerController.ts index 5323fc7f7..e5ee5bf00 100644 --- a/src/controllers/PipServerController.ts +++ b/src/controllers/PipServerController.ts @@ -200,7 +200,7 @@ export class PipServerController implements IDisposable { }; /** - * Get Python interpreter path from the MS Python extension. + * Get Python interpreter path from the Python Environments extension (ms-python.vscode-python-envs). * @returns The Python interpreter path or `null` if not found. */ getPythonInterpreterPath = async (): Promise => { diff --git a/src/util/extensionApiUtils.ts b/src/util/extensionApiUtils.ts index 1233e4ccb..0bdce1392 100644 --- a/src/util/extensionApiUtils.ts +++ b/src/util/extensionApiUtils.ts @@ -2,16 +2,8 @@ import * as vscode from 'vscode'; import type { ExtensionInfo, ExtensionVersion, McpVersion } from '../types'; import { uniqueId } from './idUtils'; -const MS_PYTHON_EXTENSION_ID = 'ms-python.python'; const MS_PYTHON_ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; -/** Microsoft Python extension api */ -interface MsPythonExtensionApi { - environments: { - getActiveEnvironmentPath: () => Promise<{ path: string }>; - }; -} - /** Options for executing a Python executable */ interface PythonCommandRunConfiguration { executable: string; @@ -116,15 +108,6 @@ export function getExtensionVersion( return version as ExtensionVersion; } -/** Get Microsoft Python extension api */ -export function getMsPythonExtensionApi(): - | vscode.Extension - | undefined { - return vscode.extensions.getExtension( - MS_PYTHON_EXTENSION_ID - ); -} - /** Get the Python Environments extension api (ms-python.vscode-python-envs) */ export function getPythonEnvsExtensionApi(): | vscode.Extension From 8e936c5a5b6165617962ee26c3aa1fd9a6c0de29 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Tue, 17 Mar 2026 16:10:01 -0500 Subject: [PATCH 09/12] tweak unit.yml to fail faster if linting fails (#DH-21980) --- .github/workflows/unit.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index e1199ace5..a580317d1 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -22,19 +22,23 @@ jobs: - name: Publish Test Summary Results if: ${{ always() }} run: | - npm run report:junit2ctrf - npm run report:ctrfsummary - sed -i 's/

Test Summary<\/h3>/

Unit Test Summary<\/h3>/' $GITHUB_STEP_SUMMARY - npm run report:prcomment + if [ -f test-reports/vitest.junit.xml ]; then + npm run report:junit2ctrf + npm run report:ctrfsummary + sed -i 's/

Test Summary<\/h3>/

Unit Test Summary<\/h3>/' $GITHUB_STEP_SUMMARY + npm run report:prcomment - # The junit-to-ctrf npm package exits with a 0 status code even if - # it fails to parse the JUnit report, so check for the file manually - # and explicilty exit with a non-zero status code if it's not found. - # We do this after npm run report:prcomment so that the PR number can - # still be associated in subsequent steps. - if [ ! -e test-reports/vitest.junit.xml ]; then - echo "No JUnit report found at test-reports/vitest.junit.xml" - exit 1 + # The junit-to-ctrf npm package exits with a 0 status code even if + # it fails to parse the JUnit report, so check for the CTRF file manually + # and explicilty exit with a non-zero status code if it's not found. + if [ ! -e test-reports/ctrf-report.json ]; then + echo "CTRF report not created - junit-to-ctrf may have failed to parse JUnit report" + exit 1 + fi + else + echo "No JUnit report found - compilation or linting may have failed before tests could run" + mkdir -p pr-comment + exit 1 fi - name: Save PR Number if: ${{ always() }} From 4996095a0214ddbcfaba56b031496114155ad59a Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Tue, 17 Mar 2026 16:32:54 -0500 Subject: [PATCH 10/12] cleanup (#DH-21980) --- .github/workflows/unit.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index a580317d1..349203cbe 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -22,24 +22,24 @@ jobs: - name: Publish Test Summary Results if: ${{ always() }} run: | - if [ -f test-reports/vitest.junit.xml ]; then - npm run report:junit2ctrf - npm run report:ctrfsummary - sed -i 's/

Test Summary<\/h3>/

Unit Test Summary<\/h3>/' $GITHUB_STEP_SUMMARY - npm run report:prcomment - - # The junit-to-ctrf npm package exits with a 0 status code even if - # it fails to parse the JUnit report, so check for the CTRF file manually - # and explicilty exit with a non-zero status code if it's not found. - if [ ! -e test-reports/ctrf-report.json ]; then - echo "CTRF report not created - junit-to-ctrf may have failed to parse JUnit report" - exit 1 - fi - else - echo "No JUnit report found - compilation or linting may have failed before tests could run" + if [ ! -f test-reports/vitest.junit.xml ]; then + echo "No JUnit report found" mkdir -p pr-comment exit 1 fi + + npm run report:junit2ctrf + npm run report:ctrfsummary + sed -i 's/

Test Summary<\/h3>/

Unit Test Summary<\/h3>/' $GITHUB_STEP_SUMMARY + npm run report:prcomment + + # The junit-to-ctrf npm package exits with a 0 status code even if + # it fails to parse the JUnit report, so check for the CTRF file manually + # and explicilty exit with a non-zero status code if it's not found. + if [ ! -f test-reports/ctrf-report.json ]; then + echo "CTRF report not created - junit-to-ctrf may have failed to parse JUnit report" + exit 1 + fi - name: Save PR Number if: ${{ always() }} run: echo ${{ github.event.number }} > pr-comment/PR-number.txt From 34873aeff5fa9845cd0680657c633e958bd99613 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Tue, 17 Mar 2026 16:40:25 -0500 Subject: [PATCH 11/12] Fixed linting error (#DH-21980) --- src/controllers/PipServerController.spec.ts | 31 +++++++++++++++------ src/util/extensionApiUtils.ts | 6 ++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/controllers/PipServerController.spec.ts b/src/controllers/PipServerController.spec.ts index fce8bbb11..45a4ff0ed 100644 --- a/src/controllers/PipServerController.spec.ts +++ b/src/controllers/PipServerController.spec.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as vscode from 'vscode'; import { PipServerController } from './PipServerController'; -import type { PythonEnvironment } from '../util'; +import type { PythonEnvironment, PythonEnvironmentApi } from '../util'; import type { IServerManager, IToastService } from '../types'; // See __mocks__/vscode.ts for the mock implementation @@ -20,7 +20,9 @@ vi.mock('../services', async () => { await vi.importActual('../services'); return { ...actual, - pollUntilTrue: vi.fn().mockReturnValue({ promise: Promise.resolve(), cancel: vi.fn() }), + pollUntilTrue: vi + .fn() + .mockReturnValue({ promise: Promise.resolve(), cancel: vi.fn() }), }; }); @@ -57,7 +59,7 @@ function createMockExtension( isActive: boolean, envResult: PythonEnvironment | undefined, packagesResult: typeof mockPackages | undefined -) { +): vscode.Extension { const api = { getEnvironment: vi.fn().mockResolvedValue(envResult), getPackages: vi.fn().mockResolvedValue(packagesResult), @@ -67,7 +69,7 @@ function createMockExtension( isActive, activate: vi.fn().mockResolvedValue(undefined), exports: api, - }; + } as unknown as vscode.Extension; } function createController(): PipServerController { @@ -182,7 +184,10 @@ describe('checkPipInstall', () => { it('returns isAvailable false on unsupported platform', async () => { vi.stubEnv('PLATFORM', 'win32'); const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + Object.defineProperty(process, 'platform', { + value: 'win32', + configurable: true, + }); const controller = createController(); const result = await controller.checkPipInstall(); @@ -220,12 +225,18 @@ describe('checkPipInstall', () => { }); it('returns isAvailable false when getEnvironment returns null during package check', async () => { - const mockExtWithEnv = createMockExtension(true, mockEnvironment, undefined); + const mockExtWithEnv = createMockExtension( + true, + mockEnvironment, + undefined + ); const mockExtNoEnv = createMockExtension(true, undefined, undefined); vi.mocked(getPythonEnvsExtensionApi) .mockReturnValueOnce( - mockExtWithEnv as unknown as ReturnType + mockExtWithEnv as unknown as ReturnType< + typeof getPythonEnvsExtensionApi + > ) .mockReturnValueOnce( mockExtNoEnv as unknown as ReturnType @@ -246,7 +257,11 @@ describe('checkPipInstall', () => { version: '1.26.0', }, ]; - const mockExt = createMockExtension(true, mockEnvironment, packagesWithoutDh); + const mockExt = createMockExtension( + true, + mockEnvironment, + packagesWithoutDh + ); vi.mocked(getPythonEnvsExtensionApi).mockReturnValue( mockExt as unknown as ReturnType ); diff --git a/src/util/extensionApiUtils.ts b/src/util/extensionApiUtils.ts index 0bdce1392..6c92cfa9f 100644 --- a/src/util/extensionApiUtils.ts +++ b/src/util/extensionApiUtils.ts @@ -67,8 +67,10 @@ interface DidChangePackagesEventArgs { export type GetEnvironmentScope = undefined | vscode.Uri; /** Python Environments extension API (ms-python.vscode-python-envs) */ -interface PythonEnvironmentApi { - getEnvironment(scope: GetEnvironmentScope): Promise; +export interface PythonEnvironmentApi { + getEnvironment( + scope: GetEnvironmentScope + ): Promise; getPackages(environment: PythonEnvironment): Promise; onDidChangePackages: vscode.Event; } From 6e4f7db4347eaead664f55ca896782aa706e3f85 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Wed, 18 Mar 2026 14:37:07 -0500 Subject: [PATCH 12/12] formatting (#DH-21980) --- src/controllers/PipServerController.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/controllers/PipServerController.ts b/src/controllers/PipServerController.ts index e5ee5bf00..a6df6fb95 100644 --- a/src/controllers/PipServerController.ts +++ b/src/controllers/PipServerController.ts @@ -62,6 +62,7 @@ export class PipServerController implements IDisposable { const pythonExtension = getPythonEnvsExtensionApi(); if (pythonExtension != null) { + console.log('[TESTING]', pythonExtension.exports); void pythonExtension.activate().then(() => { pythonExtension.exports.onDidChangePackages( ({ changes }) => { @@ -106,7 +107,11 @@ export class PipServerController implements IDisposable { * servers can be managed from the extension. */ checkPipInstall = async (): Promise< - | { isAvailable: true; interpreterPath: string; environment: PythonEnvironment } + | { + isAvailable: true; + interpreterPath: string; + environment: PythonEnvironment; + } | { isAvailable: false; interpreterPath?: never; environment?: never } > => { if (!PIP_SERVER_SUPPORTED_PLATFORMS.has(process.platform)) { @@ -148,7 +153,11 @@ export class PipServerController implements IDisposable { return { isAvailable: false }; } - return { isAvailable: true, interpreterPath: pythonInterpreterPath, environment: env }; + return { + isAvailable: true, + interpreterPath: pythonInterpreterPath, + environment: env, + }; }; /** @@ -289,7 +298,8 @@ export class PipServerController implements IDisposable { } // In case pip env has changed since last server check - const { isAvailable, interpreterPath, environment } = await this.checkPipInstall(); + const { isAvailable, interpreterPath, environment } = + await this.checkPipInstall(); this._isPipServerInstalled = isAvailable; if (!isAvailable) {