Describe the bug
The path-valued settings contributed by this extension don't consistently run VS Code's predefined-variable substitution before passing values to Vitest. Placeholders such as ${workspaceFolder} appear verbatim in the resolved path, and Vitest fails to load the config / binary.
Concretely, looking at packages/extension/src/config.ts:
vitest.vitestPackagePath does substitute ${workspaceFolder} (see config.ts:50).
vitest.rootConfig, vitest.workspaceConfig, vitest.nodeExecutable are all routed through resolveConfigPath() (config.ts:89-94, defined at config.ts:104-118), which handles absolute paths, ~/, and relative-to-workspace paths, but does not substitute ${workspaceFolder} (or any other VS Code variable). If the value starts with ${workspaceFolder}/…, isAbsolute() returns false, so resolve(workspaceFolder.fsPath, "${workspaceFolder}/…") is called and the literal placeholder survives in the output path.
vitest.terminalShellPath and vitest.debugOutFiles receive no substitution at all.
VS Code does not resolve these placeholders globally - per the Variables Reference, each extension must substitute them itself when it reads its configuration. Many extensions do (Python, ESLint, Ruff, Mypy, Rust-analyzer, …). That this extension handles ${workspaceFolder} for vitestPackagePath but not for rootConfig looks like an oversight rather than intentional behavior.
Expected behavior
${workspaceFolder} - and ideally the other commonly used predefined variables (${workspaceFolderBasename}, ${userHome}, ${env:NAME}, ${pathSeparator}, ${fileWorkspaceFolder}) - are expanded before the value is handed to Vitest, consistently across all path-valued settings (vitest.rootConfig, vitest.workspaceConfig, vitest.nodeExecutable, vitest.terminalShellPath, vitest.vitestPackagePath, vitest.debugOutFiles), matching what many other VS Code extensions already do.
For multi-root workspaces, ${workspaceFolder} should resolve to the folder that owns the setting (i.e. the folder scope of the WorkspaceConfiguration the value was read from), not always the first folder.
Reproduction
https://github.com/whme/vitest-vscode-workspaceFolder-repro
pnpm install
- Open the folder in VS Code with the Vitest extension enabled.
- Open the Testing panel / "Vitest: Show Output Channel".
- Observe: the extension fails to load the config; the Output Channel shows the literal
${workspaceFolder} embedded in the resolved path.
- Replace the
vitest.rootConfig value in .vscode/settings.json with an absolute path - tests are discovered correctly. This isolates variable substitution as the cause.
The config is deliberately placed under config/vitest.config.ts so the extension's default config search cannot find it - forcing the extension to rely on vitest.rootConfig, which is exactly the setting that fails.
Output
[INFO 2:54:51 PM] [v1.50.2] Vitest extension is activated because Vitest is installed or there is a Vite/Vitest config file in the workspace.
[INFO 2:54:51 PM] [API] Using user root config: <REDACTED>/vitest-vscode-workspaceFolder-repro/${workspaceFolder}/config/vitest.config.ts
[INFO 2:54:52 PM] [API] Resolving configs: config/vitest.config.ts
[2:54:52 PM] [API] Spawning on-demand process...
[INFO 2:54:52 PM] [API] Running Vitest v4.1.5 (config/vitest.config.ts) with "<REDACTED>/.nvm/versions/node/v22.21.1/bin/node <REDACTED>/.vscode/extensions/vitest.explorer-1.50.2/dist/worker.js"
[Error 2:54:52 PM] Current PATH: <REDACTED>
[Error 2:54:52 PM] There were errors during config load.
[Error 2:54:52 PM] [Error Error] spawn <REDACTED>/.nvm/versions/node/v22.21.1/bin/node ENOENT
Error: spawn <REDACTED>/.nvm/versions/node/v22.21.1/bin/node ENOENT
at ChildProcess._handle.onexit (node:internal/child_process:285:19)
at onErrorNT (node:internal/child_process:483:16)
at process.processTicksAndRejections (node:internal/process/task_queues:89:21)
[Error 2:54:53 PM] [Error Error] The extension could not load any config.
Error: The extension could not load any config.
at St (<REDACTED>/.vscode/extensions/vitest.explorer-1.50.2/dist/extension.js:1:70727)
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
at async $n._defineTestProfiles (<REDACTED>/.vscode/extensions/vitest.explorer-1.50.2/dist/extension.js:11:5119)
at async $n.defineTestProfiles (<REDACTED>/.vscode/extensions/vitest.explorer-1.50.2/dist/extension.js:11:4437)
at async $n.activate (<REDACTED>/.vscode/extensions/vitest.explorer-1.50.2/dist/extension.js:11:13144)
at async Zn (<REDACTED>/.vscode/extensions/vitest.explorer-1.50.2/dist/extension.js:11:3089)
at async Mb._activate (file:///usr/share/code/resources/app/out/vs/workbench/api/node/extensionHostProcess.js:501:15965)
at async Mb._waitForDepsThenActivate (file:///usr/share/code/resources/app/out/vs/workbench/api/node/extensionHostProcess.js:501:15907)
at async Mb._initialize (file:///usr/share/code/resources/app/out/vs/workbench/api/node/extensionHostProcess.js:501:15274)
> Note: in this failure mode the extension never successfully spawns Vitest, so the log doesn't contain a "Vitest version …" line - config loading dies first. The above is the full Output Channel content for the reproducer.
Extension Version
v1.50.2
Vitest Version
4.1.5
Validations
Describe the bug
The path-valued settings contributed by this extension don't consistently run VS Code's predefined-variable substitution before passing values to Vitest. Placeholders such as
${workspaceFolder}appear verbatim in the resolved path, and Vitest fails to load the config / binary.Concretely, looking at
packages/extension/src/config.ts:vitest.vitestPackagePathdoes substitute${workspaceFolder}(seeconfig.ts:50).vitest.rootConfig,vitest.workspaceConfig,vitest.nodeExecutableare all routed throughresolveConfigPath()(config.ts:89-94, defined atconfig.ts:104-118), which handles absolute paths,~/, and relative-to-workspace paths, but does not substitute${workspaceFolder}(or any other VS Code variable). If the value starts with${workspaceFolder}/…,isAbsolute()returns false, soresolve(workspaceFolder.fsPath, "${workspaceFolder}/…")is called and the literal placeholder survives in the output path.vitest.terminalShellPathandvitest.debugOutFilesreceive no substitution at all.VS Code does not resolve these placeholders globally - per the Variables Reference, each extension must substitute them itself when it reads its configuration. Many extensions do (Python, ESLint, Ruff, Mypy, Rust-analyzer, …). That this extension handles
${workspaceFolder}forvitestPackagePathbut not forrootConfiglooks like an oversight rather than intentional behavior.Expected behavior
${workspaceFolder}- and ideally the other commonly used predefined variables (${workspaceFolderBasename},${userHome},${env:NAME},${pathSeparator},${fileWorkspaceFolder}) - are expanded before the value is handed to Vitest, consistently across all path-valued settings (vitest.rootConfig,vitest.workspaceConfig,vitest.nodeExecutable,vitest.terminalShellPath,vitest.vitestPackagePath,vitest.debugOutFiles), matching what many other VS Code extensions already do.For multi-root workspaces,
${workspaceFolder}should resolve to the folder that owns the setting (i.e. the folder scope of theWorkspaceConfigurationthe value was read from), not always the first folder.Reproduction
https://github.com/whme/vitest-vscode-workspaceFolder-repro
pnpm install${workspaceFolder}embedded in the resolved path.vitest.rootConfigvalue in.vscode/settings.jsonwith an absolute path - tests are discovered correctly. This isolates variable substitution as the cause.The config is deliberately placed under
config/vitest.config.tsso the extension's default config search cannot find it - forcing the extension to rely onvitest.rootConfig, which is exactly the setting that fails.Output
Extension Version
v1.50.2
Vitest Version
4.1.5
Validations