review-copilot
Version:
ReviewCopilot - AI-powered code review assistant with customizable prompts
335 lines (334 loc) • 14.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const config_manager_1 = require("../../../config/config-manager");
const review_1 = require("../review");
const provider_factory_1 = require("../../../providers/provider-factory");
const vcs_factory_1 = require("../../../utils/vcs-factory");
// Mock dependencies
jest.mock('../../../config/config-manager');
jest.mock('../../../utils/vcs-factory');
jest.mock('../../../providers/provider-factory');
jest.mock('../../../providers/openai-provider');
jest.mock('../../../services/git-platform-factory');
// Enhanced mock for ora with proper cleanup
const mockOraInstance = {
start: jest.fn().mockReturnThis(),
stop: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
info: jest.fn().mockReturnThis(),
text: '',
};
jest.mock('ora', () => jest.fn(() => mockOraInstance));
// Mock chalk to prevent memory issues with colors
jest.mock('chalk', () => ({
blue: jest.fn((str) => str),
green: jest.fn((str) => str),
yellow: jest.fn((str) => str),
cyan: jest.fn((str) => str),
white: jest.fn((str) => str),
gray: jest.fn((str) => str),
red: jest.fn((str) => str),
bold: jest.fn((str) => str),
}));
describe('reviewCommand', () => {
// Setup test data
const mockCommit = {
hash: 'abc123',
date: '2024-01-01',
message: 'feat: test commit',
author: 'Test Author',
files: [
{
file: 'src/test.ts',
changes: 'test code changes',
},
],
};
const mockConfig = {
providers: {
openai: {
enabled: true,
apiKey: 'test-key',
model: 'gpt-4',
},
},
rules: {
codeChanges: {
enabled: true,
filePatterns: ['**/*.ts'],
prompt: 'Review this code',
},
commitMessage: {
enabled: true,
prompt: 'Review this commit message',
},
branchName: {
enabled: false,
prompt: 'Review this branch name',
},
},
};
let mockProvider;
let mockConfigManager;
let mockVcsProvider;
let consoleSpy;
beforeAll(() => {
// Mock console methods to prevent memory leaks from logging
consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
jest.spyOn(console, 'error').mockImplementation(() => { });
});
afterAll(() => {
// Restore console methods
consoleSpy.mockRestore();
jest.restoreAllMocks();
});
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks();
// Reset mock implementations
mockOraInstance.start.mockClear();
mockOraInstance.stop.mockClear();
mockOraInstance.succeed.mockClear();
mockOraInstance.fail.mockClear();
mockOraInstance.info.mockClear();
// Setup ConfigManager mock
mockConfigManager = {
loadConfig: jest.fn().mockResolvedValue(undefined),
getConfig: jest.fn().mockReturnValue(mockConfig),
};
config_manager_1.ConfigManager.getInstance.mockReturnValue(mockConfigManager);
// Setup Provider mock
mockProvider = {
review: jest.fn().mockResolvedValue({
success: true,
message: 'Review completed successfully',
}),
};
provider_factory_1.ProviderFactory.createProvider.mockReturnValue(mockProvider);
// Setup VCS provider mock
mockVcsProvider = {
getPullRequestChanges: jest.fn().mockResolvedValue([mockCommit]),
getCurrentBranchName: jest.fn().mockResolvedValue('feature/test'),
};
vcs_factory_1.getVcsProvider.mockReturnValue(mockVcsProvider);
// Mock environment variables
delete process.env.GITHUB_ACTIONS;
delete process.env.GITLAB_CI;
});
afterEach(() => {
// Clean up after each test
jest.clearAllMocks();
// Ensure all timers are cleared
jest.clearAllTimers();
// Clean up any remaining promises
return new Promise((resolve) => setImmediate(resolve));
});
describe('basic functionality', () => {
it('should successfully review commits', async () => {
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
expect(mockProvider.review).toHaveBeenCalled();
expect(mockOraInstance.start).toHaveBeenCalled();
expect(mockOraInstance.stop).toHaveBeenCalled();
expect(mockConfigManager.loadConfig).toHaveBeenCalledWith('test-config.yaml');
});
it('should handle no commits scenario', async () => {
mockVcsProvider.getPullRequestChanges.mockResolvedValue([]);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
expect(mockProvider.review).not.toHaveBeenCalled();
expect(mockOraInstance.start).toHaveBeenCalled();
expect(mockOraInstance.succeed).toHaveBeenCalled();
});
it('should handle null/undefined commits', async () => {
mockVcsProvider.getPullRequestChanges.mockResolvedValue(null);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
expect(mockProvider.review).not.toHaveBeenCalled();
expect(mockOraInstance.info).toHaveBeenCalledWith('No commits to review.');
});
it('should handle review errors gracefully', async () => {
const testError = new Error('Review failed');
mockProvider.review.mockRejectedValue(testError);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(false);
expect(mockOraInstance.start).toHaveBeenCalled();
expect(mockOraInstance.fail).toHaveBeenCalledWith('Review failed');
});
it('should handle config loading errors', async () => {
mockConfigManager.loadConfig.mockRejectedValue(new Error('Config not found'));
const result = await (0, review_1.reviewCommand)({ config: 'invalid-config.yaml' });
expect(result).toBe(false);
expect(mockOraInstance.fail).toHaveBeenCalled();
});
});
describe('file filtering', () => {
it('should only review files matching patterns', async () => {
const mockCommitWithMultipleFiles = {
...mockCommit,
files: [
{ file: 'src/test.ts', changes: 'typescript code' },
{ file: 'src/ignore.js', changes: 'javascript code' },
{ file: 'README.md', changes: 'documentation' },
],
};
mockVcsProvider.getPullRequestChanges.mockResolvedValue([
mockCommitWithMultipleFiles,
]);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
expect(mockProvider.review).toHaveBeenCalled();
// Check that only .ts files are reviewed
const reviewCalls = mockProvider.review.mock.calls;
const codeReviewCall = reviewCalls.find((call) => call[1].includes('src/test.ts'));
expect(codeReviewCall).toBeDefined();
expect(codeReviewCall?.[1]).not.toContain('src/ignore.js');
expect(codeReviewCall?.[1]).not.toContain('README.md');
});
it('should handle files with non-string changes', async () => {
const mockCommitWithInvalidChanges = {
...mockCommit,
files: [
{ file: 'src/test.ts', changes: null },
{ file: 'src/valid.ts', changes: 'valid typescript code' },
],
};
mockVcsProvider.getPullRequestChanges.mockResolvedValue([
mockCommitWithInvalidChanges,
]);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
expect(mockProvider.review).toHaveBeenCalled();
});
it('should truncate large files', async () => {
const largeContent = 'a'.repeat(60000); // Larger than MAX_FILE_SIZE
const mockCommitWithLargeFile = {
...mockCommit,
files: [{ file: 'src/large.ts', changes: largeContent }],
};
mockVcsProvider.getPullRequestChanges.mockResolvedValue([
mockCommitWithLargeFile,
]);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
// Verify content was truncated
const reviewCall = mockProvider.review.mock.calls[0];
expect(reviewCall[1]).toContain('content truncated for size limit');
});
});
describe('different review types', () => {
it('should review commit messages when enabled', async () => {
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
// Should have both code and commit message reviews
expect(mockProvider.review).toHaveBeenCalledTimes(2);
const calls = mockProvider.review.mock.calls;
const commitMessageCall = calls.find((call) => call[0] === 'Review this commit message');
expect(commitMessageCall).toBeDefined();
expect(commitMessageCall?.[1]).toBe('feat: test commit');
});
it('should skip disabled review types', async () => {
const configWithDisabledCommitReview = {
...mockConfig,
rules: {
...mockConfig.rules,
commitMessage: {
enabled: false,
prompt: 'Review this commit message',
},
},
};
mockConfigManager.getConfig.mockReturnValue(configWithDisabledCommitReview);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
// Should only have code review, not commit message review
expect(mockProvider.review).toHaveBeenCalledTimes(1);
const calls = mockProvider.review.mock.calls;
const codeReviewCall = calls.find((call) => call[0] === 'Review this code');
expect(codeReviewCall).toBeDefined();
});
it('should review branch name when enabled', async () => {
const configWithBranchReview = {
...mockConfig,
rules: {
...mockConfig.rules,
branchName: {
enabled: true,
prompt: 'Review this branch name',
},
},
};
mockConfigManager.getConfig.mockReturnValue(configWithBranchReview);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
expect(mockVcsProvider.getCurrentBranchName).toHaveBeenCalled();
// Should have code, commit message, and branch name reviews
expect(mockProvider.review).toHaveBeenCalledTimes(3);
});
});
describe('error handling', () => {
it('should handle provider creation errors', async () => {
provider_factory_1.ProviderFactory.createProvider.mockImplementation(() => {
throw new Error('Provider creation failed');
});
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(false);
expect(mockOraInstance.fail).toHaveBeenCalled();
});
it('should handle VCS provider errors', async () => {
mockVcsProvider.getPullRequestChanges.mockRejectedValue(new Error('Git error'));
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(false);
expect(mockOraInstance.fail).toHaveBeenCalled();
});
it('should handle errors with detailed OpenAI error info', async () => {
const openAIError = new Error('API Error Details: {"error":{"type":"invalid_request_error","code":"invalid_api_key","message":"Invalid API key"}}');
mockProvider.review.mockRejectedValue(openAIError);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(false);
expect(mockOraInstance.fail).toHaveBeenCalled();
});
});
describe('edge cases', () => {
it('should handle empty file patterns', async () => {
const configWithEmptyPatterns = {
...mockConfig,
rules: {
...mockConfig.rules,
codeChanges: {
enabled: true,
filePatterns: [],
prompt: 'Review this code',
},
},
};
mockConfigManager.getConfig.mockReturnValue(configWithEmptyPatterns);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
// Should use default patterns
expect(mockProvider.review).toHaveBeenCalled();
});
it('should handle commits with no files', async () => {
const commitWithNoFiles = {
...mockCommit,
files: [],
};
mockVcsProvider.getPullRequestChanges.mockResolvedValue([
commitWithNoFiles,
]);
const result = await (0, review_1.reviewCommand)({ config: 'test-config.yaml' });
expect(result).toBe(true);
// Should still review commit message
expect(mockProvider.review).toHaveBeenCalledTimes(1);
});
it('should handle baseBranch parameter', async () => {
const result = await (0, review_1.reviewCommand)({
config: 'test-config.yaml',
baseBranch: 'develop',
});
expect(result).toBe(true);
expect(mockVcsProvider.getPullRequestChanges).toHaveBeenCalledWith('develop');
});
});
});