From bd2de0790cf0b5f5d823a0781155ac3be047a6f3 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:03:12 -0800 Subject: [PATCH 1/5] port learnings into testing workflow instructions --- .../testing-workflow.instructions.md | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md index 7a5c3afc..4c2a8270 100644 --- a/.github/instructions/testing-workflow.instructions.md +++ b/.github/instructions/testing-workflow.instructions.md @@ -55,7 +55,7 @@ When implementing tests as an AI agent, choose between two main types: ### Primary Tool: `runTests` -Use the `runTests` tool to execute tests programmatically: +Use the `runTests` tool to execute tests programmatically rather than terminal commands for better integration and result parsing: ```typescript // Run specific test files @@ -80,7 +80,7 @@ await runTests({ ### Compilation Requirements -Before running tests, ensure compilation: +Before running tests, ensure compilation. Always start compilation with `npm run watch-tests` before test execution to ensure TypeScript files are built. Recompile after making import/export changes before running tests, as stubs won't work if they're applied to old compiled JavaScript that doesn't have the updated imports: ```typescript // Start watch mode for auto-compilation @@ -100,7 +100,7 @@ await run_in_terminal({ ### Alternative: Terminal Execution -For targeted test runs when `runTests` tool is unavailable: +For targeted test runs when `runTests` tool is unavailable. Note: When a targeted test run yields 0 tests, first verify the compiled JS exists under `out/test` (rootDir is `src`); absence almost always means the test file sits outside `src` or compilation hasn't run yet: ```typescript // Run specific test suite @@ -145,6 +145,8 @@ if (error.includes('AssertionError')) { ### Systematic Failure Analysis +Fix test issues iteratively - run tests, analyze failures, apply fixes, repeat until passing. When unit tests fail with VS Code API errors like `TypeError: X is not a constructor` or `Cannot read properties of undefined (reading 'Y')`, check if VS Code APIs are properly mocked in `/src/test/unittests.ts` - add missing APIs following the existing pattern. + ```typescript interface TestFailureAnalysis { type: 'compilation' | 'runtime' | 'assertion' | 'timeout'; @@ -234,6 +236,8 @@ import * as sinon from 'sinon'; import * as workspaceApis from '../../common/workspace.apis'; // Wrapper functions // Stub wrapper functions, not VS Code APIs directly +// Always mock wrapper functions (e.g., workspaceApis.getConfiguration()) instead of +// VS Code APIs directly to avoid stubbing issues const mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); ``` @@ -372,6 +376,8 @@ interface MockWorkspaceConfig { ### Mock Setup Strategy +Create minimal mock objects with only required methods and use TypeScript type assertions (e.g., `mockApi as PythonEnvironmentApi`) to satisfy interface requirements instead of implementing all interface methods when only specific methods are needed for the test. Simplify mock setup by only mocking methods actually used in tests and use `as unknown as Type` for TypeScript compatibility. + ```typescript suite('Function Integration Tests', () => { // 1. Declare all mocks @@ -397,6 +403,8 @@ suite('Function Integration Tests', () => { mockGetWorkspaceFolders.returns(undefined); // 5. Create mock configuration objects + // When fixing mock environment creation, use null to truly omit + // properties rather than undefined pythonConfig = { get: sinon.stub(), inspect: sinon.stub(), @@ -447,6 +455,7 @@ const result = await getAllExtraSearchPaths(); assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); // Verify side effects + // Use sinon.match() patterns for resilient assertions that don't break on minor output changes assert(mockTraceLog.calledWith(sinon.match(/completion/i)), 'Should log completion'); }); ``` @@ -490,6 +499,15 @@ envConfig.inspect .returns({ globalValue: ['/migrated/paths'], }); + +// Testing async functions with child processes: +// Call the function first to get a promise, then use setTimeout to emit mock events, +// then await the promise - this ensures proper timing of mock setup versus function execution + +// Cannot stub internal function calls within the same module after import - stub external +// dependencies instead (e.g., stub childProcessApis.spawnProcess rather than trying to stub +// helpers.isUvInstalled when testing helpers.shouldUseUv) because intra-module calls use +// direct references, not module exports ``` ## ๐Ÿงช Step 7: Test Categories and Patterns @@ -499,6 +517,7 @@ envConfig.inspect - Test different setting combinations - Test setting precedence (workspace > user > default) - Test configuration errors and recovery +- Always use dynamic path construction with Node.js `path` module when testing functions that resolve paths against workspace folders to ensure cross-platform compatibility ### Data Flow Tests @@ -543,31 +562,15 @@ envConfig.inspect 1. **Read test files** - Check structure and mock setup 2. **Run tests** - Establish baseline functionality -3. **Apply improvements** - Use patterns below +3. **Apply improvements** - Use patterns below. When reviewing existing tests, focus on behavior rather than implementation details in test names and assertions 4. **Verify** - Ensure tests still pass ### Common Fixes - Over-complex mocks โ†’ Minimal mocks with only needed methods - Brittle assertions โ†’ Behavior-focused with error messages -- Vague test names โ†’ Clear scenario descriptions +- Vague test names โ†’ Clear scenario descriptions (transform "should return X when Y" into "should [expected behavior] when [scenario context]") - Missing structure โ†’ Mock โ†’ Run โ†’ Assert pattern +- Untestable Node.js APIs โ†’ Create proxy abstraction functions (use function overloads to preserve intelligent typing while making functions mockable) ## ๐Ÿง  Agent Learnings - -- Always use dynamic path construction with Node.js `path` module when testing functions that resolve paths against workspace folders to ensure cross-platform compatibility (1) -- Use `runTests` tool for programmatic test execution rather than terminal commands for better integration and result parsing (1) -- Mock wrapper functions (e.g., `workspaceApis.getConfiguration()`) instead of VS Code APIs directly to avoid stubbing issues (2) -- Start compilation with `npm run watch-tests` before test execution to ensure TypeScript files are built (1) -- Use `sinon.match()` patterns for resilient assertions that don't break on minor output changes (2) -- Fix test issues iteratively - run tests, analyze failures, apply fixes, repeat until passing (1) -- When fixing mock environment creation, use `null` to truly omit properties rather than `undefined` (1) -- Always recompile TypeScript after making import/export changes before running tests, as stubs won't work if they're applied to old compiled JavaScript that doesn't have the updated imports (2) -- Create proxy abstraction functions for Node.js APIs like `cp.spawn` to enable clean testing - use function overloads to preserve Node.js's intelligent typing while making the functions mockable (1) -- When a targeted test run yields 0 tests, first verify the compiled JS exists under `out/test` (rootDir is `src`); absence almost always means the test file sits outside `src` or compilation hasn't run yet (1) -- When unit tests fail with VS Code API errors like `TypeError: X is not a constructor` or `Cannot read properties of undefined (reading 'Y')`, check if VS Code APIs are properly mocked in `/src/test/unittests.ts` - add missing Task-related APIs (`Task`, `TaskScope`, `ShellExecution`, `TaskRevealKind`, `TaskPanelKind`) and namespace mocks (`tasks`) following the existing pattern of `mockedVSCode.X = vscodeMocks.vscMockExtHostedTypes.X` (1) -- Create minimal mock objects with only required methods and use TypeScript type assertions (e.g., mockApi as PythonEnvironmentApi) to satisfy interface requirements instead of implementing all interface methods when only specific methods are needed for the test (1) -- When reviewing existing tests, focus on behavior rather than implementation details in test names and assertions - transform "should return X when Y" into "should [expected behavior] when [scenario context]" (1) -- Simplify mock setup by only mocking methods actually used in tests and use `as unknown as Type` for TypeScript compatibility (1) -- When testing async functions that use child processes, call the function first to get a promise, then use setTimeout to emit mock events, then await the promise - this ensures proper timing of mock setup versus function execution (1) -- Cannot stub internal function calls within the same module after import - stub external dependencies instead (e.g., stub `childProcessApis.spawnProcess` rather than trying to stub `helpers.isUvInstalled` when testing `helpers.shouldUseUv`) because intra-module calls use direct references, not module exports (1) From 28cfbb5a6221c297ed6e4274075d9bf2034e5ad3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:35:43 +0000 Subject: [PATCH 2/5] Initial plan From 190f9cc5434df8b41439206274753ca7a6f9f8d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:43:36 +0000 Subject: [PATCH 3/5] Add comprehensive unit tests for CondaPackageManager Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../conda/condaPackageManager.unit.test.ts | 608 ++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 src/test/managers/conda/condaPackageManager.unit.test.ts diff --git a/src/test/managers/conda/condaPackageManager.unit.test.ts b/src/test/managers/conda/condaPackageManager.unit.test.ts new file mode 100644 index 00000000..8cfcfa6b --- /dev/null +++ b/src/test/managers/conda/condaPackageManager.unit.test.ts @@ -0,0 +1,608 @@ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { CancellationError, CancellationToken, LogOutputChannel, Progress } from 'vscode'; +import { DidChangePackagesEventArgs, Package, PackageChangeKind, PythonEnvironment, PythonEnvironmentApi } from '../../../api'; +import * as winapi from '../../../common/window.apis'; +import * as condaUtils from '../../../managers/conda/condaUtils'; +import { CondaPackageManager } from '../../../managers/conda/condaPackageManager'; + +suite('CondaPackageManager Unit Tests', () => { + let mockApi: PythonEnvironmentApi; + let mockLog: LogOutputChannel; + let packageManager: CondaPackageManager; + let withProgressStub: sinon.SinonStub; + let managePackagesStub: sinon.SinonStub; + let refreshPackagesStub: sinon.SinonStub; + let getCommonCondaPackagesToInstallStub: sinon.SinonStub; + + setup(() => { + // Create minimal mocks + mockApi = {} as PythonEnvironmentApi; + mockLog = { + error: sinon.stub(), + info: sinon.stub(), + warn: sinon.stub(), + } as unknown as LogOutputChannel; + + // Stub external dependencies + withProgressStub = sinon.stub(winapi, 'withProgress'); + managePackagesStub = sinon.stub(condaUtils, 'managePackages'); + refreshPackagesStub = sinon.stub(condaUtils, 'refreshPackages'); + getCommonCondaPackagesToInstallStub = sinon.stub(condaUtils, 'getCommonCondaPackagesToInstall'); + + // Create package manager instance + packageManager = new CondaPackageManager(mockApi, mockLog); + }); + + teardown(() => { + sinon.restore(); + packageManager.dispose(); + }); + + suite('Test 1: getChanges function', () => { + test('should detect added packages', () => { + // This tests the internal getChanges function indirectly through refresh + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const after: Package[] = [ + { name: 'package1', version: '1.0.0' } as Package, + { name: 'package2', version: '2.0.0' } as Package, + ]; + + let firedEvent: DidChangePackagesEventArgs | undefined; + packageManager.onDidChangePackages((e) => { + firedEvent = e; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(after); + + return packageManager.refresh(env).then(() => { + assert.ok(firedEvent, 'Event should be fired'); + assert.strictEqual(firedEvent!.changes.length, 2, 'Should detect 2 additions'); + assert.strictEqual(firedEvent!.changes[0].kind, PackageChangeKind.add); + assert.strictEqual(firedEvent!.changes[1].kind, PackageChangeKind.add); + }); + }); + + test('should detect removed packages', () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const before: Package[] = [ + { name: 'package1', version: '1.0.0' } as Package, + { name: 'package2', version: '2.0.0' } as Package, + ]; + const after: Package[] = []; + + let firedEvent: DidChangePackagesEventArgs | undefined; + packageManager.onDidChangePackages((e) => { + firedEvent = e; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + // First refresh to set initial state + refreshPackagesStub.resolves(before); + return packageManager.refresh(env).then(() => { + // Reset event + firedEvent = undefined; + + // Second refresh with empty packages + refreshPackagesStub.resolves(after); + return packageManager.refresh(env).then(() => { + assert.ok(firedEvent, 'Event should be fired'); + assert.strictEqual(firedEvent!.changes.length, 2, 'Should detect 2 removals'); + assert.strictEqual(firedEvent!.changes[0].kind, PackageChangeKind.remove); + assert.strictEqual(firedEvent!.changes[1].kind, PackageChangeKind.remove); + }); + }); + }); + + test('should detect both additions and removals', () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const before: Package[] = [ + { name: 'old-package', version: '1.0.0' } as Package, + ]; + const after: Package[] = [ + { name: 'new-package', version: '2.0.0' } as Package, + ]; + + let firedEvent: DidChangePackagesEventArgs | undefined; + packageManager.onDidChangePackages((e) => { + firedEvent = e; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + // First refresh to set initial state + refreshPackagesStub.resolves(before); + return packageManager.refresh(env).then(() => { + // Reset event + firedEvent = undefined; + + // Second refresh with different packages + refreshPackagesStub.resolves(after); + return packageManager.refresh(env).then(() => { + assert.ok(firedEvent, 'Event should be fired'); + assert.strictEqual(firedEvent!.changes.length, 2, 'Should detect 1 removal and 1 addition'); + + const removeChanges = firedEvent!.changes.filter(c => c.kind === PackageChangeKind.remove); + const addChanges = firedEvent!.changes.filter(c => c.kind === PackageChangeKind.add); + + assert.strictEqual(removeChanges.length, 1, 'Should have 1 removal'); + assert.strictEqual(addChanges.length, 1, 'Should have 1 addition'); + assert.strictEqual(removeChanges[0].pkg.name, 'old-package'); + assert.strictEqual(addChanges[0].pkg.name, 'new-package'); + }); + }); + }); + }); + + suite('Test 2: CondaPackageManager.manage', () => { + test('should install packages when provided', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + managePackagesStub.resolves(packages); + + await packageManager.manage(env, { install: ['numpy'] }); + + assert.ok(managePackagesStub.called, 'managePackages should be called'); + const args = managePackagesStub.firstCall.args; + assert.strictEqual(args[0], env); + assert.deepStrictEqual(args[1].install, ['numpy']); + }); + + test('should uninstall packages when provided', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = []; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + managePackagesStub.resolves(packages); + + await packageManager.manage(env, { uninstall: ['numpy'] }); + + assert.ok(managePackagesStub.called, 'managePackages should be called'); + const args = managePackagesStub.firstCall.args; + assert.strictEqual(args[0], env); + assert.deepStrictEqual(args[1].uninstall, ['numpy']); + }); + + test('should prompt user when no packages specified', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'pytest', version: '7.0.0' } as Package, + ]; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + getCommonCondaPackagesToInstallStub.resolves({ install: ['pytest'], uninstall: [] }); + managePackagesStub.resolves(packages); + + await packageManager.manage(env, {} as any); + + assert.ok(getCommonCondaPackagesToInstallStub.called, 'Should prompt user for packages'); + assert.ok(managePackagesStub.called, 'managePackages should be called'); + }); + + test('should handle cancellation gracefully', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + getCommonCondaPackagesToInstallStub.resolves(undefined); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + await packageManager.manage(env, {} as any); + + assert.ok(getCommonCondaPackagesToInstallStub.called, 'Should prompt user'); + assert.ok(!managePackagesStub.called, 'Should not call managePackages when user cancels'); + }); + + test('should emit event after managing packages', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const afterPackages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + let firedEvent: DidChangePackagesEventArgs | undefined; + packageManager.onDidChangePackages((e) => { + firedEvent = e; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + managePackagesStub.resolves(afterPackages); + + await packageManager.manage(env, { install: ['numpy'] }); + + assert.ok(firedEvent, 'Event should be fired'); + assert.strictEqual(firedEvent!.environment, env); + assert.strictEqual(firedEvent!.manager, packageManager); + }); + + test('should handle errors and log them', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const error = new Error('Installation failed'); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + managePackagesStub.rejects(error); + + await packageManager.manage(env, { install: ['numpy'] }); + + assert.ok((mockLog.error as sinon.SinonStub).called, 'Should log error'); + }); + + test('should re-throw CancellationError', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + managePackagesStub.rejects(new CancellationError()); + + await assert.rejects( + async () => packageManager.manage(env, { install: ['numpy'] }), + CancellationError, + 'Should re-throw CancellationError' + ); + }); + }); + + suite('Test 3: CondaPackageManager.refresh', () => { + test('should refresh packages and update cache', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + { name: 'pandas', version: '1.3.0' } as Package, + ]; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + await packageManager.refresh(env); + + assert.ok(refreshPackagesStub.called, 'refreshPackages should be called'); + + // Verify packages are cached + const cached = await packageManager.getPackages(env); + assert.strictEqual(cached?.length, 2); + }); + + test('should emit event when packages change', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + let firedEvent: DidChangePackagesEventArgs | undefined; + packageManager.onDidChangePackages((e) => { + firedEvent = e; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + await packageManager.refresh(env); + + assert.ok(firedEvent, 'Event should be fired when packages change'); + }); + + test('should not emit event when packages unchanged', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = []; + + let eventCount = 0; + packageManager.onDidChangePackages(() => { + eventCount++; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + await packageManager.refresh(env); + await packageManager.refresh(env); + + assert.strictEqual(eventCount, 0, 'Should not emit event when no changes'); + }); + }); + + suite('Test 4: CondaPackageManager.getPackages', () => { + test('should return cached packages if available', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + // First call should trigger refresh + await packageManager.refresh(env); + + // Reset stub to verify it's not called again + refreshPackagesStub.resetHistory(); + + // Second call should use cache + const result = await packageManager.getPackages(env); + + assert.strictEqual(result?.length, 1); + assert.ok(!refreshPackagesStub.called, 'Should not call refresh when packages are cached'); + }); + + test('should refresh if packages not cached', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + const result = await packageManager.getPackages(env); + + assert.ok(refreshPackagesStub.called, 'Should call refresh when packages not cached'); + assert.strictEqual(result?.length, 1); + }); + + test('should handle multiple environments separately', async () => { + const env1: PythonEnvironment = { + envId: { id: 'env1' }, + } as PythonEnvironment; + + const env2: PythonEnvironment = { + envId: { id: 'env2' }, + } as PythonEnvironment; + + const packages1: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + const packages2: Package[] = [ + { name: 'pandas', version: '1.3.0' } as Package, + ]; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.onFirstCall().resolves(packages1); + refreshPackagesStub.onSecondCall().resolves(packages2); + + const result1 = await packageManager.getPackages(env1); + const result2 = await packageManager.getPackages(env2); + + assert.strictEqual(result1?.length, 1); + assert.strictEqual(result1?.[0].name, 'numpy'); + assert.strictEqual(result2?.length, 1); + assert.strictEqual(result2?.[0].name, 'pandas'); + }); + }); + + suite('Test 5: CondaPackageManager.dispose', () => { + test('should dispose event emitter', () => { + const disposeStub = sinon.stub(packageManager['_onDidChangePackages'], 'dispose'); + + packageManager.dispose(); + + assert.ok(disposeStub.called, 'Should dispose event emitter'); + }); + + test('should clear packages cache', () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + return packageManager.refresh(env).then(() => { + packageManager.dispose(); + + // Verify cache is cleared by checking the internal map + assert.strictEqual(packageManager['packages'].size, 0, 'Should clear packages cache'); + }); + }); + }); + + suite('Test 6: Event emission', () => { + test('should fire onDidChangePackages event with correct arguments', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + let firedEvent: DidChangePackagesEventArgs | undefined; + packageManager.onDidChangePackages((e) => { + firedEvent = e; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + await packageManager.refresh(env); + + assert.ok(firedEvent, 'Event should be fired'); + assert.strictEqual(firedEvent!.environment, env, 'Event should include environment'); + assert.strictEqual(firedEvent!.manager, packageManager, 'Event should include manager'); + assert.ok(Array.isArray(firedEvent!.changes), 'Event should include changes array'); + }); + + test('should support multiple event listeners', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + ]; + + let listener1Called = false; + let listener2Called = false; + + packageManager.onDidChangePackages(() => { + listener1Called = true; + }); + + packageManager.onDidChangePackages(() => { + listener2Called = true; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + await packageManager.refresh(env); + + assert.ok(listener1Called, 'First listener should be called'); + assert.ok(listener2Called, 'Second listener should be called'); + }); + + test('should include package changes in event', async () => { + const env: PythonEnvironment = { + envId: { id: 'test-env' }, + } as PythonEnvironment; + + const packages: Package[] = [ + { name: 'numpy', version: '1.21.0' } as Package, + { name: 'pandas', version: '1.3.0' } as Package, + ]; + + let firedEvent: DidChangePackagesEventArgs | undefined; + packageManager.onDidChangePackages((e) => { + firedEvent = e; + }); + + // Mock withProgress to execute callback immediately + withProgressStub.callsFake(async (_options, callback) => { + return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); + }); + + refreshPackagesStub.resolves(packages); + + await packageManager.refresh(env); + + assert.ok(firedEvent, 'Event should be fired'); + assert.strictEqual(firedEvent!.changes.length, 2, 'Should include all package changes'); + assert.strictEqual(firedEvent!.changes[0].kind, PackageChangeKind.add); + assert.strictEqual(firedEvent!.changes[0].pkg.name, 'numpy'); + assert.strictEqual(firedEvent!.changes[1].kind, PackageChangeKind.add); + assert.strictEqual(firedEvent!.changes[1].pkg.name, 'pandas'); + }); + }); +}); From e9dc57390c14ac19128ab7cb1d402894b3bfe895 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:02:02 +0000 Subject: [PATCH 4/5] Update tests to follow testing-workflow instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use node:assert instead of assert - Follow Mock โ†’ Run โ†’ Assert pattern with clear comments - Improve test names to describe scenario and expected behavior - Reorganize imports into logical groups - Remove "Test X:" prefixes from suite names for cleaner output Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../conda/condaPackageManager.unit.test.ts | 162 +++++++++++------- 1 file changed, 99 insertions(+), 63 deletions(-) diff --git a/src/test/managers/conda/condaPackageManager.unit.test.ts b/src/test/managers/conda/condaPackageManager.unit.test.ts index 8cfcfa6b..58e6a8ed 100644 --- a/src/test/managers/conda/condaPackageManager.unit.test.ts +++ b/src/test/managers/conda/condaPackageManager.unit.test.ts @@ -1,9 +1,12 @@ -import assert from 'assert'; +// 1. Imports - group logically +import assert from 'node:assert'; import * as sinon from 'sinon'; import { CancellationError, CancellationToken, LogOutputChannel, Progress } from 'vscode'; import { DidChangePackagesEventArgs, Package, PackageChangeKind, PythonEnvironment, PythonEnvironmentApi } from '../../../api'; import * as winapi from '../../../common/window.apis'; import * as condaUtils from '../../../managers/conda/condaUtils'; + +// 2. Function under test import { CondaPackageManager } from '../../../managers/conda/condaPackageManager'; suite('CondaPackageManager Unit Tests', () => { @@ -39,9 +42,9 @@ suite('CondaPackageManager Unit Tests', () => { packageManager.dispose(); }); - suite('Test 1: getChanges function', () => { - test('should detect added packages', () => { - // This tests the internal getChanges function indirectly through refresh + suite('getChanges function', () => { + test('should detect package additions when refreshing empty environment', () => { + // Mock - Set up environment and mock responses const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -56,14 +59,15 @@ suite('CondaPackageManager Unit Tests', () => { firedEvent = e; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(after); + // Run - Execute refresh return packageManager.refresh(env).then(() => { + // Assert - Verify changes detected correctly assert.ok(firedEvent, 'Event should be fired'); assert.strictEqual(firedEvent!.changes.length, 2, 'Should detect 2 additions'); assert.strictEqual(firedEvent!.changes[0].kind, PackageChangeKind.add); @@ -71,7 +75,8 @@ suite('CondaPackageManager Unit Tests', () => { }); }); - test('should detect removed packages', () => { + test('should detect package removals when packages are uninstalled', () => { + // Mock - Set up environment with existing packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -87,20 +92,20 @@ suite('CondaPackageManager Unit Tests', () => { firedEvent = e; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); - // First refresh to set initial state refreshPackagesStub.resolves(before); + + // Run - First refresh to set initial state return packageManager.refresh(env).then(() => { - // Reset event firedEvent = undefined; - - // Second refresh with empty packages refreshPackagesStub.resolves(after); + + // Run - Second refresh with empty packages return packageManager.refresh(env).then(() => { + // Assert - Verify removals detected assert.ok(firedEvent, 'Event should be fired'); assert.strictEqual(firedEvent!.changes.length, 2, 'Should detect 2 removals'); assert.strictEqual(firedEvent!.changes[0].kind, PackageChangeKind.remove); @@ -109,7 +114,8 @@ suite('CondaPackageManager Unit Tests', () => { }); }); - test('should detect both additions and removals', () => { + test('should detect both package additions and removals when packages change', () => { + // Mock - Set up environment with one package const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -126,20 +132,20 @@ suite('CondaPackageManager Unit Tests', () => { firedEvent = e; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); - // First refresh to set initial state refreshPackagesStub.resolves(before); + + // Run - First refresh to set initial state return packageManager.refresh(env).then(() => { - // Reset event firedEvent = undefined; - - // Second refresh with different packages refreshPackagesStub.resolves(after); + + // Run - Second refresh with different packages return packageManager.refresh(env).then(() => { + // Assert - Verify both additions and removals assert.ok(firedEvent, 'Event should be fired'); assert.strictEqual(firedEvent!.changes.length, 2, 'Should detect 1 removal and 1 addition'); @@ -155,8 +161,9 @@ suite('CondaPackageManager Unit Tests', () => { }); }); - suite('Test 2: CondaPackageManager.manage', () => { - test('should install packages when provided', async () => { + suite('CondaPackageManager.manage', () => { + test('should install packages when install list provided', async () => { + // Mock - Set up environment and install packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -165,44 +172,48 @@ suite('CondaPackageManager Unit Tests', () => { { name: 'numpy', version: '1.21.0' } as Package, ]; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); managePackagesStub.resolves(packages); + // Run - Execute manage with install option await packageManager.manage(env, { install: ['numpy'] }); + // Assert - Verify managePackages called correctly assert.ok(managePackagesStub.called, 'managePackages should be called'); const args = managePackagesStub.firstCall.args; assert.strictEqual(args[0], env); assert.deepStrictEqual(args[1].install, ['numpy']); }); - test('should uninstall packages when provided', async () => { + test('should uninstall packages when uninstall list provided', async () => { + // Mock - Set up environment and uninstall packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; const packages: Package[] = []; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); managePackagesStub.resolves(packages); + // Run - Execute manage with uninstall option await packageManager.manage(env, { uninstall: ['numpy'] }); + // Assert - Verify managePackages called correctly assert.ok(managePackagesStub.called, 'managePackages should be called'); const args = managePackagesStub.firstCall.args; assert.strictEqual(args[0], env); assert.deepStrictEqual(args[1].uninstall, ['numpy']); }); - test('should prompt user when no packages specified', async () => { + test('should prompt user for packages when none specified', async () => { + // Mock - Set up environment without packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -211,7 +222,6 @@ suite('CondaPackageManager Unit Tests', () => { { name: 'pytest', version: '7.0.0' } as Package, ]; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); @@ -219,31 +229,36 @@ suite('CondaPackageManager Unit Tests', () => { getCommonCondaPackagesToInstallStub.resolves({ install: ['pytest'], uninstall: [] }); managePackagesStub.resolves(packages); + // Run - Execute manage without packages await packageManager.manage(env, {} as any); + // Assert - Verify user was prompted assert.ok(getCommonCondaPackagesToInstallStub.called, 'Should prompt user for packages'); assert.ok(managePackagesStub.called, 'managePackages should be called'); }); - test('should handle cancellation gracefully', async () => { + test('should not install when user cancels package selection', async () => { + // Mock - Set up cancellation scenario const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; getCommonCondaPackagesToInstallStub.resolves(undefined); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); + // Run - Execute manage without packages (user cancels) await packageManager.manage(env, {} as any); + // Assert - Verify operation was cancelled assert.ok(getCommonCondaPackagesToInstallStub.called, 'Should prompt user'); assert.ok(!managePackagesStub.called, 'Should not call managePackages when user cancels'); }); - test('should emit event after managing packages', async () => { + test('should emit onDidChangePackages event after successful package management', async () => { + // Mock - Set up environment and event listener const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -257,51 +272,55 @@ suite('CondaPackageManager Unit Tests', () => { firedEvent = e; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); managePackagesStub.resolves(afterPackages); + // Run - Execute manage await packageManager.manage(env, { install: ['numpy'] }); + // Assert - Verify event was emitted assert.ok(firedEvent, 'Event should be fired'); assert.strictEqual(firedEvent!.environment, env); assert.strictEqual(firedEvent!.manager, packageManager); }); - test('should handle errors and log them', async () => { + test('should log errors when package management fails', async () => { + // Mock - Set up error scenario const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; const error = new Error('Installation failed'); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); managePackagesStub.rejects(error); + // Run - Execute manage with error await packageManager.manage(env, { install: ['numpy'] }); + // Assert - Verify error was logged assert.ok((mockLog.error as sinon.SinonStub).called, 'Should log error'); }); - test('should re-throw CancellationError', async () => { + test('should re-throw CancellationError without logging', async () => { + // Mock - Set up cancellation scenario const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); managePackagesStub.rejects(new CancellationError()); + // Run & Assert - Verify CancellationError is propagated await assert.rejects( async () => packageManager.manage(env, { install: ['numpy'] }), CancellationError, @@ -310,8 +329,9 @@ suite('CondaPackageManager Unit Tests', () => { }); }); - suite('Test 3: CondaPackageManager.refresh', () => { - test('should refresh packages and update cache', async () => { + suite('CondaPackageManager.refresh', () => { + test('should fetch packages and update internal cache', async () => { + // Mock - Set up environment with packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -321,23 +341,24 @@ suite('CondaPackageManager Unit Tests', () => { { name: 'pandas', version: '1.3.0' } as Package, ]; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Execute refresh await packageManager.refresh(env); + // Assert - Verify packages fetched and cached assert.ok(refreshPackagesStub.called, 'refreshPackages should be called'); - // Verify packages are cached const cached = await packageManager.getPackages(env); assert.strictEqual(cached?.length, 2); }); - test('should emit event when packages change', async () => { + test('should emit onDidChangePackages event when packages are modified', async () => { + // Mock - Set up environment and event listener const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -351,19 +372,21 @@ suite('CondaPackageManager Unit Tests', () => { firedEvent = e; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Execute refresh await packageManager.refresh(env); + // Assert - Verify event was emitted assert.ok(firedEvent, 'Event should be fired when packages change'); }); - test('should not emit event when packages unchanged', async () => { + test('should not emit event when package list remains unchanged', async () => { + // Mock - Set up environment with no packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -375,22 +398,24 @@ suite('CondaPackageManager Unit Tests', () => { eventCount++; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Execute refresh twice with no changes await packageManager.refresh(env); await packageManager.refresh(env); + // Assert - Verify event was not emitted assert.strictEqual(eventCount, 0, 'Should not emit event when no changes'); }); }); - suite('Test 4: CondaPackageManager.getPackages', () => { - test('should return cached packages if available', async () => { + suite('CondaPackageManager.getPackages', () => { + test('should use cached packages when available', async () => { + // Mock - Set up environment with cached packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -399,27 +424,25 @@ suite('CondaPackageManager Unit Tests', () => { { name: 'numpy', version: '1.21.0' } as Package, ]; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); - // First call should trigger refresh await packageManager.refresh(env); - - // Reset stub to verify it's not called again refreshPackagesStub.resetHistory(); - // Second call should use cache + // Run - Get packages from cache const result = await packageManager.getPackages(env); + // Assert - Verify cache was used assert.strictEqual(result?.length, 1); assert.ok(!refreshPackagesStub.called, 'Should not call refresh when packages are cached'); }); - test('should refresh if packages not cached', async () => { + test('should trigger refresh when packages not in cache', async () => { + // Mock - Set up environment without cached packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -428,20 +451,22 @@ suite('CondaPackageManager Unit Tests', () => { { name: 'numpy', version: '1.21.0' } as Package, ]; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Get packages without cache const result = await packageManager.getPackages(env); + // Assert - Verify refresh was triggered assert.ok(refreshPackagesStub.called, 'Should call refresh when packages not cached'); assert.strictEqual(result?.length, 1); }); - test('should handle multiple environments separately', async () => { + test('should maintain separate package caches for different environments', async () => { + // Mock - Set up two different environments const env1: PythonEnvironment = { envId: { id: 'env1' }, } as PythonEnvironment; @@ -458,7 +483,6 @@ suite('CondaPackageManager Unit Tests', () => { { name: 'pandas', version: '1.3.0' } as Package, ]; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); @@ -466,9 +490,11 @@ suite('CondaPackageManager Unit Tests', () => { refreshPackagesStub.onFirstCall().resolves(packages1); refreshPackagesStub.onSecondCall().resolves(packages2); + // Run - Get packages for both environments const result1 = await packageManager.getPackages(env1); const result2 = await packageManager.getPackages(env2); + // Assert - Verify each environment has its own cache assert.strictEqual(result1?.length, 1); assert.strictEqual(result1?.[0].name, 'numpy'); assert.strictEqual(result2?.length, 1); @@ -476,16 +502,20 @@ suite('CondaPackageManager Unit Tests', () => { }); }); - suite('Test 5: CondaPackageManager.dispose', () => { - test('should dispose event emitter', () => { + suite('CondaPackageManager.dispose', () => { + test('should clean up event emitter on disposal', () => { + // Mock - Spy on dispose method const disposeStub = sinon.stub(packageManager['_onDidChangePackages'], 'dispose'); + // Run - Dispose package manager packageManager.dispose(); + // Assert - Verify cleanup occurred assert.ok(disposeStub.called, 'Should dispose event emitter'); }); - test('should clear packages cache', () => { + test('should clear all cached packages on disposal', () => { + // Mock - Set up environment with cached packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -494,24 +524,25 @@ suite('CondaPackageManager Unit Tests', () => { { name: 'numpy', version: '1.21.0' } as Package, ]; - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Cache packages then dispose return packageManager.refresh(env).then(() => { packageManager.dispose(); - // Verify cache is cleared by checking the internal map + // Assert - Verify cache was cleared assert.strictEqual(packageManager['packages'].size, 0, 'Should clear packages cache'); }); }); }); - suite('Test 6: Event emission', () => { - test('should fire onDidChangePackages event with correct arguments', async () => { + suite('Event emission', () => { + test('should include environment, manager, and changes in emitted event', async () => { + // Mock - Set up environment and event listener const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -525,22 +556,24 @@ suite('CondaPackageManager Unit Tests', () => { firedEvent = e; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Trigger event await packageManager.refresh(env); + // Assert - Verify event contains all required information assert.ok(firedEvent, 'Event should be fired'); assert.strictEqual(firedEvent!.environment, env, 'Event should include environment'); assert.strictEqual(firedEvent!.manager, packageManager, 'Event should include manager'); assert.ok(Array.isArray(firedEvent!.changes), 'Event should include changes array'); }); - test('should support multiple event listeners', async () => { + test('should notify all registered listeners when event fires', async () => { + // Mock - Set up environment and multiple listeners const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -560,20 +593,22 @@ suite('CondaPackageManager Unit Tests', () => { listener2Called = true; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Trigger event await packageManager.refresh(env); + // Assert - Verify all listeners were notified assert.ok(listener1Called, 'First listener should be called'); assert.ok(listener2Called, 'Second listener should be called'); }); - test('should include package changes in event', async () => { + test('should provide detailed package change information in event', async () => { + // Mock - Set up environment with multiple packages const env: PythonEnvironment = { envId: { id: 'test-env' }, } as PythonEnvironment; @@ -588,15 +623,16 @@ suite('CondaPackageManager Unit Tests', () => { firedEvent = e; }); - // Mock withProgress to execute callback immediately withProgressStub.callsFake(async (_options, callback) => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); refreshPackagesStub.resolves(packages); + // Run - Trigger event with multiple packages await packageManager.refresh(env); + // Assert - Verify event contains detailed change information assert.ok(firedEvent, 'Event should be fired'); assert.strictEqual(firedEvent!.changes.length, 2, 'Should include all package changes'); assert.strictEqual(firedEvent!.changes[0].kind, PackageChangeKind.add); From 535ae6b7596296fdd0045cdb57f80e1f02153377 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:32:59 +0000 Subject: [PATCH 5/5] Fix linting errors: replace 'any' with proper PackageManagementOptions type Replace `{} as any` with `{ install: [] }` to satisfy PackageManagementOptions union type requirements and fix eslint @typescript-eslint/no-explicit-any errors. Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- package-lock.json | 20 ++----------------- .../conda/condaPackageManager.unit.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 212fa155..1d919925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -795,7 +795,6 @@ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -1481,7 +1480,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1525,7 +1523,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1716,7 +1713,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -2411,7 +2407,6 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5123,7 +5118,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5249,7 +5243,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -5296,7 +5289,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -6094,7 +6086,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -6590,8 +6581,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "peer": true + "dev": true }, "acorn-import-attributes": { "version": "1.9.5", @@ -6621,7 +6611,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6754,7 +6743,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, - "peer": true, "requires": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -7236,7 +7224,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9197,8 +9184,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "peer": true + "dev": true }, "uc.micro": { "version": "1.0.6", @@ -9284,7 +9270,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "peer": true, "requires": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -9316,7 +9301,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/src/test/managers/conda/condaPackageManager.unit.test.ts b/src/test/managers/conda/condaPackageManager.unit.test.ts index 58e6a8ed..2a95310e 100644 --- a/src/test/managers/conda/condaPackageManager.unit.test.ts +++ b/src/test/managers/conda/condaPackageManager.unit.test.ts @@ -229,8 +229,8 @@ suite('CondaPackageManager Unit Tests', () => { getCommonCondaPackagesToInstallStub.resolves({ install: ['pytest'], uninstall: [] }); managePackagesStub.resolves(packages); - // Run - Execute manage without packages - await packageManager.manage(env, {} as any); + // Run - Execute manage without packages (empty install array triggers prompt) + await packageManager.manage(env, { install: [] }); // Assert - Verify user was prompted assert.ok(getCommonCondaPackagesToInstallStub.called, 'Should prompt user for packages'); @@ -249,8 +249,8 @@ suite('CondaPackageManager Unit Tests', () => { return await callback({} as Progress<{ message?: string }>, {} as CancellationToken); }); - // Run - Execute manage without packages (user cancels) - await packageManager.manage(env, {} as any); + // Run - Execute manage without packages (user cancels, empty install array triggers prompt) + await packageManager.manage(env, { install: [] }); // Assert - Verify operation was cancelled assert.ok(getCommonCondaPackagesToInstallStub.called, 'Should prompt user');