From 4e9792f0da306f75a045f0b9b98f09861b74a899 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Thu, 9 Jul 2026 16:10:33 -0500 Subject: [PATCH 01/14] Split runtimePath (absolute) from runtimeDisplayPath (UI-only ~-shortened) Fixes execFile callers that received a ~-prefixed path. Adds getRuntimeDisplayPath() helper for all UI display sites; bumps discovery cache schema v2->v3 to discard stale entries. Co-Authored-By: Claude Sonnet 4.6 --- .../src/client/positron/runtime.ts | 11 ++++++---- extensions/positron-r/src/provider.ts | 11 ++++++---- .../positron-r/src/runtime-quickpick.ts | 2 +- package-lock.json | 12 ----------- src/positron-dts/positron.d.ts | 13 +++++++++++- .../utilities/interpreterDropDownUtils.ts | 4 ++-- .../browser/actions/chatRuntimeSessions.ts | 4 ++-- .../browser/languageRuntimeActions.ts | 12 +++++------ .../components/consoleInstanceInfoButton.tsx | 3 ++- .../browser/components/startupStatus.tsx | 4 ++-- .../browser/components/runtimeSessionCard.tsx | 4 ++-- .../positronStartupDiagnosticsEditor.ts | 4 ++-- .../common/languageRuntimeService.ts | 20 ++++++++++++++++++- .../common/runtimeDiscoveryCacheService.ts | 8 +++++++- 14 files changed, 71 insertions(+), 41 deletions(-) diff --git a/extensions/positron-python/src/client/positron/runtime.ts b/extensions/positron-python/src/client/positron/runtime.ts index cd9eef255080..230ae2971b5e 100644 --- a/extensions/positron-python/src/client/positron/runtime.ts +++ b/extensions/positron-python/src/client/positron/runtime.ts @@ -250,13 +250,15 @@ export async function createPythonRuntimeMetadata( digest.update(pythonVersion); const runtimeId = digest.digest('hex').substring(0, 32); - // Create the runtime path. - // TODO@softwarenerd - We will need to update this for Windows. + // runtimePath is always the full absolute path to the interpreter so + // callers can use it directly with execFile etc. runtimeDisplayPath + // carries the human-friendly ~ shorthand shown in the UI. const homedir = os.homedir(); - const runtimePath = + const runtimePath = interpreter.path; + const runtimeDisplayPath = os.platform() !== 'win32' && interpreter.path.startsWith(homedir) ? path.join('~', interpreter.path.substring(homedir.length)) - : interpreter.path; + : undefined; // Save the ID of the Python environment for use when creating the language // runtime session. @@ -292,6 +294,7 @@ export async function createPythonRuntimeMetadata( runtimeName, runtimeShortName, runtimePath, + runtimeDisplayPath, runtimeVersion: applicationEnv.packageJson.version, runtimeSource, languageId: PYTHON_LANGUAGE, diff --git a/extensions/positron-r/src/provider.ts b/extensions/positron-r/src/provider.ts index e882e9c0333a..9eb65f4f8146 100644 --- a/extensions/positron-r/src/provider.ts +++ b/extensions/positron-r/src/provider.ts @@ -642,11 +642,13 @@ export async function makeMetadata( const homedir = os.homedir(); const isUserInstallation = rInst.binpath.startsWith(homedir); - // Create the runtime path. - // TODO@softwarenerd - We will need to update this for Windows. - const runtimePath = os.platform() !== 'win32' && isUserInstallation ? + // runtimePath is always the full absolute path to the binary so callers + // can use it directly with execFile etc. runtimeDisplayPath carries the + // human-friendly ~ shorthand shown in the UI. + const runtimePath = rInst.binpath; + const runtimeDisplayPath = os.platform() !== 'win32' && isUserInstallation ? path.join('~', rInst.binpath.substring(homedir.length)) : - rInst.binpath; + undefined; // Create the Rscript path. // The Rscript path is the same as the R binary path, but with the 'R' or 'R.exe' executable @@ -727,6 +729,7 @@ export async function makeMetadata( runtimeName, runtimeShortName, runtimePath, + runtimeDisplayPath, runtimeVersion: packageJson.version, runtimeSource, languageId: 'r', diff --git a/extensions/positron-r/src/runtime-quickpick.ts b/extensions/positron-r/src/runtime-quickpick.ts index a8ce14bcc517..de9b280ba3a9 100644 --- a/extensions/positron-r/src/runtime-quickpick.ts +++ b/extensions/positron-r/src/runtime-quickpick.ts @@ -18,7 +18,7 @@ class RuntimeQuickPickItem implements vscode.QuickPickItem { public runtimeMetadata: positron.LanguageRuntimeMetadata, ) { this.label = runtimeMetadata.runtimeName; - this.description = runtimeMetadata.runtimePath; + this.description = runtimeMetadata.runtimeDisplayPath ?? runtimeMetadata.runtimePath; this.runtime = runtimeMetadata; } } diff --git a/package-lock.json b/package-lock.json index 6badce39e3d5..7837ede05b51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3678,15 +3678,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm-musl": { - "optional": true - }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm64-musl": { - "optional": true - }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-x64-musl": { - "optional": true - }, "node_modules/@parcel/watcher/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -22995,9 +22986,6 @@ "nan": "^2.23.0" } }, - "node_modules/ssh2/node_modules/cpu-features": { - "optional": true - }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 5a2c16223634..beb71af06d25 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -627,9 +627,20 @@ declare module 'positron' { * before the runtime is started. */ export interface LanguageRuntimeMetadata { - /** The path to the runtime. */ + /** + * The absolute path to the runtime executable. Always a fully-expanded + * path suitable for use with execFile or equivalent; never contains ~ + * or other shorthand. + */ runtimePath: string; + /** + * The runtime path formatted for display to the user. May use ~ as + * shorthand for the user's home directory on non-Windows platforms. + * Falls back to runtimePath when not set. + */ + runtimeDisplayPath?: string; + /** A unique identifier for this runtime; takes the form of a GUID */ runtimeId: string; diff --git a/src/vs/workbench/browser/positronNewFolderFlow/utilities/interpreterDropDownUtils.ts b/src/vs/workbench/browser/positronNewFolderFlow/utilities/interpreterDropDownUtils.ts index 790f4fbac682..c0d249d2168a 100644 --- a/src/vs/workbench/browser/positronNewFolderFlow/utilities/interpreterDropDownUtils.ts +++ b/src/vs/workbench/browser/positronNewFolderFlow/utilities/interpreterDropDownUtils.ts @@ -6,7 +6,7 @@ import { DropDownListBoxEntry } from '../../positronComponents/dropDownListBox/dropDownListBox.js'; import { DropDownListBoxItem } from '../../positronComponents/dropDownListBox/dropDownListBoxItem.js'; import { DropDownListBoxSeparator } from '../../positronComponents/dropDownListBox/dropDownListBoxSeparator.js'; -import { ILanguageRuntimeMetadata } from '../../../services/languageRuntime/common/languageRuntimeService.js'; +import { getRuntimeDisplayPath, ILanguageRuntimeMetadata } from '../../../services/languageRuntime/common/languageRuntimeService.js'; /** * InterpreterInfo interface. @@ -52,7 +52,7 @@ export const interpretersToDropdownItems = ( runtimeId: runtime.runtimeId, languageName: runtime.languageName, languageVersion: runtime.languageVersion, - runtimePath: runtime.runtimePath, + runtimePath: getRuntimeDisplayPath(runtime), runtimeSource: runtime.runtimeSource, }, }) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatRuntimeSessions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatRuntimeSessions.ts index da6b68ddf550..d40a2869a1a9 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatRuntimeSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatRuntimeSessions.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { RuntimeState } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; +import { getRuntimeDisplayPath, RuntimeState } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { IChatWidget } from '../chat.js'; @@ -77,7 +77,7 @@ export async function showRuntimeSessionsPick(accessor: ServicesAccessor, _widge return { id: session.sessionId, label, - detail: session.runtimeMetadata.runtimePath, + detail: getRuntimeDisplayPath(session.runtimeMetadata), iconPath: { dark: URI.parse(`data:image/svg+xml;base64, ${session.runtimeMetadata.base64EncodedIconSvg}`), }, diff --git a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts index 9ecf582408eb..51ad4506f514 100644 --- a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts +++ b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts @@ -13,7 +13,7 @@ import { IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../p import { IKeybindingRule, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { LANGUAGE_RUNTIME_ACTION_CATEGORY } from '../common/languageRuntime.js'; import { IPositronConsoleService, POSITRON_CONSOLE_VIEW_ID } from '../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; -import { ILanguageRuntimeMetadata, ILanguageRuntimeService, IRuntimePickerContribution, IRuntimePickerItem, LanguageRuntimeSessionMode, RuntimeCodeExecutionMode, RuntimeErrorBehavior, RuntimeStartupPhase, RuntimeState } from '../../../services/languageRuntime/common/languageRuntimeService.js'; +import { getRuntimeDisplayPath, ILanguageRuntimeMetadata, ILanguageRuntimeService, IRuntimePickerContribution, IRuntimePickerItem, LanguageRuntimeSessionMode, RuntimeCodeExecutionMode, RuntimeErrorBehavior, RuntimeStartupPhase, RuntimeState } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { ILanguageRuntimeSession, IRuntimeClientInstance, IRuntimeSessionService, RuntimeClientType, RuntimeStartMode } from '../../../services/runtimeSession/common/runtimeSessionService.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; @@ -213,7 +213,7 @@ export const selectLanguageRuntimeSession = async ( .map(session => ({ id: session.sessionId, label: session.dynState.sessionName, - detail: session.runtimeMetadata.runtimePath, + detail: getRuntimeDisplayPath(session.runtimeMetadata), description: session.sessionId === currentForegroundSession?.sessionId ? localize('positron.languageRuntime.currentlySelected', 'Currently Selected') : undefined, @@ -242,7 +242,7 @@ export const selectLanguageRuntimeSession = async ( .map(session => ({ id: session.sessionId, label: getSessionDisplayNameWithRuntime(session), - detail: session.runtimeMetadata.runtimePath, + detail: getRuntimeDisplayPath(session.runtimeMetadata), description: session.sessionId === currentForegroundSession?.sessionId ? localize('positron.languageRuntime.currentlySelected', 'Currently Selected') : undefined, @@ -264,7 +264,7 @@ export const selectLanguageRuntimeSession = async ( .map(session => ({ id: session.sessionId, label: getSessionDisplayNameWithRuntime(session), - detail: session.runtimeMetadata.runtimePath, + detail: getRuntimeDisplayPath(session.runtimeMetadata), description: session.sessionId === currentForegroundSession?.sessionId ? localize('positron.languageRuntime.currentlySelected', 'Currently Selected') : undefined, @@ -474,7 +474,7 @@ export const selectNewLanguageRuntime = async ( items.push({ id: runtime.runtimeId, label: runtime.runtimeName, - detail: runtime.runtimePath, + detail: getRuntimeDisplayPath(runtime), iconPath: { dark: URI.parse(`data:image/svg+xml;base64, ${runtime.base64EncodedIconSvg}`), }, @@ -540,7 +540,7 @@ export const selectNewLanguageRuntime = async ( items.push({ id: runtime.runtimeId, label: runtime.runtimeName, - detail: runtime.runtimePath, + detail: getRuntimeDisplayPath(runtime), iconPath: { dark: URI.parse(`data:image/svg+xml;base64, ${runtime.base64EncodedIconSvg}`), }, diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstanceInfoButton.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstanceInfoButton.tsx index 17a484089368..a483182de585 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstanceInfoButton.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstanceInfoButton.tsx @@ -21,6 +21,7 @@ import { ActionBarButton } from '../../../../../platform/positronActionBar/brows import { PositronModalPopup } from '../../../../browser/positronComponents/positronModalPopup/positronModalPopup.js'; import { PositronModalReactRenderer } from '../../../../../base/browser/positronModalReactRenderer.js'; import { ILanguageRuntimeSession, LanguageRuntimeSessionChannel } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; +import { getRuntimeDisplayPath } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; const positronConsoleInfo = localize('positron.console.info.label', "Console Information"); const localizeShowKernelOutputChannel = (channelName: string) => localize('positron.console.info.showKernelOutputChannel', "Show {0} Output Channel", channelName); @@ -153,7 +154,7 @@ const ConsoleInstanceInfoModalPopup = (props: ConsoleInstanceInfoModalPopupProps

{localize( 'positron.console.info.runtimePath', 'Path: {0}', - props.session.runtimeMetadata.runtimePath)} + getRuntimeDisplayPath(props.session.runtimeMetadata))}

{localize( 'positron.console.info.runtimeSource', 'Source: {0}', diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx index c87f60fa9f65..521dbed7dff9 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx @@ -14,7 +14,7 @@ import { localize } from '../../../../../nls.js'; import { RuntimeStartupProgress } from './runtimeStartupProgress.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ProgressBar } from '../../../../../base/browser/ui/progressbar/progressbar.js'; -import { RuntimeStartupPhase } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; +import { getRuntimeDisplayPath, RuntimeStartupPhase } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; import { IRuntimeAutoStartEvent } from '../../../../services/runtimeStartup/common/runtimeStartupService.js'; import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js'; import { EmbeddedLink } from '../../../../../base/browser/ui/positronComponents/embeddedLink/EmbeddedLink.js'; @@ -84,7 +84,7 @@ export const StartupStatus = () => { bar.worked(1); } if (runtime.runtimePath) { - setLatestRuntimePath(runtime.runtimePath); + setLatestRuntimePath(getRuntimeDisplayPath(runtime)); } })); diff --git a/src/vs/workbench/contrib/positronRuntimeSessions/browser/components/runtimeSessionCard.tsx b/src/vs/workbench/contrib/positronRuntimeSessions/browser/components/runtimeSessionCard.tsx index cd9433a4748f..e1e95aa0a975 100644 --- a/src/vs/workbench/contrib/positronRuntimeSessions/browser/components/runtimeSessionCard.tsx +++ b/src/vs/workbench/contrib/positronRuntimeSessions/browser/components/runtimeSessionCard.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from 'react'; // Other dependencies. import { ILanguageRuntimeSession } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; -import { RuntimeExitReason, RuntimeState } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; +import { getRuntimeDisplayPath, RuntimeExitReason, RuntimeState } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; import { RuntimeClientList } from './runtimeClientList.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; @@ -85,7 +85,7 @@ export const RuntimeSessionCard = (props: runtimeSessionCardProps) => { {props.session.metadata.startReason}

- {props.session.runtimeMetadata.runtimePath} + {getRuntimeDisplayPath(props.session.runtimeMetadata)}
diff --git a/src/vs/workbench/contrib/positronStartupDiagnostics/browser/positronStartupDiagnosticsEditor.ts b/src/vs/workbench/contrib/positronStartupDiagnostics/browser/positronStartupDiagnosticsEditor.ts index f279dc6e907f..36ab688191aa 100644 --- a/src/vs/workbench/contrib/positronStartupDiagnostics/browser/positronStartupDiagnosticsEditor.ts +++ b/src/vs/workbench/contrib/positronStartupDiagnostics/browser/positronStartupDiagnosticsEditor.ts @@ -33,7 +33,7 @@ import { IRuntimeDiscoveryCache, RUNTIME_DISCOVERY_CACHE_ENABLED_SETTING } from '../../../services/runtimeStartup/common/runtimeDiscoveryCacheService.js'; -import { ILanguageRuntimeService } from '../../../services/languageRuntime/common/languageRuntimeService.js'; +import { getRuntimeDisplayPath, ILanguageRuntimeService } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IOutputService } from '../../../services/output/common/output.js'; import { IAdminPolicyService } from '../../../../platform/policy/common/adminPolicyService.js'; @@ -721,7 +721,7 @@ class PositronStartupDiagnosticsContentProvider implements ITextModelContentProv table.push([ runtime.extensionId.value, runtime.runtimeName, - runtime.runtimePath + getRuntimeDisplayPath(runtime) ]); } md.table(['Extension', 'Name', 'Path'], table); diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts index 825479c479b2..2cc8e73e6c10 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts @@ -27,6 +27,14 @@ export const formatLanguageRuntimeMetadata = (metadata: ILanguageRuntimeMetadata `name: ${metadata.runtimeName} ` + `version: ${metadata.languageVersion})`; +/** + * Returns the display path for a runtime, preferring the human-friendly + * runtimeDisplayPath (which may use ~ shorthand) over the raw runtimePath. + * Use this everywhere a path is shown to the user. + */ +export const getRuntimeDisplayPath = (metadata: Pick): string => + metadata.runtimeDisplayPath ?? metadata.runtimePath; + /** * Formats a language runtime session for logging. * @@ -898,9 +906,19 @@ export function signaturesEqual( * before the runtime is started. */ export interface ILanguageRuntimeMetadata { - /** The path to the kernel. */ + /** + * The absolute path to the runtime executable. Always a fully-expanded path + * suitable for use with execFile; never contains ~ or other shorthand. + */ readonly runtimePath: string; + /** + * The runtime path formatted for display to the user. May use ~ as + * shorthand for the user's home directory on non-Windows platforms. + * Falls back to runtimePath when not set. + */ + readonly runtimeDisplayPath?: string; + /** A unique identifier for this runtime; usually a GUID */ readonly runtimeId: string; diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeDiscoveryCacheService.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeDiscoveryCacheService.ts index 5fef64271407..aacb7906694f 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeDiscoveryCacheService.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeDiscoveryCacheService.ts @@ -60,8 +60,14 @@ export const RUNTIME_DISCOVERY_CACHE_REFRESH_INTERVAL_DAYS_DEFAULT = 1; * * v2 added per-bucket `discoveryRootSignature` so warm starts can detect newly * installed interpreters by re-statting the directories the extension scans. + * + * v3: `runtimePath` is now always the fully-expanded absolute path, with the + * `~` shorthand moved to the separate `runtimeDisplayPath` field. Entries + * written by older builds may still carry a `~`-shortened `runtimePath` and + * no `runtimeDisplayPath`; bumping the version forces those entries to be + * discarded and rediscovered rather than replayed as-is. */ -export const RUNTIME_DISCOVERY_CACHE_SCHEMA_VERSION = 2; +export const RUNTIME_DISCOVERY_CACHE_SCHEMA_VERSION = 3; /** * Storage key under which all cache state is persisted. Embeds the schema From 43d4296539e8f8263dfcd98fdc48301209bd2f37 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Thu, 9 Jul 2026 16:22:14 -0500 Subject: [PATCH 02/14] Add tests for getRuntimeDisplayPath and metadata path-splitting logic Covers the three gaps PETE identified: a Vitest for the new getRuntimeDisplayPath pure helper, a Mocha suite for R makeMetadata path fields, and a focused suite for Python createPythonRuntimeMetadata path fields. Co-Authored-By: Claude Sonnet 4.6 --- .../src/test/positron/runtime.unit.test.ts | 92 ++++++++++++++++++- .../positron-r/src/test/provider.unit.test.ts | 66 +++++++++++++ .../test/common/languageRuntime.vitest.ts | 12 ++- 3 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 extensions/positron-r/src/test/provider.unit.test.ts diff --git a/extensions/positron-python/src/test/positron/runtime.unit.test.ts b/extensions/positron-python/src/test/positron/runtime.unit.test.ts index e32dacaeb7c1..8269ab9bf487 100644 --- a/extensions/positron-python/src/test/positron/runtime.unit.test.ts +++ b/extensions/positron-python/src/test/positron/runtime.unit.test.ts @@ -3,10 +3,23 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as os from 'os'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; import { assert } from 'chai'; -import { getRuntimeSourceAndShortName } from '../../client/positron/runtime'; -import { EnvironmentType } from '../../client/pythonEnvironments/info'; +import { anything, reset, when } from 'ts-mockito'; +import * as ipykernelModule from '../../client/positron/ipykernel'; +import * as moduleLocator from '../../client/pythonEnvironments/base/locators/lowLevel/moduleEnvironmentLocator'; +import * as environmentTypeComparer from '../../client/interpreter/configuration/environmentTypeComparer'; +import { createPythonRuntimeMetadata, getRuntimeSourceAndShortName } from '../../client/positron/runtime'; +import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { PythonEnvironment, EnvironmentType } from '../../client/pythonEnvironments/info'; import { ModuleMetadata } from '../../client/pythonEnvironments/base/locators/lowLevel/moduleEnvironmentLocator'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; suite('getRuntimeSourceAndShortName', () => { const MODULE_METADATA: ModuleMetadata = { @@ -65,3 +78,78 @@ suite('getRuntimeSourceAndShortName', () => { }); }); }); + +function makeInterpreter(interpreterPath: string): PythonEnvironment { + return { + path: interpreterPath, + envType: EnvironmentType.System, + envName: undefined, + version: { major: 3, minor: 10, patch: 0, raw: '3.10.0', release: { type: 0, version: 0 } }, + sysVersion: '3.10.0', + envPath: undefined, + } as unknown as PythonEnvironment; +} + +suite('createPythonRuntimeMetadata path fields', () => { + let sandbox: sinon.SinonSandbox; + let serviceContainer: TypeMoq.IMock; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Return "bundled, no issues" so the installer check branch is skipped. + sandbox.stub(ipykernelModule, 'getIpykernelBundle').resolves({ disabledReason: undefined }); + + // Module metadata resolves immediately with an empty map. + sandbox.stub(moduleLocator, 'whenModuleMetadataReady').resolves(); + moduleLocator.moduleMetadataMap.clear(); + + // Suppress the version-unsupported flag in the runtime name. + sandbox.stub(environmentTypeComparer, 'isVersionSupported').returns(true); + + // Stub vscode.workspace.getConfiguration used for the kernelSupervisor config. + when(mockedVSCodeNamespaces.workspace!).getConfiguration(anything()).thenReturn({ + get: (_key: string, def?: unknown) => def, + } as any); + + const workspaceService = TypeMoq.Mock.ofType(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + + const appEnv = TypeMoq.Mock.ofType(); + appEnv.setup((a) => a.packageJson).returns(() => ({ version: '2025.0.0' })); + + serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IApplicationEnvironment)).returns(() => appEnv.object); + }); + + teardown(() => { + sandbox.restore(); + reset(mockedVSCodeNamespaces.workspace!); + }); + + test('runtimePath is always the full absolute interpreter path', async () => { + const interpreterPath = '/usr/bin/python3'; + const metadata = await createPythonRuntimeMetadata(makeInterpreter(interpreterPath), serviceContainer.object, false); + assert.strictEqual(metadata.runtimePath, interpreterPath); + }); + + test('runtimeDisplayPath uses ~ shorthand for a home-dir interpreter on non-Windows', async function () { + if (os.platform() === 'win32') { + this.skip(); + } + const interpreterPath = path.join(os.homedir(), '.venv', 'bin', 'python'); + const metadata = await createPythonRuntimeMetadata(makeInterpreter(interpreterPath), serviceContainer.object, false); + assert.ok( + metadata.runtimeDisplayPath?.startsWith('~'), + `Expected ~ prefix, got: ${metadata.runtimeDisplayPath}`, + ); + assert.strictEqual(metadata.runtimePath, interpreterPath, 'runtimePath must remain absolute'); + }); + + test('runtimeDisplayPath is undefined for a system (non-home-dir) interpreter', async () => { + const interpreterPath = '/usr/bin/python3'; + const metadata = await createPythonRuntimeMetadata(makeInterpreter(interpreterPath), serviceContainer.object, false); + assert.strictEqual(metadata.runtimeDisplayPath, undefined); + }); +}); diff --git a/extensions/positron-r/src/test/provider.unit.test.ts b/extensions/positron-r/src/test/provider.unit.test.ts new file mode 100644 index 000000000000..a9579eb7b7a8 --- /dev/null +++ b/extensions/positron-r/src/test/provider.unit.test.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2026 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as os from 'os'; +import * as path from 'path'; +import * as Sinon from 'sinon'; +import * as vscode from 'vscode'; +import { makeMetadata } from '../provider'; +import { ReasonDiscovered, RInstallation } from '../r-installation'; + +function makeRInst(binpath: string): RInstallation { + return { + binpath, + homepath: '', + version: '4.4.0', + semVersion: { major: 4, minor: 4, patch: 0 } as any, + arch: '', + usable: true, + supported: true, + current: false, + orthogonal: false, + default: false, + packagerMetadata: undefined, + reasonDiscovered: [ReasonDiscovered.PATH], + reasonRejected: null, + } as unknown as RInstallation; +} + +suite('makeMetadata path fields', () => { + let sandbox: Sinon.SinonSandbox; + + setup(() => { + sandbox = Sinon.createSandbox(); + sandbox.stub(vscode.workspace, 'getConfiguration').returns({ + get: (_key: string, def?: unknown) => def, + } as unknown as vscode.WorkspaceConfiguration); + }); + + teardown(() => sandbox.restore()); + + test('runtimePath is always the full absolute binary path', async () => { + const binpath = '/usr/bin/R'; + const metadata = await makeMetadata(makeRInst(binpath)); + assert.strictEqual(metadata.runtimePath, binpath); + }); + + test('runtimeDisplayPath uses ~ shorthand for a home-dir install on non-Windows', async function () { + if (os.platform() === 'win32') { this.skip(); } + const binpath = path.join(os.homedir(), '.local', 'bin', 'R'); + const metadata = await makeMetadata(makeRInst(binpath)); + assert.ok( + metadata.runtimeDisplayPath?.startsWith('~'), + `Expected ~ prefix, got: ${metadata.runtimeDisplayPath}`, + ); + assert.strictEqual(metadata.runtimePath, binpath, 'runtimePath must remain absolute'); + }); + + test('runtimeDisplayPath is undefined for a system (non-home-dir) install', async () => { + const binpath = '/usr/bin/R'; + const metadata = await makeMetadata(makeRInst(binpath)); + assert.strictEqual(metadata.runtimeDisplayPath, undefined); + }); +}); diff --git a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts index f687af8ff88f..1c57f881a4d3 100644 --- a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts +++ b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts @@ -13,7 +13,7 @@ import { ILogService, NullLogger } from '../../../../../platform/log/common/log. import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { LanguageRuntimeService } from '../../common/languageRuntime.js'; -import { ILanguageRuntimeMetadata, LanguageRuntimeSessionLocation, LanguageRuntimeStartupBehavior, LanguageStartupBehavior } from '../../common/languageRuntimeService.js'; +import { getRuntimeDisplayPath, ILanguageRuntimeMetadata, LanguageRuntimeSessionLocation, LanguageRuntimeStartupBehavior, LanguageStartupBehavior } from '../../common/languageRuntimeService.js'; /** * Shared metadata fields for test stubs. Both tests use the same base shape; @@ -39,6 +39,16 @@ function makeTestMetadata(overrides: Partial): ILangua }); } +describe('getRuntimeDisplayPath', () => { + it('returns runtimeDisplayPath when set', () => { + expect(getRuntimeDisplayPath({ runtimePath: '/abs/path/R', runtimeDisplayPath: '~/bin/R' })).toBe('~/bin/R'); + }); + + it('falls back to runtimePath when runtimeDisplayPath is undefined', () => { + expect(getRuntimeDisplayPath({ runtimePath: '/abs/path/R', runtimeDisplayPath: undefined })).toBe('/abs/path/R'); + }); +}); + describe('Positron - LanguageRuntimeService', () => { describe('default configuration', () => { const ctx = createTestContainer() From 6d8f501c490392ef687e936b98ece4e51b61f631 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Fri, 10 Jul 2026 11:30:00 -0500 Subject: [PATCH 03/14] Fix stale ~-prefixed runtimePath in affiliation storage getAffiliatedRuntimes() now prefers the live registered metadata over the stored copy (same pattern getPreferredRuntime already uses), so the Clear Saved Interpreter quick pick always shows the current runtimePath. onDidRegisterRuntime heals the stored affiliation at discovery time so the storage doesn't accumulate stale values across sessions. Co-Authored-By: Claude Sonnet 4.6 --- .../src/test/positron/runtime.unit.test.ts | 2 +- .../services/runtimeStartup/common/runtimeStartup.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/extensions/positron-python/src/test/positron/runtime.unit.test.ts b/extensions/positron-python/src/test/positron/runtime.unit.test.ts index 8269ab9bf487..939d5a8ff80f 100644 --- a/extensions/positron-python/src/test/positron/runtime.unit.test.ts +++ b/extensions/positron-python/src/test/positron/runtime.unit.test.ts @@ -108,7 +108,7 @@ suite('createPythonRuntimeMetadata path fields', () => { sandbox.stub(environmentTypeComparer, 'isVersionSupported').returns(true); // Stub vscode.workspace.getConfiguration used for the kernelSupervisor config. - when(mockedVSCodeNamespaces.workspace!).getConfiguration(anything()).thenReturn({ + when(mockedVSCodeNamespaces.workspace!.getConfiguration(anything())).thenReturn({ get: (_key: string, def?: unknown) => def, } as any); diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts index fa0a72b1b27d..612f46d5fd49 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts @@ -578,7 +578,10 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup for (const languageId of languageIds) { const metadata = this.getAffiliatedRuntimeMetadata(languageId); if (metadata) { - runtimes.push(metadata); + // Prefer the live registered metadata so callers always see + // fresh field values (e.g. runtimePath is always absolute). + const live = this._languageRuntimeService.getRegisteredRuntime(metadata.runtimeId); + runtimes.push(live ?? metadata); } } return runtimes; @@ -1598,6 +1601,11 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // If the runtime is affiliated with this workspace, start it. if (metadata.runtimeId === affiliatedRuntimeId) { + // Heal any stale fields in the stored affiliation (e.g. a runtimePath + // that was ~-shortened before the runtimePath/runtimeDisplayPath split) + // by overwriting with the freshly-discovered metadata. + this.saveAffiliatedRuntime({ ...affiliated, metadata }); + try { // Check the setting to see if we should be auto-starting. From af3d4267f6cab4af7c489bd6416ef44b2307ade5 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Fri, 10 Jul 2026 12:13:49 -0500 Subject: [PATCH 04/14] Move runtimeDisplayPath computation from extensions to LanguageRuntimeService Instead of each extension manually computing a ~-shortened display path and storing it in metadata, LanguageRuntimeService.registerRuntime now enriches incoming metadata with runtimeDisplayPath via tildify() at registration time. IPathService is injected once into the service; all 9 display call sites and the getRuntimeDisplayPath() helper are unchanged. runtimeDisplayPath is removed from the public positron.d.ts API since extensions no longer need to supply it. Co-Authored-By: Claude Sonnet 4.6 --- .../src/client/positron/runtime.ts | 10 ------- extensions/positron-r/src/provider.ts | 7 ----- .../positron-r/src/runtime-quickpick.ts | 8 ++++- src/positron-dts/positron.d.ts | 7 ----- .../languageRuntime/common/languageRuntime.ts | 17 +++++++++-- .../test/common/languageRuntime.vitest.ts | 30 +++++++++++++++++-- 6 files changed, 49 insertions(+), 30 deletions(-) diff --git a/extensions/positron-python/src/client/positron/runtime.ts b/extensions/positron-python/src/client/positron/runtime.ts index 230ae2971b5e..62242b3b7804 100644 --- a/extensions/positron-python/src/client/positron/runtime.ts +++ b/extensions/positron-python/src/client/positron/runtime.ts @@ -7,7 +7,6 @@ import * as positron from 'positron'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; -import * as os from 'os'; import * as path from 'path'; import * as crypto from 'crypto'; @@ -250,15 +249,7 @@ export async function createPythonRuntimeMetadata( digest.update(pythonVersion); const runtimeId = digest.digest('hex').substring(0, 32); - // runtimePath is always the full absolute path to the interpreter so - // callers can use it directly with execFile etc. runtimeDisplayPath - // carries the human-friendly ~ shorthand shown in the UI. - const homedir = os.homedir(); const runtimePath = interpreter.path; - const runtimeDisplayPath = - os.platform() !== 'win32' && interpreter.path.startsWith(homedir) - ? path.join('~', interpreter.path.substring(homedir.length)) - : undefined; // Save the ID of the Python environment for use when creating the language // runtime session. @@ -294,7 +285,6 @@ export async function createPythonRuntimeMetadata( runtimeName, runtimeShortName, runtimePath, - runtimeDisplayPath, runtimeVersion: applicationEnv.packageJson.version, runtimeSource, languageId: PYTHON_LANGUAGE, diff --git a/extensions/positron-r/src/provider.ts b/extensions/positron-r/src/provider.ts index 9eb65f4f8146..719759abeae4 100644 --- a/extensions/positron-r/src/provider.ts +++ b/extensions/positron-r/src/provider.ts @@ -642,13 +642,7 @@ export async function makeMetadata( const homedir = os.homedir(); const isUserInstallation = rInst.binpath.startsWith(homedir); - // runtimePath is always the full absolute path to the binary so callers - // can use it directly with execFile etc. runtimeDisplayPath carries the - // human-friendly ~ shorthand shown in the UI. const runtimePath = rInst.binpath; - const runtimeDisplayPath = os.platform() !== 'win32' && isUserInstallation ? - path.join('~', rInst.binpath.substring(homedir.length)) : - undefined; // Create the Rscript path. // The Rscript path is the same as the R binary path, but with the 'R' or 'R.exe' executable @@ -729,7 +723,6 @@ export async function makeMetadata( runtimeName, runtimeShortName, runtimePath, - runtimeDisplayPath, runtimeVersion: packageJson.version, runtimeSource, languageId: 'r', diff --git a/extensions/positron-r/src/runtime-quickpick.ts b/extensions/positron-r/src/runtime-quickpick.ts index de9b280ba3a9..0d2068188f3e 100644 --- a/extensions/positron-r/src/runtime-quickpick.ts +++ b/extensions/positron-r/src/runtime-quickpick.ts @@ -3,6 +3,8 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +import * as os from 'os'; +import * as path from 'path'; import * as positron from 'positron'; import * as vscode from 'vscode'; import { rRuntimeDiscoverer, RRuntimeSource } from './provider'; @@ -18,7 +20,11 @@ class RuntimeQuickPickItem implements vscode.QuickPickItem { public runtimeMetadata: positron.LanguageRuntimeMetadata, ) { this.label = runtimeMetadata.runtimeName; - this.description = runtimeMetadata.runtimeDisplayPath ?? runtimeMetadata.runtimePath; + const homedir = os.homedir(); + this.description = + os.platform() !== 'win32' && runtimeMetadata.runtimePath.startsWith(homedir) + ? path.join('~', runtimeMetadata.runtimePath.substring(homedir.length)) + : runtimeMetadata.runtimePath; this.runtime = runtimeMetadata; } } diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index beb71af06d25..84c53a6a4f3e 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -634,13 +634,6 @@ declare module 'positron' { */ runtimePath: string; - /** - * The runtime path formatted for display to the user. May use ~ as - * shorthand for the user's home directory on non-Windows platforms. - * Falls back to runtimePath when not set. - */ - runtimeDisplayPath?: string; - /** A unique identifier for this runtime; takes the form of a GUID */ runtimeId: string; diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts index 1b6a025e46c8..897fb6971310 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts @@ -5,6 +5,7 @@ import * as nls from '../../../../nls.js'; import { Event, Emitter } from '../../../../base/common/event.js'; +import { tildify } from '../../../../base/common/labels.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -13,6 +14,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationNode, } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { IPathService } from '../../../services/path/common/pathService.js'; /** * The implementation of ILanguageRuntimeService @@ -50,7 +52,8 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti */ constructor( @ILogService private readonly _logService: ILogService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IPathService private readonly _pathService: IPathService, ) { // Call the base class's constructor. super(); @@ -131,11 +134,19 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti `the '${metadata.languageId}' language is disabled.`); } + // Enrich metadata with a workbench-computed display path (~-shortened + // on non-Windows; absolute path unchanged on Windows or system paths). + const userHome = this._pathService.userHome({ preferLocal: true }).fsPath; + const enriched: ILanguageRuntimeMetadata = { + ...metadata, + runtimeDisplayPath: tildify(metadata.runtimePath, userHome), + }; + // Add the runtime to the registered runtimes. - this._registeredRuntimesByRuntimeId.set(metadata.runtimeId, metadata); + this._registeredRuntimesByRuntimeId.set(enriched.runtimeId, enriched); // Signal that the set of registered runtimes has changed. - this._onDidRegisterRuntimeEmitter.fire(metadata); + this._onDidRegisterRuntimeEmitter.fire(enriched); // Logging. this._logService.trace(`Language runtime ${formatLanguageRuntimeMetadata(metadata)} successfully registered.`); diff --git a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts index 1c57f881a4d3..61b984b6c3ed 100644 --- a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts +++ b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts @@ -6,15 +6,22 @@ /// import { raceTimeout } from '../../../../../base/common/async.js'; +import { URI } from '../../../../../base/common/uri.js'; import { createTestContainer } from '../../../../../test/vitest/positronTestContainer.js'; import { stubInterface } from '../../../../../test/vitest/stubInterface.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { ILogService, NullLogger } from '../../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IPathService } from '../../../../services/path/common/pathService.js'; import { LanguageRuntimeService } from '../../common/languageRuntime.js'; import { getRuntimeDisplayPath, ILanguageRuntimeMetadata, LanguageRuntimeSessionLocation, LanguageRuntimeStartupBehavior, LanguageStartupBehavior } from '../../common/languageRuntimeService.js'; +const TEST_USER_HOME = URI.file('/home/testuser'); +const pathServiceStub = stubInterface({ + userHome: vi.fn().mockReturnValue(TEST_USER_HOME), +}); + /** * Shared metadata fields for test stubs. Both tests use the same base shape; * only runtimeId and languageId differ. @@ -55,6 +62,7 @@ describe('Positron - LanguageRuntimeService', () => { .withRuntimeServices() .stub(ILogService, new NullLogger()) .stub(IConfigurationService, new TestConfigurationService()) + .stub(IPathService, pathServiceStub) .build(); it('register and unregister a runtime', async () => { @@ -87,8 +95,10 @@ describe('Positron - LanguageRuntimeService', () => { await raceTimeout(didRegisterRuntime, 10, () => timedOut = true); expect(timedOut, 'Awaiting onDidRegisterRuntime event timed out').toBe(false); - // Check that the runtime was registered. - expect(languageRuntimeService.registeredRuntimes).toEqual([metadata]); + // Check that the runtime was registered (with workbench-computed display path). + expect(languageRuntimeService.registeredRuntimes).toMatchObject([ + { runtimeId: metadata.runtimeId, runtimePath: metadata.runtimePath }, + ]); // Unregister the runtime. languageRuntimeService.unregisterRuntime(metadata.runtimeId); @@ -99,6 +109,20 @@ describe('Positron - LanguageRuntimeService', () => { // No-op since we already unregistered the runtime. runtimeDisposable.dispose(); }); + + it('enriches runtimeDisplayPath via tildify on registration', () => { + const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); + + // A path under the test user home should be tildified. + const underHome = makeTestMetadata({ runtimeId: 'tilde-1', runtimePath: '/home/testuser/bin/R' }); + languageRuntimeService.registerRuntime(underHome); + expect(languageRuntimeService.registeredRuntimes[0].runtimeDisplayPath).toBe('~/bin/R'); + + // A system path should be left unchanged. + const system = makeTestMetadata({ runtimeId: 'tilde-2', runtimePath: '/usr/bin/R' }); + languageRuntimeService.registerRuntime(system); + expect(languageRuntimeService.registeredRuntimes[1].runtimeDisplayPath).toBe('/usr/bin/R'); + }); }); describe('onDidUnregisterRuntime', () => { @@ -106,6 +130,7 @@ describe('Positron - LanguageRuntimeService', () => { .withRuntimeServices() .stub(ILogService, new NullLogger()) .stub(IConfigurationService, new TestConfigurationService()) + .stub(IPathService, pathServiceStub) .build(); it('fires with the runtimeId and removes the runtime when unregistered', () => { @@ -158,6 +183,7 @@ describe('Positron - LanguageRuntimeService', () => { .withRuntimeServices() .stub(ILogService, new NullLogger()) .stub(IConfigurationService, configService) + .stub(IPathService, pathServiceStub) .build(); it('cannot register a runtime when the language is disabled in configuration', async () => { From 0a5ecf5b42fbc40a6e1085a924f7227f553673a5 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Mon, 13 Jul 2026 12:01:20 -0500 Subject: [PATCH 05/14] Remove obsolete runtimeDisplayPath extension tests, add affiliation-healing test The extensions no longer compute runtimeDisplayPath (that moved to LanguageRuntimeService), so drop the ~-shorthand assertions from the positron-python and positron-r unit tests. Add a runtimeStartup vitest covering stale ~-prefixed affiliation paths being healed on register. Also picks up incidental package-lock.json optional-dep drift. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/test/positron/runtime.unit.test.ts | 20 ------ .../positron-r/src/test/provider.unit.test.ts | 13 ---- package-lock.json | 12 ++++ .../test/common/runtimeStartup.vitest.ts | 67 +++++++++++++++++++ 4 files changed, 79 insertions(+), 33 deletions(-) diff --git a/extensions/positron-python/src/test/positron/runtime.unit.test.ts b/extensions/positron-python/src/test/positron/runtime.unit.test.ts index 939d5a8ff80f..ed5bceefb7cf 100644 --- a/extensions/positron-python/src/test/positron/runtime.unit.test.ts +++ b/extensions/positron-python/src/test/positron/runtime.unit.test.ts @@ -5,8 +5,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as os from 'os'; -import * as path from 'path'; import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { assert } from 'chai'; @@ -134,22 +132,4 @@ suite('createPythonRuntimeMetadata path fields', () => { assert.strictEqual(metadata.runtimePath, interpreterPath); }); - test('runtimeDisplayPath uses ~ shorthand for a home-dir interpreter on non-Windows', async function () { - if (os.platform() === 'win32') { - this.skip(); - } - const interpreterPath = path.join(os.homedir(), '.venv', 'bin', 'python'); - const metadata = await createPythonRuntimeMetadata(makeInterpreter(interpreterPath), serviceContainer.object, false); - assert.ok( - metadata.runtimeDisplayPath?.startsWith('~'), - `Expected ~ prefix, got: ${metadata.runtimeDisplayPath}`, - ); - assert.strictEqual(metadata.runtimePath, interpreterPath, 'runtimePath must remain absolute'); - }); - - test('runtimeDisplayPath is undefined for a system (non-home-dir) interpreter', async () => { - const interpreterPath = '/usr/bin/python3'; - const metadata = await createPythonRuntimeMetadata(makeInterpreter(interpreterPath), serviceContainer.object, false); - assert.strictEqual(metadata.runtimeDisplayPath, undefined); - }); }); diff --git a/extensions/positron-r/src/test/provider.unit.test.ts b/extensions/positron-r/src/test/provider.unit.test.ts index a9579eb7b7a8..ae63841d29eb 100644 --- a/extensions/positron-r/src/test/provider.unit.test.ts +++ b/extensions/positron-r/src/test/provider.unit.test.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'path'; import * as Sinon from 'sinon'; import * as vscode from 'vscode'; import { makeMetadata } from '../provider'; @@ -47,17 +45,6 @@ suite('makeMetadata path fields', () => { assert.strictEqual(metadata.runtimePath, binpath); }); - test('runtimeDisplayPath uses ~ shorthand for a home-dir install on non-Windows', async function () { - if (os.platform() === 'win32') { this.skip(); } - const binpath = path.join(os.homedir(), '.local', 'bin', 'R'); - const metadata = await makeMetadata(makeRInst(binpath)); - assert.ok( - metadata.runtimeDisplayPath?.startsWith('~'), - `Expected ~ prefix, got: ${metadata.runtimeDisplayPath}`, - ); - assert.strictEqual(metadata.runtimePath, binpath, 'runtimePath must remain absolute'); - }); - test('runtimeDisplayPath is undefined for a system (non-home-dir) install', async () => { const binpath = '/usr/bin/R'; const metadata = await makeMetadata(makeRInst(binpath)); diff --git a/package-lock.json b/package-lock.json index 7837ede05b51..6badce39e3d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3678,6 +3678,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm-musl": { + "optional": true + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm64-musl": { + "optional": true + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-x64-musl": { + "optional": true + }, "node_modules/@parcel/watcher/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -22986,6 +22995,9 @@ "nan": "^2.23.0" } }, + "node_modules/ssh2/node_modules/cpu-features": { + "optional": true + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", diff --git a/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts b/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts index 99e8c2efa0c0..32ea88a6bb02 100644 --- a/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts +++ b/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts @@ -33,6 +33,7 @@ import { BeforeShutdownEvent, ILifecycleService, WillShutdownEvent } from '../.. import { IPositronNewFolderService, NewFolderStartupPhase } from '../../../positronNewFolder/common/positronNewFolder.js'; import { ILanguageRuntimeSession } from '../../../runtimeSession/common/runtimeSessionService.js'; import { RuntimeStartupService } from '../../common/runtimeStartup.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ICachedRuntime, IDiscoveryCacheBucket, @@ -1043,6 +1044,72 @@ describe('Positron - RuntimeStartupService Architecture Mismatch', () => { }); }); +describe('RuntimeStartupService - affiliation healing', () => { + + const ctx = createTestContainer() + .withRuntimeServices() + .stub(IEphemeralStateService, { + getItem: () => Promise.resolve(undefined), + setItem: () => Promise.resolve(), + }) + .stub(ILifecycleService, { + onBeforeShutdown: new Emitter().event, + onWillShutdown: new Emitter().event, + }) + .stub(IPositronNewFolderService, { + onDidChangeNewFolderStartupPhase: new Emitter().event, + startupPhase: NewFolderStartupPhase.Complete, + }) + .stub(IProgressService, {}) + .stub(IWorkbenchEnvironmentService, { remoteAuthority: undefined }) + .stub(INotificationService, new TestNotificationService()) + .stub(IRuntimeDiscoveryCache, {}) + .build(); + + it('heals a stale ~-prefixed runtimePath in stored affiliation when the runtime registers', () => { + const runtimeId = 'r-stale-path-test'; + const languageId = 'r'; + const staleRuntimePath = '~/local/lib/R/bin/R'; + const freshRuntimePath = '/home/positron/local/lib/R/bin/R'; + const storageKey = `positron.affiliatedRuntimeMetadata.v2.${languageId}`; + // TestContextService uses TestWorkspace (one folder, no configuration) → + // WorkbenchState.FOLDER → affiliationStorageScope() returns WORKSPACE. + const scope = StorageScope.WORKSPACE; + + // Pre-populate storage with a stale affiliation whose runtimePath was + // shortened with a ~ prefix before the runtimePath/runtimeDisplayPath split. + const staleAffiliated = { + metadata: metadata({ languageId, runtimePath: staleRuntimePath, runtimeId, extensionId: 'ms.r' }), + lastUsed: 0, + lastStarted: 0, + }; + ctx.get(IStorageService).store(storageKey, JSON.stringify(staleAffiliated), scope, StorageTarget.MACHINE); + + const svc = ctx.disposables.add( + ctx.instantiationService.createInstance(RuntimeStartupService)) as RuntimeStartupService; + // Force into LoadingCache phase so onDidRegisterRuntime heals the affiliation. + // LoadingCache (not Discovering) is used here to skip the upsert branch that + // only runs during Discovering; the heal runs in both phases. + (svc as unknown as { _startupPhase: RuntimeStartupPhase })._startupPhase = RuntimeStartupPhase.LoadingCache; + + // Use Manual startup behavior so the service heals the stored affiliation + // without attempting to auto-start a runtime (which has no extension host). + const freshMd: ILanguageRuntimeMetadata = { + ...metadata({ languageId, runtimePath: freshRuntimePath, runtimeId, extensionId: 'ms.r' }), + startupBehavior: LanguageRuntimeStartupBehavior.Manual, + }; + // Registering the runtime fires onDidRegisterRuntime, which heals the stored affiliation. + ctx.get(ILanguageRuntimeService).registerRuntime(freshMd); + + // The stored affiliation must now carry the absolute path. + const stored = JSON.parse(ctx.get(IStorageService).get(storageKey, scope)!); + expect(stored.metadata.runtimePath).toBe(freshRuntimePath); + + // getAffiliatedRuntimes() returns the live registered metadata (absolute path). + expect(svc.getAffiliatedRuntimes()[0].runtimePath).toBe(freshRuntimePath); + }); +}); + /** * Mock notification service that captures prompt calls for testing. */ From c681632a4e863fb95389835189ca1af954b43d35 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Mon, 13 Jul 2026 12:54:32 -0500 Subject: [PATCH 06/14] Fix runtime metadata test assertions and lint headers --- .../positron-python/src/client/positron/runtime.ts | 1 - extensions/positron-r/src/test/provider.unit.test.ts | 2 +- .../test/browser/languageRuntimeActions.vitest.ts | 10 +++++++--- .../test/browser/startupStatus.vitest.tsx | 6 +++--- .../test/common/runtimeSession.vitest.ts | 6 ++++-- .../test/common/testRuntimeSessionService.ts | 6 +++++- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/extensions/positron-python/src/client/positron/runtime.ts b/extensions/positron-python/src/client/positron/runtime.ts index 62242b3b7804..5795ec68bb5c 100644 --- a/extensions/positron-python/src/client/positron/runtime.ts +++ b/extensions/positron-python/src/client/positron/runtime.ts @@ -3,7 +3,6 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -// eslint-disable-next-line import/no-unresolved import * as positron from 'positron'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; diff --git a/extensions/positron-r/src/test/provider.unit.test.ts b/extensions/positron-r/src/test/provider.unit.test.ts index ae63841d29eb..9117d19a485f 100644 --- a/extensions/positron-r/src/test/provider.unit.test.ts +++ b/extensions/positron-r/src/test/provider.unit.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (C) 2026 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt in the project root for license information. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts index bdd2b9b98de3..238041bead56 100644 --- a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts +++ b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts @@ -86,11 +86,14 @@ describe('selectNewLanguageRuntime', () => { } function registerRuntime(metadata: ILanguageRuntimeMetadata): ILanguageRuntimeMetadata { - ctx.disposables.add(ctx.get(ILanguageRuntimeService).registerRuntime(metadata)); + const runtimeService = ctx.get(ILanguageRuntimeService); + ctx.disposables.add(runtimeService.registerRuntime(metadata)); if (!preferredByLanguage.has(metadata.languageId)) { preferredByLanguage.set(metadata.languageId, metadata); } - return metadata; + // registerRuntime enriches the metadata into a new object (e.g. adds + // runtimeDisplayPath); return the stored instance the picker resolves to. + return runtimeService.getRegisteredRuntime(metadata.runtimeId) ?? metadata; } function pickItemById(id: string): IQuickPickItem | undefined { @@ -440,7 +443,8 @@ describe('selectNewLanguageRuntime', () => { .find((item): item is IQuickPickItem => item.type !== 'separator' && item.label === 'Install Python via uv')!; pick.accept(installItem); - await expect(promise).resolves.toEqual(installedRuntime); + // The picker resolves to the enriched, registered instance. + await expect(promise).resolves.toEqual(runtimeService.getRegisteredRuntime(installedRuntime.runtimeId)); expect(contribution.onSelect).toHaveBeenCalledWith('install-uv'); expect(rediscoverAllRuntimes).toHaveBeenCalledWith(/* quiet */ true); }); diff --git a/src/vs/workbench/contrib/positronConsole/test/browser/startupStatus.vitest.tsx b/src/vs/workbench/contrib/positronConsole/test/browser/startupStatus.vitest.tsx index d48ec153a49f..8a3fd7ee5ac0 100644 --- a/src/vs/workbench/contrib/positronConsole/test/browser/startupStatus.vitest.tsx +++ b/src/vs/workbench/contrib/positronConsole/test/browser/startupStatus.vitest.tsx @@ -162,7 +162,7 @@ describe('StartupStatus', () => { rtl.render(); act(() => { - const runtime = stubInterface({ runtimePath: '/usr/bin/python' }); + const runtime = stubInterface({ runtimePath: '/usr/bin/python', runtimeDisplayPath: undefined }); langMock.registeredRuntimes.push(runtime); langMock.onDidRegisterRuntime.fire(runtime); }); @@ -174,7 +174,7 @@ describe('StartupStatus', () => { rtl.render(); act(() => { - const runtime = stubInterface({ runtimePath: '/opt/local/python' }); + const runtime = stubInterface({ runtimePath: '/opt/local/python', runtimeDisplayPath: undefined }); langMock.registeredRuntimes.push(runtime); langMock.onDidRegisterRuntime.fire(runtime); }); @@ -186,7 +186,7 @@ describe('StartupStatus', () => { rtl.render(); act(() => { - const runtime = stubInterface({ runtimePath: '/usr/bin/python' }); + const runtime = stubInterface({ runtimePath: '/usr/bin/python', runtimeDisplayPath: undefined }); langMock.registeredRuntimes.push(runtime); langMock.onDidRegisterRuntime.fire(runtime); }); diff --git a/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.vitest.ts b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.vitest.ts index e09894f67cd4..abe184fd147e 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.vitest.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.vitest.ts @@ -605,8 +605,10 @@ describe('Positron - RuntimeSessionService', () => { await restoreConsole(unregisteredRuntime); - // The runtime should now be registered. - expect(languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId)).toBe(unregisteredRuntime); + // The runtime should now be registered. registerRuntime enriches the + // metadata into a new object, so compare structurally rather than by + // reference. + expect(languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId)).toEqual(unregisteredRuntime); }); it('auto start validates runtime if unregistered', async () => { diff --git a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts index 27f4d4029b83..a2a2cb4f94b2 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts @@ -94,7 +94,11 @@ export function createTestLanguageRuntimeMetadata( const manager = TestRuntimeSessionManager.instance; disposables.add(runtimeSessionService.registerSessionManager(manager)); - return runtime; + // Return the registered metadata instance rather than the local literal. + // registerRuntime enriches the metadata (e.g. adds runtimeDisplayPath) and + // stores that enriched object, which is what sessions reference; returning + // it keeps reference equality intact for callers. + return languageRuntimeService.getRegisteredRuntime(runtime.runtimeId) ?? runtime; } export interface IStartTestLanguageRuntimeSessionOptions { From c31d9a67f1626471333a515ff1a1397a15dd66a9 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Mon, 13 Jul 2026 13:04:05 -0500 Subject: [PATCH 07/14] Remove leftover runtimeDisplayPath test missed by prior cleanup commit Co-Authored-By: Claude Sonnet 4.6 --- extensions/positron-r/src/test/provider.unit.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extensions/positron-r/src/test/provider.unit.test.ts b/extensions/positron-r/src/test/provider.unit.test.ts index 9117d19a485f..8d2354c22c93 100644 --- a/extensions/positron-r/src/test/provider.unit.test.ts +++ b/extensions/positron-r/src/test/provider.unit.test.ts @@ -45,9 +45,4 @@ suite('makeMetadata path fields', () => { assert.strictEqual(metadata.runtimePath, binpath); }); - test('runtimeDisplayPath is undefined for a system (non-home-dir) install', async () => { - const binpath = '/usr/bin/R'; - const metadata = await makeMetadata(makeRInst(binpath)); - assert.strictEqual(metadata.runtimeDisplayPath, undefined); - }); }); From 328e74783e0acfcfd5c9531dff611a46dbc2df92 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Mon, 13 Jul 2026 13:24:09 -0500 Subject: [PATCH 08/14] Add LanguageRuntimeStartupBehavior and LanguageRuntimeSessionLocation enums; update mocks --- .../positron-python/src/test/mocks/pst/index.ts | 13 +++++++++++++ .../src/test/positron/runtime.unit.test.ts | 7 +++++-- extensions/positron-python/src/test/vscode-mock.ts | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/extensions/positron-python/src/test/mocks/pst/index.ts b/extensions/positron-python/src/test/mocks/pst/index.ts index 98f6f9be28e2..3904c104e3df 100644 --- a/extensions/positron-python/src/test/mocks/pst/index.ts +++ b/extensions/positron-python/src/test/mocks/pst/index.ts @@ -114,3 +114,16 @@ export enum RuntimeExitReason { ExtensionHost = 'extensionHost', Unknown = 'unknown', } + +export enum LanguageRuntimeStartupBehavior { + Immediate = 'immediate', + Implicit = 'implicit', + Explicit = 'explicit', + Manual = 'manual', +} + +export enum LanguageRuntimeSessionLocation { + Machine = 'machine', + Workspace = 'workspace', + Browser = 'browser', +} diff --git a/extensions/positron-python/src/test/positron/runtime.unit.test.ts b/extensions/positron-python/src/test/positron/runtime.unit.test.ts index ed5bceefb7cf..cf7d21cdd3cf 100644 --- a/extensions/positron-python/src/test/positron/runtime.unit.test.ts +++ b/extensions/positron-python/src/test/positron/runtime.unit.test.ts @@ -128,8 +128,11 @@ suite('createPythonRuntimeMetadata path fields', () => { test('runtimePath is always the full absolute interpreter path', async () => { const interpreterPath = '/usr/bin/python3'; - const metadata = await createPythonRuntimeMetadata(makeInterpreter(interpreterPath), serviceContainer.object, false); + const metadata = await createPythonRuntimeMetadata( + makeInterpreter(interpreterPath), + serviceContainer.object, + false, + ); assert.strictEqual(metadata.runtimePath, interpreterPath); }); - }); diff --git a/extensions/positron-python/src/test/vscode-mock.ts b/extensions/positron-python/src/test/vscode-mock.ts index 3f3e3ca0fe72..632c59cc7a4e 100644 --- a/extensions/positron-python/src/test/vscode-mock.ts +++ b/extensions/positron-python/src/test/vscode-mock.ts @@ -195,6 +195,8 @@ mockedPositron.RuntimeOnlineState = positronMocks.RuntimeOnlineState; mockedPositron.RuntimeState = positronMocks.RuntimeState; mockedPositron.LanguageRuntimeArchitecture = positronMocks.LanguageRuntimeArchitecture; mockedPositron.RuntimeExitReason = positronMocks.RuntimeExitReason; +mockedPositron.LanguageRuntimeStartupBehavior = positronMocks.LanguageRuntimeStartupBehavior; +mockedPositron.LanguageRuntimeSessionLocation = positronMocks.LanguageRuntimeSessionLocation; // --- End Positron --- // Mock TestController for vscode.tests namespace From 703c0187999d70a58acaab6f53e692363e6340d8 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Tue, 14 Jul 2026 15:40:25 -0500 Subject: [PATCH 09/14] Remove createPythonRuntimeMetadata path tests; clean up now-unused pst mock enums runtimeDisplayPath is now computed in LanguageRuntimeService, so the extension-level runtimePath test is obsolete. The LanguageRuntimeStartupBehavior and LanguageRuntimeSessionLocation enum copies in the pst mock (and their vscode-mock wiring) are no longer needed since the test that used them is gone. Co-Authored-By: Claude Sonnet 4.6 --- .../src/test/mocks/pst/index.ts | 13 ---- .../src/test/positron/runtime.unit.test.ts | 75 +------------------ .../positron-python/src/test/vscode-mock.ts | 2 - .../positron-r/src/test/provider.unit.test.ts | 48 ------------ 4 files changed, 2 insertions(+), 136 deletions(-) delete mode 100644 extensions/positron-r/src/test/provider.unit.test.ts diff --git a/extensions/positron-python/src/test/mocks/pst/index.ts b/extensions/positron-python/src/test/mocks/pst/index.ts index 3904c104e3df..98f6f9be28e2 100644 --- a/extensions/positron-python/src/test/mocks/pst/index.ts +++ b/extensions/positron-python/src/test/mocks/pst/index.ts @@ -114,16 +114,3 @@ export enum RuntimeExitReason { ExtensionHost = 'extensionHost', Unknown = 'unknown', } - -export enum LanguageRuntimeStartupBehavior { - Immediate = 'immediate', - Implicit = 'implicit', - Explicit = 'explicit', - Manual = 'manual', -} - -export enum LanguageRuntimeSessionLocation { - Machine = 'machine', - Workspace = 'workspace', - Browser = 'browser', -} diff --git a/extensions/positron-python/src/test/positron/runtime.unit.test.ts b/extensions/positron-python/src/test/positron/runtime.unit.test.ts index cf7d21cdd3cf..e32dacaeb7c1 100644 --- a/extensions/positron-python/src/test/positron/runtime.unit.test.ts +++ b/extensions/positron-python/src/test/positron/runtime.unit.test.ts @@ -3,21 +3,10 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; import { assert } from 'chai'; -import { anything, reset, when } from 'ts-mockito'; -import * as ipykernelModule from '../../client/positron/ipykernel'; -import * as moduleLocator from '../../client/pythonEnvironments/base/locators/lowLevel/moduleEnvironmentLocator'; -import * as environmentTypeComparer from '../../client/interpreter/configuration/environmentTypeComparer'; -import { createPythonRuntimeMetadata, getRuntimeSourceAndShortName } from '../../client/positron/runtime'; -import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { PythonEnvironment, EnvironmentType } from '../../client/pythonEnvironments/info'; +import { getRuntimeSourceAndShortName } from '../../client/positron/runtime'; +import { EnvironmentType } from '../../client/pythonEnvironments/info'; import { ModuleMetadata } from '../../client/pythonEnvironments/base/locators/lowLevel/moduleEnvironmentLocator'; -import { mockedVSCodeNamespaces } from '../vscode-mock'; suite('getRuntimeSourceAndShortName', () => { const MODULE_METADATA: ModuleMetadata = { @@ -76,63 +65,3 @@ suite('getRuntimeSourceAndShortName', () => { }); }); }); - -function makeInterpreter(interpreterPath: string): PythonEnvironment { - return { - path: interpreterPath, - envType: EnvironmentType.System, - envName: undefined, - version: { major: 3, minor: 10, patch: 0, raw: '3.10.0', release: { type: 0, version: 0 } }, - sysVersion: '3.10.0', - envPath: undefined, - } as unknown as PythonEnvironment; -} - -suite('createPythonRuntimeMetadata path fields', () => { - let sandbox: sinon.SinonSandbox; - let serviceContainer: TypeMoq.IMock; - - setup(() => { - sandbox = sinon.createSandbox(); - - // Return "bundled, no issues" so the installer check branch is skipped. - sandbox.stub(ipykernelModule, 'getIpykernelBundle').resolves({ disabledReason: undefined }); - - // Module metadata resolves immediately with an empty map. - sandbox.stub(moduleLocator, 'whenModuleMetadataReady').resolves(); - moduleLocator.moduleMetadataMap.clear(); - - // Suppress the version-unsupported flag in the runtime name. - sandbox.stub(environmentTypeComparer, 'isVersionSupported').returns(true); - - // Stub vscode.workspace.getConfiguration used for the kernelSupervisor config. - when(mockedVSCodeNamespaces.workspace!.getConfiguration(anything())).thenReturn({ - get: (_key: string, def?: unknown) => def, - } as any); - - const workspaceService = TypeMoq.Mock.ofType(); - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - - const appEnv = TypeMoq.Mock.ofType(); - appEnv.setup((a) => a.packageJson).returns(() => ({ version: '2025.0.0' })); - - serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); - serviceContainer.setup((s) => s.get(IApplicationEnvironment)).returns(() => appEnv.object); - }); - - teardown(() => { - sandbox.restore(); - reset(mockedVSCodeNamespaces.workspace!); - }); - - test('runtimePath is always the full absolute interpreter path', async () => { - const interpreterPath = '/usr/bin/python3'; - const metadata = await createPythonRuntimeMetadata( - makeInterpreter(interpreterPath), - serviceContainer.object, - false, - ); - assert.strictEqual(metadata.runtimePath, interpreterPath); - }); -}); diff --git a/extensions/positron-python/src/test/vscode-mock.ts b/extensions/positron-python/src/test/vscode-mock.ts index 632c59cc7a4e..3f3e3ca0fe72 100644 --- a/extensions/positron-python/src/test/vscode-mock.ts +++ b/extensions/positron-python/src/test/vscode-mock.ts @@ -195,8 +195,6 @@ mockedPositron.RuntimeOnlineState = positronMocks.RuntimeOnlineState; mockedPositron.RuntimeState = positronMocks.RuntimeState; mockedPositron.LanguageRuntimeArchitecture = positronMocks.LanguageRuntimeArchitecture; mockedPositron.RuntimeExitReason = positronMocks.RuntimeExitReason; -mockedPositron.LanguageRuntimeStartupBehavior = positronMocks.LanguageRuntimeStartupBehavior; -mockedPositron.LanguageRuntimeSessionLocation = positronMocks.LanguageRuntimeSessionLocation; // --- End Positron --- // Mock TestController for vscode.tests namespace diff --git a/extensions/positron-r/src/test/provider.unit.test.ts b/extensions/positron-r/src/test/provider.unit.test.ts deleted file mode 100644 index 8d2354c22c93..000000000000 --- a/extensions/positron-r/src/test/provider.unit.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2026 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as Sinon from 'sinon'; -import * as vscode from 'vscode'; -import { makeMetadata } from '../provider'; -import { ReasonDiscovered, RInstallation } from '../r-installation'; - -function makeRInst(binpath: string): RInstallation { - return { - binpath, - homepath: '', - version: '4.4.0', - semVersion: { major: 4, minor: 4, patch: 0 } as any, - arch: '', - usable: true, - supported: true, - current: false, - orthogonal: false, - default: false, - packagerMetadata: undefined, - reasonDiscovered: [ReasonDiscovered.PATH], - reasonRejected: null, - } as unknown as RInstallation; -} - -suite('makeMetadata path fields', () => { - let sandbox: Sinon.SinonSandbox; - - setup(() => { - sandbox = Sinon.createSandbox(); - sandbox.stub(vscode.workspace, 'getConfiguration').returns({ - get: (_key: string, def?: unknown) => def, - } as unknown as vscode.WorkspaceConfiguration); - }); - - teardown(() => sandbox.restore()); - - test('runtimePath is always the full absolute binary path', async () => { - const binpath = '/usr/bin/R'; - const metadata = await makeMetadata(makeRInst(binpath)); - assert.strictEqual(metadata.runtimePath, binpath); - }); - -}); From 2021268f071b20bcad8187db3bef27b6d48aae8d Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Tue, 14 Jul 2026 16:57:20 -0500 Subject: [PATCH 10/14] Make registerRuntime async; use remote userHome for runtimeDisplayPath tildification Uses preferLocal: false so paths are tildified against the remote home directory when connected to a remote, not the local one. Adds a ?? fallback to preserve a caller-supplied runtimeDisplayPath rather than overwriting it. Co-Authored-By: Claude Sonnet 4.6 --- .../positron/mainThreadLanguageRuntime.ts | 4 +- .../browser/languageRuntimeActions.vitest.ts | 57 ++++++++++--------- .../positronAssistantService.vitest.ts | 2 +- ...tronWebviewPreloadReconciliation.vitest.ts | 2 +- .../browser/runtimeNotebookKernel.vitest.ts | 4 +- .../runtimeNotebookKernelService.vitest.ts | 6 +- .../foregroundSessionContribution.vitest.ts | 4 +- .../languageRuntime/common/languageRuntime.ts | 11 +++- .../common/languageRuntimeService.ts | 2 +- .../test/common/languageRuntime.vitest.ts | 23 ++++---- .../runtimeSession/common/runtimeSession.ts | 4 +- .../test/common/testRuntimeSessionService.ts | 8 +-- .../runtimeStartup/common/runtimeStartup.ts | 6 +- .../test/common/runtimeStartup.vitest.ts | 4 +- 14 files changed, 72 insertions(+), 65 deletions(-) diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index 4507ac08d416..6fdd244aa8d0 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -1731,9 +1731,9 @@ export class MainThreadLanguageRuntime } // Called by the extension host to register a language runtime - $registerLanguageRuntime(metadata: ILanguageRuntimeMetadata): void { + async $registerLanguageRuntime(metadata: ILanguageRuntimeMetadata): Promise { this._registeredRuntimes.set(metadata.runtimeId, metadata); - this._languageRuntimeService.registerRuntime(metadata); + await this._languageRuntimeService.registerRuntime(metadata); } $getPreferredRuntime(languageId: string): Promise { diff --git a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts index 238041bead56..7c391fa1e483 100644 --- a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts +++ b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts @@ -85,9 +85,9 @@ describe('selectNewLanguageRuntime', () => { await vi.waitFor(() => expect(pick.show).toHaveBeenCalled()); } - function registerRuntime(metadata: ILanguageRuntimeMetadata): ILanguageRuntimeMetadata { + async function registerRuntime(metadata: ILanguageRuntimeMetadata): Promise { const runtimeService = ctx.get(ILanguageRuntimeService); - ctx.disposables.add(runtimeService.registerRuntime(metadata)); + ctx.disposables.add(await runtimeService.registerRuntime(metadata)); if (!preferredByLanguage.has(metadata.languageId)) { preferredByLanguage.set(metadata.languageId, metadata); } @@ -112,7 +112,7 @@ describe('selectNewLanguageRuntime', () => { }); it('resolves to the selected runtime metadata', async () => { - const py = registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + const py = await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); const promise = runPicker(); await waitUntilOpened(); const item = pickItemById('py-1')!; @@ -138,8 +138,8 @@ describe('selectNewLanguageRuntime', () => { describe('options.languageId', () => { it('filters runtimes to the given languageId', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1', languageId: 'python', languageName: 'Python' })); - registerRuntime(makeRuntime({ runtimeId: 'r-1', languageId: 'r', languageName: 'R', runtimeName: 'R 4.4' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1', languageId: 'python', languageName: 'Python' })); + await registerRuntime(makeRuntime({ runtimeId: 'r-1', languageId: 'r', languageName: 'R', runtimeName: 'R 4.4' })); const promise = runPicker({ languageId: 'python' }); await waitUntilOpened(); @@ -153,7 +153,7 @@ describe('selectNewLanguageRuntime', () => { }); it('passes languageId through to getPickerContributions', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); const spy = vi.spyOn(ctx.get(ILanguageRuntimeService), 'getPickerContributions'); const promise = runPicker({ languageId: 'python' }); await waitUntilOpened(); @@ -165,8 +165,8 @@ describe('selectNewLanguageRuntime', () => { describe('options.currentRuntimeId', () => { it('pre-focuses the matching item via activeItems', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); - registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0', runtimeName: 'Python 3.10' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0', runtimeName: 'Python 3.10' })); const promise = runPicker({ currentRuntimeId: 'py-2' }); await waitUntilOpened(); @@ -177,7 +177,7 @@ describe('selectNewLanguageRuntime', () => { }); it('leaves activeItems untouched when no item matches the id', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); const promise = runPicker({ currentRuntimeId: 'unknown-id' }); await waitUntilOpened(); expect(pick.activeItems).toEqual([]); @@ -188,8 +188,8 @@ describe('selectNewLanguageRuntime', () => { describe('item structure', () => { it('groups Suggested + per-environment-type runtimes with separators', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-system', runtimeSource: 'System', runtimeName: 'Python (System)' })); - registerRuntime(makeRuntime({ runtimeId: 'py-conda', runtimeSource: 'Conda', runtimeName: 'Python (Conda)' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-system', runtimeSource: 'System', runtimeName: 'Python (System)' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-conda', runtimeSource: 'Conda', runtimeName: 'Python (Conda)' })); const promise = runPicker(); await waitUntilOpened(); @@ -211,9 +211,9 @@ describe('selectNewLanguageRuntime', () => { }); it('sorts within an env type by version descending, unsupported runtimes last', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-310', languageVersion: '3.10.0', runtimeName: 'Python 3.10' })); - registerRuntime(makeRuntime({ runtimeId: 'py-312', languageVersion: '3.12.0', runtimeName: 'Python 3.12' })); - registerRuntime(makeRuntime({ + await registerRuntime(makeRuntime({ runtimeId: 'py-310', languageVersion: '3.10.0', runtimeName: 'Python 3.10' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-312', languageVersion: '3.12.0', runtimeName: 'Python 3.12' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-old', languageVersion: '3.8.0', runtimeName: 'Python 3.8 (unsupported)', extraRuntimeData: { supported: false }, })); @@ -234,21 +234,21 @@ describe('selectNewLanguageRuntime', () => { describe('reactive rebuild', () => { it('rebuilds when onDidRegisterRuntime fires mid-pick', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); const promise = runPicker(); await waitUntilOpened(); expect(pickItemById('py-1')).toBeDefined(); expect(pickItemById('py-2')).toBeUndefined(); - registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' })); expect(pickItemById('py-2')).toBeDefined(); pick.cancel(QuickInputHideReason.Gesture); await promise; }); it('rebuilds when onDidUnregisterRuntime fires mid-pick', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); - registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' })); const promise = runPicker(); await waitUntilOpened(); expect(pickItemById('py-1')).toBeDefined(); @@ -264,13 +264,13 @@ describe('selectNewLanguageRuntime', () => { }); it('preserves the previously focused item across rebuilds', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); - registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' })); const promise = runPicker({ currentRuntimeId: 'py-2' }); await waitUntilOpened(); expect(pick.activeItems[0].id).toBe('py-2'); - registerRuntime(makeRuntime({ runtimeId: 'py-3', languageVersion: '3.13.0' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-3', languageVersion: '3.13.0' })); expect(pick.activeItems[0].id).toBe('py-2'); pick.cancel(QuickInputHideReason.Gesture); await promise; @@ -279,7 +279,7 @@ describe('selectNewLanguageRuntime', () => { describe('startup phase', () => { it('re-fetches contributions when phase transitions to Complete', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); const runtimeService = ctx.get(ILanguageRuntimeService); runtimeService.setStartupPhase(RuntimeStartupPhase.Discovering); @@ -332,7 +332,7 @@ describe('selectNewLanguageRuntime', () => { it('shows a busy spinner and discovering placeholder while phase is not Complete', async () => { const runtimeService = ctx.get(ILanguageRuntimeService); runtimeService.setStartupPhase(RuntimeStartupPhase.Discovering); - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); const promise = runPicker(); await waitUntilOpened(); @@ -360,7 +360,7 @@ describe('selectNewLanguageRuntime', () => { }); it('does not show a spinner when discovery is already complete on open', async () => { - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); const promise = runPicker(); await waitUntilOpened(); expect(pick.busy).toBe(false); @@ -372,7 +372,7 @@ describe('selectNewLanguageRuntime', () => { it('toggles the spinner back on when phase leaves Complete while the picker is open', async () => { const runtimeService = ctx.get(ILanguageRuntimeService); - registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + await registerRuntime(makeRuntime({ runtimeId: 'py-1' })); // beforeEach leaves the phase at Complete. const promise = runPicker(); await waitUntilOpened(); @@ -430,7 +430,7 @@ describe('selectNewLanguageRuntime', () => { getItems: async () => [{ id: 'install-uv', label: 'Install Python via uv' }], onSelect: vi.fn(async () => { // Simulate the contribution registering a new runtime as part of onSelect. - ctx.disposables.add(runtimeService.registerRuntime(installedRuntime)); + ctx.disposables.add(await runtimeService.registerRuntime(installedRuntime)); return installedRuntime.runtimeId; }), }; @@ -444,7 +444,10 @@ describe('selectNewLanguageRuntime', () => { pick.accept(installItem); // The picker resolves to the enriched, registered instance. - await expect(promise).resolves.toEqual(runtimeService.getRegisteredRuntime(installedRuntime.runtimeId)); + // Await the promise first so onSelect (which awaits registerRuntime) has completed + // before calling getRegisteredRuntime, otherwise the runtime won't be registered yet. + const result = await promise; + expect(result).toEqual(runtimeService.getRegisteredRuntime(installedRuntime.runtimeId)); expect(contribution.onSelect).toHaveBeenCalledWith('install-uv'); expect(rediscoverAllRuntimes).toHaveBeenCalledWith(/* quiet */ true); }); diff --git a/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantService.vitest.ts b/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantService.vitest.ts index 063a286055e6..4611ae09445a 100644 --- a/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantService.vitest.ts +++ b/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantService.vitest.ts @@ -66,7 +66,7 @@ describe('PositronAssistantService', () => { ctx.instantiationService.stub(IPositronPlotsService, ctx.disposables.add(createTestPlotsServiceWithPlots())); // Create test runtime sessions - const runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + const runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); testConsoleSession = await startTestLanguageRuntimeSession( ctx.instantiationService, ctx.disposables, diff --git a/src/vs/workbench/contrib/positronWebviewPreloads/test/browser/positronWebviewPreloadReconciliation.vitest.ts b/src/vs/workbench/contrib/positronWebviewPreloads/test/browser/positronWebviewPreloadReconciliation.vitest.ts index cf7ba2d73f49..41396be0dd9c 100644 --- a/src/vs/workbench/contrib/positronWebviewPreloads/test/browser/positronWebviewPreloadReconciliation.vitest.ts +++ b/src/vs/workbench/contrib/positronWebviewPreloads/test/browser/positronWebviewPreloadReconciliation.vitest.ts @@ -233,7 +233,7 @@ describe('Positron - PositronWebviewPreloadService output reconciliation (#12887 it('reconciliation disposes an overlay but spares a widget sharing the cell', async () => { // Widgets need a live notebook session to be created. - const runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + const runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); const session = await startTestLanguageRuntimeSession(ctx.instantiationService, ctx.disposables, { runtime, notebookUri: NOTEBOOK_URI, diff --git a/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernel.vitest.ts b/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernel.vitest.ts index 3a6690aa5d3d..ad30eb5aa829 100644 --- a/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernel.vitest.ts +++ b/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernel.vitest.ts @@ -92,7 +92,7 @@ describe('Positron - RuntimeNotebookKernel', () => { })); // Create a test language runtime. - runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); // Create the runtime notebook kernel. kernel = ctx.disposables.add(ctx.instantiationService.createInstance(RuntimeNotebookKernel, runtime)); @@ -573,7 +573,7 @@ describe('Positron - RuntimeNotebookKernel - executeCodeInCell', () => { })); // Create a test language runtime. - runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); // Create the runtime notebook kernel. kernel = ctx.disposables.add(ctx.instantiationService.createInstance(RuntimeNotebookKernel, runtime)); diff --git a/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernelService.vitest.ts b/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernelService.vitest.ts index 50bee2600701..175b96417019 100644 --- a/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernelService.vitest.ts +++ b/src/vs/workbench/contrib/runtimeNotebookKernel/tests/browser/runtimeNotebookKernelService.vitest.ts @@ -94,13 +94,13 @@ describe('Positron - RuntimeNotebookKernelService', () => { ctx.instantiationService.stub(IRuntimeNotebookKernelService, runtimeNotebookKernelService); // Create a test language runtime. - runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); // Get the kernel corresponding to the test language runtime. kernel = runtimeNotebookKernelService.getKernelByRuntimeId(runtime.runtimeId)!; // Create another runtime and kernel to test swapping kernels. - anotherRuntime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + anotherRuntime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); anotherKernel = runtimeNotebookKernelService.getKernelByRuntimeId(anotherRuntime.runtimeId)!; // Register the 'test' language, otherwise cells can't be set to that language. @@ -110,7 +110,7 @@ describe('Positron - RuntimeNotebookKernelService', () => { it('kernel is added on language runtime registration', async () => { // Register a language runtime, and wait for the corresponding kernel to be added. const kernelPromise = Event.toPromise(notebookKernelService.onDidAddKernel); - const runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + const runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); const kernel = await kernelPromise; // Check the kernel's properties. diff --git a/src/vs/workbench/contrib/runtimeSession/test/browser/foregroundSessionContribution.vitest.ts b/src/vs/workbench/contrib/runtimeSession/test/browser/foregroundSessionContribution.vitest.ts index bdae8a8781d2..e07ac64994d4 100644 --- a/src/vs/workbench/contrib/runtimeSession/test/browser/foregroundSessionContribution.vitest.ts +++ b/src/vs/workbench/contrib/runtimeSession/test/browser/foregroundSessionContribution.vitest.ts @@ -118,7 +118,7 @@ describe('Positron - ForegroundSessionContribution', () => { /** Start a console session and set it as foreground. */ async function startConsoleSession() { - const runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + const runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); const session = await startTestLanguageRuntimeSession( ctx.instantiationService, ctx.disposables, { runtime, @@ -132,7 +132,7 @@ describe('Positron - ForegroundSessionContribution', () => { /** Start a notebook session for the given URI. */ async function startNotebookSession(uri: URI = notebookUri) { - const runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); + const runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables); const session = await startTestLanguageRuntimeSession( ctx.instantiationService, ctx.disposables, { runtime, diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts index 897fb6971310..06db05c85b4d 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts @@ -116,7 +116,7 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti * * @returns A disposable that unregisters the runtime */ - registerRuntime(metadata: ILanguageRuntimeMetadata): IDisposable { + async registerRuntime(metadata: ILanguageRuntimeMetadata): Promise { // If the runtime has already been registered, return early. if (this._registeredRuntimesByRuntimeId.has(metadata.runtimeId)) { return this._register(toDisposable(() => { })); @@ -136,10 +136,15 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti // Enrich metadata with a workbench-computed display path (~-shortened // on non-Windows; absolute path unchanged on Windows or system paths). - const userHome = this._pathService.userHome({ preferLocal: true }).fsPath; + // Preserve a caller-supplied runtimeDisplayPath; only compute when absent. + let runtimeDisplayPath = metadata.runtimeDisplayPath; + if (!runtimeDisplayPath) { + const userHome = (await this._pathService.userHome({ preferLocal: false })).fsPath; + runtimeDisplayPath = tildify(metadata.runtimePath, userHome); + } const enriched: ILanguageRuntimeMetadata = { ...metadata, - runtimeDisplayPath: tildify(metadata.runtimePath, userHome), + runtimeDisplayPath, }; // Add the runtime to the registered runtimes. diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts index 2cc8e73e6c10..779774bc9883 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts @@ -1200,7 +1200,7 @@ export interface ILanguageRuntimeService { * * @returns A disposable that can be used to unregister the runtime */ - registerRuntime(runtime: ILanguageRuntimeMetadata): IDisposable; + registerRuntime(runtime: ILanguageRuntimeMetadata): Promise; /** * Get the metadata for a previously registered language runtime diff --git a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts index 61b984b6c3ed..80315a347462 100644 --- a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts +++ b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts @@ -19,7 +19,7 @@ import { getRuntimeDisplayPath, ILanguageRuntimeMetadata, LanguageRuntimeSession const TEST_USER_HOME = URI.file('/home/testuser'); const pathServiceStub = stubInterface({ - userHome: vi.fn().mockReturnValue(TEST_USER_HOME), + userHome: vi.fn().mockResolvedValue(TEST_USER_HOME), }); /** @@ -31,6 +31,7 @@ function makeTestMetadata(overrides: Partial): ILangua runtimeId: 'testRuntimeId', languageId: 'testLanguageId', runtimePath: '', + runtimeDisplayPath: undefined, languageName: 'testLanguage', languageVersion: '1.0.0', base64EncodedIconSvg: undefined, @@ -88,7 +89,7 @@ describe('Positron - LanguageRuntimeService', () => { }); // Register the runtime. - const runtimeDisposable = languageRuntimeService.registerRuntime(metadata); + const runtimeDisposable = await languageRuntimeService.registerRuntime(metadata); // Check that the onDidRegisterRuntime event was fired. let timedOut = false; @@ -110,17 +111,17 @@ describe('Positron - LanguageRuntimeService', () => { runtimeDisposable.dispose(); }); - it('enriches runtimeDisplayPath via tildify on registration', () => { + it('enriches runtimeDisplayPath via tildify on registration', async () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); // A path under the test user home should be tildified. const underHome = makeTestMetadata({ runtimeId: 'tilde-1', runtimePath: '/home/testuser/bin/R' }); - languageRuntimeService.registerRuntime(underHome); + await languageRuntimeService.registerRuntime(underHome); expect(languageRuntimeService.registeredRuntimes[0].runtimeDisplayPath).toBe('~/bin/R'); // A system path should be left unchanged. const system = makeTestMetadata({ runtimeId: 'tilde-2', runtimePath: '/usr/bin/R' }); - languageRuntimeService.registerRuntime(system); + await languageRuntimeService.registerRuntime(system); expect(languageRuntimeService.registeredRuntimes[1].runtimeDisplayPath).toBe('/usr/bin/R'); }); }); @@ -133,9 +134,9 @@ describe('Positron - LanguageRuntimeService', () => { .stub(IPathService, pathServiceStub) .build(); - it('fires with the runtimeId and removes the runtime when unregistered', () => { + it('fires with the runtimeId and removes the runtime when unregistered', async () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); - languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); + await languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); const unregistered: string[] = []; ctx.disposables.add(languageRuntimeService.onDidUnregisterRuntime(id => unregistered.push(id))); @@ -157,9 +158,9 @@ describe('Positron - LanguageRuntimeService', () => { expect(unregistered).toEqual([]); }); - it('fires when the registration disposable is disposed', () => { + it('fires when the registration disposable is disposed', async () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); - const registration = languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); + const registration = await languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); const unregistered: string[] = []; ctx.disposables.add(languageRuntimeService.onDidUnregisterRuntime(id => unregistered.push(id))); @@ -196,9 +197,7 @@ describe('Positron - LanguageRuntimeService', () => { }); // Attempt to register the runtime - this should throw an error. - expect(() => { - languageRuntimeService.registerRuntime(metadata); - }).toThrow(); + await expect(languageRuntimeService.registerRuntime(metadata)).rejects.toThrow(); // Verify that no runtimes were registered. expect(languageRuntimeService.registeredRuntimes.length).toBe(0); diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index 1b9a40aa90c7..b60ac90ab97c 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -764,7 +764,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession if (!languageRuntime) { this._logService.debug(`[Reconnect ${sessionMetadata.sessionId}]: ` + `Registering runtime ${runtimeMetadata.runtimeName}`); - this._languageRuntimeService.registerRuntime(runtimeMetadata); + await this._languageRuntimeService.registerRuntime(runtimeMetadata); } const runningSessionId = this.validateRuntimeSessionStart( @@ -1715,7 +1715,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession } // Register the newly validated runtime. - this._languageRuntimeService.registerRuntime(validated); + await this._languageRuntimeService.registerRuntime(validated); // Replace the metadata we were given with the validated metadata. metadata = validated; diff --git a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts index a2a2cb4f94b2..094433ad9f77 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts @@ -62,10 +62,10 @@ export function createRuntimeServices( return instantiationService; } -export function createTestLanguageRuntimeMetadata( +export async function createTestLanguageRuntimeMetadata( instantiationService: TestInstantiationService, disposables: Pick, -): ILanguageRuntimeMetadata { +): Promise { const languageRuntimeService = instantiationService.get(ILanguageRuntimeService); const runtimeSessionService = instantiationService.get(IRuntimeSessionService); @@ -88,7 +88,7 @@ export function createTestLanguageRuntimeMetadata( sessionLocation: LanguageRuntimeSessionLocation.Browser, startupBehavior: LanguageRuntimeStartupBehavior.Implicit, }; - disposables.add(languageRuntimeService.registerRuntime(runtime)); + disposables.add(await languageRuntimeService.registerRuntime(runtime)); // Register the test runtime manager. const manager = TestRuntimeSessionManager.instance; @@ -115,7 +115,7 @@ export async function startTestLanguageRuntimeSession( options?: IStartTestLanguageRuntimeSessionOptions, ): Promise { // Get or create the runtime. - const runtime = options?.runtime ?? createTestLanguageRuntimeMetadata(instantiationService, disposables); + const runtime = options?.runtime ?? await createTestLanguageRuntimeMetadata(instantiationService, disposables); // Start the session. const runtimeSessionService = instantiationService.get(IRuntimeSessionService); diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts index 612f46d5fd49..1fbd46b6c26c 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts @@ -1163,7 +1163,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup this._discoveryCache.invalidate(bucket.extensionId, bucket.languageId, entry.metadata.runtimePath); continue; } - this._languageRuntimeService.registerRuntime(entry.metadata); + await this._languageRuntimeService.registerRuntime(entry.metadata); this._discoveryCache.sessionCounters.foregroundHits++; const fp = probe.fingerprint; @@ -1519,7 +1519,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup `[Runtime startup] Cached runtime drifted; ` + `replacing ${formatLanguageRuntimeMetadata(task.metadata)} ` + `with ${formatLanguageRuntimeMetadata(validated)}`); - this._languageRuntimeService.registerRuntime(validated); + await this._languageRuntimeService.registerRuntime(validated); } // If the validator redirected the runtime to a different binary -- // e.g. R's validateMetadata re-resolves a `current: true` entry to @@ -1801,7 +1801,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Register the runtime with the language runtime service. // Pre-registering prevents the runtime from being unnecessarily // validated later. - this._register(this._languageRuntimeService.registerRuntime(runtime)); + this._register(await this._languageRuntimeService.registerRuntime(runtime)); if (runtime.startupBehavior === LanguageRuntimeStartupBehavior.Immediate) { // Start the runtime immediately if it has Immediate startup diff --git a/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts b/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts index 32ea88a6bb02..1fcc29d72a31 100644 --- a/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts +++ b/src/vs/workbench/services/runtimeStartup/test/common/runtimeStartup.vitest.ts @@ -1066,7 +1066,7 @@ describe('RuntimeStartupService - affiliation healing', () => { .stub(IRuntimeDiscoveryCache, {}) .build(); - it('heals a stale ~-prefixed runtimePath in stored affiliation when the runtime registers', () => { + it('heals a stale ~-prefixed runtimePath in stored affiliation when the runtime registers', async () => { const runtimeId = 'r-stale-path-test'; const languageId = 'r'; const staleRuntimePath = '~/local/lib/R/bin/R'; @@ -1099,7 +1099,7 @@ describe('RuntimeStartupService - affiliation healing', () => { startupBehavior: LanguageRuntimeStartupBehavior.Manual, }; // Registering the runtime fires onDidRegisterRuntime, which heals the stored affiliation. - ctx.get(ILanguageRuntimeService).registerRuntime(freshMd); + await ctx.get(ILanguageRuntimeService).registerRuntime(freshMd); // The stored affiliation must now carry the absolute path. const stored = JSON.parse(ctx.get(IStorageService).get(storageKey, scope)!); From 98f7f4d28d167ab61655800331db0a044cdd71a8 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Tue, 14 Jul 2026 17:13:59 -0500 Subject: [PATCH 11/14] Revert registerRuntime to synchronous; cache userHome eagerly in constructor IPathService resolves remote home during workbench startup, well before any extension activates. Cache the result in the constructor via a fire-and- forget Promise so registerRuntime stays synchronous. Falls back to the local preferLocal:true overload on the rare chance it is called before the Promise resolves. Also fix TestPathService.userHome() to return Promise for the async overload (preferLocal != true), matching the real service contract. Co-Authored-By: Claude Sonnet 4.6 --- .../positron/mainThreadLanguageRuntime.ts | 4 +-- .../browser/languageRuntimeActions.vitest.ts | 6 ++-- .../languageRuntime/common/languageRuntime.ts | 19 ++++++++++-- .../common/languageRuntimeService.ts | 2 +- .../test/common/languageRuntime.vitest.ts | 29 ++++++++++++------- .../runtimeSession/common/runtimeSession.ts | 4 +-- .../test/common/testRuntimeSessionService.ts | 8 ++--- .../runtimeStartup/common/runtimeStartup.ts | 6 ++-- .../common/positronWorkbenchTestServices.ts | 5 +++- 9 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index 6fdd244aa8d0..4507ac08d416 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -1731,9 +1731,9 @@ export class MainThreadLanguageRuntime } // Called by the extension host to register a language runtime - async $registerLanguageRuntime(metadata: ILanguageRuntimeMetadata): Promise { + $registerLanguageRuntime(metadata: ILanguageRuntimeMetadata): void { this._registeredRuntimes.set(metadata.runtimeId, metadata); - await this._languageRuntimeService.registerRuntime(metadata); + this._languageRuntimeService.registerRuntime(metadata); } $getPreferredRuntime(languageId: string): Promise { diff --git a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts index 7c391fa1e483..bf39d7844235 100644 --- a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts +++ b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts @@ -87,7 +87,7 @@ describe('selectNewLanguageRuntime', () => { async function registerRuntime(metadata: ILanguageRuntimeMetadata): Promise { const runtimeService = ctx.get(ILanguageRuntimeService); - ctx.disposables.add(await runtimeService.registerRuntime(metadata)); + ctx.disposables.add(runtimeService.registerRuntime(metadata)); if (!preferredByLanguage.has(metadata.languageId)) { preferredByLanguage.set(metadata.languageId, metadata); } @@ -430,7 +430,7 @@ describe('selectNewLanguageRuntime', () => { getItems: async () => [{ id: 'install-uv', label: 'Install Python via uv' }], onSelect: vi.fn(async () => { // Simulate the contribution registering a new runtime as part of onSelect. - ctx.disposables.add(await runtimeService.registerRuntime(installedRuntime)); + ctx.disposables.add(runtimeService.registerRuntime(installedRuntime)); return installedRuntime.runtimeId; }), }; @@ -444,8 +444,6 @@ describe('selectNewLanguageRuntime', () => { pick.accept(installItem); // The picker resolves to the enriched, registered instance. - // Await the promise first so onSelect (which awaits registerRuntime) has completed - // before calling getRegisteredRuntime, otherwise the runtime won't be registered yet. const result = await promise; expect(result).toEqual(runtimeService.getRegisteredRuntime(installedRuntime.runtimeId)); expect(contribution.onSelect).toHaveBeenCalledWith('install-uv'); diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts index 06db05c85b4d..c1dbdc71a0b9 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts @@ -40,6 +40,10 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti // Map of picker contributions by handle private readonly _pickerContributions = new Map(); + // Cached user home path (remote-aware). Populated eagerly in the constructor + // so registerRuntime can run synchronously. + private _cachedUserHome: string | undefined; + //#endregion Private Properties //#region Constructor @@ -61,6 +65,13 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti this._startupPhase = observableValue( 'runtime-startup-phase', RuntimeStartupPhase.Initializing); this.onDidChangeRuntimeStartupPhase = Event.fromObservable(this._startupPhase); + + // Kick off the remote-aware home resolution eagerly. By the time any + // extension activates and calls registerRuntime, this Promise has + // already resolved (workbench startup completes before extensions run). + this._pathService.userHome({ preferLocal: false }).then(uri => { + this._cachedUserHome = uri.fsPath; + }); } /** @@ -116,7 +127,7 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti * * @returns A disposable that unregisters the runtime */ - async registerRuntime(metadata: ILanguageRuntimeMetadata): Promise { + registerRuntime(metadata: ILanguageRuntimeMetadata): IDisposable { // If the runtime has already been registered, return early. if (this._registeredRuntimesByRuntimeId.has(metadata.runtimeId)) { return this._register(toDisposable(() => { })); @@ -139,7 +150,11 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti // Preserve a caller-supplied runtimeDisplayPath; only compute when absent. let runtimeDisplayPath = metadata.runtimeDisplayPath; if (!runtimeDisplayPath) { - const userHome = (await this._pathService.userHome({ preferLocal: false })).fsPath; + // Use the cached remote-aware home if available; fall back to the + // local home (preferLocal: true is synchronous) on the rare chance + // registerRuntime is called before the constructor Promise resolves. + const userHome = this._cachedUserHome + ?? this._pathService.userHome({ preferLocal: true }).fsPath; runtimeDisplayPath = tildify(metadata.runtimePath, userHome); } const enriched: ILanguageRuntimeMetadata = { diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts index 779774bc9883..2cc8e73e6c10 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts @@ -1200,7 +1200,7 @@ export interface ILanguageRuntimeService { * * @returns A disposable that can be used to unregister the runtime */ - registerRuntime(runtime: ILanguageRuntimeMetadata): Promise; + registerRuntime(runtime: ILanguageRuntimeMetadata): IDisposable; /** * Get the metadata for a previously registered language runtime diff --git a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts index 80315a347462..57ba1e15299a 100644 --- a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts +++ b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts @@ -19,7 +19,14 @@ import { getRuntimeDisplayPath, ILanguageRuntimeMetadata, LanguageRuntimeSession const TEST_USER_HOME = URI.file('/home/testuser'); const pathServiceStub = stubInterface({ - userHome: vi.fn().mockResolvedValue(TEST_USER_HOME), + // Handle both overloads: preferLocal:true returns URI synchronously; + // preferLocal:false (or absent) returns Promise. + userHome: vi.fn().mockImplementation((options?: { preferLocal?: boolean }) => { + if (options?.preferLocal === true) { + return TEST_USER_HOME; + } + return Promise.resolve(TEST_USER_HOME); + }), }); /** @@ -89,7 +96,7 @@ describe('Positron - LanguageRuntimeService', () => { }); // Register the runtime. - const runtimeDisposable = await languageRuntimeService.registerRuntime(metadata); + const runtimeDisposable = languageRuntimeService.registerRuntime(metadata); // Check that the onDidRegisterRuntime event was fired. let timedOut = false; @@ -111,17 +118,17 @@ describe('Positron - LanguageRuntimeService', () => { runtimeDisposable.dispose(); }); - it('enriches runtimeDisplayPath via tildify on registration', async () => { + it('enriches runtimeDisplayPath via tildify on registration', () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); // A path under the test user home should be tildified. const underHome = makeTestMetadata({ runtimeId: 'tilde-1', runtimePath: '/home/testuser/bin/R' }); - await languageRuntimeService.registerRuntime(underHome); + languageRuntimeService.registerRuntime(underHome); expect(languageRuntimeService.registeredRuntimes[0].runtimeDisplayPath).toBe('~/bin/R'); // A system path should be left unchanged. const system = makeTestMetadata({ runtimeId: 'tilde-2', runtimePath: '/usr/bin/R' }); - await languageRuntimeService.registerRuntime(system); + languageRuntimeService.registerRuntime(system); expect(languageRuntimeService.registeredRuntimes[1].runtimeDisplayPath).toBe('/usr/bin/R'); }); }); @@ -134,9 +141,9 @@ describe('Positron - LanguageRuntimeService', () => { .stub(IPathService, pathServiceStub) .build(); - it('fires with the runtimeId and removes the runtime when unregistered', async () => { + it('fires with the runtimeId and removes the runtime when unregistered', () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); - await languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); + languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); const unregistered: string[] = []; ctx.disposables.add(languageRuntimeService.onDidUnregisterRuntime(id => unregistered.push(id))); @@ -158,9 +165,9 @@ describe('Positron - LanguageRuntimeService', () => { expect(unregistered).toEqual([]); }); - it('fires when the registration disposable is disposed', async () => { + it('fires when the registration disposable is disposed', () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); - const registration = await languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); + const registration = languageRuntimeService.registerRuntime(makeTestMetadata({ runtimeId: 'py-1' })); const unregistered: string[] = []; ctx.disposables.add(languageRuntimeService.onDidUnregisterRuntime(id => unregistered.push(id))); @@ -187,7 +194,7 @@ describe('Positron - LanguageRuntimeService', () => { .stub(IPathService, pathServiceStub) .build(); - it('cannot register a runtime when the language is disabled in configuration', async () => { + it('cannot register a runtime when the language is disabled in configuration', () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); // Create mock metadata for a runtime with the disabled language. @@ -197,7 +204,7 @@ describe('Positron - LanguageRuntimeService', () => { }); // Attempt to register the runtime - this should throw an error. - await expect(languageRuntimeService.registerRuntime(metadata)).rejects.toThrow(); + expect(() => languageRuntimeService.registerRuntime(metadata)).toThrow(); // Verify that no runtimes were registered. expect(languageRuntimeService.registeredRuntimes.length).toBe(0); diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index b60ac90ab97c..1b9a40aa90c7 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -764,7 +764,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession if (!languageRuntime) { this._logService.debug(`[Reconnect ${sessionMetadata.sessionId}]: ` + `Registering runtime ${runtimeMetadata.runtimeName}`); - await this._languageRuntimeService.registerRuntime(runtimeMetadata); + this._languageRuntimeService.registerRuntime(runtimeMetadata); } const runningSessionId = this.validateRuntimeSessionStart( @@ -1715,7 +1715,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession } // Register the newly validated runtime. - await this._languageRuntimeService.registerRuntime(validated); + this._languageRuntimeService.registerRuntime(validated); // Replace the metadata we were given with the validated metadata. metadata = validated; diff --git a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts index 094433ad9f77..a2a2cb4f94b2 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts @@ -62,10 +62,10 @@ export function createRuntimeServices( return instantiationService; } -export async function createTestLanguageRuntimeMetadata( +export function createTestLanguageRuntimeMetadata( instantiationService: TestInstantiationService, disposables: Pick, -): Promise { +): ILanguageRuntimeMetadata { const languageRuntimeService = instantiationService.get(ILanguageRuntimeService); const runtimeSessionService = instantiationService.get(IRuntimeSessionService); @@ -88,7 +88,7 @@ export async function createTestLanguageRuntimeMetadata( sessionLocation: LanguageRuntimeSessionLocation.Browser, startupBehavior: LanguageRuntimeStartupBehavior.Implicit, }; - disposables.add(await languageRuntimeService.registerRuntime(runtime)); + disposables.add(languageRuntimeService.registerRuntime(runtime)); // Register the test runtime manager. const manager = TestRuntimeSessionManager.instance; @@ -115,7 +115,7 @@ export async function startTestLanguageRuntimeSession( options?: IStartTestLanguageRuntimeSessionOptions, ): Promise { // Get or create the runtime. - const runtime = options?.runtime ?? await createTestLanguageRuntimeMetadata(instantiationService, disposables); + const runtime = options?.runtime ?? createTestLanguageRuntimeMetadata(instantiationService, disposables); // Start the session. const runtimeSessionService = instantiationService.get(IRuntimeSessionService); diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts index 1fbd46b6c26c..612f46d5fd49 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts @@ -1163,7 +1163,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup this._discoveryCache.invalidate(bucket.extensionId, bucket.languageId, entry.metadata.runtimePath); continue; } - await this._languageRuntimeService.registerRuntime(entry.metadata); + this._languageRuntimeService.registerRuntime(entry.metadata); this._discoveryCache.sessionCounters.foregroundHits++; const fp = probe.fingerprint; @@ -1519,7 +1519,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup `[Runtime startup] Cached runtime drifted; ` + `replacing ${formatLanguageRuntimeMetadata(task.metadata)} ` + `with ${formatLanguageRuntimeMetadata(validated)}`); - await this._languageRuntimeService.registerRuntime(validated); + this._languageRuntimeService.registerRuntime(validated); } // If the validator redirected the runtime to a different binary -- // e.g. R's validateMetadata re-resolves a `current: true` entry to @@ -1801,7 +1801,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Register the runtime with the language runtime service. // Pre-registering prevents the runtime from being unnecessarily // validated later. - this._register(await this._languageRuntimeService.registerRuntime(runtime)); + this._register(this._languageRuntimeService.registerRuntime(runtime)); if (runtime.startupBehavior === LanguageRuntimeStartupBehavior.Immediate) { // Start the runtime immediately if it has Immediate startup diff --git a/src/vs/workbench/test/common/positronWorkbenchTestServices.ts b/src/vs/workbench/test/common/positronWorkbenchTestServices.ts index a51ceceaf3d5..5c2f48d3bb7b 100644 --- a/src/vs/workbench/test/common/positronWorkbenchTestServices.ts +++ b/src/vs/workbench/test/common/positronWorkbenchTestServices.ts @@ -264,7 +264,10 @@ export class TestPathService implements IPathService { userHome(options: { preferLocal: true }): URI; userHome(options?: { preferLocal: boolean }): Promise; userHome(options?: { preferLocal: boolean }): URI | Promise { - return URI.file('/home/test'); + if (options?.preferLocal === true) { + return URI.file('/home/test'); + } + return Promise.resolve(URI.file('/home/test')); } hasValidBasename(resource: URI, basename?: string): Promise; From d965d845969e23e4dcce103e6765de1da4a0a8df Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Thu, 16 Jul 2026 09:36:11 -0500 Subject: [PATCH 12/14] Don't fall back to local-only home on userHome cache miss; soften constructor comment Co-Authored-By: Claude Sonnet 4.6 --- .../languageRuntime/common/languageRuntime.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts index c1dbdc71a0b9..890a1488e8b4 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts @@ -66,9 +66,10 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti 'runtime-startup-phase', RuntimeStartupPhase.Initializing); this.onDidChangeRuntimeStartupPhase = Event.fromObservable(this._startupPhase); - // Kick off the remote-aware home resolution eagerly. By the time any - // extension activates and calls registerRuntime, this Promise has - // already resolved (workbench startup completes before extensions run). + // Kick off the remote-aware home resolution eagerly so it's likely + // cached by the time extensions call registerRuntime. If it hasn't + // resolved yet, registerRuntime skips tildification rather than + // falling back to the local-only path. this._pathService.userHome({ preferLocal: false }).then(uri => { this._cachedUserHome = uri.fsPath; }); @@ -149,14 +150,11 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti // on non-Windows; absolute path unchanged on Windows or system paths). // Preserve a caller-supplied runtimeDisplayPath; only compute when absent. let runtimeDisplayPath = metadata.runtimeDisplayPath; - if (!runtimeDisplayPath) { - // Use the cached remote-aware home if available; fall back to the - // local home (preferLocal: true is synchronous) on the rare chance - // registerRuntime is called before the constructor Promise resolves. - const userHome = this._cachedUserHome - ?? this._pathService.userHome({ preferLocal: true }).fsPath; - runtimeDisplayPath = tildify(metadata.runtimePath, userHome); + if (!runtimeDisplayPath && this._cachedUserHome) { + runtimeDisplayPath = tildify(metadata.runtimePath, this._cachedUserHome); } + // If _cachedUserHome isn't ready yet, leave runtimeDisplayPath undefined; + // getRuntimeDisplayPath() falls back to the raw runtimePath. const enriched: ILanguageRuntimeMetadata = { ...metadata, runtimeDisplayPath, From 432bbcc7d0f2fc2079478933b64b30374456731e Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Thu, 16 Jul 2026 09:39:44 -0500 Subject: [PATCH 13/14] Use getRuntimeDisplayPath in clearAffiliatedRuntime quick pick Co-Authored-By: Claude Sonnet 4.6 --- .../contrib/languageRuntime/browser/languageRuntimeActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts index 51ad4506f514..d78768f4d6c6 100644 --- a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts +++ b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts @@ -887,7 +887,7 @@ export function registerLanguageRuntimeActions() { const runtimeQuickPickItems = runtimes.map(runtime => ({ id: runtime.runtimeId, label: `${runtime.languageName}: ${runtime.runtimeName}`, - description: runtime.runtimePath, + description: getRuntimeDisplayPath(runtime), runtime } satisfies LanguageRuntimeQuickPickItem)); From 6d6f2663f5606c03aad3dc39cc8186601f210604 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Thu, 16 Jul 2026 10:33:32 -0500 Subject: [PATCH 14/14] Flush userHome microtask before registerRuntime in tildify test The LanguageRuntimeService constructor starts a userHome() Promise to populate _cachedUserHome; registerRuntime skips tildification when it hasn't resolved yet. The test was synchronous, so the microtask never ran before registerRuntime was called. Making the test async and awaiting one Promise.resolve() lets the microtask run first. Co-Authored-By: Claude Sonnet 4.6 --- .../languageRuntime/test/common/languageRuntime.vitest.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts index 57ba1e15299a..03ab00ae3d2d 100644 --- a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts +++ b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.vitest.ts @@ -118,9 +118,13 @@ describe('Positron - LanguageRuntimeService', () => { runtimeDisposable.dispose(); }); - it('enriches runtimeDisplayPath via tildify on registration', () => { + it('enriches runtimeDisplayPath via tildify on registration', async () => { const languageRuntimeService = ctx.disposables.add(ctx.instantiationService.createInstance(LanguageRuntimeService)); + // The constructor starts a userHome() promise; let the microtask run + // before calling registerRuntime so _cachedUserHome is populated. + await Promise.resolve(); + // A path under the test user home should be tildified. const underHome = makeTestMetadata({ runtimeId: 'tilde-1', runtimePath: '/home/testuser/bin/R' }); languageRuntimeService.registerRuntime(underHome);