7cccacdc51
Implement full repository review mode that detects logical features/modules and reviews them systematically with session persistence for pause/resume capability. Key additions: - RepoScanner: scans codebase and estimates tokens/cost - FeatureAnalyzer: AI-powered detection of logical modules - FeaturePlanner: creates review execution plan - StateManager: persists sessions for resume capability - RepoOrchestrator: executes feature-by-feature reviews - MarkdownReporter: generates review reports New CLI options: --repo, --quick, --deep, --list-sessions, --session, --export, --path, --ignore, --plan-only, --reanalyze Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
238 lines
7.4 KiB
TypeScript
238 lines
7.4 KiB
TypeScript
// tests/orchestrator/repo-orchestrator.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { RepoOrchestrator } from '../../src/orchestrator/repo-orchestrator.js'
|
|
import type { AIProvider } from '../../src/providers/types.js'
|
|
import type { Reviewer } from '../../src/orchestrator/types.js'
|
|
import type { ReviewPlan, ReviewStep } from '../../src/planner/types.js'
|
|
import type { FeaturePlan, FeatureStep } from '../../src/planner/feature-planner.js'
|
|
|
|
const createMockProvider = (responses: string[]): AIProvider => {
|
|
let callCount = 0
|
|
return {
|
|
name: 'mock',
|
|
chat: vi.fn().mockImplementation(async () => responses[callCount++] || 'default'),
|
|
chatStream: vi.fn().mockImplementation(async function* () {
|
|
yield responses[callCount++] || 'default'
|
|
})
|
|
}
|
|
}
|
|
|
|
describe('RepoOrchestrator', () => {
|
|
it('should execute review steps sequentially', async () => {
|
|
const reviewerA: Reviewer = {
|
|
id: 'security',
|
|
provider: createMockProvider(['Found issue 1', 'Summary A']),
|
|
systemPrompt: 'Security expert'
|
|
}
|
|
const summarizer: Reviewer = {
|
|
id: 'summarizer',
|
|
provider: createMockProvider(['Final report']),
|
|
systemPrompt: 'Summarizer'
|
|
}
|
|
|
|
const plan: ReviewPlan = {
|
|
steps: [
|
|
{ name: 'src/core', description: 'Review core', files: [], estimatedTokens: 1000 }
|
|
],
|
|
totalEstimatedTokens: 1000,
|
|
totalEstimatedCost: 0.01
|
|
}
|
|
|
|
const orchestrator = new RepoOrchestrator([reviewerA], summarizer, {
|
|
onStepStart: vi.fn(),
|
|
onStepComplete: vi.fn()
|
|
})
|
|
|
|
const result = await orchestrator.executePlan(plan, 'test-repo')
|
|
|
|
expect(result.issues).toBeDefined()
|
|
expect(result.architectureAnalysis).toBeDefined()
|
|
})
|
|
|
|
it('should parse issues from reviewer responses', async () => {
|
|
const reviewerA: Reviewer = {
|
|
id: 'security',
|
|
provider: createMockProvider([
|
|
'ISSUE: [src/api.ts:10] - [SQL injection vulnerability] - [severity: high]'
|
|
]),
|
|
systemPrompt: 'Security expert'
|
|
}
|
|
const summarizer: Reviewer = {
|
|
id: 'summarizer',
|
|
provider: createMockProvider(['Architecture looks good']),
|
|
systemPrompt: 'Summarizer'
|
|
}
|
|
|
|
const plan: ReviewPlan = {
|
|
steps: [
|
|
{ name: 'src/api', description: 'Review API', files: [], estimatedTokens: 500 }
|
|
],
|
|
totalEstimatedTokens: 500,
|
|
totalEstimatedCost: 0.005
|
|
}
|
|
|
|
const orchestrator = new RepoOrchestrator([reviewerA], summarizer)
|
|
const result = await orchestrator.executePlan(plan, 'test-repo')
|
|
|
|
expect(result.issues.length).toBe(1)
|
|
expect(result.issues[0].location).toBe('src/api.ts:10')
|
|
expect(result.issues[0].description).toBe('SQL injection vulnerability')
|
|
expect(result.issues[0].severity).toBe('high')
|
|
})
|
|
|
|
it('should call onStepStart and onStepComplete callbacks', async () => {
|
|
const reviewerA: Reviewer = {
|
|
id: 'reviewer',
|
|
provider: createMockProvider(['No issues found']),
|
|
systemPrompt: 'Reviewer'
|
|
}
|
|
const summarizer: Reviewer = {
|
|
id: 'summarizer',
|
|
provider: createMockProvider(['Summary']),
|
|
systemPrompt: 'Summarizer'
|
|
}
|
|
|
|
const onStepStart = vi.fn()
|
|
const onStepComplete = vi.fn()
|
|
|
|
const plan: ReviewPlan = {
|
|
steps: [
|
|
{ name: 'step1', description: 'Step 1', files: [], estimatedTokens: 100 },
|
|
{ name: 'step2', description: 'Step 2', files: [], estimatedTokens: 100 }
|
|
],
|
|
totalEstimatedTokens: 200,
|
|
totalEstimatedCost: 0.002
|
|
}
|
|
|
|
const orchestrator = new RepoOrchestrator([reviewerA], summarizer, {
|
|
onStepStart,
|
|
onStepComplete
|
|
})
|
|
|
|
await orchestrator.executePlan(plan, 'test-repo')
|
|
|
|
expect(onStepStart).toHaveBeenCalledTimes(2)
|
|
expect(onStepComplete).toHaveBeenCalledTimes(2)
|
|
expect(onStepStart).toHaveBeenCalledWith(plan.steps[0], 0, 2)
|
|
expect(onStepStart).toHaveBeenCalledWith(plan.steps[1], 1, 2)
|
|
})
|
|
|
|
it('should debate high-severity issues', async () => {
|
|
const reviewerA: Reviewer = {
|
|
id: 'security',
|
|
provider: createMockProvider([
|
|
'ISSUE: [src/auth.ts:5] - [Hardcoded credentials] - [severity: high]',
|
|
'This is definitely a critical issue'
|
|
]),
|
|
systemPrompt: 'Security expert'
|
|
}
|
|
const summarizer: Reviewer = {
|
|
id: 'summarizer',
|
|
provider: createMockProvider(['Architecture analysis']),
|
|
systemPrompt: 'Summarizer'
|
|
}
|
|
|
|
const onDebate = vi.fn()
|
|
|
|
const plan: ReviewPlan = {
|
|
steps: [
|
|
{ name: 'src/auth', description: 'Review auth', files: [], estimatedTokens: 300 }
|
|
],
|
|
totalEstimatedTokens: 300,
|
|
totalEstimatedCost: 0.003
|
|
}
|
|
|
|
const orchestrator = new RepoOrchestrator([reviewerA], summarizer, { onDebate })
|
|
const result = await orchestrator.executePlan(plan, 'test-repo')
|
|
|
|
expect(onDebate).toHaveBeenCalled()
|
|
expect(result.issues[0].debateSummary).toBeDefined()
|
|
})
|
|
|
|
it('should return correct token usage from plan', async () => {
|
|
const reviewer: Reviewer = {
|
|
id: 'reviewer',
|
|
provider: createMockProvider(['No issues']),
|
|
systemPrompt: 'Reviewer'
|
|
}
|
|
const summarizer: Reviewer = {
|
|
id: 'summarizer',
|
|
provider: createMockProvider(['Summary']),
|
|
systemPrompt: 'Summarizer'
|
|
}
|
|
|
|
const plan: ReviewPlan = {
|
|
steps: [{ name: 'step1', description: 'Step 1', files: [], estimatedTokens: 1500 }],
|
|
totalEstimatedTokens: 1500,
|
|
totalEstimatedCost: 0.015
|
|
}
|
|
|
|
const orchestrator = new RepoOrchestrator([reviewer], summarizer)
|
|
const result = await orchestrator.executePlan(plan, 'test-repo')
|
|
|
|
expect(result.tokenUsage.total).toBe(1500)
|
|
expect(result.tokenUsage.cost).toBe(0.015)
|
|
})
|
|
})
|
|
|
|
describe('RepoOrchestrator - Feature Based', () => {
|
|
const mockProvider = {
|
|
chat: vi.fn().mockResolvedValue('ISSUE: [test.ts:10] - [test issue] - [severity: medium]')
|
|
}
|
|
|
|
const mockReviewer = {
|
|
id: 'reviewer1',
|
|
provider: mockProvider,
|
|
systemPrompt: 'Review code'
|
|
}
|
|
|
|
const mockSummarizer = {
|
|
id: 'summarizer',
|
|
provider: mockProvider,
|
|
systemPrompt: 'Summarize'
|
|
}
|
|
|
|
it('should execute feature plan and track results', async () => {
|
|
const featurePlan: FeaturePlan = {
|
|
steps: [
|
|
{
|
|
featureId: 'write',
|
|
name: 'Write Operations',
|
|
description: 'Insert and update',
|
|
files: [{ path: '/a.ts', relativePath: 'a.ts', language: 'ts', lines: 100, size: 1000 }],
|
|
estimatedTokens: 250
|
|
}
|
|
],
|
|
totalEstimatedTokens: 250,
|
|
totalEstimatedCost: 0.0025
|
|
}
|
|
|
|
const orchestrator = new RepoOrchestrator([mockReviewer], mockSummarizer, {})
|
|
const result = await orchestrator.executeFeaturePlan(featurePlan, 'test-repo')
|
|
|
|
expect(result.featureResults).toBeDefined()
|
|
expect(result.featureResults['write']).toBeDefined()
|
|
expect(result.featureResults['write'].issues.length).toBeGreaterThanOrEqual(0)
|
|
})
|
|
|
|
it('should call onFeatureComplete callback', async () => {
|
|
const onFeatureComplete = vi.fn()
|
|
|
|
const featurePlan: FeaturePlan = {
|
|
steps: [
|
|
{ featureId: 'write', name: 'Write', description: '', files: [], estimatedTokens: 100 }
|
|
],
|
|
totalEstimatedTokens: 100,
|
|
totalEstimatedCost: 0.001
|
|
}
|
|
|
|
const orchestrator = new RepoOrchestrator([mockReviewer], mockSummarizer, {
|
|
onFeatureComplete
|
|
})
|
|
|
|
await orchestrator.executeFeaturePlan(featurePlan, 'test-repo')
|
|
|
|
expect(onFeatureComplete).toHaveBeenCalledWith('write', expect.any(Object))
|
|
})
|
|
})
|