From 08c70993af532e1ed24cc80e0f4ce784410eb756 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 03:07:08 +0000 Subject: [PATCH 1/6] Initial plan From eb529f1cbedde07daf1faf35f4101e90b3982927 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 03:14:33 +0000 Subject: [PATCH 2/6] Initial analysis of requirements file discovery issue Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- test-glob.js | 25 +++++++++++++++++++++++++ test-glob2.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 test-glob.js create mode 100644 test-glob2.js diff --git a/test-glob.js b/test-glob.js new file mode 100644 index 00000000..ca52128c --- /dev/null +++ b/test-glob.js @@ -0,0 +1,25 @@ +const glob = require('glob'); +const fs = require('fs'); +const path = require('path'); + +// Create test directory +const testDir = '/tmp/glob-test-req'; +if (!fs.existsSync(testDir)) { + fs.mkdirSync(testDir, { recursive: true }); +} + +// Create test files +fs.writeFileSync(path.join(testDir, 'requirements.txt'), ''); +fs.writeFileSync(path.join(testDir, 'dev-requirements.txt'), ''); +fs.writeFileSync(path.join(testDir, 'test-requirements.txt'), ''); + +// Test pattern +const pattern = '**/*requirements*.txt'; +console.log('Testing pattern:', pattern); +console.log('CWD:', testDir); + +const matched = glob.sync(pattern, { cwd: testDir }); +console.log('Matched files:', matched); + +// Cleanup +fs.rmSync(testDir, { recursive: true, force: true }); diff --git a/test-glob2.js b/test-glob2.js new file mode 100644 index 00000000..9e585cf2 --- /dev/null +++ b/test-glob2.js @@ -0,0 +1,37 @@ +const glob = require('glob'); +const fs = require('fs'); +const path = require('path'); + +// Create test directory structure +const testDir = '/tmp/glob-test-req2'; +if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); +} +fs.mkdirSync(testDir, { recursive: true }); +fs.mkdirSync(path.join(testDir, 'subdir'), { recursive: true }); + +// Create test files at root +fs.writeFileSync(path.join(testDir, 'requirements.txt'), ''); +fs.writeFileSync(path.join(testDir, 'dev-requirements.txt'), ''); +fs.writeFileSync(path.join(testDir, 'test-requirements.txt'), ''); + +// Create test files in subdir +fs.writeFileSync(path.join(testDir, 'subdir', 'requirements.txt'), ''); +fs.writeFileSync(path.join(testDir, 'subdir', 'dev-requirements.txt'), ''); + +// Test different patterns +const patterns = [ + '**/*requirements*.txt', + '*requirements*.txt', + '**requirements*.txt', + '{*requirements*.txt,**/*requirements*.txt}', +]; + +patterns.forEach(pattern => { + console.log('\nPattern:', pattern); + const matched = glob.sync(pattern, { cwd: testDir }); + console.log('Matched:', matched.sort()); +}); + +// Cleanup +fs.rmSync(testDir, { recursive: true, force: true }); From bc01e817420256e11d2e87efe7a572b83972b70a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 03:21:23 +0000 Subject: [PATCH 3/6] Add explicit root-level requirements file search pattern - Add `*requirements*.txt` pattern to find requirements files at workspace root - This ensures files like dev-requirements.txt, test-requirements.txt are found - Add deduplication logic to handle overlapping search patterns - All existing unit tests pass Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/managers/builtin/pipUtils.ts | 8 ++++++- test-glob.js | 25 --------------------- test-glob2.js | 37 -------------------------------- 3 files changed, 7 insertions(+), 63 deletions(-) delete mode 100644 test-glob.js delete mode 100644 test-glob2.js diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index ef3ef88b..5944b39c 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -183,13 +183,19 @@ export async function getProjectInstallable( const results: Uri[] = ( await Promise.all([ findFiles('**/*requirements*.txt', exclude, undefined, token), + findFiles('*requirements*.txt', exclude, undefined, token), findFiles('**/requirements/*.txt', exclude, undefined, token), findFiles('**/pyproject.toml', exclude, undefined, token), ]) ).flat(); + // Deduplicate by fsPath + const uniqueResults = Array.from( + new Map(results.map((uri) => [uri.fsPath, uri])).values(), + ); + const fsPaths = projects.map((p) => p.uri.fsPath); - const filtered = results + const filtered = uniqueResults .filter((uri) => { const p = api.getPythonProject(uri)?.uri.fsPath; return p && fsPaths.includes(p); diff --git a/test-glob.js b/test-glob.js deleted file mode 100644 index ca52128c..00000000 --- a/test-glob.js +++ /dev/null @@ -1,25 +0,0 @@ -const glob = require('glob'); -const fs = require('fs'); -const path = require('path'); - -// Create test directory -const testDir = '/tmp/glob-test-req'; -if (!fs.existsSync(testDir)) { - fs.mkdirSync(testDir, { recursive: true }); -} - -// Create test files -fs.writeFileSync(path.join(testDir, 'requirements.txt'), ''); -fs.writeFileSync(path.join(testDir, 'dev-requirements.txt'), ''); -fs.writeFileSync(path.join(testDir, 'test-requirements.txt'), ''); - -// Test pattern -const pattern = '**/*requirements*.txt'; -console.log('Testing pattern:', pattern); -console.log('CWD:', testDir); - -const matched = glob.sync(pattern, { cwd: testDir }); -console.log('Matched files:', matched); - -// Cleanup -fs.rmSync(testDir, { recursive: true, force: true }); diff --git a/test-glob2.js b/test-glob2.js deleted file mode 100644 index 9e585cf2..00000000 --- a/test-glob2.js +++ /dev/null @@ -1,37 +0,0 @@ -const glob = require('glob'); -const fs = require('fs'); -const path = require('path'); - -// Create test directory structure -const testDir = '/tmp/glob-test-req2'; -if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); -} -fs.mkdirSync(testDir, { recursive: true }); -fs.mkdirSync(path.join(testDir, 'subdir'), { recursive: true }); - -// Create test files at root -fs.writeFileSync(path.join(testDir, 'requirements.txt'), ''); -fs.writeFileSync(path.join(testDir, 'dev-requirements.txt'), ''); -fs.writeFileSync(path.join(testDir, 'test-requirements.txt'), ''); - -// Create test files in subdir -fs.writeFileSync(path.join(testDir, 'subdir', 'requirements.txt'), ''); -fs.writeFileSync(path.join(testDir, 'subdir', 'dev-requirements.txt'), ''); - -// Test different patterns -const patterns = [ - '**/*requirements*.txt', - '*requirements*.txt', - '**requirements*.txt', - '{*requirements*.txt,**/*requirements*.txt}', -]; - -patterns.forEach(pattern => { - console.log('\nPattern:', pattern); - const matched = glob.sync(pattern, { cwd: testDir }); - console.log('Matched:', matched.sort()); -}); - -// Cleanup -fs.rmSync(testDir, { recursive: true, force: true }); From d1b84fc1937ffe21082597eed2e4e38b60f64bd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 03:27:47 +0000 Subject: [PATCH 4/6] Add comprehensive tests for getProjectInstallable - Test finding dev-requirements.txt at workspace root - Test deduplication of files found by multiple patterns - Test finding files in subdirectories and requirements/ folders - Test filtering files by project directories - All 181 unit tests passing (5 new tests added) Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../managers/builtin/pipUtils.unit.test.ts | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/test/managers/builtin/pipUtils.unit.test.ts diff --git a/src/test/managers/builtin/pipUtils.unit.test.ts b/src/test/managers/builtin/pipUtils.unit.test.ts new file mode 100644 index 00000000..442f75d3 --- /dev/null +++ b/src/test/managers/builtin/pipUtils.unit.test.ts @@ -0,0 +1,186 @@ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { getProjectInstallable } from '../../../managers/builtin/pipUtils'; +import * as wapi from '../../../common/workspace.apis'; +import * as winapi from '../../../common/window.apis'; + +suite('Pip Utils - getProjectInstallable', () => { + let findFilesStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + let mockApi: any; + + setup(() => { + findFilesStub = sinon.stub(wapi, 'findFiles'); + // Stub withProgress to immediately execute the callback + withProgressStub = sinon.stub(winapi, 'withProgress'); + withProgressStub.callsFake(async (_options: any, callback: any) => { + return await callback(undefined, { isCancellationRequested: false }); + }); + + mockApi = { + getPythonProject: (uri: Uri) => { + // Return a project for any URI in /workspace + if (uri.fsPath.startsWith('/workspace')) { + return { uri: Uri.file('/workspace') }; + } + return undefined; + }, + }; + }); + + teardown(() => { + sinon.restore(); + }); + + test('should find dev-requirements.txt at workspace root', async () => { + // Arrange: Mock findFiles to return both requirements.txt and dev-requirements.txt + findFilesStub.callsFake((pattern: string) => { + if (pattern === '**/*requirements*.txt') { + // This pattern might not match root-level files in VS Code + return Promise.resolve([]); + } else if (pattern === '*requirements*.txt') { + // This pattern should match root-level files + return Promise.resolve([ + Uri.file('/workspace/requirements.txt'), + Uri.file('/workspace/dev-requirements.txt'), + Uri.file('/workspace/test-requirements.txt'), + ]); + } else if (pattern === '**/requirements/*.txt') { + return Promise.resolve([]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act: Call getProjectInstallable + const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; + const result = await getProjectInstallable(mockApi, projects); + + // Assert: Should find all three requirements files + assert.strictEqual(result.length, 3, 'Should find three requirements files'); + + const names = result.map((r) => r.name).sort(); + assert.deepStrictEqual( + names, + ['dev-requirements.txt', 'requirements.txt', 'test-requirements.txt'], + 'Should find requirements.txt, dev-requirements.txt, and test-requirements.txt', + ); + + // Verify each file has correct properties + result.forEach((item) => { + assert.strictEqual(item.group, 'Requirements', 'Should be in Requirements group'); + assert.ok(item.args, 'Should have args'); + assert.strictEqual(item.args?.length, 2, 'Should have 2 args'); + assert.strictEqual(item.args?.[0], '-r', 'First arg should be -r'); + assert.ok(item.uri, 'Should have a URI'); + }); + }); + + test('should deduplicate files found by multiple patterns', async () => { + // Arrange: Mock both patterns to return the same file + findFilesStub.callsFake((pattern: string) => { + if (pattern === '**/*requirements*.txt') { + return Promise.resolve([ + Uri.file('/workspace/dev-requirements.txt'), + ]); + } else if (pattern === '*requirements*.txt') { + return Promise.resolve([ + Uri.file('/workspace/dev-requirements.txt'), + Uri.file('/workspace/requirements.txt'), + ]); + } else if (pattern === '**/requirements/*.txt') { + return Promise.resolve([]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act: Call getProjectInstallable + const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; + const result = await getProjectInstallable(mockApi, projects); + + // Assert: Should deduplicate and only have 2 unique files + assert.strictEqual(result.length, 2, 'Should deduplicate and have 2 unique files'); + + const names = result.map((r) => r.name).sort(); + assert.deepStrictEqual( + names, + ['dev-requirements.txt', 'requirements.txt'], + 'Should have deduplicated results', + ); + }); + + test('should find requirements files in subdirectories', async () => { + // Arrange: Mock findFiles to return files in subdirectories + findFilesStub.callsFake((pattern: string) => { + if (pattern === '**/*requirements*.txt') { + return Promise.resolve([ + Uri.file('/workspace/subdir/dev-requirements.txt'), + ]); + } else if (pattern === '*requirements*.txt') { + return Promise.resolve([ + Uri.file('/workspace/requirements.txt'), + ]); + } else if (pattern === '**/requirements/*.txt') { + return Promise.resolve([ + Uri.file('/workspace/requirements/test.txt'), + ]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act: Call getProjectInstallable + const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; + const result = await getProjectInstallable(mockApi, projects); + + // Assert: Should find all files + assert.strictEqual(result.length, 3, 'Should find three files'); + + const names = result.map((r) => r.name).sort(); + assert.deepStrictEqual( + names, + ['dev-requirements.txt', 'requirements.txt', 'test.txt'], + 'Should find files at different levels', + ); + }); + + test('should return empty array when no projects provided', async () => { + // Act: Call with no projects + const result = await getProjectInstallable(mockApi, undefined); + + // Assert: Should return empty array + assert.strictEqual(result.length, 0, 'Should return empty array'); + assert.ok(!findFilesStub.called, 'Should not call findFiles when no projects'); + }); + + test('should filter out files not in project directories', async () => { + // Arrange: Mock findFiles to return files from multiple directories + findFilesStub.callsFake((pattern: string) => { + if (pattern === '*requirements*.txt') { + return Promise.resolve([ + Uri.file('/workspace/requirements.txt'), + Uri.file('/other-dir/requirements.txt'), // Should be filtered out + ]); + } else { + return Promise.resolve([]); + } + }); + + // Act: Call with only /workspace project + const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; + const result = await getProjectInstallable(mockApi, projects); + + // Assert: Should only include files from /workspace + assert.strictEqual(result.length, 1, 'Should only include files from project directory'); + const firstResult = result[0]; + assert.ok(firstResult, 'Should have at least one result'); + assert.strictEqual(firstResult.name, 'requirements.txt'); + assert.ok(firstResult.uri, 'Should have a URI'); + assert.ok(firstResult.uri.fsPath.startsWith('/workspace'), 'Should be in workspace directory'); + }); +}); From af93b59ae4c42e855c7c1fb7a18802a823f0f2ae Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:15:17 -0700 Subject: [PATCH 5/6] update tests and instructions --- .../testing-workflow.instructions.md | 9 +- .../managers/builtin/pipUtils.unit.test.ts | 116 ++++++++++-------- 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md index 2c56f53a..b5be4b3a 100644 --- a/.github/instructions/testing-workflow.instructions.md +++ b/.github/instructions/testing-workflow.instructions.md @@ -537,17 +537,16 @@ envConfig.inspect - ❌ Tests that don't clean up mocks properly - ❌ Overly complex test setup that's hard to understand -## 🧠 Agent Learning Patterns - -### Key Implementation Insights +## 🧠 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 (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 (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 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) diff --git a/src/test/managers/builtin/pipUtils.unit.test.ts b/src/test/managers/builtin/pipUtils.unit.test.ts index 442f75d3..300a2b72 100644 --- a/src/test/managers/builtin/pipUtils.unit.test.ts +++ b/src/test/managers/builtin/pipUtils.unit.test.ts @@ -1,28 +1,44 @@ import assert from 'assert'; +import * as path from 'path'; import * as sinon from 'sinon'; -import { Uri } from 'vscode'; -import { getProjectInstallable } from '../../../managers/builtin/pipUtils'; -import * as wapi from '../../../common/workspace.apis'; +import { CancellationToken, Progress, ProgressOptions, Uri } from 'vscode'; +import { PythonEnvironmentApi, PythonProject } from '../../../api'; import * as winapi from '../../../common/window.apis'; +import * as wapi from '../../../common/workspace.apis'; +import { getProjectInstallable } from '../../../managers/builtin/pipUtils'; suite('Pip Utils - getProjectInstallable', () => { let findFilesStub: sinon.SinonStub; let withProgressStub: sinon.SinonStub; - let mockApi: any; + // Minimal mock that only implements the methods we need for this test + // Using type assertion to satisfy TypeScript since we only need getPythonProject + let mockApi: { getPythonProject: (uri: Uri) => PythonProject | undefined }; setup(() => { findFilesStub = sinon.stub(wapi, 'findFiles'); // Stub withProgress to immediately execute the callback withProgressStub = sinon.stub(winapi, 'withProgress'); - withProgressStub.callsFake(async (_options: any, callback: any) => { - return await callback(undefined, { isCancellationRequested: false }); - }); - + withProgressStub.callsFake( + async ( + _options: ProgressOptions, + callback: ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, + ) => Thenable, + ) => { + return await callback( + {} as Progress<{ message?: string; increment?: number }>, + { isCancellationRequested: false } as CancellationToken, + ); + }, + ); + + const workspacePath = path.resolve('/workspace'); mockApi = { getPythonProject: (uri: Uri) => { - // Return a project for any URI in /workspace - if (uri.fsPath.startsWith('/workspace')) { - return { uri: Uri.file('/workspace') }; + // Return a project for any URI in workspace + if (uri.fsPath.startsWith(workspacePath)) { + return { name: 'workspace', uri: Uri.file(workspacePath) }; } return undefined; }, @@ -41,10 +57,11 @@ suite('Pip Utils - getProjectInstallable', () => { return Promise.resolve([]); } else if (pattern === '*requirements*.txt') { // This pattern should match root-level files + const workspacePath = path.resolve('/workspace'); return Promise.resolve([ - Uri.file('/workspace/requirements.txt'), - Uri.file('/workspace/dev-requirements.txt'), - Uri.file('/workspace/test-requirements.txt'), + Uri.file(path.join(workspacePath, 'requirements.txt')), + Uri.file(path.join(workspacePath, 'dev-requirements.txt')), + Uri.file(path.join(workspacePath, 'test-requirements.txt')), ]); } else if (pattern === '**/requirements/*.txt') { return Promise.resolve([]); @@ -55,12 +72,13 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act: Call getProjectInstallable - const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; - const result = await getProjectInstallable(mockApi, projects); + const workspacePath = path.resolve('/workspace'); + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); // Assert: Should find all three requirements files assert.strictEqual(result.length, 3, 'Should find three requirements files'); - + const names = result.map((r) => r.name).sort(); assert.deepStrictEqual( names, @@ -82,13 +100,13 @@ suite('Pip Utils - getProjectInstallable', () => { // Arrange: Mock both patterns to return the same file findFilesStub.callsFake((pattern: string) => { if (pattern === '**/*requirements*.txt') { - return Promise.resolve([ - Uri.file('/workspace/dev-requirements.txt'), - ]); + const workspacePath = path.resolve('/workspace'); + return Promise.resolve([Uri.file(path.join(workspacePath, 'dev-requirements.txt'))]); } else if (pattern === '*requirements*.txt') { + const workspacePath = path.resolve('/workspace'); return Promise.resolve([ - Uri.file('/workspace/dev-requirements.txt'), - Uri.file('/workspace/requirements.txt'), + Uri.file(path.join(workspacePath, 'dev-requirements.txt')), + Uri.file(path.join(workspacePath, 'requirements.txt')), ]); } else if (pattern === '**/requirements/*.txt') { return Promise.resolve([]); @@ -99,35 +117,29 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act: Call getProjectInstallable - const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; - const result = await getProjectInstallable(mockApi, projects); + const workspacePath = path.resolve('/workspace'); + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); // Assert: Should deduplicate and only have 2 unique files assert.strictEqual(result.length, 2, 'Should deduplicate and have 2 unique files'); - + const names = result.map((r) => r.name).sort(); - assert.deepStrictEqual( - names, - ['dev-requirements.txt', 'requirements.txt'], - 'Should have deduplicated results', - ); + assert.deepStrictEqual(names, ['dev-requirements.txt', 'requirements.txt'], 'Should have deduplicated results'); }); test('should find requirements files in subdirectories', async () => { // Arrange: Mock findFiles to return files in subdirectories findFilesStub.callsFake((pattern: string) => { if (pattern === '**/*requirements*.txt') { - return Promise.resolve([ - Uri.file('/workspace/subdir/dev-requirements.txt'), - ]); + const workspacePath = path.resolve('/workspace'); + return Promise.resolve([Uri.file(path.join(workspacePath, 'subdir', 'dev-requirements.txt'))]); } else if (pattern === '*requirements*.txt') { - return Promise.resolve([ - Uri.file('/workspace/requirements.txt'), - ]); + const workspacePath = path.resolve('/workspace'); + return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements.txt'))]); } else if (pattern === '**/requirements/*.txt') { - return Promise.resolve([ - Uri.file('/workspace/requirements/test.txt'), - ]); + const workspacePath = path.resolve('/workspace'); + return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements', 'test.txt'))]); } else if (pattern === '**/pyproject.toml') { return Promise.resolve([]); } @@ -135,12 +147,13 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act: Call getProjectInstallable - const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; - const result = await getProjectInstallable(mockApi, projects); + const workspacePath = path.resolve('/workspace'); + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); // Assert: Should find all files assert.strictEqual(result.length, 3, 'Should find three files'); - + const names = result.map((r) => r.name).sort(); assert.deepStrictEqual( names, @@ -151,7 +164,7 @@ suite('Pip Utils - getProjectInstallable', () => { test('should return empty array when no projects provided', async () => { // Act: Call with no projects - const result = await getProjectInstallable(mockApi, undefined); + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, undefined); // Assert: Should return empty array assert.strictEqual(result.length, 0, 'Should return empty array'); @@ -162,25 +175,28 @@ suite('Pip Utils - getProjectInstallable', () => { // Arrange: Mock findFiles to return files from multiple directories findFilesStub.callsFake((pattern: string) => { if (pattern === '*requirements*.txt') { + const workspacePath = path.resolve('/workspace'); + const otherPath = path.resolve('/other-dir'); return Promise.resolve([ - Uri.file('/workspace/requirements.txt'), - Uri.file('/other-dir/requirements.txt'), // Should be filtered out + Uri.file(path.join(workspacePath, 'requirements.txt')), + Uri.file(path.join(otherPath, 'requirements.txt')), // Should be filtered out ]); } else { return Promise.resolve([]); } }); - // Act: Call with only /workspace project - const projects = [{ name: 'workspace', uri: Uri.file('/workspace') }]; - const result = await getProjectInstallable(mockApi, projects); + // Act: Call with only workspace project + const workspacePath = path.resolve('/workspace'); + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); - // Assert: Should only include files from /workspace + // Assert: Should only include files from workspace assert.strictEqual(result.length, 1, 'Should only include files from project directory'); const firstResult = result[0]; assert.ok(firstResult, 'Should have at least one result'); assert.strictEqual(firstResult.name, 'requirements.txt'); assert.ok(firstResult.uri, 'Should have a URI'); - assert.ok(firstResult.uri.fsPath.startsWith('/workspace'), 'Should be in workspace directory'); + assert.ok(firstResult.uri.fsPath.startsWith(workspacePath), 'Should be in workspace directory'); }); }); From 855cdf5b5819a0388260e24571e69bdb90c92ba2 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:26:49 -0700 Subject: [PATCH 6/6] . --- src/managers/builtin/pipUtils.ts | 4 +-- .../managers/builtin/pipUtils.unit.test.ts | 26 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 5944b39c..f8c5279f 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -190,9 +190,7 @@ export async function getProjectInstallable( ).flat(); // Deduplicate by fsPath - const uniqueResults = Array.from( - new Map(results.map((uri) => [uri.fsPath, uri])).values(), - ); + const uniqueResults = Array.from(new Map(results.map((uri) => [uri.fsPath, uri])).values()); const fsPaths = projects.map((p) => p.uri.fsPath); const filtered = uniqueResults diff --git a/src/test/managers/builtin/pipUtils.unit.test.ts b/src/test/managers/builtin/pipUtils.unit.test.ts index 300a2b72..076596e8 100644 --- a/src/test/managers/builtin/pipUtils.unit.test.ts +++ b/src/test/managers/builtin/pipUtils.unit.test.ts @@ -33,7 +33,7 @@ suite('Pip Utils - getProjectInstallable', () => { }, ); - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; mockApi = { getPythonProject: (uri: Uri) => { // Return a project for any URI in workspace @@ -57,7 +57,7 @@ suite('Pip Utils - getProjectInstallable', () => { return Promise.resolve([]); } else if (pattern === '*requirements*.txt') { // This pattern should match root-level files - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; return Promise.resolve([ Uri.file(path.join(workspacePath, 'requirements.txt')), Uri.file(path.join(workspacePath, 'dev-requirements.txt')), @@ -72,7 +72,7 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act: Call getProjectInstallable - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); @@ -100,10 +100,10 @@ suite('Pip Utils - getProjectInstallable', () => { // Arrange: Mock both patterns to return the same file findFilesStub.callsFake((pattern: string) => { if (pattern === '**/*requirements*.txt') { - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; return Promise.resolve([Uri.file(path.join(workspacePath, 'dev-requirements.txt'))]); } else if (pattern === '*requirements*.txt') { - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; return Promise.resolve([ Uri.file(path.join(workspacePath, 'dev-requirements.txt')), Uri.file(path.join(workspacePath, 'requirements.txt')), @@ -117,7 +117,7 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act: Call getProjectInstallable - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); @@ -132,13 +132,13 @@ suite('Pip Utils - getProjectInstallable', () => { // Arrange: Mock findFiles to return files in subdirectories findFilesStub.callsFake((pattern: string) => { if (pattern === '**/*requirements*.txt') { - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; return Promise.resolve([Uri.file(path.join(workspacePath, 'subdir', 'dev-requirements.txt'))]); } else if (pattern === '*requirements*.txt') { - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements.txt'))]); } else if (pattern === '**/requirements/*.txt') { - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements', 'test.txt'))]); } else if (pattern === '**/pyproject.toml') { return Promise.resolve([]); @@ -147,7 +147,7 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act: Call getProjectInstallable - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); @@ -175,8 +175,8 @@ suite('Pip Utils - getProjectInstallable', () => { // Arrange: Mock findFiles to return files from multiple directories findFilesStub.callsFake((pattern: string) => { if (pattern === '*requirements*.txt') { - const workspacePath = path.resolve('/workspace'); - const otherPath = path.resolve('/other-dir'); + const workspacePath = Uri.file('/test/path/root').fsPath; + const otherPath = Uri.file('/other-dir').fsPath; return Promise.resolve([ Uri.file(path.join(workspacePath, 'requirements.txt')), Uri.file(path.join(otherPath, 'requirements.txt')), // Should be filtered out @@ -187,7 +187,7 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act: Call with only workspace project - const workspacePath = path.resolve('/workspace'); + const workspacePath = Uri.file('/test/path/root').fsPath; const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects);