From 7e5af22cde7e28c9e563c4861270aaf82625f2a5 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:20:19 -0800 Subject: [PATCH 1/7] add integration test --- .../envCreation.integration.test.ts | 303 +++++++++++++++ .../envDiscovery.integration.test.ts | 229 +++++++++++ .../interpreterSelection.integration.test.ts | 318 +++++++++++++++ .../packageManagement.integration.test.ts | 363 ++++++++++++++++++ .../pythonProjects.integration.test.ts | 286 ++++++++++++++ .../settingsBehavior.integration.test.ts | 319 +++++++++++++++ .../terminalActivation.integration.test.ts | 356 +++++++++++++++++ 7 files changed, 2174 insertions(+) create mode 100644 src/test/integration/envCreation.integration.test.ts create mode 100644 src/test/integration/envDiscovery.integration.test.ts create mode 100644 src/test/integration/interpreterSelection.integration.test.ts create mode 100644 src/test/integration/packageManagement.integration.test.ts create mode 100644 src/test/integration/pythonProjects.integration.test.ts create mode 100644 src/test/integration/settingsBehavior.integration.test.ts create mode 100644 src/test/integration/terminalActivation.integration.test.ts diff --git a/src/test/integration/envCreation.integration.test.ts b/src/test/integration/envCreation.integration.test.ts new file mode 100644 index 00000000..cf36d5c5 --- /dev/null +++ b/src/test/integration/envCreation.integration.test.ts @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Environment Creation + * + * PURPOSE: + * Verify that environment creation works correctly through the API, + * respecting configured managers and options. + * + * WHAT THIS TESTS: + * 1. createEnvironment API is available and callable + * 2. Creation respects defaultEnvManager setting + * 3. Created environments appear in discovery + * 4. Environment removal works correctly + * + * NOTE: These tests may create actual virtual environments on disk. + * Tests that create environments should clean up after themselves. + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { PythonEnvironment, PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { sleep, waitForCondition } from '../testUtils'; + +suite('Integration: Environment Creation', function () { + this.timeout(120_000); // Environment creation can be slow + + let api: PythonEnvironmentApi; + + suiteSetup(async function () { + this.timeout(30_000); + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + }); + + /** + * Test: createEnvironment API is available + * + * The API should have a createEnvironment method. + */ + test('createEnvironment API is available', async function () { + assert.ok(typeof api.createEnvironment === 'function', 'createEnvironment should be a function'); + }); + + /** + * Test: removeEnvironment API is available + * + * The API should have a removeEnvironment method. + */ + test('removeEnvironment API is available', async function () { + assert.ok(typeof api.removeEnvironment === 'function', 'removeEnvironment should be a function'); + }); + + /** + * Test: Managers that support creation are available + * + * At least one environment manager (venv or conda) should support creation. + */ + test('At least one manager supports environment creation', async function () { + // Get all environments to force managers to load + await api.getEnvironments('all'); + + // Check if we have global Python installations that can create venvs + const globalEnvs = await api.getEnvironments('global'); + + // If we have global Python installations, venv creation should be possible + if (globalEnvs.length > 0) { + console.log(`Found ${globalEnvs.length} global Python installations for venv creation`); + } + }); + + /** + * Test: Created environment appears in discovery + * + * After creating an environment, it should be discoverable via getEnvironments. + * This test creates a real environment and cleans it up. + */ + test('Created environment appears in discovery', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + // Check if we have Python available for venv creation + const globalEnvs = await api.getEnvironments('global'); + if (globalEnvs.length === 0) { + console.log('No global Python installations found, skipping creation test'); + this.skip(); + return; + } + + const workspaceUri = workspaceFolders[0].uri; + let createdEnv: PythonEnvironment | undefined; + + try { + // Create environment with quickCreate to avoid prompts + createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true }); + + if (!createdEnv) { + // Creation may have been cancelled or failed silently + console.log('Environment creation returned undefined (may require user input)'); + this.skip(); + return; + } + + // Refresh and verify the environment appears + await api.refreshEnvironments(workspaceUri); + const environments = await api.getEnvironments(workspaceUri); + + const found = environments.some( + (env) => + env.envId.id === createdEnv!.envId.id || + env.environmentPath.fsPath === createdEnv!.environmentPath.fsPath, + ); + + assert.ok(found, 'Created environment should appear in discovery'); + } finally { + // Cleanup: Remove the created environment + if (createdEnv) { + try { + await api.removeEnvironment(createdEnv); + } catch (e) { + console.log('Cleanup failed (may already be removed):', e); + } + } + } + }); + + /** + * Test: Environment removal removes from discovery + * + * After removing an environment, it should no longer appear in discovery. + */ + test('Removed environment disappears from discovery', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const globalEnvs = await api.getEnvironments('global'); + if (globalEnvs.length === 0) { + this.skip(); + return; + } + + const workspaceUri = workspaceFolders[0].uri; + let createdEnv: PythonEnvironment | undefined; + + try { + // Create environment + createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true }); + + if (!createdEnv) { + this.skip(); + return; + } + + // Record the environment ID + const envId = createdEnv.envId.id; + + // Remove environment + await api.removeEnvironment(createdEnv); + createdEnv = undefined; // Mark as cleaned up + + // Give time for removal to complete + await sleep(1000); + + // Refresh and verify it's gone + await api.refreshEnvironments(workspaceUri); + const environments = await api.getEnvironments(workspaceUri); + + const stillExists = environments.some((env) => env.envId.id === envId); + + assert.ok(!stillExists, 'Removed environment should not appear in discovery'); + } finally { + // Cleanup in case removal failed + if (createdEnv) { + try { + await api.removeEnvironment(createdEnv); + } catch { + // Ignore cleanup errors + } + } + } + }); + + /** + * Test: Creating environment for 'global' scope + * + * The 'global' scope should allow creating environments not tied to a workspace. + * Note: This may require special permissions or configurations. + */ + test('Global scope creation is handled', async function () { + // This test verifies the API handles global scope without crashing + // Actual creation may require specific configurations + + try { + // Attempt global creation - this may prompt for user input + // so we use quickCreate and expect it might fail + const result = await api.createEnvironment('global', { quickCreate: true }); + + // If it succeeds, clean up + if (result) { + await api.removeEnvironment(result); + } + } catch (e) { + // Global creation not supported or no manager available + // This is acceptable - the test verifies the API doesn't crash + console.log('Global creation not available:', (e as Error).message); + } + }); + + /** + * Test: Creation with multiple URIs selects manager + * + * When passing multiple URIs, the API should handle manager selection. + */ + test('Multiple URI scope is handled', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length < 2) { + this.skip(); + return; + } + + const uris = workspaceFolders.map((f) => f.uri); + + try { + // This may prompt for manager selection + const result = await api.createEnvironment(uris, { quickCreate: true }); + + if (result) { + await api.removeEnvironment(result); + } + } catch (e) { + // Multi-URI creation may not be fully supported in all configurations + console.log('Multi-URI creation result:', (e as Error).message); + } + }); + + /** + * Test: Creation returns properly structured environment + * + * A successfully created environment should have all required fields. + */ + test('Created environment has proper structure', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const globalEnvs = await api.getEnvironments('global'); + if (globalEnvs.length === 0) { + this.skip(); + return; + } + + const workspaceUri = workspaceFolders[0].uri; + let createdEnv: PythonEnvironment | undefined; + + try { + createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true }); + + if (!createdEnv) { + this.skip(); + return; + } + + // Verify structure + assert.ok(createdEnv.envId, 'Created env must have envId'); + assert.ok(createdEnv.envId.id, 'envId must have id'); + assert.ok(createdEnv.envId.managerId, 'envId must have managerId'); + assert.ok(createdEnv.name, 'Created env must have name'); + assert.ok(createdEnv.displayName, 'Created env must have displayName'); + assert.ok(createdEnv.environmentPath, 'Created env must have environmentPath'); + } finally { + if (createdEnv) { + try { + await api.removeEnvironment(createdEnv); + } catch { + // Ignore cleanup errors + } + } + } + }); +}); diff --git a/src/test/integration/envDiscovery.integration.test.ts b/src/test/integration/envDiscovery.integration.test.ts new file mode 100644 index 00000000..0e0ab3f2 --- /dev/null +++ b/src/test/integration/envDiscovery.integration.test.ts @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Environment Discovery + * + * PURPOSE: + * Verify that environment discovery correctly finds and reports Python + * environments based on configuration settings and search paths. + * + * WHAT THIS TESTS: + * 1. Discovery respects workspaceSearchPaths setting + * 2. Discovery respects globalSearchPaths setting + * 3. Refresh clears stale cache and finds new environments + * 4. Different scopes (all, global) return appropriate environments + * + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { DidChangeEnvironmentsEventArgs, PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { TestEventHandler, waitForCondition } from '../testUtils'; + +suite('Integration: Environment Discovery', function () { + this.timeout(60_000); + + let api: PythonEnvironmentApi; + + suiteSetup(async function () { + this.timeout(30_000); + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + assert.ok(typeof api.getEnvironments === 'function', 'getEnvironments method not available'); + }); + + /** + * Test: Discovery returns environments after refresh + * + * Verifies that refreshing environments triggers discovery and + * the API returns a valid array of environments. + */ + test('Refresh triggers discovery and returns environments', async function () { + // Trigger refresh + await api.refreshEnvironments(undefined); + + // Get environments after refresh + const environments = await api.getEnvironments('all'); + + // Should return an array + assert.ok(Array.isArray(environments), 'Expected environments to be an array'); + + // Log count for debugging (may be 0 if no Python installed) + console.log(`Discovered ${environments.length} environments`); + }); + + /** + * Test: Global scope returns only global environments + * + * Global environments are system-wide Python installations that serve + * as bases for virtual environments. + */ + test('Global scope returns base Python installations', async function () { + const globalEnvs = await api.getEnvironments('global'); + const allEnvs = await api.getEnvironments('all'); + + assert.ok(Array.isArray(globalEnvs), 'Global scope should return array'); + assert.ok(Array.isArray(allEnvs), 'All scope should return array'); + + // Global should be subset of or equal to all + assert.ok( + globalEnvs.length <= allEnvs.length, + `Global envs (${globalEnvs.length}) should not exceed all envs (${allEnvs.length})`, + ); + }); + + /** + * Test: Change events fire during refresh + * + * When environments are discovered or removed, the onDidChangeEnvironments + * event should fire. + */ + test('onDidChangeEnvironments fires during refresh', async function () { + const handler = new TestEventHandler( + api.onDidChangeEnvironments, + 'onDidChangeEnvironments', + ); + + try { + // Reset any previous events + handler.reset(); + + // Trigger refresh + await api.refreshEnvironments(undefined); + + // Give time for events to propagate + // Note: Events may not fire if no environments change + // This test verifies the event mechanism works when changes occur + const environments = await api.getEnvironments('all'); + + if (environments.length > 0) { + // If we have environments, we should have received events during discovery + // (unless this is a repeat run with cached data) + console.log(`Event fired ${handler.count} times during refresh`); + } + } finally { + handler.dispose(); + } + }); + + /** + * Test: Environments have valid structure + * + * Each discovered environment should have the required properties + * for the extension to work correctly. + */ + test('Discovered environments have valid structure', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + for (const env of environments) { + // Must have envId + assert.ok(env.envId, 'Environment must have envId'); + assert.ok(env.envId.id, 'envId must have id'); + assert.ok(env.envId.managerId, 'envId must have managerId'); + + // Must have basic info + assert.ok(typeof env.name === 'string', 'Environment must have name'); + assert.ok(typeof env.displayName === 'string', 'Environment must have displayName'); + assert.ok(typeof env.version === 'string', 'Environment must have version'); + + // Must have environment path + assert.ok(env.environmentPath, 'Environment must have environmentPath'); + assert.ok(env.environmentPath instanceof vscode.Uri, 'environmentPath must be a Uri'); + } + }); + + /** + * Test: resolveEnvironment returns full details + * + * The resolveEnvironment method should return complete environment + * information including execution info. + */ + test('resolveEnvironment returns execution info', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // Pick first environment and resolve it + const env = environments[0]; + const resolved = await api.resolveEnvironment(env.environmentPath); + + if (!resolved) { + // Some environments may not be resolvable (broken/stale) + console.log('Environment could not be resolved:', env.displayName); + return; + } + + // Resolved environment should have execInfo + assert.ok(resolved.execInfo, 'Resolved environment should have execInfo'); + assert.ok(resolved.execInfo.run, 'execInfo should have run configuration'); + assert.ok(resolved.execInfo.run.executable, 'run should have executable path'); + }); + + /** + * Test: Workspace-scoped discovery finds workspace environments + * + * When a workspace folder is open and contains environments, + * querying with the workspace URI should find them. + */ + test('Workspace scope returns workspace environments', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const workspaceUri = workspaceFolders[0].uri; + const workspaceEnvs = await api.getEnvironments(workspaceUri); + + assert.ok(Array.isArray(workspaceEnvs), 'Workspace scope should return array'); + + // Log for debugging + console.log(`Found ${workspaceEnvs.length} environments in workspace`); + }); + + /** + * Test: Multiple refreshes are idempotent + * + * Calling refresh multiple times should not cause duplicate + * environments or errors. + */ + test('Multiple refreshes do not create duplicates', async function () { + // First refresh + await api.refreshEnvironments(undefined); + const firstCount = (await api.getEnvironments('all')).length; + + // Second refresh + await api.refreshEnvironments(undefined); + const secondCount = (await api.getEnvironments('all')).length; + + // Third refresh + await api.refreshEnvironments(undefined); + const thirdCount = (await api.getEnvironments('all')).length; + + // Counts should be consistent (may vary slightly due to system changes) + assert.ok( + Math.abs(firstCount - secondCount) <= 1 && Math.abs(secondCount - thirdCount) <= 1, + `Environment counts should be stable: ${firstCount}, ${secondCount}, ${thirdCount}`, + ); + }); +}); diff --git a/src/test/integration/interpreterSelection.integration.test.ts b/src/test/integration/interpreterSelection.integration.test.ts new file mode 100644 index 00000000..1dbb6886 --- /dev/null +++ b/src/test/integration/interpreterSelection.integration.test.ts @@ -0,0 +1,318 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Interpreter Selection Priority + * + * PURPOSE: + * Verify that interpreter selection follows the correct priority order + * and respects user configuration. + * + * WHAT THIS TESTS: + * 1. Projects settings override other sources + * 2. Explicit setEnvironment overrides auto-discovery + * 3. Auto-discovery prefers workspace-local environments + * 4. getEnvironment returns consistent results + * + * Priority order (from docs): + * 1. pythonProjects[] - project-specific config + * 2. defaultEnvManager - if explicitly set + * 3. python.defaultInterpreterPath - legacy setting + * 4. Auto-discovery - workspace-local .venv, then global + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { DidChangeEnvironmentEventArgs, PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { TestEventHandler, waitForCondition } from '../testUtils'; + +suite('Integration: Interpreter Selection Priority', function () { + this.timeout(60_000); + + let api: PythonEnvironmentApi; + + suiteSetup(async function () { + this.timeout(30_000); + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + }); + + /** + * Test: getEnvironment without scope returns global selection + * + * When no scope is specified, should return the currently active + * global environment. + */ + test('getEnvironment without scope returns global selection', async function () { + const env = await api.getEnvironment(undefined); + + // May be undefined if no environment is selected/available + if (env) { + assert.ok(env.envId, 'Environment should have envId'); + assert.ok(env.displayName, 'Environment should have displayName'); + } + }); + + /** + * Test: setEnvironment persists selection + * + * After calling setEnvironment, subsequent getEnvironment calls + * should return the same environment. + */ + test('setEnvironment persists selection', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const envToSet = environments[0]; + + // Set environment globally + await api.setEnvironment(undefined, envToSet); + + // Get and verify + const retrieved = await api.getEnvironment(undefined); + + assert.ok(retrieved, 'Should have environment after setting'); + assert.strictEqual(retrieved.envId.id, envToSet.envId.id, 'Retrieved environment should match set environment'); + }); + + /** + * Test: Project-scoped selection is independent of global + * + * Setting an environment for a specific project should not affect + * the global selection or other projects. + */ + test('Project selection is independent of global', async function () { + const environments = await api.getEnvironments('all'); + const projects = api.getPythonProjects(); + + if (environments.length < 2 || projects.length === 0) { + this.skip(); + return; + } + + const globalEnv = environments[0]; + const projectEnv = environments[1]; + const project = projects[0]; + + // Set global environment + await api.setEnvironment(undefined, globalEnv); + + // Set different environment for project + await api.setEnvironment(project.uri, projectEnv); + + // Verify global is unchanged + const globalRetrieved = await api.getEnvironment(undefined); + assert.ok(globalRetrieved, 'Global should have environment'); + assert.strictEqual(globalRetrieved.envId.id, globalEnv.envId.id, 'Global selection should be unchanged'); + + // Verify project has its own selection + const projectRetrieved = await api.getEnvironment(project.uri); + assert.ok(projectRetrieved, 'Project should have environment'); + assert.strictEqual(projectRetrieved.envId.id, projectEnv.envId.id, 'Project should have its own selection'); + }); + + /** + * Test: Change event fires with correct old/new values + * + * The onDidChangeEnvironment event should include both the old + * and new environment values. + */ + test('Change event includes old and new values', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length < 2) { + this.skip(); + return; + } + + const oldEnv = environments[0]; + const newEnv = environments[1]; + + // Set initial environment + await api.setEnvironment(undefined, oldEnv); + + const handler = new TestEventHandler( + api.onDidChangeEnvironment, + 'onDidChangeEnvironment', + ); + + try { + // Change to new environment + await api.setEnvironment(undefined, newEnv); + + // Wait for event + await handler.assertFired(5000); + + const event = handler.last; + assert.ok(event, 'Event should have fired'); + assert.ok(event.new, 'Event should have new environment'); + assert.strictEqual(event.new.envId.id, newEnv.envId.id, 'New should match set environment'); + } finally { + handler.dispose(); + } + }); + + /** + * Test: File URI inherits project environment + * + * When querying for a file within a project, should return + * the project's environment. + */ + test('File inherits project environment', async function () { + const environments = await api.getEnvironments('all'); + const projects = api.getPythonProjects(); + + if (environments.length === 0 || projects.length === 0) { + this.skip(); + return; + } + + const project = projects[0]; + const env = environments[0]; + + // Set project environment + await api.setEnvironment(project.uri, env); + + // Query for a file inside the project + const fileUri = vscode.Uri.joinPath(project.uri, 'subdir', 'script.py'); + const fileEnv = await api.getEnvironment(fileUri); + + assert.ok(fileEnv, 'File should inherit project environment'); + assert.strictEqual(fileEnv.envId.id, env.envId.id, 'File should use project environment'); + }); + + /** + * Test: Selection is consistent across multiple calls + * + * Calling getEnvironment multiple times should return the same result. + */ + test('Selection is consistent across calls', async function () { + const env1 = await api.getEnvironment(undefined); + const env2 = await api.getEnvironment(undefined); + const env3 = await api.getEnvironment(undefined); + + if (!env1) { + // No environment selected - that's consistent + assert.strictEqual(env2, undefined, 'Should consistently return undefined'); + assert.strictEqual(env3, undefined, 'Should consistently return undefined'); + return; + } + + assert.ok(env2, 'Second call should return environment'); + assert.ok(env3, 'Third call should return environment'); + + assert.strictEqual(env1.envId.id, env2.envId.id, 'First and second should match'); + assert.strictEqual(env2.envId.id, env3.envId.id, 'Second and third should match'); + }); + + /** + * Test: Setting same environment doesn't fire extra events + * + * Setting the same environment twice should not fire change event + * on the second call. + */ + test('Setting same environment is idempotent', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + // Set environment first time + await api.setEnvironment(undefined, env); + + const handler = new TestEventHandler( + api.onDidChangeEnvironment, + 'onDidChangeEnvironment', + ); + + try { + // Set same environment again + await api.setEnvironment(undefined, env); + + // Wait a bit and check - should not fire (or fire with same old/new) + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Either no event fired, or event had old === new + if (handler.fired) { + // If event fired, it should be with same env + console.log('Event fired for same env selection:', handler.last?.new?.displayName); + } + } finally { + handler.dispose(); + } + }); + + /** + * Test: Environment selection works with URI arrays + * + * setEnvironment should handle array of URIs for multi-select scenarios. + */ + test('setEnvironment handles URI array', async function () { + const environments = await api.getEnvironments('all'); + const projects = api.getPythonProjects(); + + if (environments.length === 0 || projects.length < 2) { + this.skip(); + return; + } + + const env = environments[0]; + const uris = projects.slice(0, 2).map((p) => p.uri); + + // Set environment for multiple URIs at once + await api.setEnvironment(uris, env); + + // Verify both projects have the environment + for (const uri of uris) { + const retrieved = await api.getEnvironment(uri); + assert.ok(retrieved, `URI ${uri.fsPath} should have environment`); + assert.strictEqual(retrieved.envId.id, env.envId.id, `URI ${uri.fsPath} should have set environment`); + } + }); + + /** + * Test: Clearing selection falls back to auto-discovery + * + * After clearing an explicit selection, auto-discovery should + * provide a fallback environment if available. + */ + test('Clearing selection allows auto-discovery fallback', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // Set an explicit environment + await api.setEnvironment(undefined, environments[0]); + + // Clear the selection + await api.setEnvironment(undefined, undefined); + + // Get environment - may return auto-discovered env or undefined + const autoEnv = await api.getEnvironment(undefined); + + // This test verifies the operation completes without error + // The result depends on available environments and settings + console.log('After clearing selection:', autoEnv?.displayName ?? 'none'); + }); +}); diff --git a/src/test/integration/packageManagement.integration.test.ts b/src/test/integration/packageManagement.integration.test.ts new file mode 100644 index 00000000..cd10793c --- /dev/null +++ b/src/test/integration/packageManagement.integration.test.ts @@ -0,0 +1,363 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Package Management + * + * PURPOSE: + * Verify that package management works correctly for different + * environment types and managers. + * + * WHAT THIS TESTS: + * 1. getPackages returns packages for environments + * 2. Package installation via API + * 3. Package uninstallation via API + * 4. Refresh updates package list + * 5. Events fire when packages change + * + * NOTE: Some tests may install/uninstall actual packages. + * These should use safe test packages that don't have side effects. + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { DidChangePackagesEventArgs, PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { sleep, TestEventHandler, waitForCondition } from '../testUtils'; + +suite('Integration: Package Management', function () { + this.timeout(120_000); // Package operations can be slow + + let api: PythonEnvironmentApi; + + suiteSetup(async function () { + this.timeout(30_000); + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + }); + + /** + * Test: Package management APIs are available + * + * The API should have all package management methods. + */ + test('Package management APIs are available', async function () { + assert.ok(typeof api.getPackages === 'function', 'getPackages should be a function'); + assert.ok(typeof api.refreshPackages === 'function', 'refreshPackages should be a function'); + assert.ok(typeof api.managePackages === 'function', 'managePackages should be a function'); + assert.ok(api.onDidChangePackages, 'onDidChangePackages should be available'); + }); + + /** + * Test: getPackages returns array for environment + * + * For a valid environment, getPackages should return a list of packages. + */ + test('getPackages returns packages for environment', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // Try to find an environment that likely has packages (not system Python) + let targetEnv = environments[0]; + for (const env of environments) { + // Prefer environments that are likely virtual envs with packages + if (env.displayName.includes('venv') || env.displayName.includes('.venv')) { + targetEnv = env; + break; + } + } + + const packages = await api.getPackages(targetEnv); + + // May be undefined if package manager not available + if (packages === undefined) { + console.log('Package manager not available for:', targetEnv.displayName); + return; + } + + assert.ok(Array.isArray(packages), 'getPackages should return array'); + console.log(`Found ${packages.length} packages in ${targetEnv.displayName}`); + }); + + /** + * Test: Packages have valid structure + * + * Each package should have required properties. + */ + test('Packages have valid structure', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const packages = await api.getPackages(environments[0]); + + if (!packages || packages.length === 0) { + this.skip(); + return; + } + + for (const pkg of packages) { + assert.ok(pkg.pkgId, 'Package must have pkgId'); + assert.ok(pkg.pkgId.id, 'pkgId must have id'); + assert.ok(pkg.pkgId.managerId, 'pkgId must have managerId'); + assert.ok(pkg.pkgId.environmentId, 'pkgId must have environmentId'); + assert.ok(typeof pkg.name === 'string', 'Package must have name'); + assert.ok(pkg.name.length > 0, 'Package name should not be empty'); + assert.ok(typeof pkg.displayName === 'string', 'Package must have displayName'); + } + }); + + /** + * Test: refreshPackages updates package list + * + * After refreshing, the package list should be up to date. + */ + test('refreshPackages updates list', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + // Get initial packages + const initial = await api.getPackages(env); + + if (initial === undefined) { + this.skip(); + return; + } + + // Refresh + await api.refreshPackages(env); + + // Get updated packages + const after = await api.getPackages(env); + + assert.ok(Array.isArray(after), 'Should return array after refresh'); + + // Package counts should be similar (no external changes during test) + // Allow some variance for cache effects + }); + + /** + * Test: Standard library packages typically present + * + * Most Python environments should have pip installed. + */ + test('Common packages are discoverable', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // Find a virtual environment (more likely to have pip) + let targetEnv = environments.find( + (env) => + env.displayName.includes('venv') || + env.displayName.includes('.venv') || + env.envId.managerId.includes('venv'), + ); + + if (!targetEnv) { + // Fall back to first environment + targetEnv = environments[0]; + } + + const packages = await api.getPackages(targetEnv); + + if (!packages || packages.length === 0) { + console.log('No packages found in:', targetEnv.displayName); + this.skip(); + return; + } + + // Look for common packages + const pipInstalled = packages.some((p) => p.name.toLowerCase() === 'pip'); + const setuptoolsInstalled = packages.some((p) => p.name.toLowerCase() === 'setuptools'); + + console.log(`pip installed: ${pipInstalled}, setuptools installed: ${setuptoolsInstalled}`); + console.log(`Total packages: ${packages.length}`); + }); + + /** + * Test: Different environments can have different packages + * + * Package lists should be environment-specific. + */ + test('Package lists are environment-specific', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length < 2) { + this.skip(); + return; + } + + const env1 = environments[0]; + const env2 = environments[1]; + + const packages1 = await api.getPackages(env1); + const packages2 = await api.getPackages(env2); + + // Both should return valid results (or undefined for same reason) + if (packages1 === undefined || packages2 === undefined) { + console.log('Package manager not available for one or both environments'); + return; + } + + assert.ok(Array.isArray(packages1), 'Env1 packages should be array'); + assert.ok(Array.isArray(packages2), 'Env2 packages should be array'); + + console.log(`Env1 (${env1.displayName}): ${packages1.length} packages`); + console.log(`Env2 (${env2.displayName}): ${packages2.length} packages`); + }); + + /** + * Test: Package install and uninstall flow + * + * This test installs and uninstalls a small test package. + * Uses 'cowsay' as it's small and has no dependencies. + */ + test('Package install and uninstall works', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // Find a virtual environment we can safely modify + const targetEnv = environments.find( + (env) => + (env.displayName.includes('venv') || env.displayName.includes('.venv')) && + env.envId.managerId.includes('venv'), + ); + + if (!targetEnv) { + console.log('No modifiable virtual environment found'); + this.skip(); + return; + } + + const testPackage = 'cowsay'; + + try { + // Check if already installed + const initialPackages = await api.getPackages(targetEnv); + if (!initialPackages) { + this.skip(); + return; + } + + const wasInstalled = initialPackages.some((p) => p.name.toLowerCase() === testPackage); + + if (wasInstalled) { + // Uninstall first + await api.managePackages(targetEnv, { uninstall: [testPackage] }); + await sleep(2000); + } + + // Install package + await api.managePackages(targetEnv, { install: [testPackage] }); + + // Refresh and verify + await api.refreshPackages(targetEnv); + const afterInstall = await api.getPackages(targetEnv); + + const isNowInstalled = afterInstall?.some((p) => p.name.toLowerCase() === testPackage); + assert.ok(isNowInstalled, `${testPackage} should be installed`); + + // Uninstall + await api.managePackages(targetEnv, { uninstall: [testPackage] }); + + // Refresh and verify + await api.refreshPackages(targetEnv); + const afterUninstall = await api.getPackages(targetEnv); + + const isStillInstalled = afterUninstall?.some((p) => p.name.toLowerCase() === testPackage); + assert.ok(!isStillInstalled, `${testPackage} should be uninstalled`); + } catch (e) { + console.log('Package management test error:', (e as Error).message); + // Don't fail - environment may not support package management + } + }); + + /** + * Test: onDidChangePackages event fires + * + * When packages change, the event should fire. + */ + test('onDidChangePackages event is available', async function () { + assert.ok(api.onDidChangePackages, 'onDidChangePackages should be available'); + + // Verify it's subscribable + const handler = new TestEventHandler( + api.onDidChangePackages, + 'onDidChangePackages', + ); + + // Just verify we can subscribe without error + handler.dispose(); + }); + + /** + * Test: createPackageItem creates valid package + * + * The createPackageItem API should create properly structured packages. + */ + test('createPackageItem creates valid structure', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // This test verifies the API exists and is callable + // Full testing requires a registered package manager + assert.ok(typeof api.createPackageItem === 'function', 'createPackageItem should be a function'); + }); + + /** + * Test: Invalid environment returns undefined packages + * + * For an environment without a package manager, should return undefined. + */ + test('Missing package manager returns undefined', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // System Python or unusual environments may not have package managers + for (const env of environments) { + const packages = await api.getPackages(env); + // Result should be either array or undefined, never throw + if (packages !== undefined) { + assert.ok(Array.isArray(packages), 'Should be array if defined'); + } + } + }); +}); diff --git a/src/test/integration/pythonProjects.integration.test.ts b/src/test/integration/pythonProjects.integration.test.ts new file mode 100644 index 00000000..9b1f696c --- /dev/null +++ b/src/test/integration/pythonProjects.integration.test.ts @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Python Projects + * + * PURPOSE: + * Verify that Python project management works correctly - adding projects, + * assigning environments, and persisting settings. + * + * WHAT THIS TESTS: + * 1. Adding projects via API + * 2. Removing projects via API + * 3. Project-environment associations + * 4. Events fire when projects change + * 5. Workspace folders are treated as default projects + * + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { TestEventHandler, waitForCondition } from '../testUtils'; + +suite('Integration: Python Projects', function () { + this.timeout(60_000); + + let api: PythonEnvironmentApi; + + suiteSetup(async function () { + this.timeout(30_000); + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + assert.ok(typeof api.getPythonProjects === 'function', 'getPythonProjects method not available'); + }); + + /** + * Test: Workspace folders are default projects + * + * When a workspace is open, the workspace folder(s) should be + * automatically treated as Python projects. + */ + test('Workspace folders appear as default projects', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const projects = api.getPythonProjects(); + assert.ok(Array.isArray(projects), 'getPythonProjects should return array'); + + // Each workspace folder should be a project + for (const folder of workspaceFolders) { + const found = projects.some( + (p) => p.uri.fsPath === folder.uri.fsPath || p.uri.toString() === folder.uri.toString(), + ); + assert.ok(found, `Workspace folder ${folder.name} should be a project`); + } + }); + + /** + * Test: getPythonProject returns correct project for URI + * + * Given a URI within a project, getPythonProject should return + * the containing project. + */ + test('getPythonProject returns project for URI', async function () { + const projects = api.getPythonProjects(); + + if (projects.length === 0) { + this.skip(); + return; + } + + const project = projects[0]; + const foundProject = api.getPythonProject(project.uri); + + assert.ok(foundProject, 'Should find project by its URI'); + assert.strictEqual(foundProject.uri.toString(), project.uri.toString(), 'Found project should match original'); + }); + + /** + * Test: getPythonProject returns undefined for unknown URI + * + * Querying a path that's not within any project should return undefined. + */ + test('getPythonProject returns undefined for unknown URI', async function () { + // Use a path that definitely won't be a project + const unknownUri = vscode.Uri.file('/nonexistent/path/that/wont/exist'); + const project = api.getPythonProject(unknownUri); + + assert.strictEqual(project, undefined, 'Should return undefined for unknown path'); + }); + + /** + * Test: Projects have required structure + * + * Each project should have the minimum required properties. + */ + test('Projects have valid structure', async function () { + const projects = api.getPythonProjects(); + + if (projects.length === 0) { + this.skip(); + return; + } + + for (const project of projects) { + assert.ok(typeof project.name === 'string', 'Project must have name'); + assert.ok(project.name.length > 0, 'Project name should not be empty'); + assert.ok(project.uri, 'Project must have URI'); + assert.ok(project.uri instanceof vscode.Uri, 'Project URI must be a Uri'); + } + }); + + /** + * Test: Environment can be set and retrieved for project + * + * After setting an environment for a project, getEnvironment should + * return that environment. + */ + test('setEnvironment and getEnvironment work for project', async function () { + const projects = api.getPythonProjects(); + + if (projects.length === 0) { + this.skip(); + return; + } + + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const project = projects[0]; + const env = environments[0]; + + // Set environment for project + await api.setEnvironment(project.uri, env); + + // Get environment and verify + const retrievedEnv = await api.getEnvironment(project.uri); + + assert.ok(retrievedEnv, 'Should get environment after setting'); + assert.strictEqual(retrievedEnv.envId.id, env.envId.id, 'Retrieved environment should match set environment'); + }); + + /** + * Test: onDidChangeEnvironment fires when project environment changes + * + * Setting an environment for a project should fire the change event. + */ + test('onDidChangeEnvironment fires on project environment change', async function () { + const projects = api.getPythonProjects(); + const environments = await api.getEnvironments('all'); + + if (projects.length === 0 || environments.length === 0) { + this.skip(); + return; + } + + const handler = new TestEventHandler(api.onDidChangeEnvironment, 'onDidChangeEnvironment'); + + try { + const project = projects[0]; + const env = environments[0]; + + // Set environment + await api.setEnvironment(project.uri, env); + + // Event should fire + await handler.assertFired(5000); + + const event = handler.last; + assert.ok(event, 'Event should have value'); + assert.ok(event.new, 'Event should have new environment'); + } finally { + handler.dispose(); + } + }); + + /** + * Test: Environment can be unset for project + * + * Setting undefined as environment should clear the association. + */ + test('setEnvironment with undefined clears association', async function () { + const projects = api.getPythonProjects(); + const environments = await api.getEnvironments('all'); + + if (projects.length === 0 || environments.length === 0) { + this.skip(); + return; + } + + const project = projects[0]; + const env = environments[0]; + + // Set environment first + await api.setEnvironment(project.uri, env); + + // Clear environment + await api.setEnvironment(project.uri, undefined); + + // Note: getEnvironment may still return an auto-selected environment + // based on discovery. The key test is that no error occurs. + }); + + /** + * Test: Multiple projects can have different environments + * + * In a multi-project workspace, each project can have its own environment. + */ + test('Different projects can have different environments', async function () { + const projects = api.getPythonProjects(); + const environments = await api.getEnvironments('all'); + + if (projects.length < 2 || environments.length < 2) { + this.skip(); + return; + } + + const project1 = projects[0]; + const project2 = projects[1]; + const env1 = environments[0]; + const env2 = environments[1]; + + // Set different environments for different projects + await api.setEnvironment(project1.uri, env1); + await api.setEnvironment(project2.uri, env2); + + // Verify each project has its assigned environment + const retrieved1 = await api.getEnvironment(project1.uri); + const retrieved2 = await api.getEnvironment(project2.uri); + + assert.ok(retrieved1, 'Project 1 should have environment'); + assert.ok(retrieved2, 'Project 2 should have environment'); + assert.strictEqual(retrieved1.envId.id, env1.envId.id, 'Project 1 should have env1'); + assert.strictEqual(retrieved2.envId.id, env2.envId.id, 'Project 2 should have env2'); + }); + + /** + * Test: File within project resolves to project environment + * + * A file path inside a project should resolve to that project's environment. + */ + test('File in project uses project environment', async function () { + const projects = api.getPythonProjects(); + const environments = await api.getEnvironments('all'); + + if (projects.length === 0 || environments.length === 0) { + this.skip(); + return; + } + + const project = projects[0]; + const env = environments[0]; + + // Set environment for project + await api.setEnvironment(project.uri, env); + + // Create a hypothetical file path inside the project + const fileUri = vscode.Uri.joinPath(project.uri, 'some_script.py'); + + // Get environment for the file + const fileEnv = await api.getEnvironment(fileUri); + + // Should inherit project's environment + assert.ok(fileEnv, 'File should get environment from project'); + assert.strictEqual(fileEnv.envId.id, env.envId.id, 'File should use project environment'); + }); +}); diff --git a/src/test/integration/settingsBehavior.integration.test.ts b/src/test/integration/settingsBehavior.integration.test.ts new file mode 100644 index 00000000..9e8906d3 --- /dev/null +++ b/src/test/integration/settingsBehavior.integration.test.ts @@ -0,0 +1,319 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Settings Behavior + * + * PURPOSE: + * Verify that settings are read and written correctly, and that + * the extension respects VS Code's settings hierarchy. + * + * WHAT THIS TESTS: + * 1. Opening workspace doesn't pollute settings + * 2. Manual selection writes to settings + * 3. Settings scope is respected + * 4. Environment variables API works + * + * NOTE: These tests interact with VS Code settings. + * Care should be taken to restore original settings after tests. + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { waitForCondition } from '../testUtils'; + +suite('Integration: Settings Behavior', function () { + this.timeout(60_000); + + let api: PythonEnvironmentApi; + + suiteSetup(async function () { + this.timeout(30_000); + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + }); + + /** + * Test: Extension settings are accessible + * + * The python-envs configuration section should be accessible. + */ + test('Extension settings section is accessible', async function () { + const config = vscode.workspace.getConfiguration('python-envs'); + + assert.ok(config, 'python-envs configuration should be accessible'); + + // Check some expected settings exist + const defaultEnvManager = config.get('defaultEnvManager'); + const defaultPackageManager = config.get('defaultPackageManager'); + + // Settings should have values (may be defaults) + console.log('defaultEnvManager:', defaultEnvManager); + console.log('defaultPackageManager:', defaultPackageManager); + }); + + /** + * Test: workspaceSearchPaths setting is accessible + * + * The search paths setting should be readable. + */ + test('workspaceSearchPaths setting is readable', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const config = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); + const searchPaths = config.get('workspaceSearchPaths'); + + // Default should be ["./**/.venv"] + assert.ok( + Array.isArray(searchPaths) || searchPaths === undefined, + 'workspaceSearchPaths should be array or undefined', + ); + + if (searchPaths) { + console.log('workspaceSearchPaths:', searchPaths); + } + }); + + /** + * Test: globalSearchPaths setting is accessible + * + * The global search paths setting should be readable. + */ + test('globalSearchPaths setting is readable', async function () { + const config = vscode.workspace.getConfiguration('python-envs'); + const globalPaths = config.get('globalSearchPaths'); + + assert.ok( + Array.isArray(globalPaths) || globalPaths === undefined, + 'globalSearchPaths should be array or undefined', + ); + + if (globalPaths) { + console.log('globalSearchPaths:', globalPaths); + } + }); + + /** + * Test: pythonProjects setting structure + * + * The pythonProjects setting should have the correct structure. + */ + test('pythonProjects setting has correct structure', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const config = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); + const projects = + config.get>('pythonProjects'); + + if (projects && projects.length > 0) { + for (const project of projects) { + assert.ok(typeof project.path === 'string', 'Project should have path'); + } + } + + console.log('pythonProjects:', JSON.stringify(projects, null, 2)); + }); + + /** + * Test: Settings inspection shows scope + * + * Using inspect() should show which scope a setting comes from. + */ + test('Settings inspection shows scope information', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const config = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); + const inspection = config.inspect('defaultEnvManager'); + + assert.ok(inspection, 'Inspection should return result'); + assert.ok( + 'defaultValue' in inspection || 'globalValue' in inspection, + 'Inspection should have value properties', + ); + + console.log('defaultEnvManager inspection:', JSON.stringify(inspection, null, 2)); + }); + + /** + * Test: Environment variables API is available + * + * The getEnvironmentVariables API should be callable. + */ + test('getEnvironmentVariables API is available', async function () { + assert.ok(typeof api.getEnvironmentVariables === 'function', 'getEnvironmentVariables should be a function'); + assert.ok(api.onDidChangeEnvironmentVariables, 'onDidChangeEnvironmentVariables should be available'); + }); + + /** + * Test: getEnvironmentVariables returns object + * + * The method should return an environment variables object. + */ + test('getEnvironmentVariables returns variables', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const envVars = await api.getEnvironmentVariables(workspaceFolders[0].uri); + + assert.ok(typeof envVars === 'object', 'Should return object'); + + // Should have some common environment variables + const hasPath = 'PATH' in envVars || 'Path' in envVars; + console.log('Has PATH:', hasPath); + }); + + /** + * Test: getEnvironmentVariables with overrides + * + * Passing overrides should merge them into the result. + */ + test('getEnvironmentVariables applies overrides', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const testVar = 'TEST_INTEGRATION_VAR'; + const testValue = 'test_value_12345'; + + const envVars = await api.getEnvironmentVariables(workspaceFolders[0].uri, [{ [testVar]: testValue }]); + + assert.strictEqual(envVars[testVar], testValue, 'Override should be applied'); + }); + + /** + * Test: getEnvironmentVariables with undefined uri + * + * Calling with undefined uri should return global environment. + */ + test('getEnvironmentVariables works with undefined uri', async function () { + const envVars = await api.getEnvironmentVariables(undefined as unknown as vscode.Uri); + + assert.ok(typeof envVars === 'object', 'Should return object for undefined uri'); + }); + + /** + * Test: Terminal settings are accessible + * + * Terminal-specific settings should be accessible. + */ + test('Terminal settings are accessible', async function () { + const config = vscode.workspace.getConfiguration('python-envs'); + + const activationType = config.get('terminal.autoActivationType'); + const showButton = config.get('terminal.showActivateButton'); + + console.log('terminal.autoActivationType:', activationType); + console.log('terminal.showActivateButton:', showButton); + }); + + /** + * Test: alwaysUseUv setting is accessible + * + * The uv preference setting should be readable. + */ + test('alwaysUseUv setting is accessible', async function () { + const config = vscode.workspace.getConfiguration('python-envs'); + const alwaysUseUv = config.get('alwaysUseUv'); + + // Default is true according to docs + console.log('alwaysUseUv:', alwaysUseUv); + }); + + /** + * Test: Legacy python settings are accessible + * + * Legacy python.* settings should be readable for migration. + */ + test('Legacy python settings are accessible', async function () { + const pythonConfig = vscode.workspace.getConfiguration('python'); + + // These are legacy settings that may have values + const venvPath = pythonConfig.get('venvPath'); + const venvFolders = pythonConfig.get('venvFolders'); + const defaultInterpreterPath = pythonConfig.get('defaultInterpreterPath'); + const condaPath = pythonConfig.get('condaPath'); + + console.log('Legacy settings:'); + console.log(' venvPath:', venvPath); + console.log(' venvFolders:', venvFolders); + console.log(' defaultInterpreterPath:', defaultInterpreterPath); + console.log(' condaPath:', condaPath); + }); + + /** + * Test: Settings update is reflected in API + * + * When settings change, the API should reflect the changes. + * Note: This test doesn't actually modify settings to avoid side effects. + */ + test('Settings and API are connected', async function () { + // Get current environment + const currentEnv = await api.getEnvironment(undefined); + + // Get current settings + const config = vscode.workspace.getConfiguration('python-envs'); + const currentProjects = config.get('pythonProjects'); + + // Just verify we can read both without error + console.log('Current env:', currentEnv?.displayName ?? 'none'); + console.log('Projects setting exists:', currentProjects !== undefined); + }); + + /** + * Test: Workspace folder scope is respected + * + * Settings at workspace folder level should be isolated. + */ + test('Workspace folder settings scope is respected', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length < 2) { + // Need at least 2 folders to test isolation + this.skip(); + return; + } + + const config1 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); + const config2 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[1].uri); + + // Both should be independently configurable + const inspect1 = config1.inspect('pythonProjects'); + const inspect2 = config2.inspect('pythonProjects'); + + console.log('Folder 1 pythonProjects:', inspect1?.workspaceFolderValue); + console.log('Folder 2 pythonProjects:', inspect2?.workspaceFolderValue); + }); +}); diff --git a/src/test/integration/terminalActivation.integration.test.ts b/src/test/integration/terminalActivation.integration.test.ts new file mode 100644 index 00000000..51394595 --- /dev/null +++ b/src/test/integration/terminalActivation.integration.test.ts @@ -0,0 +1,356 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Terminal Activation + * + * PURPOSE: + * Verify that terminal creation and activation work correctly + * with different environments and settings. + * + * WHAT THIS TESTS: + * 1. Terminal creation API + * 2. Terminal runs with correct environment + * 3. runInTerminal executes commands + * 4. runInDedicatedTerminal uses consistent terminal + * + * NOTE: Terminal tests interact with real VS Code terminals. + * Tests should be careful about cleanup. + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { PythonEnvironmentApi, PythonTerminalCreateOptions, PythonTerminalExecutionOptions } from '../../api'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { waitForCondition } from '../testUtils'; + +suite('Integration: Terminal Activation', function () { + this.timeout(60_000); + + let api: PythonEnvironmentApi; + const createdTerminals: vscode.Terminal[] = []; + + suiteSetup(async function () { + this.timeout(30_000); + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + }); + + suiteTeardown(async function () { + // Clean up created terminals + for (const terminal of createdTerminals) { + terminal.dispose(); + } + createdTerminals.length = 0; + }); + + /** + * Test: Terminal APIs are available + * + * The API should have all terminal-related methods. + */ + test('Terminal APIs are available', async function () { + assert.ok(typeof api.createTerminal === 'function', 'createTerminal should be a function'); + assert.ok(typeof api.runInTerminal === 'function', 'runInTerminal should be a function'); + assert.ok(typeof api.runInDedicatedTerminal === 'function', 'runInDedicatedTerminal should be a function'); + assert.ok(typeof api.runAsTask === 'function', 'runAsTask should be a function'); + assert.ok(typeof api.runInBackground === 'function', 'runInBackground should be a function'); + }); + + /** + * Test: createTerminal creates a terminal + * + * The createTerminal method should create a new VS Code terminal. + */ + test('createTerminal creates new terminal', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + const initialTerminalCount = vscode.window.terminals.length; + + const options: PythonTerminalCreateOptions = { + name: 'Test Terminal', + }; + + const terminal = await api.createTerminal(env, options); + createdTerminals.push(terminal); + + assert.ok(terminal, 'createTerminal should return a terminal'); + assert.ok( + terminal.name.includes('Test Terminal') || terminal.name.includes('Python'), + 'Terminal should have appropriate name', + ); + + // Verify terminal was created + assert.ok(vscode.window.terminals.length >= initialTerminalCount, 'Terminal count should increase'); + }); + + /** + * Test: Terminal can be created with custom options + * + * Various terminal options should be respected. + */ + test('createTerminal respects options', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + const options: PythonTerminalCreateOptions = { + name: 'Custom Options Terminal', + hideFromUser: false, + }; + + const terminal = await api.createTerminal(env, options); + createdTerminals.push(terminal); + + assert.ok(terminal, 'Terminal should be created'); + // Name may include environment info, but should contain our custom name + console.log('Created terminal name:', terminal.name); + }); + + /** + * Test: runInTerminal returns terminal + * + * runInTerminal should execute in a terminal and return it. + */ + test('runInTerminal returns terminal', async function () { + const environments = await api.getEnvironments('all'); + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (environments.length === 0 || !workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + const options: PythonTerminalExecutionOptions = { + cwd: workspaceFolders[0].uri, + args: ['--version'], + show: false, + }; + + const terminal = await api.runInTerminal(env, options); + createdTerminals.push(terminal); + + assert.ok(terminal, 'runInTerminal should return terminal'); + assert.ok(terminal instanceof Object, 'Should be a terminal object'); + }); + + /** + * Test: runInDedicatedTerminal reuses terminal + * + * Multiple calls with same key should use same terminal. + */ + test('runInDedicatedTerminal reuses terminal for same key', async function () { + const environments = await api.getEnvironments('all'); + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (environments.length === 0 || !workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + const terminalKey = 'test-dedicated-terminal'; + + const options: PythonTerminalExecutionOptions = { + cwd: workspaceFolders[0].uri, + args: ['--version'], + show: false, + }; + + // First call + const terminal1 = await api.runInDedicatedTerminal(terminalKey, env, options); + createdTerminals.push(terminal1); + + // Second call with same key + const terminal2 = await api.runInDedicatedTerminal(terminalKey, env, options); + + // Should be the same terminal (or at least same name) + assert.ok(terminal1, 'First call should return terminal'); + assert.ok(terminal2, 'Second call should return terminal'); + + // Note: Terminal instances may be different objects but refer to same terminal + console.log('Terminal 1 name:', terminal1.name); + console.log('Terminal 2 name:', terminal2.name); + }); + + /** + * Test: Different keys get different terminals + * + * Different terminal keys should create different terminals. + */ + test('runInDedicatedTerminal uses different terminals for different keys', async function () { + const environments = await api.getEnvironments('all'); + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (environments.length === 0 || !workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + const options: PythonTerminalExecutionOptions = { + cwd: workspaceFolders[0].uri, + args: ['--version'], + show: false, + }; + + const terminal1 = await api.runInDedicatedTerminal('key-1', env, options); + createdTerminals.push(terminal1); + + const terminal2 = await api.runInDedicatedTerminal('key-2', env, options); + if (terminal1 !== terminal2) { + createdTerminals.push(terminal2); + } + + assert.ok(terminal1, 'First terminal should exist'); + assert.ok(terminal2, 'Second terminal should exist'); + }); + + /** + * Test: createTerminal with disableActivation option + * + * When disableActivation is true, environment should not be activated. + */ + test('disableActivation option is accepted', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + const options: PythonTerminalCreateOptions = { + name: 'No Activation Terminal', + disableActivation: true, + }; + + // Should not throw + const terminal = await api.createTerminal(env, options); + createdTerminals.push(terminal); + + assert.ok(terminal, 'Terminal should be created with disableActivation'); + }); + + /** + * Test: runAsTask returns TaskExecution + * + * runAsTask should start a task and return its execution. + */ + test('runAsTask returns task execution', async function () { + const environments = await api.getEnvironments('all'); + const projects = api.getPythonProjects(); + + if (environments.length === 0 || projects.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + try { + const execution = await api.runAsTask(env, { + name: 'Test Python Task', + args: ['--version'], + project: projects[0], + }); + + assert.ok(execution, 'runAsTask should return execution'); + assert.ok(execution.task, 'Execution should have task'); + + // Clean up - terminate the task + vscode.tasks.taskExecutions.forEach((t) => { + if (t === execution) { + t.terminate(); + } + }); + } catch (e) { + // Tasks may fail in test environment + console.log('runAsTask error:', (e as Error).message); + } + }); + + /** + * Test: runInBackground returns PythonProcess + * + * runInBackground should spawn a background process. + */ + test('runInBackground returns process', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // Find an environment with Python executable + let targetEnv = environments[0]; + + try { + const process = await api.runInBackground(targetEnv, { + args: ['--version'], + }); + + assert.ok(process, 'runInBackground should return process'); + assert.ok(process.stdout, 'Process should have stdout'); + assert.ok(process.stderr, 'Process should have stderr'); + assert.ok(typeof process.kill === 'function', 'Process should have kill method'); + + // Clean up + process.kill(); + } catch (e) { + // Background process may fail if Python not properly configured + console.log('runInBackground error:', (e as Error).message); + } + }); + + /** + * Test: Terminal uses correct environment + * + * Commands in terminal should use the specified Python environment. + */ + test('Terminal uses specified environment', async function () { + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + + const terminal = await api.createTerminal(env, { + name: 'Env Check Terminal', + }); + createdTerminals.push(terminal); + + // Terminal should be configured with the environment + // We can verify the terminal was created without error + assert.ok(terminal, 'Terminal should be created for environment'); + console.log('Created terminal for:', env.displayName); + }); +}); From 74708129763c892fd7305774cf518fa2c2e53908 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:36:11 -0800 Subject: [PATCH 2/7] verification 1 --- .../envCreation.integration.test.ts | 70 +++++++++---- .../envDiscovery.integration.test.ts | 31 ++++-- .../packageManagement.integration.test.ts | 58 +++++------ .../pythonProjects.integration.test.ts | 25 ++++- .../settingsBehavior.integration.test.ts | 98 ++++++++++++++++--- .../terminalActivation.integration.test.ts | 68 ++++++------- 6 files changed, 231 insertions(+), 119 deletions(-) diff --git a/src/test/integration/envCreation.integration.test.ts b/src/test/integration/envCreation.integration.test.ts index cf36d5c5..d6891ba2 100644 --- a/src/test/integration/envCreation.integration.test.ts +++ b/src/test/integration/envCreation.integration.test.ts @@ -66,6 +66,7 @@ suite('Integration: Environment Creation', function () { * Test: Managers that support creation are available * * At least one environment manager (venv or conda) should support creation. + * This test verifies that global Python installations are discoverable. */ test('At least one manager supports environment creation', async function () { // Get all environments to force managers to load @@ -74,10 +75,20 @@ suite('Integration: Environment Creation', function () { // Check if we have global Python installations that can create venvs const globalEnvs = await api.getEnvironments('global'); - // If we have global Python installations, venv creation should be possible - if (globalEnvs.length > 0) { - console.log(`Found ${globalEnvs.length} global Python installations for venv creation`); + // Assert we have at least one global Python that can serve as base for venv creation + assert.ok( + globalEnvs.length > 0, + 'At least one global Python installation should be available for environment creation. ' + + 'If this fails, ensure Python is installed and discoverable on this system.', + ); + + // Verify the global environments have required properties for creation + for (const env of globalEnvs) { + assert.ok(env.envId, 'Global environment must have envId'); + assert.ok(env.environmentPath, 'Global environment must have environmentPath'); } + + console.log(`Found ${globalEnvs.length} global Python installations for venv creation`); }); /** @@ -206,23 +217,31 @@ suite('Integration: Environment Creation', function () { * Note: This may require special permissions or configurations. */ test('Global scope creation is handled', async function () { - // This test verifies the API handles global scope without crashing - // Actual creation may require specific configurations + // This test verifies the API handles global scope correctly + let createdEnv: PythonEnvironment | undefined; try { // Attempt global creation - this may prompt for user input - // so we use quickCreate and expect it might fail - const result = await api.createEnvironment('global', { quickCreate: true }); + // so we use quickCreate and expect it might return undefined + createdEnv = await api.createEnvironment('global', { quickCreate: true }); - // If it succeeds, clean up - if (result) { - await api.removeEnvironment(result); + if (createdEnv) { + // If creation succeeded, verify the environment has valid structure + assert.ok(createdEnv.envId, 'Created global env must have envId'); + assert.ok(createdEnv.envId.id, 'Created global env must have envId.id'); + assert.ok(createdEnv.environmentPath, 'Created global env must have environmentPath'); + } else { + // quickCreate returned undefined - this is acceptable behavior + // (e.g., no manager configured for global creation) + console.log('Global creation returned undefined (quickCreate not fully supported)'); + } + } finally { + // Cleanup: always try to remove if created + if (createdEnv) { + await api.removeEnvironment(createdEnv); } - } catch (e) { - // Global creation not supported or no manager available - // This is acceptable - the test verifies the API doesn't crash - console.log('Global creation not available:', (e as Error).message); } + // Test passes if we got here without throwing - API handled scope correctly }); /** @@ -239,18 +258,27 @@ suite('Integration: Environment Creation', function () { } const uris = workspaceFolders.map((f) => f.uri); + let createdEnv: PythonEnvironment | undefined; try { - // This may prompt for manager selection - const result = await api.createEnvironment(uris, { quickCreate: true }); + // This may prompt for manager selection - quickCreate should handle it + createdEnv = await api.createEnvironment(uris, { quickCreate: true }); - if (result) { - await api.removeEnvironment(result); + if (createdEnv) { + // Verify created environment has valid structure + assert.ok(createdEnv.envId, 'Multi-URI created env must have envId'); + assert.ok(createdEnv.environmentPath, 'Multi-URI created env must have environmentPath'); + } else { + // quickCreate returned undefined - acceptable for multi-URI scenario + console.log('Multi-URI creation returned undefined (quickCreate not fully supported)'); + } + } finally { + // Cleanup: always try to remove if created + if (createdEnv) { + await api.removeEnvironment(createdEnv); } - } catch (e) { - // Multi-URI creation may not be fully supported in all configurations - console.log('Multi-URI creation result:', (e as Error).message); } + // Test passes if we got here without throwing - API handled multi-URI correctly }); /** diff --git a/src/test/integration/envDiscovery.integration.test.ts b/src/test/integration/envDiscovery.integration.test.ts index 0e0ab3f2..1afb65de 100644 --- a/src/test/integration/envDiscovery.integration.test.ts +++ b/src/test/integration/envDiscovery.integration.test.ts @@ -99,19 +99,30 @@ suite('Integration: Environment Discovery', function () { // Reset any previous events handler.reset(); - // Trigger refresh + // First, check if we have environments on this system + const preCheckEnvs = await api.getEnvironments('all'); + + if (preCheckEnvs.length === 0) { + // No environments discovered - can't test events + console.log('No environments available to test event firing'); + this.skip(); + return; + } + + // Trigger refresh - this should fire events for discovered environments await api.refreshEnvironments(undefined); - // Give time for events to propagate - // Note: Events may not fire if no environments change - // This test verifies the event mechanism works when changes occur - const environments = await api.getEnvironments('all'); + // Wait for events to propagate (discovery is async) + await handler.assertFiredAtLeast(1, 10_000); - if (environments.length > 0) { - // If we have environments, we should have received events during discovery - // (unless this is a repeat run with cached data) - console.log(`Event fired ${handler.count} times during refresh`); - } + // Verify event has valid structure + const event = handler.first; + assert.ok(event, 'Event should have a value'); + + // The event should have either added or removed environments + assert.ok('added' in event || 'removed' in event, 'Event should have added or removed property'); + + console.log(`Event fired ${handler.count} times during refresh`); } finally { handler.dispose(); } diff --git a/src/test/integration/packageManagement.integration.test.ts b/src/test/integration/packageManagement.integration.test.ts index cd10793c..228dc5a0 100644 --- a/src/test/integration/packageManagement.integration.test.ts +++ b/src/test/integration/packageManagement.integration.test.ts @@ -261,45 +261,41 @@ suite('Integration: Package Management', function () { const testPackage = 'cowsay'; - try { - // Check if already installed - const initialPackages = await api.getPackages(targetEnv); - if (!initialPackages) { - this.skip(); - return; - } + // Check if already installed + const initialPackages = await api.getPackages(targetEnv); + if (!initialPackages) { + console.log('Package manager not available for this environment'); + this.skip(); + return; + } - const wasInstalled = initialPackages.some((p) => p.name.toLowerCase() === testPackage); + const wasInstalled = initialPackages.some((p) => p.name.toLowerCase() === testPackage); - if (wasInstalled) { - // Uninstall first - await api.managePackages(targetEnv, { uninstall: [testPackage] }); - await sleep(2000); - } + if (wasInstalled) { + // Uninstall first + await api.managePackages(targetEnv, { uninstall: [testPackage] }); + await sleep(2000); + } - // Install package - await api.managePackages(targetEnv, { install: [testPackage] }); + // Install package + await api.managePackages(targetEnv, { install: [testPackage] }); - // Refresh and verify - await api.refreshPackages(targetEnv); - const afterInstall = await api.getPackages(targetEnv); + // Refresh and verify + await api.refreshPackages(targetEnv); + const afterInstall = await api.getPackages(targetEnv); - const isNowInstalled = afterInstall?.some((p) => p.name.toLowerCase() === testPackage); - assert.ok(isNowInstalled, `${testPackage} should be installed`); + const isNowInstalled = afterInstall?.some((p) => p.name.toLowerCase() === testPackage); + assert.ok(isNowInstalled, `${testPackage} should be installed after managePackages install`); - // Uninstall - await api.managePackages(targetEnv, { uninstall: [testPackage] }); + // Uninstall + await api.managePackages(targetEnv, { uninstall: [testPackage] }); - // Refresh and verify - await api.refreshPackages(targetEnv); - const afterUninstall = await api.getPackages(targetEnv); + // Refresh and verify + await api.refreshPackages(targetEnv); + const afterUninstall = await api.getPackages(targetEnv); - const isStillInstalled = afterUninstall?.some((p) => p.name.toLowerCase() === testPackage); - assert.ok(!isStillInstalled, `${testPackage} should be uninstalled`); - } catch (e) { - console.log('Package management test error:', (e as Error).message); - // Don't fail - environment may not support package management - } + const isStillInstalled = afterUninstall?.some((p) => p.name.toLowerCase() === testPackage); + assert.ok(!isStillInstalled, `${testPackage} should be uninstalled after managePackages uninstall`); }); /** diff --git a/src/test/integration/pythonProjects.integration.test.ts b/src/test/integration/pythonProjects.integration.test.ts index 9b1f696c..91b28793 100644 --- a/src/test/integration/pythonProjects.integration.test.ts +++ b/src/test/integration/pythonProjects.integration.test.ts @@ -196,7 +196,8 @@ suite('Integration: Python Projects', function () { /** * Test: Environment can be unset for project * - * Setting undefined as environment should clear the association. + * Setting undefined as environment should clear the explicit association. + * After clearing, getEnvironment may return auto-discovered env or undefined. */ test('setEnvironment with undefined clears association', async function () { const projects = api.getPythonProjects(); @@ -213,11 +214,29 @@ suite('Integration: Python Projects', function () { // Set environment first await api.setEnvironment(project.uri, env); + // Verify it was set + const beforeClear = await api.getEnvironment(project.uri); + assert.ok(beforeClear, 'Environment should be set before clearing'); + assert.strictEqual(beforeClear.envId.id, env.envId.id, 'Should have the explicitly set environment'); + // Clear environment await api.setEnvironment(project.uri, undefined); - // Note: getEnvironment may still return an auto-selected environment - // based on discovery. The key test is that no error occurs. + // After clearing, if there's still an environment, it should either be: + // 1. undefined (no auto-discovery) + // 2. Different from the explicitly set one (auto-discovered fallback) + // 3. Same as before if it happens to be auto-discovered too (edge case) + const afterClear = await api.getEnvironment(project.uri); + + // The key assertion: the operation completed without error + // and the API behaves consistently (returns env or undefined) + if (afterClear) { + assert.ok(afterClear.envId, 'If environment returned, it must have valid envId'); + assert.ok(afterClear.envId.id, 'If environment returned, envId must have id'); + } + // If undefined, that's also valid - explicit selection was cleared + + console.log('After clearing:', afterClear?.displayName ?? 'undefined (no auto-discovery)'); }); /** diff --git a/src/test/integration/settingsBehavior.integration.test.ts b/src/test/integration/settingsBehavior.integration.test.ts index 9e8906d3..0fbac48d 100644 --- a/src/test/integration/settingsBehavior.integration.test.ts +++ b/src/test/integration/settingsBehavior.integration.test.ts @@ -47,18 +47,27 @@ suite('Integration: Settings Behavior', function () { /** * Test: Extension settings are accessible * - * The python-envs configuration section should be accessible. + * The python-envs configuration section should be accessible with expected types. */ test('Extension settings section is accessible', async function () { const config = vscode.workspace.getConfiguration('python-envs'); assert.ok(config, 'python-envs configuration should be accessible'); - // Check some expected settings exist + // Check some expected settings exist and have correct types const defaultEnvManager = config.get('defaultEnvManager'); const defaultPackageManager = config.get('defaultPackageManager'); - // Settings should have values (may be defaults) + // Assert settings have expected types (string or undefined) + assert.ok( + typeof defaultEnvManager === 'string' || defaultEnvManager === undefined, + `defaultEnvManager should be string or undefined, got ${typeof defaultEnvManager}`, + ); + assert.ok( + typeof defaultPackageManager === 'string' || defaultPackageManager === undefined, + `defaultPackageManager should be string or undefined, got ${typeof defaultPackageManager}`, + ); + console.log('defaultEnvManager:', defaultEnvManager); console.log('defaultPackageManager:', defaultPackageManager); }); @@ -227,7 +236,7 @@ suite('Integration: Settings Behavior', function () { /** * Test: Terminal settings are accessible * - * Terminal-specific settings should be accessible. + * Terminal-specific settings should be accessible with expected types. */ test('Terminal settings are accessible', async function () { const config = vscode.workspace.getConfiguration('python-envs'); @@ -235,6 +244,19 @@ suite('Integration: Settings Behavior', function () { const activationType = config.get('terminal.autoActivationType'); const showButton = config.get('terminal.showActivateButton'); + // Assert settings have expected types + // activationType should be string (enum value) or undefined + assert.ok( + typeof activationType === 'string' || activationType === undefined, + `terminal.autoActivationType should be string or undefined, got ${typeof activationType}`, + ); + + // showButton should be boolean or undefined + assert.ok( + typeof showButton === 'boolean' || showButton === undefined, + `terminal.showActivateButton should be boolean or undefined, got ${typeof showButton}`, + ); + console.log('terminal.autoActivationType:', activationType); console.log('terminal.showActivateButton:', showButton); }); @@ -242,20 +264,25 @@ suite('Integration: Settings Behavior', function () { /** * Test: alwaysUseUv setting is accessible * - * The uv preference setting should be readable. + * The uv preference setting should be readable and have expected type. */ test('alwaysUseUv setting is accessible', async function () { const config = vscode.workspace.getConfiguration('python-envs'); const alwaysUseUv = config.get('alwaysUseUv'); - // Default is true according to docs + // alwaysUseUv should be boolean or undefined + assert.ok( + typeof alwaysUseUv === 'boolean' || alwaysUseUv === undefined, + `alwaysUseUv should be boolean or undefined, got ${typeof alwaysUseUv}`, + ); + console.log('alwaysUseUv:', alwaysUseUv); }); /** * Test: Legacy python settings are accessible * - * Legacy python.* settings should be readable for migration. + * Legacy python.* settings should be readable for migration with expected types. */ test('Legacy python settings are accessible', async function () { const pythonConfig = vscode.workspace.getConfiguration('python'); @@ -266,6 +293,24 @@ suite('Integration: Settings Behavior', function () { const defaultInterpreterPath = pythonConfig.get('defaultInterpreterPath'); const condaPath = pythonConfig.get('condaPath'); + // Assert types are as expected + assert.ok( + typeof venvPath === 'string' || venvPath === undefined, + `venvPath should be string or undefined, got ${typeof venvPath}`, + ); + assert.ok( + Array.isArray(venvFolders) || venvFolders === undefined, + `venvFolders should be array or undefined, got ${typeof venvFolders}`, + ); + assert.ok( + typeof defaultInterpreterPath === 'string' || defaultInterpreterPath === undefined, + `defaultInterpreterPath should be string or undefined, got ${typeof defaultInterpreterPath}`, + ); + assert.ok( + typeof condaPath === 'string' || condaPath === undefined, + `condaPath should be string or undefined, got ${typeof condaPath}`, + ); + console.log('Legacy settings:'); console.log(' venvPath:', venvPath); console.log(' venvFolders:', venvFolders); @@ -274,28 +319,39 @@ suite('Integration: Settings Behavior', function () { }); /** - * Test: Settings update is reflected in API + * Test: Settings and API are connected * - * When settings change, the API should reflect the changes. - * Note: This test doesn't actually modify settings to avoid side effects. + * Verify that settings API and environment API both work and return consistent data. */ test('Settings and API are connected', async function () { - // Get current environment + // Get current environment from API const currentEnv = await api.getEnvironment(undefined); // Get current settings const config = vscode.workspace.getConfiguration('python-envs'); - const currentProjects = config.get('pythonProjects'); + const pythonProjects = config.get('pythonProjects'); - // Just verify we can read both without error + // Assert we can read settings + assert.ok( + pythonProjects === undefined || Array.isArray(pythonProjects), + 'pythonProjects should be undefined or array', + ); + + // If we have an environment, verify it has valid structure + if (currentEnv) { + assert.ok(currentEnv.envId, 'Current environment should have envId'); + assert.ok(currentEnv.displayName, 'Current environment should have displayName'); + } + + // This test verifies both APIs are working together without errors console.log('Current env:', currentEnv?.displayName ?? 'none'); - console.log('Projects setting exists:', currentProjects !== undefined); + console.log('Projects setting type:', Array.isArray(pythonProjects) ? 'array' : typeof pythonProjects); }); /** * Test: Workspace folder scope is respected * - * Settings at workspace folder level should be isolated. + * Settings at workspace folder level should be independently inspectable. */ test('Workspace folder settings scope is respected', async function () { const workspaceFolders = vscode.workspace.workspaceFolders; @@ -309,10 +365,20 @@ suite('Integration: Settings Behavior', function () { const config1 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); const config2 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[1].uri); - // Both should be independently configurable + // Both should be independently inspectable const inspect1 = config1.inspect('pythonProjects'); const inspect2 = config2.inspect('pythonProjects'); + // Assert inspect returns valid results for both folders + assert.ok(inspect1, 'Should be able to inspect settings for folder 1'); + assert.ok(inspect2, 'Should be able to inspect settings for folder 2'); + + // Assert the inspection objects have the expected structure + assert.ok('key' in inspect1, 'Inspection should have key property'); + assert.ok('key' in inspect2, 'Inspection should have key property'); + assert.strictEqual(inspect1.key, 'python-envs.pythonProjects', 'Key should be python-envs.pythonProjects'); + assert.strictEqual(inspect2.key, 'python-envs.pythonProjects', 'Key should be python-envs.pythonProjects'); + console.log('Folder 1 pythonProjects:', inspect1?.workspaceFolderValue); console.log('Folder 2 pythonProjects:', inspect2?.workspaceFolderValue); }); diff --git a/src/test/integration/terminalActivation.integration.test.ts b/src/test/integration/terminalActivation.integration.test.ts index 51394595..6f601ead 100644 --- a/src/test/integration/terminalActivation.integration.test.ts +++ b/src/test/integration/terminalActivation.integration.test.ts @@ -272,26 +272,23 @@ suite('Integration: Terminal Activation', function () { const env = environments[0]; - try { - const execution = await api.runAsTask(env, { - name: 'Test Python Task', - args: ['--version'], - project: projects[0], - }); - - assert.ok(execution, 'runAsTask should return execution'); - assert.ok(execution.task, 'Execution should have task'); - - // Clean up - terminate the task - vscode.tasks.taskExecutions.forEach((t) => { - if (t === execution) { - t.terminate(); - } - }); - } catch (e) { - // Tasks may fail in test environment - console.log('runAsTask error:', (e as Error).message); - } + const execution = await api.runAsTask(env, { + name: 'Test Python Task', + args: ['--version'], + project: projects[0], + }); + + assert.ok(execution, 'runAsTask should return execution'); + assert.ok(execution.task, 'Execution should have task'); + assert.ok(execution.task.name, 'Task should have name'); + assert.ok(execution.task.definition, 'Task should have definition'); + + // Clean up - terminate the task + vscode.tasks.taskExecutions.forEach((t) => { + if (t === execution) { + t.terminate(); + } + }); }); /** @@ -308,24 +305,19 @@ suite('Integration: Terminal Activation', function () { } // Find an environment with Python executable - let targetEnv = environments[0]; - - try { - const process = await api.runInBackground(targetEnv, { - args: ['--version'], - }); - - assert.ok(process, 'runInBackground should return process'); - assert.ok(process.stdout, 'Process should have stdout'); - assert.ok(process.stderr, 'Process should have stderr'); - assert.ok(typeof process.kill === 'function', 'Process should have kill method'); - - // Clean up - process.kill(); - } catch (e) { - // Background process may fail if Python not properly configured - console.log('runInBackground error:', (e as Error).message); - } + const targetEnv = environments[0]; + + const process = await api.runInBackground(targetEnv, { + args: ['--version'], + }); + + assert.ok(process, 'runInBackground should return process'); + assert.ok(process.stdout, 'Process should have stdout'); + assert.ok(process.stderr, 'Process should have stderr'); + assert.ok(typeof process.kill === 'function', 'Process should have kill method'); + + // Clean up + process.kill(); }); /** From 92f86fde45e262cae3bb6bece586c1ad6ad16951 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:55:07 -0800 Subject: [PATCH 3/7] v3 --- .../envCreation.integration.test.ts | 12 ++++- .../envDiscovery.integration.test.ts | 15 ++++-- .../interpreterSelection.integration.test.ts | 46 ++++++++++++++++--- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/test/integration/envCreation.integration.test.ts b/src/test/integration/envCreation.integration.test.ts index d6891ba2..0e3fe9ef 100644 --- a/src/test/integration/envCreation.integration.test.ts +++ b/src/test/integration/envCreation.integration.test.ts @@ -236,9 +236,17 @@ suite('Integration: Environment Creation', function () { console.log('Global creation returned undefined (quickCreate not fully supported)'); } } finally { - // Cleanup: always try to remove if created + // Cleanup: try to remove if created, but handle dialog errors in test mode if (createdEnv) { - await api.removeEnvironment(createdEnv); + try { + await api.removeEnvironment(createdEnv); + } catch (e) { + // Ignore dialog errors in test mode - VS Code blocks dialogs + if (!String(e).includes('DialogService')) { + throw e; + } + console.log('Skipping cleanup for global environment (dialog blocked in tests)'); + } } } // Test passes if we got here without throwing - API handled scope correctly diff --git a/src/test/integration/envDiscovery.integration.test.ts b/src/test/integration/envDiscovery.integration.test.ts index 1afb65de..b6af679f 100644 --- a/src/test/integration/envDiscovery.integration.test.ts +++ b/src/test/integration/envDiscovery.integration.test.ts @@ -116,11 +116,16 @@ suite('Integration: Environment Discovery', function () { await handler.assertFiredAtLeast(1, 10_000); // Verify event has valid structure - const event = handler.first; - assert.ok(event, 'Event should have a value'); - - // The event should have either added or removed environments - assert.ok('added' in event || 'removed' in event, 'Event should have added or removed property'); + // DidChangeEnvironmentsEventArgs is an array of {kind, environment} + const events = handler.first; + assert.ok(events, 'Event should have a value'); + assert.ok(Array.isArray(events), 'Event should be an array'); + assert.ok(events.length > 0, 'Event array should not be empty'); + + // Each event item should have kind and environment properties + const firstItem = events[0]; + assert.ok('kind' in firstItem, 'Event item should have kind property'); + assert.ok('environment' in firstItem, 'Event item should have environment property'); console.log(`Event fired ${handler.count} times during refresh`); } finally { diff --git a/src/test/integration/interpreterSelection.integration.test.ts b/src/test/integration/interpreterSelection.integration.test.ts index 1dbb6886..b4b8e1fb 100644 --- a/src/test/integration/interpreterSelection.integration.test.ts +++ b/src/test/integration/interpreterSelection.integration.test.ts @@ -31,6 +31,7 @@ suite('Integration: Interpreter Selection Priority', function () { this.timeout(60_000); let api: PythonEnvironmentApi; + let originalEnv: import('../../api').PythonEnvironment | undefined; suiteSetup(async function () { this.timeout(30_000); @@ -45,6 +46,22 @@ suite('Integration: Interpreter Selection Priority', function () { api = extension.exports as PythonEnvironmentApi; assert.ok(api, 'API not available'); + + // Save original state for restoration + originalEnv = await api.getEnvironment(undefined); + }); + + // Reset to original state after each test to prevent state pollution + teardown(async function () { + try { + if (originalEnv) { + await api.setEnvironment(undefined, originalEnv); + } else { + await api.setEnvironment(undefined, undefined); + } + } catch { + // Ignore errors during reset + } }); /** @@ -250,10 +267,20 @@ suite('Integration: Interpreter Selection Priority', function () { // Wait a bit and check - should not fire (or fire with same old/new) await new Promise((resolve) => setTimeout(resolve, 1000)); - // Either no event fired, or event had old === new + // Either no event fired, or event fired (with valid structure) + // The exact behavior depends on implementation details if (handler.fired) { - // If event fired, it should be with same env - console.log('Event fired for same env selection:', handler.last?.new?.displayName); + const event = handler.last; + assert.ok(event, 'Event should have value if fired'); + assert.ok(event.new, 'Event should have new environment'); + console.log( + 'Event fired for same env selection:', + `old=${event.old?.envId.id}, new=${event.new.envId.id}`, + ); + // Note: old and new may differ if there was intermediate state from teardown + } else { + // No event fired - this is the ideal idempotent behavior + console.log('No event fired for same env selection (idempotent behavior)'); } } finally { handler.dispose(); @@ -304,6 +331,8 @@ suite('Integration: Interpreter Selection Priority', function () { // Set an explicit environment await api.setEnvironment(undefined, environments[0]); + const beforeClear = await api.getEnvironment(undefined); + assert.ok(beforeClear, 'Should have environment before clearing'); // Clear the selection await api.setEnvironment(undefined, undefined); @@ -311,8 +340,13 @@ suite('Integration: Interpreter Selection Priority', function () { // Get environment - may return auto-discovered env or undefined const autoEnv = await api.getEnvironment(undefined); - // This test verifies the operation completes without error - // The result depends on available environments and settings - console.log('After clearing selection:', autoEnv?.displayName ?? 'none'); + // Key assertion: clearing should have changed something or returned undefined + if (autoEnv) { + // If an environment is returned, it should have valid structure + assert.ok(autoEnv.envId, 'Auto-discovered env must have envId'); + assert.ok(autoEnv.envId.id, 'Auto-discovered env must have envId.id'); + } + + console.log('After clearing selection:', autoEnv?.displayName ?? 'none (cleared successfully)'); }); }); From b2f66ab466a654f5151ad8a7eeedfffffb2c72f5 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:07:00 -0800 Subject: [PATCH 4/7] v4 --- .../envCreation.integration.test.ts | 15 +++-- .../envDiscovery.integration.test.ts | 13 ++-- .../interpreterSelection.integration.test.ts | 25 ++++--- .../packageManagement.integration.test.ts | 66 ++++++++++++------- .../pythonProjects.integration.test.ts | 23 ++++++- .../terminalActivation.integration.test.ts | 5 +- 6 files changed, 96 insertions(+), 51 deletions(-) diff --git a/src/test/integration/envCreation.integration.test.ts b/src/test/integration/envCreation.integration.test.ts index 0e3fe9ef..91f61462 100644 --- a/src/test/integration/envCreation.integration.test.ts +++ b/src/test/integration/envCreation.integration.test.ts @@ -231,9 +231,10 @@ suite('Integration: Environment Creation', function () { assert.ok(createdEnv.envId.id, 'Created global env must have envId.id'); assert.ok(createdEnv.environmentPath, 'Created global env must have environmentPath'); } else { - // quickCreate returned undefined - this is acceptable behavior - // (e.g., no manager configured for global creation) - console.log('Global creation returned undefined (quickCreate not fully supported)'); + // quickCreate returned undefined - skip this test as feature not available + console.log('Global creation not supported with quickCreate, skipping'); + this.skip(); + return; } } finally { // Cleanup: try to remove if created, but handle dialog errors in test mode @@ -249,7 +250,6 @@ suite('Integration: Environment Creation', function () { } } } - // Test passes if we got here without throwing - API handled scope correctly }); /** @@ -277,8 +277,10 @@ suite('Integration: Environment Creation', function () { assert.ok(createdEnv.envId, 'Multi-URI created env must have envId'); assert.ok(createdEnv.environmentPath, 'Multi-URI created env must have environmentPath'); } else { - // quickCreate returned undefined - acceptable for multi-URI scenario - console.log('Multi-URI creation returned undefined (quickCreate not fully supported)'); + // quickCreate returned undefined - skip this test as feature not available + console.log('Multi-URI creation not supported with quickCreate, skipping'); + this.skip(); + return; } } finally { // Cleanup: always try to remove if created @@ -286,7 +288,6 @@ suite('Integration: Environment Creation', function () { await api.removeEnvironment(createdEnv); } } - // Test passes if we got here without throwing - API handled multi-URI correctly }); /** diff --git a/src/test/integration/envDiscovery.integration.test.ts b/src/test/integration/envDiscovery.integration.test.ts index b6af679f..d456cc68 100644 --- a/src/test/integration/envDiscovery.integration.test.ts +++ b/src/test/integration/envDiscovery.integration.test.ts @@ -96,9 +96,6 @@ suite('Integration: Environment Discovery', function () { ); try { - // Reset any previous events - handler.reset(); - // First, check if we have environments on this system const preCheckEnvs = await api.getEnvironments('all'); @@ -109,6 +106,9 @@ suite('Integration: Environment Discovery', function () { return; } + // Reset handler RIGHT BEFORE the action we're testing + handler.reset(); + // Trigger refresh - this should fire events for discovered environments await api.refreshEnvironments(undefined); @@ -120,14 +120,12 @@ suite('Integration: Environment Discovery', function () { const events = handler.first; assert.ok(events, 'Event should have a value'); assert.ok(Array.isArray(events), 'Event should be an array'); - assert.ok(events.length > 0, 'Event array should not be empty'); + assert.ok(events.length > 0, 'Should have received environment change events'); // Each event item should have kind and environment properties const firstItem = events[0]; assert.ok('kind' in firstItem, 'Event item should have kind property'); assert.ok('environment' in firstItem, 'Event item should have environment property'); - - console.log(`Event fired ${handler.count} times during refresh`); } finally { handler.dispose(); } @@ -183,8 +181,9 @@ suite('Integration: Environment Discovery', function () { const resolved = await api.resolveEnvironment(env.environmentPath); if (!resolved) { - // Some environments may not be resolvable (broken/stale) + // Environment could not be resolved - this might be expected for broken envs console.log('Environment could not be resolved:', env.displayName); + this.skip(); return; } diff --git a/src/test/integration/interpreterSelection.integration.test.ts b/src/test/integration/interpreterSelection.integration.test.ts index b4b8e1fb..a922786e 100644 --- a/src/test/integration/interpreterSelection.integration.test.ts +++ b/src/test/integration/interpreterSelection.integration.test.ts @@ -273,11 +273,15 @@ suite('Integration: Interpreter Selection Priority', function () { const event = handler.last; assert.ok(event, 'Event should have value if fired'); assert.ok(event.new, 'Event should have new environment'); - console.log( - 'Event fired for same env selection:', - `old=${event.old?.envId.id}, new=${event.new.envId.id}`, - ); - // Note: old and new may differ if there was intermediate state from teardown + + // If event fired, old and new should be the same (idempotent) + if (event.old) { + assert.strictEqual( + event.old.envId.id, + event.new.envId.id, + 'Idempotent operation should have same old and new environment', + ); + } } else { // No event fired - this is the ideal idempotent behavior console.log('No event fired for same env selection (idempotent behavior)'); @@ -340,13 +344,16 @@ suite('Integration: Interpreter Selection Priority', function () { // Get environment - may return auto-discovered env or undefined const autoEnv = await api.getEnvironment(undefined); - // Key assertion: clearing should have changed something or returned undefined + // Key assertion: clearing should have an effect + // Either returns undefined OR returns a different environment (auto-discovered) if (autoEnv) { - // If an environment is returned, it should have valid structure assert.ok(autoEnv.envId, 'Auto-discovered env must have envId'); assert.ok(autoEnv.envId.id, 'Auto-discovered env must have envId.id'); + // Note: autoEnv might equal beforeClear if auto-discovery picks the same one, + // but the explicit selection should be cleared + } else { + // Undefined is a valid result - no auto-discovery available + console.log('Selection cleared, no auto-discovery fallback'); } - - console.log('After clearing selection:', autoEnv?.displayName ?? 'none (cleared successfully)'); }); }); diff --git a/src/test/integration/packageManagement.integration.test.ts b/src/test/integration/packageManagement.integration.test.ts index 228dc5a0..d7761cab 100644 --- a/src/test/integration/packageManagement.integration.test.ts +++ b/src/test/integration/packageManagement.integration.test.ts @@ -196,8 +196,11 @@ suite('Integration: Package Management', function () { const pipInstalled = packages.some((p) => p.name.toLowerCase() === 'pip'); const setuptoolsInstalled = packages.some((p) => p.name.toLowerCase() === 'setuptools'); - console.log(`pip installed: ${pipInstalled}, setuptools installed: ${setuptoolsInstalled}`); - console.log(`Total packages: ${packages.length}`); + // Virtual environment should have pip or at least some packages + assert.ok(pipInstalled || packages.length > 0, 'Virtual environment should have pip or at least some packages'); + console.log( + `pip installed: ${pipInstalled}, setuptools installed: ${setuptoolsInstalled}, total: ${packages.length}`, + ); }); /** @@ -270,32 +273,47 @@ suite('Integration: Package Management', function () { } const wasInstalled = initialPackages.some((p) => p.name.toLowerCase() === testPackage); + let packageInstalled = wasInstalled; + + try { + if (wasInstalled) { + // Uninstall first + await api.managePackages(targetEnv, { uninstall: [testPackage] }); + packageInstalled = false; + await sleep(2000); + } - if (wasInstalled) { - // Uninstall first - await api.managePackages(targetEnv, { uninstall: [testPackage] }); - await sleep(2000); - } - - // Install package - await api.managePackages(targetEnv, { install: [testPackage] }); - - // Refresh and verify - await api.refreshPackages(targetEnv); - const afterInstall = await api.getPackages(targetEnv); - - const isNowInstalled = afterInstall?.some((p) => p.name.toLowerCase() === testPackage); - assert.ok(isNowInstalled, `${testPackage} should be installed after managePackages install`); + // Install package + await api.managePackages(targetEnv, { install: [testPackage] }); + packageInstalled = true; - // Uninstall - await api.managePackages(targetEnv, { uninstall: [testPackage] }); + // Refresh and verify + await api.refreshPackages(targetEnv); + const afterInstall = await api.getPackages(targetEnv); - // Refresh and verify - await api.refreshPackages(targetEnv); - const afterUninstall = await api.getPackages(targetEnv); + const isNowInstalled = afterInstall?.some((p) => p.name.toLowerCase() === testPackage); + assert.ok(isNowInstalled, `${testPackage} should be installed after managePackages install`); - const isStillInstalled = afterUninstall?.some((p) => p.name.toLowerCase() === testPackage); - assert.ok(!isStillInstalled, `${testPackage} should be uninstalled after managePackages uninstall`); + // Uninstall + await api.managePackages(targetEnv, { uninstall: [testPackage] }); + packageInstalled = false; + + // Refresh and verify + await api.refreshPackages(targetEnv); + const afterUninstall = await api.getPackages(targetEnv); + + const isStillInstalled = afterUninstall?.some((p) => p.name.toLowerCase() === testPackage); + assert.ok(!isStillInstalled, `${testPackage} should be uninstalled after managePackages uninstall`); + } finally { + // Ensure cleanup even if assertions fail + if (packageInstalled) { + try { + await api.managePackages(targetEnv, { uninstall: [testPackage] }); + } catch { + console.log('Cleanup: failed to uninstall test package'); + } + } + } }); /** diff --git a/src/test/integration/pythonProjects.integration.test.ts b/src/test/integration/pythonProjects.integration.test.ts index 91b28793..aa8cc569 100644 --- a/src/test/integration/pythonProjects.integration.test.ts +++ b/src/test/integration/pythonProjects.integration.test.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { PythonEnvironmentApi } from '../../api'; +import { PythonEnvironment, PythonEnvironmentApi } from '../../api'; import { ENVS_EXTENSION_ID } from '../constants'; import { TestEventHandler, waitForCondition } from '../testUtils'; @@ -27,6 +27,7 @@ suite('Integration: Python Projects', function () { this.timeout(60_000); let api: PythonEnvironmentApi; + let originalProjectEnvs: Map; suiteSetup(async function () { this.timeout(30_000); @@ -42,6 +43,26 @@ suite('Integration: Python Projects', function () { api = extension.exports as PythonEnvironmentApi; assert.ok(api, 'API not available'); assert.ok(typeof api.getPythonProjects === 'function', 'getPythonProjects method not available'); + + // Save original state for restoration + originalProjectEnvs = new Map(); + const projects = api.getPythonProjects(); + for (const project of projects) { + const env = await api.getEnvironment(project.uri); + originalProjectEnvs.set(project.uri.toString(), env); + } + }); + + suiteTeardown(async function () { + // Restore original state + for (const [uriStr, env] of originalProjectEnvs) { + try { + const uri = vscode.Uri.parse(uriStr); + await api.setEnvironment(uri, env); + } catch { + // Ignore errors during cleanup + } + } }); /** diff --git a/src/test/integration/terminalActivation.integration.test.ts b/src/test/integration/terminalActivation.integration.test.ts index 6f601ead..c3e0f8a0 100644 --- a/src/test/integration/terminalActivation.integration.test.ts +++ b/src/test/integration/terminalActivation.integration.test.ts @@ -190,9 +190,8 @@ suite('Integration: Terminal Activation', function () { assert.ok(terminal1, 'First call should return terminal'); assert.ok(terminal2, 'Second call should return terminal'); - // Note: Terminal instances may be different objects but refer to same terminal - console.log('Terminal 1 name:', terminal1.name); - console.log('Terminal 2 name:', terminal2.name); + // Verify same terminal is reused (terminal names should match for same key) + assert.strictEqual(terminal1.name, terminal2.name, 'Same key should reuse the same terminal'); }); /** From 7b1b3f244b1e9a4098fcafb9bb214ec38fb01e19 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 13 Feb 2026 08:51:39 -0800 Subject: [PATCH 5/7] v5 --- .vscode-test.mjs | 1 + .vscode/settings.json | 5 ++++- .../interpreterSelection.integration.test.ts | 12 ++++++++++-- .../packageManagement.integration.test.ts | 4 ++-- .../integration/pythonProjects.integration.test.ts | 9 ++++++--- src/test/integration/test-workspace/.gitkeep | 0 .../integration/test-workspace/.vscode/settings.json | 3 +++ .../test-workspace/integration-tests.code-workspace | 11 +++++++++++ .../integration/test-workspace/project-a/.gitkeep | 0 .../test-workspace/project-a/.vscode/settings.json | 9 +++++++++ .../integration/test-workspace/project-b/.gitkeep | 0 .../test-workspace/project-b/.vscode/settings.json | 9 +++++++++ 12 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 src/test/integration/test-workspace/.gitkeep create mode 100644 src/test/integration/test-workspace/.vscode/settings.json create mode 100644 src/test/integration/test-workspace/integration-tests.code-workspace create mode 100644 src/test/integration/test-workspace/project-a/.gitkeep create mode 100644 src/test/integration/test-workspace/project-a/.vscode/settings.json create mode 100644 src/test/integration/test-workspace/project-b/.gitkeep create mode 100644 src/test/integration/test-workspace/project-b/.vscode/settings.json diff --git a/.vscode-test.mjs b/.vscode-test.mjs index f6a5edf7..8b81f98c 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -48,6 +48,7 @@ export default defineConfig([ { label: 'integrationTests', files: 'out/test/integration/**/*.integration.test.js', + workspaceFolder: 'src/test/integration/test-workspace/integration-tests.code-workspace', mocha: { ui: 'tdd', timeout: 60000, diff --git a/.vscode/settings.json b/.vscode/settings.json index ed95cc87..c298dc6c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,8 @@ "python-envs.pythonProjects": [], "git.branchRandomName.enable": true, "git.branchProtection": ["main"], - "git.branchProtectionPrompt": "alwaysCommitToNewBranch" + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", + "chat.tools.terminal.autoApprove": { + "npx tsc": true + } } diff --git a/src/test/integration/interpreterSelection.integration.test.ts b/src/test/integration/interpreterSelection.integration.test.ts index a922786e..16b094ef 100644 --- a/src/test/integration/interpreterSelection.integration.test.ts +++ b/src/test/integration/interpreterSelection.integration.test.ts @@ -255,6 +255,14 @@ suite('Integration: Interpreter Selection Priority', function () { // Set environment first time await api.setEnvironment(undefined, env); + // Wait for any async config changes to settle before testing idempotency + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Verify the environment was actually set + const currentEnv = await api.getEnvironment(undefined); + assert.ok(currentEnv, 'Environment should be set before idempotency test'); + assert.strictEqual(currentEnv.envId.id, env.envId.id, 'Environment should match what we just set'); + const handler = new TestEventHandler( api.onDidChangeEnvironment, 'onDidChangeEnvironment', @@ -284,7 +292,7 @@ suite('Integration: Interpreter Selection Priority', function () { } } else { // No event fired - this is the ideal idempotent behavior - console.log('No event fired for same env selection (idempotent behavior)'); + assert.ok(!handler.fired, 'No event should fire when setting same environment'); } } finally { handler.dispose(); @@ -353,7 +361,7 @@ suite('Integration: Interpreter Selection Priority', function () { // but the explicit selection should be cleared } else { // Undefined is a valid result - no auto-discovery available - console.log('Selection cleared, no auto-discovery fallback'); + assert.strictEqual(autoEnv, undefined, 'Cleared selection with no auto-discovery should return undefined'); } }); }); diff --git a/src/test/integration/packageManagement.integration.test.ts b/src/test/integration/packageManagement.integration.test.ts index d7761cab..06ffdbc9 100644 --- a/src/test/integration/packageManagement.integration.test.ts +++ b/src/test/integration/packageManagement.integration.test.ts @@ -84,7 +84,7 @@ suite('Integration: Package Management', function () { // May be undefined if package manager not available if (packages === undefined) { - console.log('Package manager not available for:', targetEnv.displayName); + this.skip(); return; } @@ -224,7 +224,7 @@ suite('Integration: Package Management', function () { // Both should return valid results (or undefined for same reason) if (packages1 === undefined || packages2 === undefined) { - console.log('Package manager not available for one or both environments'); + this.skip(); return; } diff --git a/src/test/integration/pythonProjects.integration.test.ts b/src/test/integration/pythonProjects.integration.test.ts index aa8cc569..c635703a 100644 --- a/src/test/integration/pythonProjects.integration.test.ts +++ b/src/test/integration/pythonProjects.integration.test.ts @@ -254,10 +254,13 @@ suite('Integration: Python Projects', function () { if (afterClear) { assert.ok(afterClear.envId, 'If environment returned, it must have valid envId'); assert.ok(afterClear.envId.id, 'If environment returned, envId must have id'); + } else { + assert.strictEqual( + afterClear, + undefined, + 'Cleared association should return undefined when no auto-discovery', + ); } - // If undefined, that's also valid - explicit selection was cleared - - console.log('After clearing:', afterClear?.displayName ?? 'undefined (no auto-discovery)'); }); /** diff --git a/src/test/integration/test-workspace/.gitkeep b/src/test/integration/test-workspace/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/test/integration/test-workspace/.vscode/settings.json b/src/test/integration/test-workspace/.vscode/settings.json new file mode 100644 index 00000000..c9ebf2d2 --- /dev/null +++ b/src/test/integration/test-workspace/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system" +} \ No newline at end of file diff --git a/src/test/integration/test-workspace/integration-tests.code-workspace b/src/test/integration/test-workspace/integration-tests.code-workspace new file mode 100644 index 00000000..fca712c5 --- /dev/null +++ b/src/test/integration/test-workspace/integration-tests.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "project-a", + }, + { + "path": "project-b", + }, + ], + "settings": {}, +} diff --git a/src/test/integration/test-workspace/project-a/.gitkeep b/src/test/integration/test-workspace/project-a/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/test/integration/test-workspace/project-a/.vscode/settings.json b/src/test/integration/test-workspace/project-a/.vscode/settings.json new file mode 100644 index 00000000..4988ec07 --- /dev/null +++ b/src/test/integration/test-workspace/project-a/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "python-envs.pythonProjects": [ + { + "path": ".", + "envManager": "ms-python.python:system", + "packageManager": "ms-python.python:pip" + } + ] +} \ No newline at end of file diff --git a/src/test/integration/test-workspace/project-b/.gitkeep b/src/test/integration/test-workspace/project-b/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/test/integration/test-workspace/project-b/.vscode/settings.json b/src/test/integration/test-workspace/project-b/.vscode/settings.json new file mode 100644 index 00000000..4988ec07 --- /dev/null +++ b/src/test/integration/test-workspace/project-b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "python-envs.pythonProjects": [ + { + "path": ".", + "envManager": "ms-python.python:system", + "packageManager": "ms-python.python:pip" + } + ] +} \ No newline at end of file From 482e7d9f4b51b587492fc08244f28eb6ff86e7f4 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:41:50 -0800 Subject: [PATCH 6/7] add integration tests --- .github/workflows/pr-check.yml | 50 +++++ .vscode-test.mjs | 21 +- .vscode/settings.json | 3 +- package.json | 1 + .../envCreation.integration.test.ts | 38 ---- .../interpreterSelection.integration.test.ts | 28 --- .../multiWorkspace.integration.test.ts | 196 ++++++++++++++++++ .../pythonProjects.integration.test.ts | 33 --- .../settingsBehavior.integration.test.ts | 35 ---- .../project-a/.vscode/settings.json | 8 +- 10 files changed, 269 insertions(+), 144 deletions(-) create mode 100644 src/test/integration/multiroot/multiWorkspace.integration.test.ts diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 03efdd0d..2da3c3ce 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -223,3 +223,53 @@ jobs: - name: Run Integration Tests (non-Linux) if: runner.os != 'Linux' run: npm run integration-test + + integration-tests-multiroot: + name: Integration Tests (Multi-Root) + runs-on: ${{ matrix.os }} + needs: [smoke-tests] + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.9', '3.12', '3.14'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: npm ci + + - name: Compile Extension + run: npm run compile + + - name: Compile Tests + run: npm run compile-tests + + - name: Configure Test Settings + run: | + mkdir -p .vscode-test/user-data/User + echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json + shell: bash + + - name: Run Integration Tests Multi-Root (Linux) + if: runner.os == 'Linux' + uses: GabrielBB/xvfb-action@v1 + with: + run: npm run integration-test-multiroot + + - name: Run Integration Tests Multi-Root (non-Linux) + if: runner.os != 'Linux' + run: npm run integration-test-multiroot diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 8b81f98c..0f2f8d7b 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -47,8 +47,8 @@ export default defineConfig([ }, { label: 'integrationTests', - files: 'out/test/integration/**/*.integration.test.js', - workspaceFolder: 'src/test/integration/test-workspace/integration-tests.code-workspace', + files: 'out/test/integration/*.integration.test.js', + workspaceFolder: 'src/test/integration/test-workspace/project-a', mocha: { ui: 'tdd', timeout: 60000, @@ -65,6 +65,23 @@ export default defineConfig([ // the native Python tools (pet binary). We use inspect() for // useEnvironmentsExtension check, so Python extension's default is ignored. }, + { + label: 'integrationTestsMultiRoot', + files: 'out/test/integration/multiroot/*.integration.test.js', + workspaceFolder: 'src/test/integration/test-workspace/integration-tests.code-workspace', + mocha: { + ui: 'tdd', + timeout: 60000, + retries: 1, + }, + env: { + VSC_PYTHON_INTEGRATION_TEST: '1', + }, + launchArgs: [ + `--user-data-dir=${userDataDir}`, + '--disable-workspace-trust', + ], + }, { label: 'extensionTests', files: 'out/test/**/*.test.js', diff --git a/.vscode/settings.json b/.vscode/settings.json index c298dc6c..23d0d934 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "git.branchProtection": ["main"], "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "chat.tools.terminal.autoApprove": { - "npx tsc": true + "npx tsc": true, + "mkdir": true } } diff --git a/package.json b/package.json index dedcee6b..92b14fd7 100644 --- a/package.json +++ b/package.json @@ -685,6 +685,7 @@ "smoke-test": "vscode-test --label smokeTests", "e2e-test": "vscode-test --label e2eTests --install-extensions ms-python.python", "integration-test": "vscode-test --label integrationTests --install-extensions ms-python.python", + "integration-test-multiroot": "vscode-test --label integrationTestsMultiRoot --install-extensions ms-python.python", "vsce-package": "vsce package -o ms-python-envs-insiders.vsix" }, "devDependencies": { diff --git a/src/test/integration/envCreation.integration.test.ts b/src/test/integration/envCreation.integration.test.ts index 91f61462..39d5bef9 100644 --- a/src/test/integration/envCreation.integration.test.ts +++ b/src/test/integration/envCreation.integration.test.ts @@ -252,44 +252,6 @@ suite('Integration: Environment Creation', function () { } }); - /** - * Test: Creation with multiple URIs selects manager - * - * When passing multiple URIs, the API should handle manager selection. - */ - test('Multiple URI scope is handled', async function () { - const workspaceFolders = vscode.workspace.workspaceFolders; - - if (!workspaceFolders || workspaceFolders.length < 2) { - this.skip(); - return; - } - - const uris = workspaceFolders.map((f) => f.uri); - let createdEnv: PythonEnvironment | undefined; - - try { - // This may prompt for manager selection - quickCreate should handle it - createdEnv = await api.createEnvironment(uris, { quickCreate: true }); - - if (createdEnv) { - // Verify created environment has valid structure - assert.ok(createdEnv.envId, 'Multi-URI created env must have envId'); - assert.ok(createdEnv.environmentPath, 'Multi-URI created env must have environmentPath'); - } else { - // quickCreate returned undefined - skip this test as feature not available - console.log('Multi-URI creation not supported with quickCreate, skipping'); - this.skip(); - return; - } - } finally { - // Cleanup: always try to remove if created - if (createdEnv) { - await api.removeEnvironment(createdEnv); - } - } - }); - /** * Test: Creation returns properly structured environment * diff --git a/src/test/integration/interpreterSelection.integration.test.ts b/src/test/integration/interpreterSelection.integration.test.ts index 16b094ef..fc58f6bc 100644 --- a/src/test/integration/interpreterSelection.integration.test.ts +++ b/src/test/integration/interpreterSelection.integration.test.ts @@ -299,34 +299,6 @@ suite('Integration: Interpreter Selection Priority', function () { } }); - /** - * Test: Environment selection works with URI arrays - * - * setEnvironment should handle array of URIs for multi-select scenarios. - */ - test('setEnvironment handles URI array', async function () { - const environments = await api.getEnvironments('all'); - const projects = api.getPythonProjects(); - - if (environments.length === 0 || projects.length < 2) { - this.skip(); - return; - } - - const env = environments[0]; - const uris = projects.slice(0, 2).map((p) => p.uri); - - // Set environment for multiple URIs at once - await api.setEnvironment(uris, env); - - // Verify both projects have the environment - for (const uri of uris) { - const retrieved = await api.getEnvironment(uri); - assert.ok(retrieved, `URI ${uri.fsPath} should have environment`); - assert.strictEqual(retrieved.envId.id, env.envId.id, `URI ${uri.fsPath} should have set environment`); - } - }); - /** * Test: Clearing selection falls back to auto-discovery * diff --git a/src/test/integration/multiroot/multiWorkspace.integration.test.ts b/src/test/integration/multiroot/multiWorkspace.integration.test.ts new file mode 100644 index 00000000..2d80ab55 --- /dev/null +++ b/src/test/integration/multiroot/multiWorkspace.integration.test.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Integration Test: Multi-Root Workspace + * + * PURPOSE: + * Verify behavior that requires multiple workspace folders open simultaneously. + * These tests run in a multi-root workspace (.code-workspace) with 2+ folders. + * + * WHAT THIS TESTS: + * 1. Different projects can have independent environment selections + * 2. setEnvironment handles URI arrays across projects + * 3. Settings scope is isolated between workspace folders + * 4. Environment creation with multiple URI scopes + * + * NOTE: These tests require a multi-root workspace with at least 2 workspace folders. + * They will skip if run in a single-folder workspace. + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { PythonEnvironment, PythonEnvironmentApi } from '../../../api'; +import { ENVS_EXTENSION_ID } from '../../constants'; +import { waitForCondition } from '../../testUtils'; + +suite('Integration: Multi-Root Workspace', function () { + this.timeout(120_000); + + let api: PythonEnvironmentApi; + let originalProjectEnvs: Map; + + suiteSetup(async function () { + this.timeout(30_000); + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length < 2) { + this.skip(); + return; + } + + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate'); + } + + api = extension.exports as PythonEnvironmentApi; + assert.ok(api, 'API not available'); + + // Save original state for restoration + originalProjectEnvs = new Map(); + const projects = api.getPythonProjects(); + for (const project of projects) { + const env = await api.getEnvironment(project.uri); + originalProjectEnvs.set(project.uri.toString(), env); + } + }); + + suiteTeardown(async function () { + if (!originalProjectEnvs) { + return; + } + // Restore original state + for (const [uriStr, env] of originalProjectEnvs) { + try { + const uri = vscode.Uri.parse(uriStr); + await api.setEnvironment(uri, env); + } catch { + // Best effort restore + } + } + }); + + /** + * Test: Multiple projects can have different environments + * + * In a multi-project workspace, each project can have its own environment. + */ + test('Different projects can have different environments', async function () { + const projects = api.getPythonProjects(); + const environments = await api.getEnvironments('all'); + + if (projects.length < 2 || environments.length < 2) { + this.skip(); + return; + } + + const project1 = projects[0]; + const project2 = projects[1]; + const env1 = environments[0]; + const env2 = environments[1]; + + // Set different environments for different projects + await api.setEnvironment(project1.uri, env1); + await api.setEnvironment(project2.uri, env2); + + // Verify each project has its assigned environment + const retrieved1 = await api.getEnvironment(project1.uri); + const retrieved2 = await api.getEnvironment(project2.uri); + + assert.ok(retrieved1, 'Project 1 should have environment'); + assert.ok(retrieved2, 'Project 2 should have environment'); + assert.strictEqual(retrieved1.envId.id, env1.envId.id, 'Project 1 should have env1'); + assert.strictEqual(retrieved2.envId.id, env2.envId.id, 'Project 2 should have env2'); + }); + + /** + * Test: setEnvironment handles URI arrays across projects + * + * setEnvironment should handle array of URIs for multi-select scenarios. + */ + test('setEnvironment handles URI array', async function () { + const environments = await api.getEnvironments('all'); + const projects = api.getPythonProjects(); + + if (environments.length === 0 || projects.length < 2) { + this.skip(); + return; + } + + const env = environments[0]; + const uris = projects.slice(0, 2).map((p) => p.uri); + + // Set environment for multiple URIs at once + await api.setEnvironment(uris, env); + + // Verify both projects have the environment + for (const uri of uris) { + const retrieved = await api.getEnvironment(uri); + assert.ok(retrieved, `URI ${uri.fsPath} should have environment`); + assert.strictEqual(retrieved.envId.id, env.envId.id, `URI ${uri.fsPath} should have set environment`); + } + }); + + /** + * Test: Workspace folder settings scope is respected + * + * Settings at workspace folder level should be independently inspectable + * across different folders. + */ + test('Workspace folder settings scope is respected', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders!; + + const config1 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); + const config2 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[1].uri); + + // Both should be independently inspectable + const inspect1 = config1.inspect('pythonProjects'); + const inspect2 = config2.inspect('pythonProjects'); + + // Assert inspect returns valid results for both folders + assert.ok(inspect1, 'Should be able to inspect settings for folder 1'); + assert.ok(inspect2, 'Should be able to inspect settings for folder 2'); + + // Assert the inspection objects have the expected structure + assert.ok('key' in inspect1, 'Inspection should have key property'); + assert.ok('key' in inspect2, 'Inspection should have key property'); + assert.strictEqual(inspect1.key, 'python-envs.pythonProjects', 'Key should be python-envs.pythonProjects'); + assert.strictEqual(inspect2.key, 'python-envs.pythonProjects', 'Key should be python-envs.pythonProjects'); + }); + + /** + * Test: Creation with multiple URIs selects manager + * + * When passing multiple workspace folder URIs, the API should handle + * manager selection and create an environment. + */ + test('Multiple URI scope creation is handled', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders!; + const uris = workspaceFolders.map((f) => f.uri); + let createdEnv: PythonEnvironment | undefined; + + try { + // This may prompt for manager selection - quickCreate should handle it + createdEnv = await api.createEnvironment(uris, { quickCreate: true }); + + if (createdEnv) { + // Verify created environment has valid structure + assert.ok(createdEnv.envId, 'Multi-URI created env must have envId'); + assert.ok(createdEnv.environmentPath, 'Multi-URI created env must have environmentPath'); + } else { + // quickCreate returned undefined - skip this test as feature not available + this.skip(); + return; + } + } finally { + // Cleanup: always try to remove if created + if (createdEnv) { + await api.removeEnvironment(createdEnv); + } + } + }); +}); diff --git a/src/test/integration/pythonProjects.integration.test.ts b/src/test/integration/pythonProjects.integration.test.ts index c635703a..d8fc12d5 100644 --- a/src/test/integration/pythonProjects.integration.test.ts +++ b/src/test/integration/pythonProjects.integration.test.ts @@ -263,39 +263,6 @@ suite('Integration: Python Projects', function () { } }); - /** - * Test: Multiple projects can have different environments - * - * In a multi-project workspace, each project can have its own environment. - */ - test('Different projects can have different environments', async function () { - const projects = api.getPythonProjects(); - const environments = await api.getEnvironments('all'); - - if (projects.length < 2 || environments.length < 2) { - this.skip(); - return; - } - - const project1 = projects[0]; - const project2 = projects[1]; - const env1 = environments[0]; - const env2 = environments[1]; - - // Set different environments for different projects - await api.setEnvironment(project1.uri, env1); - await api.setEnvironment(project2.uri, env2); - - // Verify each project has its assigned environment - const retrieved1 = await api.getEnvironment(project1.uri); - const retrieved2 = await api.getEnvironment(project2.uri); - - assert.ok(retrieved1, 'Project 1 should have environment'); - assert.ok(retrieved2, 'Project 2 should have environment'); - assert.strictEqual(retrieved1.envId.id, env1.envId.id, 'Project 1 should have env1'); - assert.strictEqual(retrieved2.envId.id, env2.envId.id, 'Project 2 should have env2'); - }); - /** * Test: File within project resolves to project environment * diff --git a/src/test/integration/settingsBehavior.integration.test.ts b/src/test/integration/settingsBehavior.integration.test.ts index 0fbac48d..040723b0 100644 --- a/src/test/integration/settingsBehavior.integration.test.ts +++ b/src/test/integration/settingsBehavior.integration.test.ts @@ -347,39 +347,4 @@ suite('Integration: Settings Behavior', function () { console.log('Current env:', currentEnv?.displayName ?? 'none'); console.log('Projects setting type:', Array.isArray(pythonProjects) ? 'array' : typeof pythonProjects); }); - - /** - * Test: Workspace folder scope is respected - * - * Settings at workspace folder level should be independently inspectable. - */ - test('Workspace folder settings scope is respected', async function () { - const workspaceFolders = vscode.workspace.workspaceFolders; - - if (!workspaceFolders || workspaceFolders.length < 2) { - // Need at least 2 folders to test isolation - this.skip(); - return; - } - - const config1 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); - const config2 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[1].uri); - - // Both should be independently inspectable - const inspect1 = config1.inspect('pythonProjects'); - const inspect2 = config2.inspect('pythonProjects'); - - // Assert inspect returns valid results for both folders - assert.ok(inspect1, 'Should be able to inspect settings for folder 1'); - assert.ok(inspect2, 'Should be able to inspect settings for folder 2'); - - // Assert the inspection objects have the expected structure - assert.ok('key' in inspect1, 'Inspection should have key property'); - assert.ok('key' in inspect2, 'Inspection should have key property'); - assert.strictEqual(inspect1.key, 'python-envs.pythonProjects', 'Key should be python-envs.pythonProjects'); - assert.strictEqual(inspect2.key, 'python-envs.pythonProjects', 'Key should be python-envs.pythonProjects'); - - console.log('Folder 1 pythonProjects:', inspect1?.workspaceFolderValue); - console.log('Folder 2 pythonProjects:', inspect2?.workspaceFolderValue); - }); }); diff --git a/src/test/integration/test-workspace/project-a/.vscode/settings.json b/src/test/integration/test-workspace/project-a/.vscode/settings.json index 4988ec07..c9ebf2d2 100644 --- a/src/test/integration/test-workspace/project-a/.vscode/settings.json +++ b/src/test/integration/test-workspace/project-a/.vscode/settings.json @@ -1,9 +1,3 @@ { - "python-envs.pythonProjects": [ - { - "path": ".", - "envManager": "ms-python.python:system", - "packageManager": "ms-python.python:pip" - } - ] + "python-envs.defaultEnvManager": "ms-python.python:system" } \ No newline at end of file From 367ce59309694726cda54c14fded53bb1ce1d09d Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:58:29 -0800 Subject: [PATCH 7/7] refinement based on my comments --- .../envCreation.integration.test.ts | 75 ++----- .../envManagerApi.integration.test.ts | 165 -------------- .../multiWorkspace.integration.test.ts | 4 +- .../settingsBehavior.integration.test.ts | 203 +++--------------- 4 files changed, 45 insertions(+), 402 deletions(-) delete mode 100644 src/test/integration/envManagerApi.integration.test.ts diff --git a/src/test/integration/envCreation.integration.test.ts b/src/test/integration/envCreation.integration.test.ts index 39d5bef9..14644f53 100644 --- a/src/test/integration/envCreation.integration.test.ts +++ b/src/test/integration/envCreation.integration.test.ts @@ -42,70 +42,29 @@ suite('Integration: Environment Creation', function () { api = extension.exports as PythonEnvironmentApi; assert.ok(api, 'API not available'); - }); - - /** - * Test: createEnvironment API is available - * - * The API should have a createEnvironment method. - */ - test('createEnvironment API is available', async function () { assert.ok(typeof api.createEnvironment === 'function', 'createEnvironment should be a function'); - }); - - /** - * Test: removeEnvironment API is available - * - * The API should have a removeEnvironment method. - */ - test('removeEnvironment API is available', async function () { assert.ok(typeof api.removeEnvironment === 'function', 'removeEnvironment should be a function'); }); - /** - * Test: Managers that support creation are available - * - * At least one environment manager (venv or conda) should support creation. - * This test verifies that global Python installations are discoverable. - */ - test('At least one manager supports environment creation', async function () { - // Get all environments to force managers to load - await api.getEnvironments('all'); - - // Check if we have global Python installations that can create venvs - const globalEnvs = await api.getEnvironments('global'); - - // Assert we have at least one global Python that can serve as base for venv creation - assert.ok( - globalEnvs.length > 0, - 'At least one global Python installation should be available for environment creation. ' + - 'If this fails, ensure Python is installed and discoverable on this system.', - ); - - // Verify the global environments have required properties for creation - for (const env of globalEnvs) { - assert.ok(env.envId, 'Global environment must have envId'); - assert.ok(env.environmentPath, 'Global environment must have environmentPath'); - } - - console.log(`Found ${globalEnvs.length} global Python installations for venv creation`); - }); + // ========================================================================= + // ENVIRONMENT CREATION BEHAVIOR TESTS + // These tests verify actual user-facing creation and removal workflows. + // ========================================================================= /** * Test: Created environment appears in discovery * - * After creating an environment, it should be discoverable via getEnvironments. - * This test creates a real environment and cleans it up. + * BEHAVIOR TESTED: User creates an environment via quickCreate, + * then the environment should be discoverable via getEnvironments. */ test('Created environment appears in discovery', async function () { + // --- SETUP: Ensure we have prerequisites --- const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { this.skip(); return; } - // Check if we have Python available for venv creation const globalEnvs = await api.getEnvironments('global'); if (globalEnvs.length === 0) { console.log('No global Python installations found, skipping creation test'); @@ -117,17 +76,16 @@ suite('Integration: Environment Creation', function () { let createdEnv: PythonEnvironment | undefined; try { - // Create environment with quickCreate to avoid prompts + // --- ACTION: User creates environment --- createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true }); if (!createdEnv) { - // Creation may have been cancelled or failed silently console.log('Environment creation returned undefined (may require user input)'); this.skip(); return; } - // Refresh and verify the environment appears + // --- VERIFY: Created environment is discoverable --- await api.refreshEnvironments(workspaceUri); const environments = await api.getEnvironments(workspaceUri); @@ -153,11 +111,12 @@ suite('Integration: Environment Creation', function () { /** * Test: Environment removal removes from discovery * - * After removing an environment, it should no longer appear in discovery. + * BEHAVIOR TESTED: User removes an environment, then it should + * no longer appear in discovery results. */ test('Removed environment disappears from discovery', async function () { + // --- SETUP: Create an environment to remove --- const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { this.skip(); return; @@ -173,25 +132,21 @@ suite('Integration: Environment Creation', function () { let createdEnv: PythonEnvironment | undefined; try { - // Create environment createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true }); - if (!createdEnv) { this.skip(); return; } - // Record the environment ID const envId = createdEnv.envId.id; - // Remove environment + // --- ACTION: User removes environment --- await api.removeEnvironment(createdEnv); - createdEnv = undefined; // Mark as cleaned up + createdEnv = undefined; - // Give time for removal to complete await sleep(1000); - // Refresh and verify it's gone + // --- VERIFY: Environment is no longer discoverable --- await api.refreshEnvironments(workspaceUri); const environments = await api.getEnvironments(workspaceUri); diff --git a/src/test/integration/envManagerApi.integration.test.ts b/src/test/integration/envManagerApi.integration.test.ts deleted file mode 100644 index 596b2a66..00000000 --- a/src/test/integration/envManagerApi.integration.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -/** - * Integration Test: Environment Manager + API - * - * PURPOSE: - * Verify that the environment manager component correctly exposes data - * through the extension API. This tests the integration between internal - * managers and the public API surface. - * - * WHAT THIS TESTS: - * 1. API reflects environment manager state - * 2. Changes through API update manager state - * 3. Events fire when state changes - * - */ - -import * as assert from 'assert'; -import * as vscode from 'vscode'; -import { ENVS_EXTENSION_ID } from '../constants'; -import { TestEventHandler, waitForCondition } from '../testUtils'; - -suite('Integration: Environment Manager + API', function () { - // Shorter timeout for faster feedback - this.timeout(45_000); - - // The API is FLAT - methods are directly on the api object, not nested - let api: { - getEnvironments(scope: 'all' | 'global'): Promise; - refreshEnvironments(scope: undefined): Promise; - onDidChangeEnvironments?: vscode.Event; - }; - - suiteSetup(async function () { - // Set a shorter timeout for setup specifically - this.timeout(20_000); - - const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); - assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); - - if (!extension.isActive) { - await extension.activate(); - await waitForCondition(() => extension.isActive, 15_000, 'Extension did not activate'); - } - - api = extension.exports; - assert.ok(typeof api?.getEnvironments === 'function', 'getEnvironments method not available'); - }); - - /** - * Test: API and manager stay in sync after refresh - * - */ - test('API reflects manager state after refresh', async function () { - // Get initial state (verify we can call API before refresh) - await api.getEnvironments('all'); - - // Trigger refresh - await api.refreshEnvironments(undefined); - - // Get state after refresh - const afterRefresh = await api.getEnvironments('all'); - - // Verify we got an actual array back (not undefined, null, or other type) - assert.ok(Array.isArray(afterRefresh), `Expected environments array, got ${typeof afterRefresh}`); - - // Verify the API returns consistent data on repeated calls - const secondCall = await api.getEnvironments('all'); - assert.strictEqual(afterRefresh.length, secondCall.length, 'Repeated API calls should return consistent data'); - }); - - /** - * Test: Events fire when environments change - * - */ - test('Change events fire on refresh', async function () { - // Skip if event is not available - if (!api.onDidChangeEnvironments) { - this.skip(); - return; - } - - const handler = new TestEventHandler(api.onDidChangeEnvironments, 'onDidChangeEnvironments'); - - try { - // Trigger a refresh which should fire events - await api.refreshEnvironments(undefined); - - // Assert that at least one event fired during refresh - await handler.assertFiredAtLeast(1, 5000); - const event = handler.first; - assert.ok(event !== undefined, 'Event should have a value'); - } finally { - handler.dispose(); - } - }); - - /** - * Test: Global vs all environments are different scopes - * - */ - test('Different scopes return appropriate environments', async function () { - const allEnvs = await api.getEnvironments('all'); - const globalEnvs = await api.getEnvironments('global'); - - // Both should return arrays - assert.ok(Array.isArray(allEnvs), 'all scope should return array'); - assert.ok(Array.isArray(globalEnvs), 'global scope should return array'); - - // Global should be subset of or equal to all - // (all includes global + workspace-specific) - assert.ok( - globalEnvs.length <= allEnvs.length, - `Global envs (${globalEnvs.length}) should not exceed all envs (${allEnvs.length})`, - ); - }); - - /** - * Test: Environment objects are properly structured - * - */ - test('Environment objects have consistent structure', async function () { - const environments = await api.getEnvironments('all'); - - if (environments.length === 0) { - this.skip(); - return; - } - - // Check each environment has basic required properties with valid values - for (const env of environments) { - const e = env as Record; - - // Must have some form of identifier - assert.ok('id' in e || 'envId' in e, 'Environment must have id or envId'); - - // If it has an id, it should be a non-empty string - if ('id' in e) { - assert.strictEqual(typeof e.id, 'string', 'Environment id should be a string'); - assert.ok((e.id as string).length > 0, 'Environment id should not be empty'); - } - - // If it has envId, verify it's a valid object with required properties - if ('envId' in e && e.envId !== null && e.envId !== undefined) { - const envId = e.envId as Record; - assert.strictEqual(typeof envId, 'object', 'envId should be an object'); - assert.ok('id' in envId, 'envId should have an id property'); - assert.ok('managerId' in envId, 'envId should have a managerId property'); - assert.strictEqual(typeof envId.id, 'string', 'envId.id should be a string'); - assert.ok((envId.id as string).length > 0, 'envId.id should not be empty'); - } - - // Verify name is a non-empty string if present - if ('name' in e && e.name !== undefined) { - assert.strictEqual(typeof e.name, 'string', 'Environment name should be a string'); - } - - // Verify displayName is a non-empty string if present - if ('displayName' in e && e.displayName !== undefined) { - assert.strictEqual(typeof e.displayName, 'string', 'Environment displayName should be a string'); - } - } - }); -}); diff --git a/src/test/integration/multiroot/multiWorkspace.integration.test.ts b/src/test/integration/multiroot/multiWorkspace.integration.test.ts index 2d80ab55..0e0c138b 100644 --- a/src/test/integration/multiroot/multiWorkspace.integration.test.ts +++ b/src/test/integration/multiroot/multiWorkspace.integration.test.ts @@ -138,7 +138,7 @@ suite('Integration: Multi-Root Workspace', function () { /** * Test: Workspace folder settings scope is respected * - * Settings at workspace folder level should be independently inspectable + * Settings at workspace folder level should be independently accessible * across different folders. */ test('Workspace folder settings scope is respected', async function () { @@ -147,7 +147,7 @@ suite('Integration: Multi-Root Workspace', function () { const config1 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); const config2 = vscode.workspace.getConfiguration('python-envs', workspaceFolders[1].uri); - // Both should be independently inspectable + // Both should be independently accessible via inspect() const inspect1 = config1.inspect('pythonProjects'); const inspect2 = config2.inspect('pythonProjects'); diff --git a/src/test/integration/settingsBehavior.integration.test.ts b/src/test/integration/settingsBehavior.integration.test.ts index 040723b0..d86e0eaf 100644 --- a/src/test/integration/settingsBehavior.integration.test.ts +++ b/src/test/integration/settingsBehavior.integration.test.ts @@ -5,17 +5,17 @@ * Integration Test: Settings Behavior * * PURPOSE: - * Verify that settings are read and written correctly, and that - * the extension respects VS Code's settings hierarchy. + * Verify that the extension's settings-related APIs work correctly + * and interact properly with VS Code's settings system. * * WHAT THIS TESTS: - * 1. Opening workspace doesn't pollute settings - * 2. Manual selection writes to settings - * 3. Settings scope is respected - * 4. Environment variables API works + * 1. Environment variables API works correctly + * 2. Extension correctly defines required settings in package.json + * 3. Settings and API work together consistently * - * NOTE: These tests interact with VS Code settings. - * Care should be taken to restore original settings after tests. + * NOTE: These tests focus on extension behavior, not VS Code's + * configuration system. Tests just reading settings without + * exercising extension code belong elsewhere. */ import * as assert from 'assert'; @@ -42,14 +42,22 @@ suite('Integration: Settings Behavior', function () { api = extension.exports as PythonEnvironmentApi; assert.ok(api, 'API not available'); + assert.ok(typeof api.getEnvironmentVariables === 'function', 'getEnvironmentVariables should be a function'); + assert.ok(api.onDidChangeEnvironmentVariables, 'onDidChangeEnvironmentVariables should be available'); }); + // ========================================================================= + // EXTENSION SETTINGS SCHEMA TESTS + // Verify that settings defined in package.json are accessible. + // ========================================================================= + /** - * Test: Extension settings are accessible + * Test: Extension settings are defined in package.json * * The python-envs configuration section should be accessible with expected types. + * This verifies our package.json contributes the correct settings schema. */ - test('Extension settings section is accessible', async function () { + test('Extension settings are defined in package.json', async function () { const config = vscode.workspace.getConfiguration('python-envs'); assert.ok(config, 'python-envs configuration should be accessible'); @@ -72,56 +80,11 @@ suite('Integration: Settings Behavior', function () { console.log('defaultPackageManager:', defaultPackageManager); }); - /** - * Test: workspaceSearchPaths setting is accessible - * - * The search paths setting should be readable. - */ - test('workspaceSearchPaths setting is readable', async function () { - const workspaceFolders = vscode.workspace.workspaceFolders; - - if (!workspaceFolders || workspaceFolders.length === 0) { - this.skip(); - return; - } - - const config = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); - const searchPaths = config.get('workspaceSearchPaths'); - - // Default should be ["./**/.venv"] - assert.ok( - Array.isArray(searchPaths) || searchPaths === undefined, - 'workspaceSearchPaths should be array or undefined', - ); - - if (searchPaths) { - console.log('workspaceSearchPaths:', searchPaths); - } - }); - - /** - * Test: globalSearchPaths setting is accessible - * - * The global search paths setting should be readable. - */ - test('globalSearchPaths setting is readable', async function () { - const config = vscode.workspace.getConfiguration('python-envs'); - const globalPaths = config.get('globalSearchPaths'); - - assert.ok( - Array.isArray(globalPaths) || globalPaths === undefined, - 'globalSearchPaths should be array or undefined', - ); - - if (globalPaths) { - console.log('globalSearchPaths:', globalPaths); - } - }); - /** * Test: pythonProjects setting structure * - * The pythonProjects setting should have the correct structure. + * The pythonProjects setting should have the correct structure when set. + * This validates our package.json schema for pythonProjects. */ test('pythonProjects setting has correct structure', async function () { const workspaceFolders = vscode.workspace.workspaceFolders; @@ -144,40 +107,10 @@ suite('Integration: Settings Behavior', function () { console.log('pythonProjects:', JSON.stringify(projects, null, 2)); }); - /** - * Test: Settings inspection shows scope - * - * Using inspect() should show which scope a setting comes from. - */ - test('Settings inspection shows scope information', async function () { - const workspaceFolders = vscode.workspace.workspaceFolders; - - if (!workspaceFolders || workspaceFolders.length === 0) { - this.skip(); - return; - } - - const config = vscode.workspace.getConfiguration('python-envs', workspaceFolders[0].uri); - const inspection = config.inspect('defaultEnvManager'); - - assert.ok(inspection, 'Inspection should return result'); - assert.ok( - 'defaultValue' in inspection || 'globalValue' in inspection, - 'Inspection should have value properties', - ); - - console.log('defaultEnvManager inspection:', JSON.stringify(inspection, null, 2)); - }); - - /** - * Test: Environment variables API is available - * - * The getEnvironmentVariables API should be callable. - */ - test('getEnvironmentVariables API is available', async function () { - assert.ok(typeof api.getEnvironmentVariables === 'function', 'getEnvironmentVariables should be a function'); - assert.ok(api.onDidChangeEnvironmentVariables, 'onDidChangeEnvironmentVariables should be available'); - }); + // ========================================================================= + // ENVIRONMENT VARIABLES API TESTS + // These tests verify the extension's getEnvironmentVariables API behavior. + // ========================================================================= /** * Test: getEnvironmentVariables returns object @@ -233,90 +166,10 @@ suite('Integration: Settings Behavior', function () { assert.ok(typeof envVars === 'object', 'Should return object for undefined uri'); }); - /** - * Test: Terminal settings are accessible - * - * Terminal-specific settings should be accessible with expected types. - */ - test('Terminal settings are accessible', async function () { - const config = vscode.workspace.getConfiguration('python-envs'); - - const activationType = config.get('terminal.autoActivationType'); - const showButton = config.get('terminal.showActivateButton'); - - // Assert settings have expected types - // activationType should be string (enum value) or undefined - assert.ok( - typeof activationType === 'string' || activationType === undefined, - `terminal.autoActivationType should be string or undefined, got ${typeof activationType}`, - ); - - // showButton should be boolean or undefined - assert.ok( - typeof showButton === 'boolean' || showButton === undefined, - `terminal.showActivateButton should be boolean or undefined, got ${typeof showButton}`, - ); - - console.log('terminal.autoActivationType:', activationType); - console.log('terminal.showActivateButton:', showButton); - }); - - /** - * Test: alwaysUseUv setting is accessible - * - * The uv preference setting should be readable and have expected type. - */ - test('alwaysUseUv setting is accessible', async function () { - const config = vscode.workspace.getConfiguration('python-envs'); - const alwaysUseUv = config.get('alwaysUseUv'); - - // alwaysUseUv should be boolean or undefined - assert.ok( - typeof alwaysUseUv === 'boolean' || alwaysUseUv === undefined, - `alwaysUseUv should be boolean or undefined, got ${typeof alwaysUseUv}`, - ); - - console.log('alwaysUseUv:', alwaysUseUv); - }); - - /** - * Test: Legacy python settings are accessible - * - * Legacy python.* settings should be readable for migration with expected types. - */ - test('Legacy python settings are accessible', async function () { - const pythonConfig = vscode.workspace.getConfiguration('python'); - - // These are legacy settings that may have values - const venvPath = pythonConfig.get('venvPath'); - const venvFolders = pythonConfig.get('venvFolders'); - const defaultInterpreterPath = pythonConfig.get('defaultInterpreterPath'); - const condaPath = pythonConfig.get('condaPath'); - - // Assert types are as expected - assert.ok( - typeof venvPath === 'string' || venvPath === undefined, - `venvPath should be string or undefined, got ${typeof venvPath}`, - ); - assert.ok( - Array.isArray(venvFolders) || venvFolders === undefined, - `venvFolders should be array or undefined, got ${typeof venvFolders}`, - ); - assert.ok( - typeof defaultInterpreterPath === 'string' || defaultInterpreterPath === undefined, - `defaultInterpreterPath should be string or undefined, got ${typeof defaultInterpreterPath}`, - ); - assert.ok( - typeof condaPath === 'string' || condaPath === undefined, - `condaPath should be string or undefined, got ${typeof condaPath}`, - ); - - console.log('Legacy settings:'); - console.log(' venvPath:', venvPath); - console.log(' venvFolders:', venvFolders); - console.log(' defaultInterpreterPath:', defaultInterpreterPath); - console.log(' condaPath:', condaPath); - }); + // ========================================================================= + // SETTINGS + API INTEGRATION TESTS + // These tests verify settings and API work together correctly. + // ========================================================================= /** * Test: Settings and API are connected