Skip to content

Commit

Permalink
test(core): improve test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
yamadashy committed Nov 21, 2024
1 parent e34a8da commit fdee489
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 83 deletions.
2 changes: 2 additions & 0 deletions bin/repomix.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,7 @@ function setupErrorHandlers() {
} else {
console.error('Fatal Error:', error);
}

process.exit(EXIT_CODES.ERROR);
}
})();
39 changes: 15 additions & 24 deletions src/cli/actions/remoteAction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { exec } from 'node:child_process';
import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { promisify } from 'node:util';
import pc from 'picocolors';
Expand All @@ -9,6 +8,7 @@ import { logger } from '../../shared/logger.js';
import type { CliOptions } from '../cliRun.js';
import Spinner from '../cliSpinner.js';
import { runDefaultAction } from './defaultAction.js';
import { DisposableTempDir } from '../../core/file/disposableTempDir.js';

const execAsync = promisify(exec);

Check failure on line 13 in src/cli/actions/remoteAction.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 16.x)

tests/cli/actions/remoteAction.test.ts > remoteAction functions > runRemoteAction > should clone the repository

TypeError: Object not disposable ❯ __typeError src/cli/actions/remoteAction.ts:13:9 ❯ __using src/cli/actions/remoteAction.ts:21:40 ❯ Module.runRemoteAction src/cli/actions/remoteAction.ts:24:25 ❯ tests/cli/actions/remoteAction.test.ts:27:7

Check failure on line 13 in src/cli/actions/remoteAction.ts

View workflow job for this annotation

GitHub Actions / Test (macos-latest, 16.x)

tests/cli/actions/remoteAction.test.ts > remoteAction functions > runRemoteAction > should clone the repository

TypeError: Object not disposable ❯ __typeError src/cli/actions/remoteAction.ts:13:9 ❯ __using src/cli/actions/remoteAction.ts:21:40 ❯ Module.runRemoteAction src/cli/actions/remoteAction.ts:24:25 ❯ tests/cli/actions/remoteAction.test.ts:27:7

Expand All @@ -18,22 +18,24 @@ 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...');
spinner.start();

try {
spinner.start();
await cloneRepository(formattedUrl, tempDir);
spinner.succeed('Repository cloned successfully!');
logger.log('');
await using tempDir = await DisposableTempDir.create('repomix-');
const tempDirPath = tempDir.getPath();

const result = await runDefaultAction(tempDir, tempDir, options);
await copyOutputToCurrentDirectory(tempDir, process.cwd(), result.config.output.filePath);
} finally {
// Clean up the temporary directory
await cleanupTempDirectory(tempDir);
try {
await cloneRepository(formatGitUrl(repoUrl), tempDirPath);
} catch (error) {
spinner.fail('Error during repository cloning.');
throw error;
}

spinner.succeed('Repository cloned successfully!');
logger.log('');

const result = await runDefaultAction(tempDirPath, tempDirPath, options);
await copyOutputToCurrentDirectory(tempDirPath, process.cwd(), result.config.output.filePath);
};

export const formatGitUrl = (url: string): string => {
Expand All @@ -52,12 +54,6 @@ export const formatGitUrl = (url: string): string => {
return url;
};

export const createTempDirectory = async (): Promise<string> => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'repomix-'));
logger.trace(`Created temporary directory. (path: ${pc.dim(tempDir)})`);
return tempDir;
};

export const cloneRepository = async (url: string, directory: string): Promise<void> => {
logger.log(`Clone repository: ${url} to temporary directory. ${pc.dim(`path: ${directory}`)}`);
logger.log('');
Expand All @@ -69,11 +65,6 @@ export const cloneRepository = async (url: string, directory: string): Promise<v
}
};

export const cleanupTempDirectory = async (directory: string): Promise<void> => {
logger.trace(`Cleaning up temporary directory: ${directory}`);
await fs.rm(directory, { recursive: true, force: true });
};

export const checkGitInstallation = async (): Promise<boolean> => {
try {
const result = await execAsync('git --version');
Expand Down
33 changes: 33 additions & 0 deletions src/core/file/disposableTempDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { logger } from '../../shared/logger.js';
import pc from 'picocolors';

export class DisposableTempDir implements AsyncDisposable {
private readonly directory: string;

private constructor(directory: string) {
this.directory = directory;
}

static async create(dirPrefix: string): Promise<DisposableTempDir> {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), dirPrefix));
logger.trace(`Created temporary directory. (path: ${pc.dim(tempDir)})`);
return new DisposableTempDir(tempDir);
}

getPath(): string {
return this.directory;
}

async [Symbol.asyncDispose](): Promise<void> {
try {
logger.trace(`Cleaning up temporary directory: ${this.directory}`);
await fs.rm(this.directory, { recursive: true, force: true });
} catch (error) {
// Log error but don't throw to ensure cleanup completes
logger.warn(`Failed to cleanup temporary directory: ${this.directory}`, error);
}
}
}
53 changes: 23 additions & 30 deletions tests/cli/actions/remoteAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
cleanupTempDirectory,
checkGitInstallation,
copyOutputToCurrentDirectory,
createTempDirectory,
formatGitUrl,
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<typeof import('node:fs/promises')>();
return {
...actual,
copyFile: vi.fn(), // Mock the copyFile function
};
});
vi.mock('../../../src/shared/logger');

describe('remoteAction functions', () => {
beforeEach(() => {
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');
Expand All @@ -37,29 +53,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';
Expand Down
130 changes: 130 additions & 0 deletions tests/cli/cliPrint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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 path from 'node:path';
import { createMockConfig } 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 suspiciousFiles: SuspiciousFileResult[] = [
{
filePath: path.join('/root', 'config/secrets.txt'),
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:config/secrets.txt');
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.');
});
});
});
Loading

0 comments on commit fdee489

Please sign in to comment.