Files
magpie/tests/e2e/repo-review.test.ts
xiaofanluan 7cccacdc51 feat: add repository-wide code review with feature analysis
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>
2026-01-27 22:58:22 +08:00

324 lines
12 KiB
TypeScript

// tests/e2e/repo-review.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { RepoScanner, shouldIgnore, detectLanguage } from '../../src/repo-scanner'
import { ReviewPlanner, FeaturePlanner } from '../../src/planner'
import { RepoOrchestrator } from '../../src/orchestrator/repo-orchestrator'
import { MarkdownReporter } from '../../src/reporter'
import { StateManager } from '../../src/state'
import type { FeatureAnalysis, ReviewSession } from '../../src/state/types'
import { computeCodebaseHash } from '../../src/feature-analyzer/hash'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
describe('Repo Review Integration', () => {
it('should complete full review pipeline', async () => {
// Mock file system
const mockFiles = [
{ path: '/p/src/a.ts', relativePath: 'src/a.ts', language: 'typescript', lines: 100, size: 1024 },
{ path: '/p/src/b.ts', relativePath: 'src/b.ts', language: 'typescript', lines: 50, size: 512 }
]
const planner = new ReviewPlanner(mockFiles)
const plan = planner.createPlan()
expect(plan.steps.length).toBeGreaterThan(0)
expect(plan.totalEstimatedTokens).toBeGreaterThan(0)
const reporter = new MarkdownReporter()
const result = {
repoName: 'test',
timestamp: new Date(),
stats: { totalFiles: 2, totalLines: 150, languages: { typescript: 2 }, estimatedTokens: 384, estimatedCost: 0.00384 },
architectureAnalysis: 'Test analysis',
issues: [],
tokenUsage: { total: 1000, cost: 0.01 }
}
const report = reporter.generate(result)
expect(report).toContain('test')
expect(report).toContain('150 lines of code')
})
it('should filter files correctly', () => {
expect(shouldIgnore('node_modules/test.js', [])).toBe(true)
expect(shouldIgnore('src/index.ts', [])).toBe(false)
expect(detectLanguage('test.ts')).toBe('typescript')
expect(detectLanguage('test.py')).toBe('python')
})
it('should plan review steps by directory', () => {
const mockFiles = [
{ path: '/p/src/core/a.ts', relativePath: 'src/core/a.ts', language: 'typescript', lines: 100, size: 1024 },
{ path: '/p/src/utils/b.ts', relativePath: 'src/utils/b.ts', language: 'typescript', lines: 50, size: 512 },
{ path: '/p/tests/test.ts', relativePath: 'tests/test.ts', language: 'typescript', lines: 30, size: 256 }
]
const planner = new ReviewPlanner(mockFiles)
const plan = planner.createPlan()
// Should have at least 2 steps (src/core, src/utils, tests grouped differently)
expect(plan.steps.length).toBeGreaterThanOrEqual(2)
// Check that files are distributed among steps
const totalFilesInSteps = plan.steps.reduce((sum, s) => sum + s.files.length, 0)
expect(totalFilesInSteps).toBe(3)
})
it('should generate report with all sections', () => {
const reporter = new MarkdownReporter()
const result = {
repoName: 'magpie',
timestamp: new Date('2026-01-26'),
stats: { totalFiles: 10, totalLines: 1000, languages: { typescript: 8, javascript: 2 }, estimatedTokens: 5000, estimatedCost: 0.05 },
architectureAnalysis: 'Well-structured codebase',
architectureStrengths: ['Clean separation of concerns', 'Good test coverage'],
architectureImprovements: ['Consider adding dependency injection'],
issues: [
{ id: 1, location: 'src/auth.ts:42', description: 'Potential SQL injection', severity: 'high' as const, consensus: '2/2' },
{ id: 2, location: 'src/api.ts:100', description: 'Missing error handling', severity: 'medium' as const, consensus: '2/2' }
],
tokenUsage: { total: 10000, cost: 0.10 }
}
const report = reporter.generate(result)
// Check header
expect(report).toContain('# Repository Review Report: magpie')
expect(report).toContain('10 files')
expect(report).toContain('1000 lines of code')
// Check sections
expect(report).toContain('## Executive Summary')
expect(report).toContain('## Architecture Assessment')
expect(report).toContain('## Issue List')
expect(report).toContain('## Token Usage Statistics')
// Check issues
expect(report).toContain('🔴 High Priority')
expect(report).toContain('Potential SQL injection')
expect(report).toContain('🟡 Medium Priority')
expect(report).toContain('Missing error handling')
})
})
describe('Feature-Based Repo Review Integration', () => {
let tempDir: string
let stateManager: StateManager
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'magpie-e2e-'))
stateManager = new StateManager(tempDir)
await stateManager.init()
})
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true })
})
it('should compute consistent codebase hash', () => {
const files = [
{ path: '/p/a.ts', relativePath: 'a.ts', language: 'typescript', lines: 100, size: 1024 },
{ path: '/p/b.ts', relativePath: 'b.ts', language: 'typescript', lines: 50, size: 512 }
]
const hash1 = computeCodebaseHash(files)
const hash2 = computeCodebaseHash(files)
expect(hash1).toBe(hash2)
// Different order should produce same hash
const hash3 = computeCodebaseHash([files[1], files[0]])
expect(hash1).toBe(hash3)
// Different files should produce different hash
const hash4 = computeCodebaseHash([
{ path: '/p/c.ts', relativePath: 'c.ts', language: 'typescript', lines: 100, size: 1024 }
])
expect(hash1).not.toBe(hash4)
})
it('should create feature-based plan from analysis', () => {
const analysis: FeatureAnalysis = {
features: [
{
id: 'write',
name: 'Write Operations',
description: 'Insert and update operations',
entryPoints: ['src/insert.ts'],
files: [
{ path: '/p/src/insert.ts', relativePath: 'src/insert.ts', language: 'typescript', lines: 200, size: 2048 },
{ path: '/p/src/update.ts', relativePath: 'src/update.ts', language: 'typescript', lines: 150, size: 1536 }
],
estimatedTokens: 2000
},
{
id: 'query',
name: 'Query Operations',
description: 'Search and read operations',
entryPoints: ['src/query.ts'],
files: [
{ path: '/p/src/query.ts', relativePath: 'src/query.ts', language: 'typescript', lines: 300, size: 3072 },
{ path: '/p/src/search.ts', relativePath: 'src/search.ts', language: 'typescript', lines: 250, size: 2560 }
],
estimatedTokens: 3000
}
],
uncategorized: [],
analyzedAt: new Date(),
codebaseHash: 'abc123'
}
const planner = new FeaturePlanner(analysis)
// Test full plan
const fullPlan = planner.createPlan(['write', 'query'])
expect(fullPlan.steps).toHaveLength(2)
expect(fullPlan.steps[0].featureId).toBe('write')
expect(fullPlan.steps[1].featureId).toBe('query')
expect(fullPlan.totalEstimatedTokens).toBe(5000)
// Test partial plan
const partialPlan = planner.createPlan(['query'])
expect(partialPlan.steps).toHaveLength(1)
expect(partialPlan.steps[0].featureId).toBe('query')
expect(partialPlan.totalEstimatedTokens).toBe(3000)
})
it('should persist and restore review session', async () => {
const session: ReviewSession = {
id: 'test-session-1',
startedAt: new Date('2024-01-01T10:00:00Z'),
updatedAt: new Date('2024-01-01T11:00:00Z'),
status: 'in_progress',
config: {
focusAreas: ['security', 'performance'],
selectedFeatures: ['write', 'query', 'auth']
},
plan: {
features: [],
totalFeatures: 3,
selectedCount: 3
},
progress: {
currentFeatureIndex: 1,
completedFeatures: ['write'],
featureResults: {
write: {
featureId: 'write',
issues: [
{ id: 1, location: 'src/insert.ts:42', description: 'No input validation', severity: 'high', consensus: '2/2' }
],
summary: 'Write operations need input validation',
reviewedAt: new Date('2024-01-01T10:30:00Z')
}
}
}
}
// Save session
await stateManager.saveSession(session)
// Load session
const loaded = await stateManager.loadSession('test-session-1')
expect(loaded).not.toBeNull()
expect(loaded!.id).toBe('test-session-1')
expect(loaded!.status).toBe('in_progress')
expect(loaded!.progress.completedFeatures).toEqual(['write'])
expect(loaded!.progress.featureResults.write.issues).toHaveLength(1)
// Find incomplete sessions
const incomplete = await stateManager.findIncompleteSessions()
expect(incomplete).toHaveLength(1)
expect(incomplete[0].id).toBe('test-session-1')
// List all sessions
const all = await stateManager.listAllSessions()
expect(all).toHaveLength(1)
})
it('should support session resume workflow', async () => {
// Create initial session with partial progress
const session: ReviewSession = {
id: 'resume-test-1',
startedAt: new Date(),
updatedAt: new Date(),
status: 'paused',
config: {
focusAreas: ['security'],
selectedFeatures: ['f1', 'f2', 'f3']
},
plan: {
features: [],
totalFeatures: 3,
selectedCount: 3
},
progress: {
currentFeatureIndex: 1,
completedFeatures: ['f1'],
featureResults: {
f1: { featureId: 'f1', issues: [], summary: 'No issues', reviewedAt: new Date() }
}
}
}
await stateManager.saveSession(session)
// Simulate resume - load and check remaining work
const loaded = await stateManager.loadSession('resume-test-1')!
expect(loaded).not.toBeNull()
const remaining = loaded!.config.selectedFeatures.filter(
id => !loaded!.progress.completedFeatures.includes(id)
)
expect(remaining).toEqual(['f2', 'f3'])
// Simulate completing another feature
loaded!.progress.completedFeatures.push('f2')
loaded!.progress.featureResults.f2 = {
featureId: 'f2',
issues: [],
summary: 'No issues',
reviewedAt: new Date()
}
loaded!.progress.currentFeatureIndex = 2
loaded!.status = 'in_progress'
loaded!.updatedAt = new Date()
await stateManager.saveSession(loaded!)
// Verify progress saved
const reloaded = await stateManager.loadSession('resume-test-1')
expect(reloaded!.progress.completedFeatures).toEqual(['f1', 'f2'])
expect(reloaded!.status).toBe('in_progress')
})
it('should cache and retrieve feature analysis', async () => {
const analysis: FeatureAnalysis = {
features: [
{
id: 'core',
name: 'Core Module',
description: 'Core functionality',
entryPoints: ['src/core.ts'],
files: [{ path: '/p/src/core.ts', relativePath: 'src/core.ts', language: 'typescript', lines: 500, size: 5120 }],
estimatedTokens: 5000
}
],
uncategorized: [
{ path: '/p/utils.ts', relativePath: 'utils.ts', language: 'typescript', lines: 50, size: 512 }
],
analyzedAt: new Date('2024-01-01'),
codebaseHash: 'test-hash-123'
}
await stateManager.saveFeatureAnalysis(analysis)
const loaded = await stateManager.loadFeatureAnalysis()
expect(loaded).not.toBeNull()
expect(loaded!.features).toHaveLength(1)
expect(loaded!.features[0].id).toBe('core')
expect(loaded!.uncategorized).toHaveLength(1)
expect(loaded!.codebaseHash).toBe('test-hash-123')
})
})