mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat: add CodeRabbit integration for AI-powered code reviews
This commit introduces the CodeRabbit service and its associated routes, enabling users to trigger, manage, and check the status of code reviews through a new API. Key features include: - New routes for triggering code reviews, checking status, and stopping reviews. - Integration with the CodeRabbit CLI for authentication and status checks. - UI components for displaying code review results and settings management. - Unit tests for the new code review functionality to ensure reliability. This enhancement aims to streamline the code review process and leverage AI capabilities for improved code quality.
This commit is contained in:
196
apps/server/tests/unit/routes/code-review/providers.test.ts
Normal file
196
apps/server/tests/unit/routes/code-review/providers.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Unit tests for code-review providers route handler
|
||||
*
|
||||
* Tests:
|
||||
* - Returns provider status list
|
||||
* - Returns recommended provider
|
||||
* - Force refresh functionality
|
||||
* - Error handling
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Request, Response } from 'express';
|
||||
import { createProvidersHandler } from '@/routes/code-review/routes/providers.js';
|
||||
import type { CodeReviewService } from '@/services/code-review-service.js';
|
||||
import { createMockExpressContext } from '../../../utils/mocks.js';
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@automaker/utils', async () => {
|
||||
const actual = await vi.importActual<typeof import('@automaker/utils')>('@automaker/utils');
|
||||
return {
|
||||
...actual,
|
||||
createLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('code-review/providers route', () => {
|
||||
let mockCodeReviewService: CodeReviewService;
|
||||
let req: Request;
|
||||
let res: Response;
|
||||
|
||||
const mockProviderStatuses = [
|
||||
{
|
||||
provider: 'claude' as const,
|
||||
available: true,
|
||||
authenticated: true,
|
||||
version: '1.0.0',
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
provider: 'codex' as const,
|
||||
available: true,
|
||||
authenticated: false,
|
||||
version: '0.5.0',
|
||||
issues: ['Not authenticated'],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockCodeReviewService = {
|
||||
getProviderStatus: vi.fn().mockResolvedValue(mockProviderStatuses),
|
||||
getBestProvider: vi.fn().mockResolvedValue('claude'),
|
||||
executeReview: vi.fn(),
|
||||
refreshProviderStatus: vi.fn(),
|
||||
initialize: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const context = createMockExpressContext();
|
||||
req = context.req;
|
||||
res = context.res;
|
||||
req.query = {};
|
||||
});
|
||||
|
||||
describe('successful responses', () => {
|
||||
it('should return provider status and recommended provider', async () => {
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
providers: mockProviderStatuses,
|
||||
recommended: 'claude',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use cached status by default', async () => {
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(mockCodeReviewService.getProviderStatus).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should force refresh when refresh=true query param is set', async () => {
|
||||
req.query = { refresh: 'true' };
|
||||
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(mockCodeReviewService.getProviderStatus).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should handle no recommended provider', async () => {
|
||||
mockCodeReviewService.getBestProvider = vi.fn().mockResolvedValue(null);
|
||||
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
providers: mockProviderStatuses,
|
||||
recommended: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty provider list', async () => {
|
||||
mockCodeReviewService.getProviderStatus = vi.fn().mockResolvedValue([]);
|
||||
mockCodeReviewService.getBestProvider = vi.fn().mockResolvedValue(null);
|
||||
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
providers: [],
|
||||
recommended: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle getProviderStatus errors', async () => {
|
||||
mockCodeReviewService.getProviderStatus = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Failed to detect CLIs'));
|
||||
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Failed to detect CLIs',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle getBestProvider errors gracefully', async () => {
|
||||
mockCodeReviewService.getBestProvider = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Detection failed'));
|
||||
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Detection failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider priority', () => {
|
||||
it('should recommend claude when available and authenticated', async () => {
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
recommended: 'claude',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should recommend codex when claude is not available', async () => {
|
||||
mockCodeReviewService.getBestProvider = vi.fn().mockResolvedValue('codex');
|
||||
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
recommended: 'codex',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should recommend cursor as fallback', async () => {
|
||||
mockCodeReviewService.getBestProvider = vi.fn().mockResolvedValue('cursor');
|
||||
|
||||
const handler = createProvidersHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
recommended: 'cursor',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
109
apps/server/tests/unit/routes/code-review/status.test.ts
Normal file
109
apps/server/tests/unit/routes/code-review/status.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Unit tests for code-review status route handler
|
||||
*
|
||||
* Tests:
|
||||
* - Returns correct running status
|
||||
* - Returns correct project path
|
||||
* - Handles errors gracefully
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Request, Response } from 'express';
|
||||
import { createStatusHandler } from '@/routes/code-review/routes/status.js';
|
||||
import { createMockExpressContext } from '../../../utils/mocks.js';
|
||||
|
||||
// Mock the common module to control running state
|
||||
vi.mock('@/routes/code-review/common.js', () => {
|
||||
return {
|
||||
isRunning: vi.fn(),
|
||||
getReviewStatus: vi.fn(),
|
||||
getCurrentProjectPath: vi.fn(),
|
||||
setRunningState: vi.fn(),
|
||||
getAbortController: vi.fn(),
|
||||
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
|
||||
logError: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@automaker/utils', async () => {
|
||||
const actual = await vi.importActual<typeof import('@automaker/utils')>('@automaker/utils');
|
||||
return {
|
||||
...actual,
|
||||
createLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('code-review/status route', () => {
|
||||
let req: Request;
|
||||
let res: Response;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const context = createMockExpressContext();
|
||||
req = context.req;
|
||||
res = context.res;
|
||||
});
|
||||
|
||||
describe('when no review is running', () => {
|
||||
it('should return isRunning: false with null projectPath', async () => {
|
||||
const { getReviewStatus } = await import('@/routes/code-review/common.js');
|
||||
vi.mocked(getReviewStatus).mockReturnValue({
|
||||
isRunning: false,
|
||||
projectPath: null,
|
||||
});
|
||||
|
||||
const handler = createStatusHandler();
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
isRunning: false,
|
||||
projectPath: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a review is running', () => {
|
||||
it('should return isRunning: true with the current project path', async () => {
|
||||
const { getReviewStatus } = await import('@/routes/code-review/common.js');
|
||||
vi.mocked(getReviewStatus).mockReturnValue({
|
||||
isRunning: true,
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
const handler = createStatusHandler();
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
isRunning: true,
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle errors gracefully', async () => {
|
||||
const { getReviewStatus } = await import('@/routes/code-review/common.js');
|
||||
vi.mocked(getReviewStatus).mockImplementation(() => {
|
||||
throw new Error('Unexpected error');
|
||||
});
|
||||
|
||||
const handler = createStatusHandler();
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Unexpected error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
129
apps/server/tests/unit/routes/code-review/stop.test.ts
Normal file
129
apps/server/tests/unit/routes/code-review/stop.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Unit tests for code-review stop route handler
|
||||
*
|
||||
* Tests:
|
||||
* - Stopping when no review is running
|
||||
* - Stopping a running review
|
||||
* - Abort controller behavior
|
||||
* - Error handling
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Request, Response } from 'express';
|
||||
import { createStopHandler } from '@/routes/code-review/routes/stop.js';
|
||||
import { createMockExpressContext } from '../../../utils/mocks.js';
|
||||
|
||||
// Mock the common module
|
||||
vi.mock('@/routes/code-review/common.js', () => {
|
||||
return {
|
||||
isRunning: vi.fn(),
|
||||
getAbortController: vi.fn(),
|
||||
setRunningState: vi.fn(),
|
||||
getReviewStatus: vi.fn(),
|
||||
getCurrentProjectPath: vi.fn(),
|
||||
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
|
||||
logError: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@automaker/utils', async () => {
|
||||
const actual = await vi.importActual<typeof import('@automaker/utils')>('@automaker/utils');
|
||||
return {
|
||||
...actual,
|
||||
createLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('code-review/stop route', () => {
|
||||
let req: Request;
|
||||
let res: Response;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const context = createMockExpressContext();
|
||||
req = context.req;
|
||||
res = context.res;
|
||||
});
|
||||
|
||||
describe('when no review is running', () => {
|
||||
it('should return success with message that nothing is running', async () => {
|
||||
const { isRunning } = await import('@/routes/code-review/common.js');
|
||||
vi.mocked(isRunning).mockReturnValue(false);
|
||||
|
||||
const handler = createStopHandler();
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'No code review is currently running',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a review is running', () => {
|
||||
it('should abort the review and reset running state', async () => {
|
||||
const { isRunning, getAbortController, setRunningState } =
|
||||
await import('@/routes/code-review/common.js');
|
||||
|
||||
const mockAbortController = {
|
||||
abort: vi.fn(),
|
||||
signal: { aborted: false },
|
||||
};
|
||||
|
||||
vi.mocked(isRunning).mockReturnValue(true);
|
||||
vi.mocked(getAbortController).mockReturnValue(mockAbortController as any);
|
||||
|
||||
const handler = createStopHandler();
|
||||
await handler(req, res);
|
||||
|
||||
expect(mockAbortController.abort).toHaveBeenCalled();
|
||||
expect(setRunningState).toHaveBeenCalledWith(false, null, null);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Code review stopped',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle case when abort controller is null', async () => {
|
||||
const { isRunning, getAbortController, setRunningState } =
|
||||
await import('@/routes/code-review/common.js');
|
||||
|
||||
vi.mocked(isRunning).mockReturnValue(true);
|
||||
vi.mocked(getAbortController).mockReturnValue(null);
|
||||
|
||||
const handler = createStopHandler();
|
||||
await handler(req, res);
|
||||
|
||||
expect(setRunningState).toHaveBeenCalledWith(false, null, null);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Code review stopped',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle errors gracefully', async () => {
|
||||
const { isRunning } = await import('@/routes/code-review/common.js');
|
||||
vi.mocked(isRunning).mockImplementation(() => {
|
||||
throw new Error('Unexpected error');
|
||||
});
|
||||
|
||||
const handler = createStopHandler();
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Unexpected error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
384
apps/server/tests/unit/routes/code-review/trigger.test.ts
Normal file
384
apps/server/tests/unit/routes/code-review/trigger.test.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* Unit tests for code-review trigger route handler
|
||||
*
|
||||
* Tests:
|
||||
* - Parameter validation
|
||||
* - Request body validation (security)
|
||||
* - Concurrent review prevention
|
||||
* - Review execution
|
||||
* - Error handling
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Request, Response } from 'express';
|
||||
import { createTriggerHandler } from '@/routes/code-review/routes/trigger.js';
|
||||
import type { CodeReviewService } from '@/services/code-review-service.js';
|
||||
import { createMockExpressContext } from '../../../utils/mocks.js';
|
||||
|
||||
// Mock the common module to control running state
|
||||
vi.mock('@/routes/code-review/common.js', () => {
|
||||
let running = false;
|
||||
return {
|
||||
isRunning: vi.fn(() => running),
|
||||
setRunningState: vi.fn((state: boolean) => {
|
||||
running = state;
|
||||
}),
|
||||
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
|
||||
logError: vi.fn(),
|
||||
getAbortController: vi.fn(() => null),
|
||||
getCurrentProjectPath: vi.fn(() => null),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@automaker/utils', async () => {
|
||||
const actual = await vi.importActual<typeof import('@automaker/utils')>('@automaker/utils');
|
||||
return {
|
||||
...actual,
|
||||
createLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('code-review/trigger route', () => {
|
||||
let mockCodeReviewService: CodeReviewService;
|
||||
let req: Request;
|
||||
let res: Response;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Reset running state
|
||||
const { setRunningState, isRunning } = await import('@/routes/code-review/common.js');
|
||||
vi.mocked(setRunningState)(false);
|
||||
vi.mocked(isRunning).mockReturnValue(false);
|
||||
|
||||
mockCodeReviewService = {
|
||||
executeReview: vi.fn().mockResolvedValue({
|
||||
id: 'review-123',
|
||||
verdict: 'approved',
|
||||
summary: 'No issues found',
|
||||
comments: [],
|
||||
stats: {
|
||||
totalComments: 0,
|
||||
bySeverity: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
||||
byCategory: {},
|
||||
autoFixedCount: 0,
|
||||
},
|
||||
filesReviewed: ['src/index.ts'],
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
reviewedAt: new Date().toISOString(),
|
||||
durationMs: 1000,
|
||||
}),
|
||||
getProviderStatus: vi.fn(),
|
||||
getBestProvider: vi.fn(),
|
||||
refreshProviderStatus: vi.fn(),
|
||||
initialize: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const context = createMockExpressContext();
|
||||
req = context.req;
|
||||
res = context.res;
|
||||
});
|
||||
|
||||
describe('parameter validation', () => {
|
||||
it('should return 400 if projectPath is missing', async () => {
|
||||
req.body = {};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'projectPath is required',
|
||||
});
|
||||
expect(mockCodeReviewService.executeReview).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 if files is not an array', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
files: 'not-an-array',
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'files must be an array',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if too many files', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
files: Array.from({ length: 150 }, (_, i) => `file${i}.ts`),
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Maximum 100 files allowed per request',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if file path is too long', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
files: ['a'.repeat(600)],
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'File path too long',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if baseRef is not a string', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
baseRef: 123,
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'baseRef must be a string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if baseRef is too long', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
baseRef: 'a'.repeat(300),
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'baseRef is too long',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if categories is not an array', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
categories: 'security',
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'categories must be an array',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if category is invalid', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
categories: ['security', 'invalid_category'],
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Invalid category: invalid_category',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if autoFix is not a boolean', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
autoFix: 'true',
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'autoFix must be a boolean',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if thinkingLevel is invalid', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
thinkingLevel: 'invalid',
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Invalid thinkingLevel: invalid',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('concurrent review prevention', () => {
|
||||
it('should return 409 if a review is already in progress', async () => {
|
||||
const { isRunning } = await import('@/routes/code-review/common.js');
|
||||
vi.mocked(isRunning).mockReturnValue(true);
|
||||
|
||||
req.body = { projectPath: '/test/project' };
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'A code review is already in progress',
|
||||
});
|
||||
expect(mockCodeReviewService.executeReview).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('successful review execution', () => {
|
||||
it('should trigger review and return success immediately', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Code review started',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass all options to executeReview', async () => {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
files: ['src/index.ts', 'src/utils.ts'],
|
||||
baseRef: 'main',
|
||||
categories: ['security', 'performance'],
|
||||
autoFix: true,
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
thinkingLevel: 'high',
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
// Wait for async execution
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(mockCodeReviewService.executeReview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
projectPath: '/test/project',
|
||||
files: ['src/index.ts', 'src/utils.ts'],
|
||||
baseRef: 'main',
|
||||
categories: ['security', 'performance'],
|
||||
autoFix: true,
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
thinkingLevel: 'high',
|
||||
abortController: expect.any(AbortController),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept valid categories', async () => {
|
||||
const validCategories = [
|
||||
'tech_stack',
|
||||
'security',
|
||||
'code_quality',
|
||||
'implementation',
|
||||
'architecture',
|
||||
'performance',
|
||||
'testing',
|
||||
'documentation',
|
||||
];
|
||||
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
categories: validCategories,
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Code review started',
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept valid thinking levels', async () => {
|
||||
for (const level of ['low', 'medium', 'high']) {
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
thinkingLevel: level,
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Code review started',
|
||||
});
|
||||
|
||||
vi.clearAllMocks();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle service errors gracefully', async () => {
|
||||
mockCodeReviewService.executeReview = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
req.body = {
|
||||
projectPath: '/test/project',
|
||||
};
|
||||
|
||||
const handler = createTriggerHandler(mockCodeReviewService);
|
||||
await handler(req, res);
|
||||
|
||||
// Response is sent immediately (async execution)
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Code review started',
|
||||
});
|
||||
|
||||
// Wait for async error handling
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Running state should be reset
|
||||
const { setRunningState } = await import('@/routes/code-review/common.js');
|
||||
expect(setRunningState).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user