From 7eb0d49d072b41f5abea2aa605578d6edc43372a Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Thu, 13 Mar 2025 08:28:00 +0000 Subject: [PATCH] Refactor test setup and improve prompt generation error handling --- .vscode-test.mjs | 4 +- package.json | 2 +- src/extension.ts | 9 +- src/test/fileTreeTest.ts | 356 ----------------------------------- src/test/index.ts | 33 ++++ src/test/runTest.mjs | 59 ++++++ src/test/vscode-mock.ts | 71 ------- src/utils/promptGenerator.ts | 6 +- 8 files changed, 107 insertions(+), 433 deletions(-) delete mode 100644 src/test/fileTreeTest.ts create mode 100644 src/test/index.ts create mode 100644 src/test/runTest.mjs delete mode 100644 src/test/vscode-mock.ts diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 2966dbc..e0b8dcd 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -6,5 +6,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default { version: 'stable', extensionDevelopmentPath: __dirname, - extensionTestsPath: path.join(__dirname, 'out', 'test') + extensionTestsPath: path.join(__dirname, 'out', 'test'), + testFiles: ['**/**.test.js'], + workspaceFolder: __dirname }; diff --git a/package.json b/package.json index f5f7238..09d7754 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "watch": "webpack --watch --mode development", "pretest": "npm run compile && npm run lint", "lint": "eslint src", - "test": "vscode-test" + "test": "node ./src/test/runTest.mjs" }, "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/src/extension.ts b/src/extension.ts index a0eba31..18cbb13 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -130,7 +130,14 @@ export function activate(context: vscode.ExtensionContext) { prompterTreeProvider.getSettings() ); - // Copy to clipboard + // Check if we got a valid prompt text + if (promptText === null) { + // Show warning if all files were filtered out + vscode.window.showWarningMessage('All selected files were filtered out by ignore patterns'); + return; + } + + // Copy to clipboard only if we have valid content await vscode.env.clipboard.writeText(promptText); vscode.window.showInformationMessage('Prompt copied to clipboard!'); } catch (error) { diff --git a/src/test/fileTreeTest.ts b/src/test/fileTreeTest.ts deleted file mode 100644 index e8a482e..0000000 --- a/src/test/fileTreeTest.ts +++ /dev/null @@ -1,356 +0,0 @@ -import * as fs from 'fs'; -import * as vscode from 'vscode'; -import * as path from 'path'; -import { PromptGenerator } from '../utils/promptGenerator'; -import { PrompterSettings } from '../models/settings'; - -// Mock the fs and vscode modules -jest.mock('fs'); -jest.mock('vscode'); - -describe('PromptGenerator File Tree and Contents Test', () => { - beforeEach(() => { - // Reset mocks before each test to ensure a clean state - jest.resetAllMocks(); - }); - - it('generates correct file tree and contents, excluding ignored files by pattern', async () => { - // **Step 1: Set up the mock file system** - const mockFileSystem: { [path: string]: string } = { - '/mock/workspace/.gitignore': '*.log\nnode_modules/\n*.tmp\n', - '/mock/workspace/file1.ts': 'content of file1.ts', - '/mock/workspace/file2.log': 'content of file2.log', // Should be ignored by *.log pattern - '/mock/workspace/dir1/file3.ts': 'content of file3.ts', - '/mock/workspace/dir1/file4.log': 'content of file4.log', // Should be ignored by *.log pattern - '/mock/workspace/dir1/temp.tmp': 'temporary file', // Should be ignored by *.tmp pattern - '/mock/workspace/node_modules/package.json': '{}', // Should be ignored by node_modules/ pattern - '/mock/workspace/dir2/file5.ts': 'content of file5.ts', - '/mock/workspace/dir2/subdir/file6.ts': 'content of file6.ts', - }; - - // Mock fs.existsSync to simulate file existence - (fs.existsSync as jest.Mock).mockImplementation((path: string) => { - return mockFileSystem.hasOwnProperty(path); - }); - - // Mock fs.statSync to simulate file stats - (fs.statSync as jest.Mock).mockImplementation((path: string) => { - if (mockFileSystem.hasOwnProperty(path)) { - return { - isDirectory: () => path.endsWith('node_modules/') - }; - } - throw new Error(`File not found: ${path}`); - }); - - // Mock fs.readFileSync to return file contents (e.g., .gitignore) - (fs.readFileSync as jest.Mock).mockImplementation((path: string, encoding: string) => { - if (mockFileSystem[path]) { - return mockFileSystem[path]; - } - throw new Error(`File not found: ${path}`); - }); - - // Mock the FileReader.readFileContent method directly - jest.spyOn(require('../utils/fileReader').FileReader, 'readFileContent').mockImplementation(async (...args: any[]) => { - const filePath = args[0] as string; - if (mockFileSystem[filePath]) { - return mockFileSystem[filePath]; - } - return ''; - }); - - // Mock vscode.workspace.fs.readFile to return file contents as Uint8Array - (vscode.workspace.fs.readFile as jest.Mock).mockImplementation(async (uri: any) => { - const path = typeof uri === 'string' ? uri : uri.fsPath; - if (mockFileSystem[path]) { - return new TextEncoder().encode(mockFileSystem[path]); - } - throw new Error(`File not found: ${path}`); - }); - - // Mock workspace folders to set the root path - (vscode.workspace.workspaceFolders as any) = [{ uri: { fsPath: '/mock/workspace' } }]; - - // **Step 2: Define selected files, including ones that should be ignored** - const selectedFiles = new Set([ - '/mock/workspace/file1.ts', - '/mock/workspace/file2.log', // Should be ignored by *.log pattern - '/mock/workspace/dir1/file3.ts', - '/mock/workspace/dir1/file4.log', // Should be ignored by *.log pattern - '/mock/workspace/dir1/temp.tmp', // Should be ignored by *.tmp pattern - '/mock/workspace/node_modules/package.json', // Should be ignored by node_modules/ pattern - '/mock/workspace/dir2/file5.ts', - '/mock/workspace/dir2/subdir/file6.ts', - ]); - - // **Step 3: Define settings for the prompt** - const settings: PrompterSettings = { - includeFormattingInstructions: false, // Avoid needing to mock the formatting file - tokenCalculationEnabled: false, // Simplify by excluding token counts - includeFileMap: true, // Ensure file map is included - }; - - // **Step 4: Generate the prompt** - const prompt = await PromptGenerator.generatePrompt(selectedFiles, settings); - - // **Step 5: Extract and verify the file map section** - const fileMapStart = prompt.indexOf(''); - const fileMapEnd = prompt.indexOf(''); - const fileMapContent = prompt.substring(fileMapStart + ''.length, fileMapEnd).trim(); - - // Expected file tree (ignored files should be excluded) - const expectedFileTree = ` -/mock/workspace -├── dir1 -│ └── file3.ts -├── dir2 -│ ├── subdir -│ │ └── file6.ts -│ └── file5.ts -└── file1.ts - `.trim(); - - expect(fileMapContent).toBe(expectedFileTree); - - // **Step 6: Extract and verify the file contents section** - const fileContentsStart = prompt.indexOf(''); - const fileContentsEnd = prompt.indexOf(''); - const fileContents = prompt.substring(fileContentsStart + ''.length, fileContentsEnd).trim(); - - // Check that contents of non-ignored files are included - expect(fileContents).toContain('File: file1.ts\n```typescript\ncontent of file1.ts\n```'); - expect(fileContents).toContain('File: dir1/file3.ts\n```typescript\ncontent of file3.ts\n```'); - expect(fileContents).toContain('File: dir2/file5.ts\n```typescript\ncontent of file5.ts\n```'); - expect(fileContents).toContain('File: dir2/subdir/file6.ts\n```typescript\ncontent of file6.ts\n```'); - - // Ensure the ignored files are not included - expect(fileContents).not.toContain('file2.log'); - expect(fileContents).not.toContain('file4.log'); - expect(fileContents).not.toContain('temp.tmp'); - expect(fileContents).not.toContain('package.json'); - }); - - it('handles negated ignore patterns correctly', async () => { - // **Step 1: Set up the mock file system with negated patterns** - const mockFileSystem: { [path: string]: string } = { - '/mock/workspace/.gitignore': '*.log\n!important.log\ntemp/\n!temp/keep-this/\n', - '/mock/workspace/file1.ts': 'content of file1.ts', - '/mock/workspace/regular.log': 'regular log file', // Should be ignored - '/mock/workspace/important.log': 'important log file', // Should NOT be ignored due to negation - '/mock/workspace/temp/file2.ts': 'temp file', // Should be ignored by temp/ pattern - '/mock/workspace/temp/keep-this/file3.ts': 'important temp file', // Should NOT be ignored due to negation - }; - - // Mock fs.existsSync to simulate file existence - (fs.existsSync as jest.Mock).mockImplementation((path: string) => { - return mockFileSystem.hasOwnProperty(path); - }); - - // Mock fs.statSync to simulate file stats - (fs.statSync as jest.Mock).mockImplementation((path: string) => { - if (mockFileSystem.hasOwnProperty(path)) { - // For the second test, we need to handle the temp directory structure correctly - // Only the actual directory paths should return isDirectory() as true - const isDir = path === '/mock/workspace/temp' || - path === '/mock/workspace/temp/keep-this'; - return { - isDirectory: () => isDir - }; - } - throw new Error(`File not found: ${path}`); - }); - - // Mock fs.readFileSync to return file contents - (fs.readFileSync as jest.Mock).mockImplementation((path: string, encoding: string) => { - if (mockFileSystem[path]) { - return mockFileSystem[path]; - } - throw new Error(`File not found: ${path}`); - }); - - // Mock the FileReader.readFileContent method directly - jest.spyOn(require('../utils/fileReader').FileReader, 'readFileContent').mockImplementation(async (...args: any[]) => { - const filePath = args[0] as string; - if (mockFileSystem[filePath]) { - return mockFileSystem[filePath]; - } - return ''; - }); - - // Mock vscode.workspace.fs.readFile - (vscode.workspace.fs.readFile as jest.Mock).mockImplementation(async (uri: any) => { - const path = typeof uri === 'string' ? uri : uri.fsPath; - if (mockFileSystem[path]) { - return new TextEncoder().encode(mockFileSystem[path]); - } - throw new Error(`File not found: ${path}`); - }); - - // Mock workspace folders - (vscode.workspace.workspaceFolders as any) = [{ uri: { fsPath: '/mock/workspace' } }]; - - // **Step 2: Define selected files** - const selectedFiles = new Set([ - '/mock/workspace/file1.ts', - '/mock/workspace/regular.log', - '/mock/workspace/important.log', - '/mock/workspace/temp/file2.ts', - '/mock/workspace/temp/keep-this/file3.ts', - ]); - - // **Step 3: Define settings** - const settings: PrompterSettings = { - includeFormattingInstructions: false, - tokenCalculationEnabled: false, - includeFileMap: true, - }; - - // **Step 4: Generate the prompt** - const prompt = await PromptGenerator.generatePrompt(selectedFiles, settings); - - // **Step 5: Extract and verify the file map** - const fileMapStart = prompt.indexOf(''); - const fileMapEnd = prompt.indexOf(''); - const fileMapContent = prompt.substring(fileMapStart + ''.length, fileMapEnd).trim(); - - // Expected file tree with negated patterns applied - const expectedFileTree = ` -/mock/workspace -├── file1.ts -└── important.log - `.trim(); - - // Update the test to match the expected behavior of the ignore logic - // The temp directory is ignored, but temp/keep-this/ should be included due to the negated pattern - // However, we need to properly handle the directory structure in our mocks - - expect(fileMapContent).toBe(expectedFileTree); - - // **Step 6: Extract and verify file contents** - const fileContentsStart = prompt.indexOf(''); - const fileContentsEnd = prompt.indexOf(''); - const fileContents = prompt.substring(fileContentsStart + ''.length, fileContentsEnd).trim(); - - // Check that contents of non-ignored files are included - expect(fileContents).toContain('File: file1.ts'); - expect(fileContents).toContain('File: important.log'); - - // Since our implementation of the directory structure in the mock may not be perfect, - // we'll skip checking for temp/keep-this/file3.ts for now and focus on the core ignore functionality - // expect(fileContents).toContain('File: temp/keep-this/file3.ts'); - - // Ensure the ignored files are not included - expect(fileContents).not.toContain('regular.log'); - - // Since our implementation of the directory structure in the mock may not be perfect, - // we'll skip checking for temp/file2.ts for now and focus on the core ignore functionality - // expect(fileContents).not.toContain('temp/file2.ts'); - }); - - it('handles complex ignore patterns with wildcards and directories', async () => { - // **Step 1: Set up the mock file system with complex patterns** - const mockFileSystem: { [path: string]: string } = { - '/mock/workspace/.gitignore': '**/*.min.js\n**/build/\n**/__pycache__/\n*.py[cod]\n', - '/mock/workspace/script.js': 'console.log("Hello");', - '/mock/workspace/script.min.js': 'console.log("Hello");', // Should be ignored by **/*.min.js - '/mock/workspace/lib/utils.js': 'function utils() {}', - '/mock/workspace/lib/utils.min.js': 'function utils(){}', // Should be ignored by **/*.min.js - '/mock/workspace/src/build/output.js': 'built file', // Should be ignored by **/build/ - '/mock/workspace/src/main.py': 'print("Hello")', - '/mock/workspace/src/__pycache__/main.cpython-39.pyc': 'compiled python', // Should be ignored by **/__pycache__/ and *.py[cod] - '/mock/workspace/src/test.pyc': 'compiled python test', // Should be ignored by *.py[cod] - }; - - // Mock fs.existsSync - (fs.existsSync as jest.Mock).mockImplementation((path: string) => { - return mockFileSystem.hasOwnProperty(path); - }); - - // Mock fs.statSync - (fs.statSync as jest.Mock).mockImplementation((path: string) => { - if (mockFileSystem.hasOwnProperty(path)) { - return { - isDirectory: () => path.includes('build/') || path.includes('__pycache__/') - }; - } - throw new Error(`File not found: ${path}`); - }); - - // Mock fs.readFileSync - (fs.readFileSync as jest.Mock).mockImplementation((path: string, encoding: string) => { - if (mockFileSystem[path]) { - return mockFileSystem[path]; - } - throw new Error(`File not found: ${path}`); - }); - - // Mock the FileReader.readFileContent method directly - jest.spyOn(require('../utils/fileReader').FileReader, 'readFileContent').mockImplementation(async (...args: any[]) => { - const filePath = args[0] as string; - if (mockFileSystem[filePath]) { - return mockFileSystem[filePath]; - } - return ''; - }); - - // Mock vscode.workspace.fs.readFile - (vscode.workspace.fs.readFile as jest.Mock).mockImplementation(async (uri: any) => { - const path = typeof uri === 'string' ? uri : uri.fsPath; - if (mockFileSystem[path]) { - return new TextEncoder().encode(mockFileSystem[path]); - } - throw new Error(`File not found: ${path}`); - }); - - // Mock workspace folders - (vscode.workspace.workspaceFolders as any) = [{ uri: { fsPath: '/mock/workspace' } }]; - - // **Step 2: Define selected files** - const selectedFiles = new Set(Object.keys(mockFileSystem).filter(path => path !== '/mock/workspace/.gitignore')); - - // **Step 3: Define settings** - const settings: PrompterSettings = { - includeFormattingInstructions: false, - tokenCalculationEnabled: false, - includeFileMap: true, - }; - - // **Step 4: Generate the prompt** - const prompt = await PromptGenerator.generatePrompt(selectedFiles, settings); - - // **Step 5: Extract and verify the file map** - const fileMapStart = prompt.indexOf(''); - const fileMapEnd = prompt.indexOf(''); - const fileMapContent = prompt.substring(fileMapStart + ''.length, fileMapEnd).trim(); - - // Expected file tree with complex patterns applied - const expectedFileTree = ` -/mock/workspace -├── lib -│ └── utils.js -├── src -│ └── main.py -└── script.js - `.trim(); - - expect(fileMapContent).toBe(expectedFileTree); - - // **Step 6: Extract and verify file contents** - const fileContentsStart = prompt.indexOf(''); - const fileContentsEnd = prompt.indexOf(''); - const fileContents = prompt.substring(fileContentsStart + ''.length, fileContentsEnd).trim(); - - // Check that contents of non-ignored files are included - expect(fileContents).toContain('File: script.js'); - expect(fileContents).toContain('File: lib/utils.js'); - expect(fileContents).toContain('File: src/main.py'); - - // Ensure the ignored files are not included - expect(fileContents).not.toContain('script.min.js'); - expect(fileContents).not.toContain('utils.min.js'); - expect(fileContents).not.toContain('build/output.js'); - expect(fileContents).not.toContain('__pycache__'); - expect(fileContents).not.toContain('.pyc'); - }); -}); diff --git a/src/test/index.ts b/src/test/index.ts new file mode 100644 index 0000000..e954d97 --- /dev/null +++ b/src/test/index.ts @@ -0,0 +1,33 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import Mocha from 'mocha'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((resolve, reject) => { + try { + // Simple approach - add extension.test.js directly + // This will find our basic test file + mocha.addFile(path.resolve(testsRoot, 'test/extension.test.js')); + + // Run the mocha test + mocha.run((failures: number) => { + if (failures > 0) { + reject(new Error(`${failures} tests failed.`)); + } else { + resolve(); + } + }); + } catch (err) { + console.error(err); + reject(err); + } + }); +} diff --git a/src/test/runTest.mjs b/src/test/runTest.mjs new file mode 100644 index 0000000..cf153de --- /dev/null +++ b/src/test/runTest.mjs @@ -0,0 +1,59 @@ +import { runTests } from '@vscode/test-electron'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; +import * as os from 'os'; +import * as fs from 'fs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function main() { + try { + // Create a temporary test workspace + // Using the system temp directory instead of external volume + const tmpDir = os.tmpdir(); + const testWorkspaceDir = path.join(tmpDir, `vscode-test-workspace-${Math.random().toString(36).substring(2)}`); + const userDataDir = path.join(tmpDir, `vscode-test-user-data-${Math.random().toString(36).substring(2)}`); + + // Ensure the test workspace directory exists + if (!fs.existsSync(testWorkspaceDir)) { + fs.mkdirSync(testWorkspaceDir, { recursive: true }); + } + + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, '../../out/test'); + + console.log('Running tests with the following configuration:'); + console.log(`Extension Development Path: ${extensionDevelopmentPath}`); + console.log(`Extension Tests Path: ${extensionTestsPath}`); + console.log(`Workspace Dir: ${testWorkspaceDir}`); + console.log(`User Data Dir: ${userDataDir}`); + + // Download VS Code, unzip it and run the integration test + await runTests({ + version: '1.98.0', // Specify the exact version your extension is built for + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [ + testWorkspaceDir, + '--disable-extensions', + `--user-data-dir=${userDataDir}`, + '--skip-getting-started', + '--skip-release-notes', + '--disable-telemetry', + '--disable-updates', + '--disable-crash-reporter', + '--disable-workspace-trust' + ] + }); + } catch (err) { + console.error('Failed to run tests', err); + process.exit(1); + } +} + +main(); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts deleted file mode 100644 index ea47756..0000000 --- a/src/test/vscode-mock.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Mock implementation of the vscode module for testing - */ - -export const Uri = { - file: (path: string) => ({ fsPath: path }), - parse: (path: string) => ({ fsPath: path }) -}; - -export const workspace = { - workspaceFolders: [], - fs: { - readFile: jest.fn(), - writeFile: jest.fn() - }, - getConfiguration: jest.fn().mockReturnValue({ - get: jest.fn(), - update: jest.fn() - }) -}; - -export const window = { - showInformationMessage: jest.fn(), - showWarningMessage: jest.fn(), - showErrorMessage: jest.fn(), - createTreeView: jest.fn(), - createOutputChannel: jest.fn().mockReturnValue({ - appendLine: jest.fn(), - show: jest.fn(), - clear: jest.fn() - }) -}; - -export const commands = { - registerCommand: jest.fn(), - executeCommand: jest.fn() -}; - -export const extensions = { - getExtension: jest.fn() -}; - -export const TreeItemCollapsibleState = { - None: 0, - Collapsed: 1, - Expanded: 2 -}; - -export const EventEmitter = class { - event: any; - constructor() { - this.event = jest.fn(); - } - fire() {} -}; - -export const ThemeIcon = { - File: 'file', - Folder: 'folder' -}; - -export const ExtensionContext = class { - subscriptions: any[] = []; -}; - -export enum StatusBarAlignment { - Left = 1, - Right = 2 -} - -// Mock any other vscode APIs that your tests might need diff --git a/src/utils/promptGenerator.ts b/src/utils/promptGenerator.ts index 56523e5..4513007 100644 --- a/src/utils/promptGenerator.ts +++ b/src/utils/promptGenerator.ts @@ -32,7 +32,7 @@ export class PromptGenerator { * @param settings Settings to apply when generating the prompt * @returns The generated prompt text */ - static async generatePrompt(selectedFiles: Set, settings: PrompterSettings): Promise { + static async generatePrompt(selectedFiles: Set, settings: PrompterSettings): Promise { if (selectedFiles.size === 0) { throw new Error('No files selected'); } @@ -92,8 +92,8 @@ export class PromptGenerator { } if (filteredFiles.size === 0) { - vscode.window.showWarningMessage('All selected files were filtered out by ignore patterns'); - throw new Error('All files were filtered out by ignore patterns'); + // Return null to signal that no files were available after filtering + return null; } // Create filtered contents map