Skip to main content
Glama
refactor-tool.test.ts14.1 kB
import { describe, test, expect, beforeEach, afterEach } from 'vitest'; import { writeFileSync, existsSync, rmSync, mkdirSync, readFileSync } from 'fs'; import { performRefactor, formatRefactorResults, } from '../../src/core/refactor-tool.js'; describe('Refactor Tool', () => { const testDir = 'tests/temp-refactor'; beforeEach(() => { // Clean up and create test directory if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } mkdirSync(testDir, { recursive: true }); // Create test files writeFileSync( `${testDir}/variables.js`, `const oldVariable = 'test'; const anotherOld = 123; let someVar = 'keep this'; const finalOld = true;` ); writeFileSync( `${testDir}/functions.ts`, `function oldFunction() { return 'old'; } export function exportedOldFunction() { return oldFunction(); } const arrowOldFunction = () => 'arrow';` ); writeFileSync( `${testDir}/context-test.js`, `import legacy_sdk from 'old-package'; const legacy_sdk_local = 'local variable'; console.log(legacy_sdk_local); legacy_sdk.initialize();` ); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); describe('performRefactor', () => { const testCases = [ { name: 'should perform basic refactoring', options: { searchPattern: 'const (\\w+) = ', replacePattern: 'let $1 = ', filePattern: `${testDir}/variables.js`, dryRun: false, }, expected: { resultCount: 1, filePath: `${testDir}/variables.js`, replacements: 3, modified: true, fileContains: [ 'let oldVariable = ', 'let anotherOld = ', 'let finalOld = ', 'let someVar = ', ], }, }, { name: 'should perform dry-run without modifying files', options: { searchPattern: 'const (\\w+) = ', replacePattern: 'let $1 = ', filePattern: `${testDir}/variables.js`, dryRun: true, }, expected: { resultCount: 1, replacements: 3, modified: true, fileUnchanged: true, }, }, { name: 'should work with capture groups', options: { searchPattern: 'function (\\w+)\\(', replacePattern: 'function new$1(', filePattern: `${testDir}/functions.ts`, dryRun: false, }, expected: { resultCount: 1, replacements: 2, fileContains: [ 'function newoldFunction(', 'function newexportedOldFunction(', ], }, }, { name: 'should work with context filtering', options: { searchPattern: 'legacy_sdk', replacePattern: 'new_sdk', contextPattern: 'import', filePattern: `${testDir}/context-test.js`, dryRun: false, }, expected: { resultCount: 1, replacementsGreaterThan: 0, fileContains: ['import new_sdk from', 'legacy_sdk_local'], }, }, { name: 'should handle multiple files', options: { searchPattern: 'old', replacePattern: 'new', filePattern: `${testDir}/**/*.{js,ts}`, dryRun: false, }, expected: { resultCountGreaterThan: 1, totalReplacementsGreaterThan: 0, }, }, { name: 'should return empty results when no matches found', options: { searchPattern: 'nonexistent.*pattern', replacePattern: 'replacement', filePattern: `${testDir}/**/*.js`, dryRun: false, }, expected: { resultCount: 0, }, }, { name: 'should collect match information correctly', options: { searchPattern: 'const (\\w+)', replacePattern: 'let $1', filePattern: `${testDir}/variables.js`, dryRun: true, }, expected: { resultCount: 1, matchCount: 3, matchProperties: ['line', 'content', 'original', 'replaced'], matchContentContains: ['const', 'let'], }, }, ]; testCases.forEach(({ name, options, expected }) => { test(name, async () => { const originalContent = expected.fileUnchanged ? readFileSync(`${testDir}/variables.js`, 'utf-8') : null; const results = await performRefactor(options); if (expected.resultCount !== undefined) { expect(results).toHaveLength(expected.resultCount); } if (expected.resultCountGreaterThan !== undefined) { expect(results.length).toBeGreaterThan( expected.resultCountGreaterThan ); } if (results.length > 0) { if (expected.filePath) { expect(results[0].filePath).toBe(expected.filePath); } if (expected.replacements !== undefined && results.length > 0) { expect(results[0].replacements).toBe(expected.replacements); } if (expected.replacementsGreaterThan !== undefined) { expect(results[0].replacements).toBeGreaterThan( expected.replacementsGreaterThan ); } if (expected.modified !== undefined) { expect(results[0].modified).toBe(expected.modified); } if (expected.matchCount !== undefined) { expect(results[0].matches).toHaveLength(expected.matchCount); } if (expected.matchProperties) { const match = results[0].matches[0]; expected.matchProperties.forEach(prop => { expect(match).toHaveProperty(prop); }); } if (expected.matchContentContains) { const match = results[0].matches[0]; const hasInOriginal = expected.matchContentContains.some( text => match.original && match.original.includes(text) ); const hasInReplaced = expected.matchContentContains.some( text => match.replaced && match.replaced.includes(text) ); expect(hasInOriginal || hasInReplaced).toBe(true); } } if (expected.totalReplacementsGreaterThan !== undefined) { const totalReplacements = results.reduce( (sum, result) => sum + result.replacements, 0 ); expect(totalReplacements).toBeGreaterThan( expected.totalReplacementsGreaterThan ); } if (expected.fileContains) { let filePath = options.filePattern; // Use the specific file based on the test case if (name.includes('capture groups')) { filePath = `${testDir}/functions.ts`; } else if (name.includes('context')) { filePath = `${testDir}/context-test.js`; } else if (name.includes('complex')) { filePath = `${testDir}/complex.js`; } else { // For basic refactoring and other tests filePath = `${testDir}/variables.js`; } const content = readFileSync(filePath, 'utf-8'); expected.fileContains.forEach(text => { expect(content).toContain(text); }); } if (expected.fileUnchanged && originalContent) { const content = readFileSync(`${testDir}/variables.js`, 'utf-8'); expect(content).toBe(originalContent); } }); }); }); describe('formatRefactorResults', () => { const testCases = [ { name: 'should format single result correctly', setupFn: async () => { return await performRefactor({ searchPattern: 'const', replacePattern: 'let', filePattern: `${testDir}/variables.js`, dryRun: true, }); }, isDryRun: false, expected: { contains: [ 'Refactoring completed:', `${testDir}/variables.js:`, 'replacements', 'Total:', ], }, }, { name: 'should format dry-run results correctly', setupFn: async () => { return await performRefactor({ searchPattern: 'const', replacePattern: 'let', filePattern: `${testDir}/variables.js`, dryRun: true, }); }, isDryRun: true, expected: { contains: ['(dry run)'], }, }, { name: 'should format empty results correctly', setupFn: () => [], isDryRun: false, expected: { equals: 'No matches found for the given pattern', }, }, { name: 'should format multiple results correctly', setupFn: async () => { return await performRefactor({ searchPattern: 'old', replacePattern: 'new', filePattern: `${testDir}/**/*.{js,ts}`, dryRun: true, }); }, isDryRun: false, expected: { contains: ['Refactoring completed:', 'Total:'], minLines: 3, }, }, { name: 'should calculate total replacements correctly', setupFn: async () => { return await performRefactor({ searchPattern: 'old', replacePattern: 'new', filePattern: `${testDir}/**/*.{js,ts}`, dryRun: true, }); }, isDryRun: false, expected: { calculateTotal: true, }, }, ]; testCases.forEach(({ name, setupFn, isDryRun, expected }) => { test(name, async () => { const results = await setupFn(); const formatted = formatRefactorResults(results, isDryRun); if (expected.equals) { expect(formatted).toBe(expected.equals); } if (expected.contains) { expected.contains.forEach(text => { expect(formatted).toContain(text); }); } if (expected.minLines) { expect(formatted.split('\n').length).toBeGreaterThan( expected.minLines ); } if (expected.calculateTotal) { const totalReplacements = results.reduce( (sum, result) => sum + result.replacements, 0 ); expect(formatted).toContain( `Total: ${totalReplacements} replacements` ); } }); }); }); describe('edge cases', () => { const testCases = [ { name: 'should handle files with no content', setupFn: () => { writeFileSync(`${testDir}/empty.js`, ''); }, options: { searchPattern: 'anything', replacePattern: 'replacement', filePattern: `${testDir}/empty.js`, dryRun: false, }, expected: { resultCount: 0, }, }, { name: 'should handle invalid regex gracefully', setupFn: null, options: { searchPattern: '[invalid regex', replacePattern: 'replacement', filePattern: `${testDir}/**/*.js`, dryRun: false, }, expected: { shouldThrow: true, }, }, { name: 'should handle non-existent file patterns', setupFn: null, options: { searchPattern: 'function', replacePattern: 'method', filePattern: 'non-existent/**/*.js', dryRun: false, }, expected: { resultCount: 0, }, }, { name: 'should handle complex replacement patterns', setupFn: () => { writeFileSync( `${testDir}/complex.js`, `function getName() { return 'name'; } function getAge() { return 25; }` ); }, options: { searchPattern: 'function get(\\w+)\\(\\)', replacePattern: 'const get$1 = ()', filePattern: `${testDir}/complex.js`, dryRun: false, }, expected: { resultCount: 1, replacements: 2, fileContains: ['const getName = ()', 'const getAge = ()'], }, }, { name: 'should preserve file content when no matches', setupFn: null, options: { searchPattern: 'nonexistent', replacePattern: 'replacement', filePattern: `${testDir}/variables.js`, dryRun: false, }, expected: { resultCount: 0, fileUnchanged: true, }, }, ]; testCases.forEach(({ name, setupFn, options, expected }) => { test(name, async () => { const originalContent = expected.fileUnchanged ? readFileSync(`${testDir}/variables.js`, 'utf-8') : null; if (setupFn) { setupFn(); } if (expected.shouldThrow) { await expect(performRefactor(options)).rejects.toThrow(); } else { const results = await performRefactor(options); expect(results).toHaveLength(expected.resultCount); if (expected.replacements !== undefined && results.length > 0) { expect(results[0]?.replacements).toBe(expected.replacements); } if (expected.fileContains) { const content = readFileSync(`${testDir}/complex.js`, 'utf-8'); expected.fileContains.forEach(text => { expect(content).toContain(text); }); } if (expected.fileUnchanged && originalContent) { const content = readFileSync(`${testDir}/variables.js`, 'utf-8'); expect(content).toBe(originalContent); } } }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/myuon/refactor-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server