From f135876ae44c8d245ed619fd1c152de76e74df80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:47:31 +0000 Subject: [PATCH 1/4] Initial plan From 715e705f0d8c60715b608d523ce7a42e52bfbe5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:52:06 +0000 Subject: [PATCH 2/4] fix: substitute VS Code variables in path-valued settings Agent-Logs-Url: https://github.com/vitest-dev/vscode/sessions/58efde35-b9e7-4cd3-8d80-47947e30b2b1 Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- packages/extension/src/config.ts | 39 ++++++++++++++-------- test/unit/config.test.ts | 55 ++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/packages/extension/src/config.ts b/packages/extension/src/config.ts index 55e605b8..86d3e084 100644 --- a/packages/extension/src/config.ts +++ b/packages/extension/src/config.ts @@ -1,12 +1,27 @@ import type { WorkspaceConfiguration, WorkspaceFolder } from 'vscode' import { homedir } from 'node:os' -import { dirname, isAbsolute, resolve } from 'node:path' +import { dirname, isAbsolute, resolve, sep } from 'node:path' import * as vscode from 'vscode' import { configGlob } from './constants' export const extensionId = 'vitest.explorer' export const testControllerId = 'vitest' +export function substituteVariables(value: string, workspaceFolder?: WorkspaceFolder): string { + const folder = workspaceFolder ?? vscode.workspace.workspaceFolders?.[0] + return value + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{workspaceFolder\}/g, folder?.uri.fsPath ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{workspaceFolderBasename\}/g, folder?.name ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{userHome\}/g, homedir()) + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{env:([^}]+)\}/g, (_, name) => process.env[name] ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{pathSeparator\}/g, sep) +} + export function getConfigValue( rootConfig: WorkspaceConfiguration, folderConfig: WorkspaceConfiguration, @@ -43,12 +58,8 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { const vitestPackagePath = get('vitestPackagePath') const resolvedVitestPackagePath = - workspaceFolder && vitestPackagePath - ? resolve( - workspaceFolder.uri.fsPath, - // eslint-disable-next-line no-template-curly-in-string - vitestPackagePath.replace('${workspaceFolder}', workspaceFolder.uri.fsPath), - ) + vitestPackagePath + ? resolveConfigPath(substituteVariables(vitestPackagePath, workspaceFolder), workspaceFolder) : vitestPackagePath const logLevel = get('logLevel', 'info') @@ -74,24 +85,24 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { env: get>('nodeEnv', null), debugEnv: get>('debugNodeEnv', null), debugExclude: get('debugExclude'), - debugOutFiles, + debugOutFiles: debugOutFiles?.map(f => substituteVariables(f, workspaceFolder)), filesWatcherInclude, runtime, forceCancelTimeout, watchOnStartup, terminalShellArgs, - terminalShellPath, + terminalShellPath: terminalShellPath ? resolveConfigPath(substituteVariables(terminalShellPath, workspaceFolder), workspaceFolder) : terminalShellPath, shellType, applyDiagnostic, cliArguments, nodeExecArgs, vitestPackagePath: resolvedVitestPackagePath, - workspaceConfig: resolveConfigPath(workspaceConfig), - rootConfig: resolveConfigPath(rootConfigFile), + workspaceConfig: resolveConfigPath(workspaceConfig ? substituteVariables(workspaceConfig, workspaceFolder) : workspaceConfig, workspaceFolder), + rootConfig: resolveConfigPath(rootConfigFile ? substituteVariables(rootConfigFile, workspaceFolder) : rootConfigFile, workspaceFolder), configSearchPatternInclude, configSearchPatternExclude, ignoreWorkspace, - nodeExecutable: resolveConfigPath(nodeExecutable), + nodeExecutable: resolveConfigPath(nodeExecutable ? substituteVariables(nodeExecutable, workspaceFolder) : nodeExecutable, workspaceFolder), disableWorkspaceWarning: get('disableWorkspaceWarning', false), debuggerPort: get('debuggerPort') || undefined, debuggerAddress: get('debuggerAddress', undefined) || undefined, @@ -101,11 +112,13 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { } } -export function resolveConfigPath(path: string | undefined) { +export function resolveConfigPath(path: string | undefined, workspaceFolder?: WorkspaceFolder) { if (!path || isAbsolute(path)) return path if (path.startsWith('~/')) { return resolve(homedir(), path.slice(2)) } + // if a workspaceFolder was provided, resolve relative to it + if (workspaceFolder) return resolve(workspaceFolder.uri.fsPath, path) // if there is a workspace file, then it should be relative to it because // this option cannot be configured on a workspace folder level if (vscode.workspace.workspaceFile) diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index 639ce2c7..7a6dc393 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -1,8 +1,59 @@ -import { resolve } from 'node:path' +import { resolve, sep } from 'node:path' import { homedir } from 'node:os' import { expect } from 'chai' -import { resolveConfigPath } from '../../packages/extension/src/config' +import type { Uri, WorkspaceFolder } from 'vscode' +import { resolveConfigPath, substituteVariables } from '../../packages/extension/src/config' + +function mockWorkspaceFolder(fsPath: string, name: string): WorkspaceFolder { + return { + uri: { fsPath } as Uri, + name, + index: 0, + } +} it('correctly resolves ~', () => { expect(resolveConfigPath('~/test')).to.equal(resolve(homedir(), 'test')) }) + +describe('substituteVariables', () => { + it('substitutes ${workspaceFolder}', () => { + const folder = mockWorkspaceFolder('/my/workspace', 'workspace') + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${workspaceFolder}/src', folder)).to.equal('/my/workspace/src') + }) + + it('substitutes ${workspaceFolderBasename}', () => { + const folder = mockWorkspaceFolder('/my/workspace', 'myproject') + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${workspaceFolderBasename}', folder)).to.equal('myproject') + }) + + it('substitutes ${userHome}', () => { + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${userHome}/projects')).to.equal(`${homedir()}/projects`) + }) + + it('substitutes ${env:NAME}', () => { + process.env.TEST_VAR_VITEST_EXT = 'hello' + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${env:TEST_VAR_VITEST_EXT}/path')).to.equal('hello/path') + delete process.env.TEST_VAR_VITEST_EXT + }) + + it('substitutes ${pathSeparator}', () => { + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('foo${pathSeparator}bar')).to.equal(`foo${sep}bar`) + }) + + it('substitutes multiple occurrences', () => { + const folder = mockWorkspaceFolder('/ws', 'ws') + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${workspaceFolder}/a/${workspaceFolder}/b', folder)).to.equal('/ws/a//ws/b') + }) + + it('leaves unrecognized variables unchanged', () => { + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${unknown}/path')).to.equal('${unknown}/path') + }) +}) From cdea46873d4caa0cb9e999479825a30b1a95f130 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:54:07 +0000 Subject: [PATCH 3/4] refactor: extract resolvePathWithSubstitution helper and fix test example Agent-Logs-Url: https://github.com/vitest-dev/vscode/sessions/58efde35-b9e7-4cd3-8d80-47947e30b2b1 Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- packages/extension/src/config.ts | 22 ++++++++++++++-------- test/unit/config.test.ts | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/extension/src/config.ts b/packages/extension/src/config.ts index 86d3e084..d3b682b3 100644 --- a/packages/extension/src/config.ts +++ b/packages/extension/src/config.ts @@ -22,6 +22,13 @@ export function substituteVariables(value: string, workspaceFolder?: WorkspaceFo .replace(/\$\{pathSeparator\}/g, sep) } +function resolvePathWithSubstitution(path: string | undefined, workspaceFolder?: WorkspaceFolder) { + return resolveConfigPath( + path ? substituteVariables(path, workspaceFolder) : path, + workspaceFolder, + ) +} + export function getConfigValue( rootConfig: WorkspaceConfiguration, folderConfig: WorkspaceConfiguration, @@ -57,10 +64,9 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { get('configSearchPatternInclude', configGlob) || configGlob const vitestPackagePath = get('vitestPackagePath') - const resolvedVitestPackagePath = - vitestPackagePath - ? resolveConfigPath(substituteVariables(vitestPackagePath, workspaceFolder), workspaceFolder) - : vitestPackagePath + const resolvedVitestPackagePath = vitestPackagePath + ? resolvePathWithSubstitution(vitestPackagePath, workspaceFolder) + : vitestPackagePath const logLevel = get('logLevel', 'info') @@ -91,18 +97,18 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { forceCancelTimeout, watchOnStartup, terminalShellArgs, - terminalShellPath: terminalShellPath ? resolveConfigPath(substituteVariables(terminalShellPath, workspaceFolder), workspaceFolder) : terminalShellPath, + terminalShellPath: resolvePathWithSubstitution(terminalShellPath, workspaceFolder), shellType, applyDiagnostic, cliArguments, nodeExecArgs, vitestPackagePath: resolvedVitestPackagePath, - workspaceConfig: resolveConfigPath(workspaceConfig ? substituteVariables(workspaceConfig, workspaceFolder) : workspaceConfig, workspaceFolder), - rootConfig: resolveConfigPath(rootConfigFile ? substituteVariables(rootConfigFile, workspaceFolder) : rootConfigFile, workspaceFolder), + workspaceConfig: resolvePathWithSubstitution(workspaceConfig, workspaceFolder), + rootConfig: resolvePathWithSubstitution(rootConfigFile, workspaceFolder), configSearchPatternInclude, configSearchPatternExclude, ignoreWorkspace, - nodeExecutable: resolveConfigPath(nodeExecutable ? substituteVariables(nodeExecutable, workspaceFolder) : nodeExecutable, workspaceFolder), + nodeExecutable: resolvePathWithSubstitution(nodeExecutable, workspaceFolder), disableWorkspaceWarning: get('disableWorkspaceWarning', false), debuggerPort: get('debuggerPort') || undefined, debuggerAddress: get('debuggerAddress', undefined) || undefined, diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index 7a6dc393..fe3aeba9 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -47,9 +47,9 @@ describe('substituteVariables', () => { }) it('substitutes multiple occurrences', () => { - const folder = mockWorkspaceFolder('/ws', 'ws') + const folder = mockWorkspaceFolder('/ws', 'myws') // eslint-disable-next-line no-template-curly-in-string - expect(substituteVariables('${workspaceFolder}/a/${workspaceFolder}/b', folder)).to.equal('/ws/a//ws/b') + expect(substituteVariables('${workspaceFolder}/a/${workspaceFolderBasename}/b', folder)).to.equal('/ws/a/myws/b') }) it('leaves unrecognized variables unchanged', () => { From 75212410b9637e3aa331dc2be35ad4227f2525e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:29:01 +0000 Subject: [PATCH 4/4] style: apply oxfmt formatting Agent-Logs-Url: https://github.com/vitest-dev/vscode/sessions/7f2f0e5f-200e-486f-b687-3bc35e2dc22a Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- packages/extension/src/config.ts | 26 ++++++++++++++------------ test/unit/config.test.ts | 4 +++- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/extension/src/config.ts b/packages/extension/src/config.ts index d3b682b3..60cf85ef 100644 --- a/packages/extension/src/config.ts +++ b/packages/extension/src/config.ts @@ -9,17 +9,19 @@ export const testControllerId = 'vitest' export function substituteVariables(value: string, workspaceFolder?: WorkspaceFolder): string { const folder = workspaceFolder ?? vscode.workspace.workspaceFolders?.[0] - return value - // eslint-disable-next-line no-template-curly-in-string - .replace(/\$\{workspaceFolder\}/g, folder?.uri.fsPath ?? '') - // eslint-disable-next-line no-template-curly-in-string - .replace(/\$\{workspaceFolderBasename\}/g, folder?.name ?? '') - // eslint-disable-next-line no-template-curly-in-string - .replace(/\$\{userHome\}/g, homedir()) - // eslint-disable-next-line no-template-curly-in-string - .replace(/\$\{env:([^}]+)\}/g, (_, name) => process.env[name] ?? '') - // eslint-disable-next-line no-template-curly-in-string - .replace(/\$\{pathSeparator\}/g, sep) + return ( + value + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{workspaceFolder\}/g, folder?.uri.fsPath ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{workspaceFolderBasename\}/g, folder?.name ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{userHome\}/g, homedir()) + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{env:([^}]+)\}/g, (_, name) => process.env[name] ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{pathSeparator\}/g, sep) + ) } function resolvePathWithSubstitution(path: string | undefined, workspaceFolder?: WorkspaceFolder) { @@ -91,7 +93,7 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { env: get>('nodeEnv', null), debugEnv: get>('debugNodeEnv', null), debugExclude: get('debugExclude'), - debugOutFiles: debugOutFiles?.map(f => substituteVariables(f, workspaceFolder)), + debugOutFiles: debugOutFiles?.map((f) => substituteVariables(f, workspaceFolder)), filesWatcherInclude, runtime, forceCancelTimeout, diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index fe3aeba9..47effe48 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -49,7 +49,9 @@ describe('substituteVariables', () => { it('substitutes multiple occurrences', () => { const folder = mockWorkspaceFolder('/ws', 'myws') // eslint-disable-next-line no-template-curly-in-string - expect(substituteVariables('${workspaceFolder}/a/${workspaceFolderBasename}/b', folder)).to.equal('/ws/a/myws/b') + expect( + substituteVariables('${workspaceFolder}/a/${workspaceFolderBasename}/b', folder), + ).to.equal('/ws/a/myws/b') }) it('leaves unrecognized variables unchanged', () => {