From c12d4ee86d606a7352eec9aae5085ce4299b8807 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:06:03 +0000 Subject: [PATCH 1/3] Initial plan From 6fb1e8ac094bc53380a788dbf0ea7c0447d3947e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:11:48 +0000 Subject: [PATCH 2/3] Add POETRY_VIRTUALENVS_IN_PROJECT env var support to poetryUtils.ts Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com> --- package-lock.json | 20 +++++- src/managers/poetry/poetryUtils.ts | 46 ++++++++++---- .../managers/poetry/poetryUtils.unit.test.ts | 63 +++++++++++++++++++ 3 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 src/test/managers/poetry/poetryUtils.unit.test.ts diff --git a/package-lock.json b/package-lock.json index 42751388..1b975f18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -795,6 +795,7 @@ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -1480,6 +1481,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1523,6 +1525,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1713,6 +1716,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -2423,6 +2427,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5176,6 +5181,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5301,6 +5307,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -5347,6 +5354,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -6144,6 +6152,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -6639,7 +6648,8 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true + "dev": true, + "peer": true }, "acorn-import-attributes": { "version": "1.9.5", @@ -6669,6 +6679,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6801,6 +6812,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -7295,6 +7307,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9280,7 +9293,8 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true + "dev": true, + "peer": true }, "uc.micro": { "version": "1.0.6", @@ -9366,6 +9380,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, + "peer": true, "requires": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -9397,6 +9412,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/src/managers/poetry/poetryUtils.ts b/src/managers/poetry/poetryUtils.ts index 6a8abddd..a19699dc 100644 --- a/src/managers/poetry/poetryUtils.ts +++ b/src/managers/poetry/poetryUtils.ts @@ -17,6 +17,19 @@ import { } from '../common/nativePythonFinder'; import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils'; +/** + * Checks if the POETRY_VIRTUALENVS_IN_PROJECT environment variable is set to a truthy value. + * When true, Poetry creates virtualenvs in the project's `.venv` directory. + * Mirrors the PET server logic in `pet-poetry/src/env_variables.rs`. + */ +export function isPoetryVirtualenvsInProject(): boolean { + const value = process.env.POETRY_VIRTUALENVS_IN_PROJECT; + if (value === undefined) { + return false; + } + return value === '1' || value.toLowerCase() === 'true'; +} + async function findPoetry(): Promise { try { return await which('poetry'); @@ -251,19 +264,28 @@ async function nativeToPythonEnv( // Determine if the environment is in Poetry's global virtualenvs directory let isGlobalPoetryEnv = false; - const virtualenvsPath = poetryVirtualenvsPath; // Use the cached value if available - if (virtualenvsPath) { - const normalizedVirtualenvsPath = path.normalize(virtualenvsPath); - isGlobalPoetryEnv = normalizedPrefix.startsWith(normalizedVirtualenvsPath); + + // If POETRY_VIRTUALENVS_IN_PROJECT is set, environments are created in-project (.venv) + // and should not be classified as global + if (isPoetryVirtualenvsInProject() && info.project) { + isGlobalPoetryEnv = false; } else { - // Fall back to checking the default location if we haven't cached the path yet - const homeDir = getUserHomeDir(); - if (homeDir) { - const defaultPath = path.normalize(path.join(homeDir, '.cache', 'pypoetry', 'virtualenvs')); - isGlobalPoetryEnv = normalizedPrefix.startsWith(defaultPath); - - // Try to get the actual path asynchronously for next time - getPoetryVirtualenvsPath(_poetry).catch((e) => traceError(`Error getting Poetry virtualenvs path: ${e}`)); + const virtualenvsPath = poetryVirtualenvsPath; // Use the cached value if available + if (virtualenvsPath) { + const normalizedVirtualenvsPath = path.normalize(virtualenvsPath); + isGlobalPoetryEnv = normalizedPrefix.startsWith(normalizedVirtualenvsPath); + } else { + // Fall back to checking the default location if we haven't cached the path yet + const homeDir = getUserHomeDir(); + if (homeDir) { + const defaultPath = path.normalize(path.join(homeDir, '.cache', 'pypoetry', 'virtualenvs')); + isGlobalPoetryEnv = normalizedPrefix.startsWith(defaultPath); + + // Try to get the actual path asynchronously for next time + getPoetryVirtualenvsPath(_poetry).catch((e) => + traceError(`Error getting Poetry virtualenvs path: ${e}`), + ); + } } } diff --git a/src/test/managers/poetry/poetryUtils.unit.test.ts b/src/test/managers/poetry/poetryUtils.unit.test.ts new file mode 100644 index 00000000..08607784 --- /dev/null +++ b/src/test/managers/poetry/poetryUtils.unit.test.ts @@ -0,0 +1,63 @@ +import assert from 'node:assert'; +import { isPoetryVirtualenvsInProject } from '../../../managers/poetry/poetryUtils'; + +suite('isPoetryVirtualenvsInProject', () => { + let originalEnv: string | undefined; + + setup(() => { + originalEnv = process.env.POETRY_VIRTUALENVS_IN_PROJECT; + }); + + teardown(() => { + if (originalEnv === undefined) { + delete process.env.POETRY_VIRTUALENVS_IN_PROJECT; + } else { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = originalEnv; + } + }); + + test('should return false when env var is not set', () => { + delete process.env.POETRY_VIRTUALENVS_IN_PROJECT; + assert.strictEqual(isPoetryVirtualenvsInProject(), false); + }); + + test('should return true when env var is "true"', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'true'; + assert.strictEqual(isPoetryVirtualenvsInProject(), true); + }); + + test('should return true when env var is "True" (case insensitive)', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'True'; + assert.strictEqual(isPoetryVirtualenvsInProject(), true); + }); + + test('should return true when env var is "TRUE" (case insensitive)', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'TRUE'; + assert.strictEqual(isPoetryVirtualenvsInProject(), true); + }); + + test('should return true when env var is "1"', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = '1'; + assert.strictEqual(isPoetryVirtualenvsInProject(), true); + }); + + test('should return false when env var is "false"', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'false'; + assert.strictEqual(isPoetryVirtualenvsInProject(), false); + }); + + test('should return false when env var is "0"', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = '0'; + assert.strictEqual(isPoetryVirtualenvsInProject(), false); + }); + + test('should return false when env var is empty string', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = ''; + assert.strictEqual(isPoetryVirtualenvsInProject(), false); + }); + + test('should return false when env var is arbitrary string', () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'yes'; + assert.strictEqual(isPoetryVirtualenvsInProject(), false); + }); +}); From 02c500a623edb179de9fde4fb44c853750b75481 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:59:04 +0000 Subject: [PATCH 3/3] Address PR review: revert package-lock.json, make env var injectable, use guard clause, add nativeToPythonEnv integration tests Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com> --- package-lock.json | 20 +-- src/managers/poetry/poetryUtils.ts | 14 +- .../managers/poetry/poetryUtils.unit.test.ts | 158 ++++++++++++++---- 3 files changed, 135 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b975f18..42751388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -795,7 +795,6 @@ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -1481,7 +1480,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1525,7 +1523,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1716,7 +1713,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -2427,7 +2423,6 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5181,7 +5176,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5307,7 +5301,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -5354,7 +5347,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -6152,7 +6144,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -6648,8 +6639,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "peer": true + "dev": true }, "acorn-import-attributes": { "version": "1.9.5", @@ -6679,7 +6669,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6812,7 +6801,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, - "peer": true, "requires": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -7307,7 +7295,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9293,8 +9280,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "peer": true + "dev": true }, "uc.micro": { "version": "1.0.6", @@ -9380,7 +9366,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "peer": true, "requires": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -9412,7 +9397,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/src/managers/poetry/poetryUtils.ts b/src/managers/poetry/poetryUtils.ts index a19699dc..217f5885 100644 --- a/src/managers/poetry/poetryUtils.ts +++ b/src/managers/poetry/poetryUtils.ts @@ -21,9 +21,10 @@ import { getShellActivationCommands, shortVersion, sortEnvironments } from '../c * Checks if the POETRY_VIRTUALENVS_IN_PROJECT environment variable is set to a truthy value. * When true, Poetry creates virtualenvs in the project's `.venv` directory. * Mirrors the PET server logic in `pet-poetry/src/env_variables.rs`. + * @param envValue Optional override for the env var value (used for testing). */ -export function isPoetryVirtualenvsInProject(): boolean { - const value = process.env.POETRY_VIRTUALENVS_IN_PROJECT; +export function isPoetryVirtualenvsInProject(envValue?: string): boolean { + const value = envValue ?? process.env.POETRY_VIRTUALENVS_IN_PROJECT; if (value === undefined) { return false; } @@ -243,7 +244,7 @@ export async function getPoetryVersion(poetry: string): Promise { + test('should return false when env var is not set', () => { + assert.strictEqual(isPoetryVirtualenvsInProject(undefined), false); + }); + + test('should return true when env var is "true"', () => { + assert.strictEqual(isPoetryVirtualenvsInProject('true'), true); + }); + + test('should return true when env var is "True" (case insensitive)', () => { + assert.strictEqual(isPoetryVirtualenvsInProject('True'), true); + }); + + test('should return true when env var is "TRUE" (case insensitive)', () => { + assert.strictEqual(isPoetryVirtualenvsInProject('TRUE'), true); + }); + + test('should return true when env var is "1"', () => { + assert.strictEqual(isPoetryVirtualenvsInProject('1'), true); + }); + + test('should return false when env var is "false"', () => { + assert.strictEqual(isPoetryVirtualenvsInProject('false'), false); + }); + + test('should return false when env var is "0"', () => { + assert.strictEqual(isPoetryVirtualenvsInProject('0'), false); + }); + + test('should return false when env var is empty string', () => { + assert.strictEqual(isPoetryVirtualenvsInProject(''), false); + }); + + test('should return false when env var is arbitrary string', () => { + assert.strictEqual(isPoetryVirtualenvsInProject('yes'), false); + }); + + test('should read from process.env when no argument given', () => { + const original = process.env.POETRY_VIRTUALENVS_IN_PROJECT; + try { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'true'; + assert.strictEqual(isPoetryVirtualenvsInProject(), true); + + delete process.env.POETRY_VIRTUALENVS_IN_PROJECT; + assert.strictEqual(isPoetryVirtualenvsInProject(), false); + } finally { + if (original === undefined) { + delete process.env.POETRY_VIRTUALENVS_IN_PROJECT; + } else { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = original; + } + } + }); +}); + +suite('nativeToPythonEnv - POETRY_VIRTUALENVS_IN_PROJECT integration', () => { + let capturedInfo: PythonEnvironmentInfo | undefined; let originalEnv: string | undefined; + const mockApi = { + createPythonEnvironmentItem: (info: PythonEnvironmentInfo, _manager: EnvironmentManager) => { + capturedInfo = info; + return { ...info, envId: { id: 'test-id', managerId: 'test-manager' } } as PythonEnvironment; + }, + } as unknown as PythonEnvironmentApi; + + const mockManager = {} as EnvironmentManager; + + const baseEnvInfo: NativeEnvInfo = { + prefix: '/home/user/myproject/.venv', + executable: '/home/user/myproject/.venv/bin/python', + version: '3.12.0', + name: 'myproject-venv', + project: '/home/user/myproject', + }; + setup(() => { + capturedInfo = undefined; originalEnv = process.env.POETRY_VIRTUALENVS_IN_PROJECT; + + sinon.stub(utils, 'getShellActivationCommands').resolves({ + shellActivation: new Map(), + shellDeactivation: new Map(), + }); }); teardown(() => { + sinon.restore(); if (originalEnv === undefined) { delete process.env.POETRY_VIRTUALENVS_IN_PROJECT; } else { @@ -16,48 +100,60 @@ suite('isPoetryVirtualenvsInProject', () => { } }); - test('should return false when env var is not set', () => { - delete process.env.POETRY_VIRTUALENVS_IN_PROJECT; - assert.strictEqual(isPoetryVirtualenvsInProject(), false); - }); - - test('should return true when env var is "true"', () => { + test('env var set + project present → environment is NOT classified as global', async () => { process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'true'; - assert.strictEqual(isPoetryVirtualenvsInProject(), true); - }); - test('should return true when env var is "True" (case insensitive)', () => { - process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'True'; - assert.strictEqual(isPoetryVirtualenvsInProject(), true); - }); + const result = await nativeToPythonEnv(baseEnvInfo, mockApi, mockManager, '/usr/bin/poetry'); - test('should return true when env var is "TRUE" (case insensitive)', () => { - process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'TRUE'; - assert.strictEqual(isPoetryVirtualenvsInProject(), true); + assert.ok(result, 'Should return a PythonEnvironment'); + assert.ok(capturedInfo, 'Should have captured environment info'); + assert.strictEqual(capturedInfo!.group, undefined, 'In-project env should not have POETRY_GLOBAL group'); }); - test('should return true when env var is "1"', () => { + test('env var set to "1" + project present → environment is NOT classified as global', async () => { process.env.POETRY_VIRTUALENVS_IN_PROJECT = '1'; - assert.strictEqual(isPoetryVirtualenvsInProject(), true); - }); - test('should return false when env var is "false"', () => { - process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'false'; - assert.strictEqual(isPoetryVirtualenvsInProject(), false); + const result = await nativeToPythonEnv(baseEnvInfo, mockApi, mockManager, '/usr/bin/poetry'); + + assert.ok(result, 'Should return a PythonEnvironment'); + assert.ok(capturedInfo, 'Should have captured environment info'); + assert.strictEqual(capturedInfo!.group, undefined, 'In-project env should not have POETRY_GLOBAL group'); }); - test('should return false when env var is "0"', () => { - process.env.POETRY_VIRTUALENVS_IN_PROJECT = '0'; - assert.strictEqual(isPoetryVirtualenvsInProject(), false); + test('env var set + project absent → falls through to normal global check', async () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'true'; + const envWithoutProject: NativeEnvInfo = { + ...baseEnvInfo, + project: undefined, + }; + + const result = await nativeToPythonEnv(envWithoutProject, mockApi, mockManager, '/usr/bin/poetry'); + + assert.ok(result, 'Should return a PythonEnvironment'); + assert.ok(capturedInfo, 'Should have captured environment info'); + // Without project, falls through to global check; since prefix is not in global dir, group is undefined + assert.strictEqual(capturedInfo!.group, undefined, 'Non-global path without project should not be global'); }); - test('should return false when env var is empty string', () => { - process.env.POETRY_VIRTUALENVS_IN_PROJECT = ''; - assert.strictEqual(isPoetryVirtualenvsInProject(), false); + test('env var NOT set → original classification behavior is preserved', async () => { + delete process.env.POETRY_VIRTUALENVS_IN_PROJECT; + + const result = await nativeToPythonEnv(baseEnvInfo, mockApi, mockManager, '/usr/bin/poetry'); + + assert.ok(result, 'Should return a PythonEnvironment'); + assert.ok(capturedInfo, 'Should have captured environment info'); + // Prefix is not in global virtualenvs dir, so not classified as global + assert.strictEqual(capturedInfo!.group, undefined, 'Non-global path should not be global'); }); - test('should return false when env var is arbitrary string', () => { - process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'yes'; - assert.strictEqual(isPoetryVirtualenvsInProject(), false); + test('env var set to "false" → original classification behavior is preserved', async () => { + process.env.POETRY_VIRTUALENVS_IN_PROJECT = 'false'; + + const result = await nativeToPythonEnv(baseEnvInfo, mockApi, mockManager, '/usr/bin/poetry'); + + assert.ok(result, 'Should return a PythonEnvironment'); + assert.ok(capturedInfo, 'Should have captured environment info'); + // Falls through to normal check since env var is falsy + assert.strictEqual(capturedInfo!.group, undefined, 'Non-global path should not be global'); }); });