From 47b179beb1cd85ef6125013974ba840a8e741615 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Thu, 21 Nov 2024 20:06:00 +0900 Subject: [PATCH] test(core): improve test coverage --- bin/repomix.cjs | 2 + src/cli/actions/remoteAction.ts | 21 ++-- tests/cli/actions/remoteAction.test.ts | 51 +++++----- tests/cli/cliPrint.test.ts | 132 +++++++++++++++++++++++++ tests/config/globalDirectory.test.ts | 94 ++++++++++++------ 5 files changed, 238 insertions(+), 62 deletions(-) create mode 100644 tests/cli/cliPrint.test.ts diff --git a/bin/repomix.cjs b/bin/repomix.cjs index 1be49d5..78f210f 100755 --- a/bin/repomix.cjs +++ b/bin/repomix.cjs @@ -51,5 +51,7 @@ function setupErrorHandlers() { } else { console.error('Fatal Error:', error); } + + process.exit(EXIT_CODES.ERROR); } })(); diff --git a/src/cli/actions/remoteAction.ts b/src/cli/actions/remoteAction.ts index 1d4144a..c6ea7b2 100644 --- a/src/cli/actions/remoteAction.ts +++ b/src/cli/actions/remoteAction.ts @@ -18,21 +18,28 @@ export const runRemoteAction = async (repoUrl: string, options: CliOptions): Pro throw new RepomixError('Git is not installed or not in the system PATH.'); } - const formattedUrl = formatGitUrl(repoUrl); - const tempDir = await createTempDirectory(); const spinner = new Spinner('Cloning repository...'); + const tempDirPath = await createTempDirectory(); + try { spinner.start(); - await cloneRepository(formattedUrl, tempDir); + + // Clone the repository + await cloneRepository(formatGitUrl(repoUrl), tempDirPath); + spinner.succeed('Repository cloned successfully!'); logger.log(''); - const result = await runDefaultAction(tempDir, tempDir, options); - await copyOutputToCurrentDirectory(tempDir, process.cwd(), result.config.output.filePath); + // Run the default action on the cloned repository + const result = await runDefaultAction(tempDirPath, tempDirPath, options); + await copyOutputToCurrentDirectory(tempDirPath, process.cwd(), result.config.output.filePath); + } catch (error) { + spinner.fail('Error during repository cloning. cleanup...'); + throw error; } finally { - // Clean up the temporary directory - await cleanupTempDirectory(tempDir); + // Cleanup the temporary directory + await cleanupTempDirectory(tempDirPath); } }; diff --git a/tests/cli/actions/remoteAction.test.ts b/tests/cli/actions/remoteAction.test.ts index 18f473f..4e6d169 100644 --- a/tests/cli/actions/remoteAction.test.ts +++ b/tests/cli/actions/remoteAction.test.ts @@ -1,17 +1,23 @@ import * as fs from 'node:fs/promises'; -import os from 'node:os'; +import * as os from 'node:os'; import path from 'node:path'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { + checkGitInstallation, cleanupTempDirectory, copyOutputToCurrentDirectory, createTempDirectory, formatGitUrl, + runRemoteAction, } from '../../../src/cli/actions/remoteAction.js'; -vi.mock('node:child_process'); -vi.mock('node:fs/promises'); -vi.mock('node:os'); +vi.mock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + copyFile: vi.fn(), + }; +}); vi.mock('../../../src/shared/logger'); describe('remoteAction functions', () => { @@ -19,6 +25,20 @@ describe('remoteAction functions', () => { vi.resetAllMocks(); }); + describe('runRemoteAction', () => { + test('should clone the repository', async () => { + vi.mocked(fs.copyFile).mockResolvedValue(undefined); + await runRemoteAction('yamadashy/repomix', {}); + }); + }); + + describe('checkGitInstallation Integration', () => { + test('should detect git installation in real environment', async () => { + const result = await checkGitInstallation(); + expect(result).toBe(true); + }); + }); + describe('formatGitUrl', () => { test('should convert GitHub shorthand to full URL', () => { expect(formatGitUrl('user/repo')).toBe('https://github.com/user/repo.git'); @@ -37,29 +57,6 @@ describe('remoteAction functions', () => { }); }); - describe('createTempDirectory', () => { - test('should create temporary directory', async () => { - const mockTempDir = '/mock/temp/dir'; - vi.mocked(os.tmpdir).mockReturnValue('/mock/temp'); - vi.mocked(fs.mkdtemp).mockResolvedValue(mockTempDir); - - const result = await createTempDirectory(); - expect(result).toBe(mockTempDir); - expect(fs.mkdtemp).toHaveBeenCalledWith(path.join('/mock/temp', 'repomix-')); - }); - }); - - describe('cleanupTempDirectory', () => { - test('should cleanup directory', async () => { - const mockDir = '/mock/temp/dir'; - vi.mocked(fs.rm).mockResolvedValue(); - - await cleanupTempDirectory(mockDir); - - expect(fs.rm).toHaveBeenCalledWith(mockDir, { recursive: true, force: true }); - }); - }); - describe('copyOutputToCurrentDirectory', () => { test('should copy output file', async () => { const sourceDir = '/source/dir'; diff --git a/tests/cli/cliPrint.test.ts b/tests/cli/cliPrint.test.ts new file mode 100644 index 0000000..be852fb --- /dev/null +++ b/tests/cli/cliPrint.test.ts @@ -0,0 +1,132 @@ +import path from 'node:path'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { printCompletion, printSecurityCheck, printSummary, printTopFiles } from '../../src/cli/cliPrint.js'; +import type { SuspiciousFileResult } from '../../src/core/security/securityCheck.js'; +import { logger } from '../../src/shared/logger.js'; +import { createMockConfig, isWindows } from '../testing/testUtils.js'; + +vi.mock('../../src/shared/logger'); +vi.mock('picocolors', () => ({ + default: { + white: (str: string) => `WHITE:${str}`, + dim: (str: string) => `DIM:${str}`, + green: (str: string) => `GREEN:${str}`, + yellow: (str: string) => `YELLOW:${str}`, + red: (str: string) => `RED:${str}`, + cyan: (str: string) => `CYAN:${str}`, + }, +})); + +describe('cliPrint', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('printSummary', () => { + test('should print summary with suspicious files and security check enabled', () => { + const config = createMockConfig({ + security: { enableSecurityCheck: true }, + }); + const suspiciousFiles: SuspiciousFileResult[] = [ + { filePath: 'suspicious.txt', messages: ['Contains sensitive data'] }, + ]; + + printSummary(10, 1000, 200, 'output.txt', suspiciousFiles, config); + + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('1 suspicious file(s) detected and excluded')); + }); + + test('should print summary with security check disabled', () => { + const config = createMockConfig({ + security: { enableSecurityCheck: false }, + }); + + printSummary(10, 1000, 200, 'output.txt', [], config); + + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Security check disabled')); + }); + }); + + describe('printSecurityCheck', () => { + test('should skip printing when security check is disabled', () => { + const config = createMockConfig({ + security: { enableSecurityCheck: false }, + }); + + printSecurityCheck('/root', [], config); + expect(logger.log).not.toHaveBeenCalled(); + }); + + test('should print message when no suspicious files found', () => { + const config = createMockConfig({ + security: { enableSecurityCheck: true }, + }); + + printSecurityCheck('/root', [], config); + + expect(logger.log).toHaveBeenCalledWith('WHITE:🔎 Security Check:'); + expect(logger.log).toHaveBeenCalledWith('DIM:──────────────────'); + expect(logger.log).toHaveBeenCalledWith('GREEN:✔ WHITE:No suspicious files detected.'); + }); + + test('should print details of suspicious files when found', () => { + const config = createMockConfig({ + security: { enableSecurityCheck: true }, + }); + const configRelativePath = path.join('config', 'secrets.txt'); + const suspiciousFiles: SuspiciousFileResult[] = [ + { + filePath: path.join('/root', configRelativePath), + messages: ['Contains API key', 'Contains password'], + }, + ]; + + printSecurityCheck('/root', suspiciousFiles, config); + + expect(logger.log).toHaveBeenCalledWith('YELLOW:1 suspicious file(s) detected and excluded from the output:'); + expect(logger.log).toHaveBeenCalledWith(`WHITE:1. WHITE:${configRelativePath}`); + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Contains API key')); + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Contains password')); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('Please review these files for potential sensitive information.'), + ); + }); + }); + + describe('printTopFiles', () => { + test('should print top files sorted by character count', () => { + const fileCharCounts = { + 'src/index.ts': 1000, + 'src/utils.ts': 500, + 'README.md': 2000, + }; + const fileTokenCounts = { + 'src/index.ts': 200, + 'src/utils.ts': 100, + 'README.md': 400, + }; + + printTopFiles(fileCharCounts, fileTokenCounts, 2); + + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Top 2 Files')); + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('README.md')); + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('src/index.ts')); + expect(logger.log).not.toHaveBeenCalledWith(expect.stringContaining('src/utils.ts')); + }); + + test('should handle empty file list', () => { + printTopFiles({}, {}, 5); + + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Top 5 Files')); + }); + }); + + describe('printCompletion', () => { + test('should print completion message', () => { + printCompletion(); + + expect(logger.log).toHaveBeenCalledWith('GREEN:🎉 All Done!'); + expect(logger.log).toHaveBeenCalledWith('WHITE:Your repository has been successfully packed.'); + }); + }); +}); diff --git a/tests/config/globalDirectory.test.ts b/tests/config/globalDirectory.test.ts index fea0b72..6239fbd 100644 --- a/tests/config/globalDirectory.test.ts +++ b/tests/config/globalDirectory.test.ts @@ -1,49 +1,87 @@ import os from 'node:os'; import path from 'node:path'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { getGlobalDirectory } from '../../src/config/globalDirectory.js'; -import { isLinux, isMac, isWindows } from '../testing/testUtils.js'; vi.mock('node:os'); -describe('globalDirectory', () => { +describe('getGlobalDirectory', () => { + const originalPlatform = process.platform; + const originalEnv = process.env; + beforeEach(() => { vi.resetAllMocks(); - process.env = {}; + process.env = { ...originalEnv }; }); - test.runIf(isWindows)('should return correct path for Windows', () => { - vi.mocked(os.platform).mockReturnValue('win32'); - vi.mocked(os.homedir).mockReturnValue('C:\\Users\\TestUser'); - process.env.LOCALAPPDATA = 'C:\\Users\\TestUser\\AppData\\Local'; - - const result = getGlobalDirectory(); - expect(result).toBe(path.join('C:\\Users\\TestUser\\AppData\\Local', 'Repomix')); + afterEach(() => { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + process.env = originalEnv; }); - test.runIf(isWindows)('should use homedir if LOCALAPPDATA is not set on Windows', () => { - vi.mocked(os.platform).mockReturnValue('win32'); - vi.mocked(os.homedir).mockReturnValue('C:\\Users\\TestUser'); - process.env.LOCALAPPDATA = undefined; + describe('Windows platform', () => { + test('should use LOCALAPPDATA when available', () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + process.env.LOCALAPPDATA = 'C:\\Users\\Test\\AppData\\Local'; - const result = getGlobalDirectory(); - expect(result).toBe(path.join('C:\\Users\\TestUser', 'AppData', 'Local', 'Repomix')); + const result = getGlobalDirectory(); + expect(result).toBe(path.join('C:\\Users\\Test\\AppData\\Local', 'Repomix')); + }); + + test('should fall back to homedir when LOCALAPPDATA is not available', () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + process.env.LOCALAPPDATA = undefined; + vi.mocked(os.homedir).mockReturnValue('C:\\Users\\Test'); + + const result = getGlobalDirectory(); + expect(result).toBe(path.join('C:\\Users\\Test', 'AppData', 'Local', 'Repomix')); + }); }); - test.runIf(isLinux)('should use XDG_CONFIG_HOME on Unix systems if set', () => { - vi.mocked(os.platform).mockReturnValue('linux'); - process.env.XDG_CONFIG_HOME = '/custom/config'; + describe('Unix platforms', () => { + test('should use XDG_CONFIG_HOME when available', () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + process.env.XDG_CONFIG_HOME = '/custom/config'; - const result = getGlobalDirectory(); - expect(result).toBe(path.join('/custom/config', 'repomix')); + const result = getGlobalDirectory(); + expect(result).toBe(path.join('/custom/config', 'repomix')); + }); + + test('should fall back to ~/.config on Linux', () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + process.env.XDG_CONFIG_HOME = undefined; + vi.mocked(os.homedir).mockReturnValue('/home/test'); + + const result = getGlobalDirectory(); + expect(result).toBe(path.join('/home/test', '.config', 'repomix')); + }); + + test('should fall back to ~/.config on macOS', () => { + Object.defineProperty(process, 'platform', { value: 'darwin' }); + process.env.XDG_CONFIG_HOME = undefined; + vi.mocked(os.homedir).mockReturnValue('/Users/test'); + + const result = getGlobalDirectory(); + expect(result).toBe(path.join('/Users/test', '.config', 'repomix')); + }); }); - test.runIf(isMac)('should use ~/.config on Unix systems if XDG_CONFIG_HOME is not set', () => { - vi.mocked(os.platform).mockReturnValue('darwin'); - vi.mocked(os.homedir).mockReturnValue('/Users/TestUser'); - process.env.XDG_CONFIG_HOME = undefined; + describe('Edge cases', () => { + test('should handle empty homedir', () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + process.env.XDG_CONFIG_HOME = undefined; + vi.mocked(os.homedir).mockReturnValue(''); - const result = getGlobalDirectory(); - expect(result).toBe(path.join('/Users/TestUser', '.config', 'repomix')); + const result = getGlobalDirectory(); + expect(result).toBe(path.join('', '.config', 'repomix')); + }); + + test('should handle unusual XDG_CONFIG_HOME paths', () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + process.env.XDG_CONFIG_HOME = '////multiple///slashes///'; + + const result = getGlobalDirectory(); + expect(result).toBe(path.join('////multiple///slashes///', 'repomix')); + }); }); });