Files
proffesor-for-testing__agen…/tests/code-intelligence/inference/ComponentBoundaryAnalyzer.test.ts
Profa 8bb2ad1aee feat(v2.6.6): Web-Tree-Sitter migration, MinCut analysis, CriticalPathDetector
## Web-Tree-Sitter Migration (Phases 1-5)
- New WebTreeSitterParser replacing native tree-sitter bindings
- Eliminates npm install warnings about native compilation
- Updated all language extractors (TS, JS, Python, Go, Rust)

## MinCut Analysis Integration (Phases 1-6)
- New src/code-intelligence/analysis/mincut/ module
- MinCutAnalyzer: Stoer-Wagner algorithm for graph partitioning
- GraphAdapter: Code graph to MinCut format conversion
- CircularDependencyDetector: Dependency cycle detection
- ModuleCouplingAnalyzer: Module coupling analysis

## CriticalPathDetector for Coverage Analysis
- New src/coverage/CriticalPathDetector.ts
- Identifies structurally critical code paths using MinCut
- Prioritizes coverage gaps by criticality score
- Integrated with CoverageAnalyzerAgent

## Fleet Topology Management
- New src/fleet/topology/ module
- MinCut-based test file partitioning
- Integrated with test-execute-parallel MCP handler

## Other Changes
- MCP tool count corrected: 102 → 105 tools
- New CLI commands: aqe kg mincut, aqe kg circular
- 19 new tests for CriticalPathDetector

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 11:02:28 +00:00

361 lines
11 KiB
TypeScript

/**
* Unit tests for ComponentBoundaryAnalyzer
*/
import { ComponentBoundaryAnalyzer } from '../../../src/code-intelligence/inference/ComponentBoundaryAnalyzer';
import type { Component, ComponentRelationship } from '../../../src/code-intelligence/inference/types';
import * as fs from 'fs/promises';
import * as path from 'path';
// Mock fast-glob
jest.mock('fast-glob');
import fg from 'fast-glob';
const mockFg = fg as jest.MockedFunction<typeof fg>;
// Mock fs/promises
jest.mock('fs/promises');
const mockFs = fs as jest.Mocked<typeof fs>;
describe('ComponentBoundaryAnalyzer', () => {
const rootDir = '/test/src';
beforeEach(() => {
jest.clearAllMocks();
});
describe('Component Detection', () => {
it('should detect layered architecture components', async () => {
// Mock file system structure
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
'/test/src/services/UserService.ts',
'/test/src/services/AuthService.ts',
'/test/src/repositories/UserRepository.ts',
'/test/src/repositories/AuthRepository.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
expect(result.components).toHaveLength(3);
expect(result.components.map(c => c.type)).toEqual(['layer', 'layer', 'layer']);
const componentNames = result.components.map(c => c.name).sort();
expect(componentNames).toContain('Controllers');
expect(componentNames).toContain('Services');
expect(componentNames).toContain('Repositories');
});
it('should detect feature-based components', async () => {
mockFg.mockResolvedValue([
'/test/src/features/user/controller.ts',
'/test/src/features/user/service.ts',
'/test/src/features/user/repository.ts',
'/test/src/features/auth/controller.ts',
'/test/src/features/auth/service.ts',
'/test/src/features/auth/repository.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
expect(result.components).toHaveLength(2);
expect(result.components.every(c => c.type === 'feature')).toBe(true);
});
it('should respect minFilesPerComponent threshold', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
'/test/src/services/UserService.ts',
'/test/src/services/AuthService.ts',
'/test/src/utils/helper.ts', // Only 1 file in utils
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
expect(result.components).toHaveLength(2);
expect(result.components.map(c => c.name)).not.toContain('Utils');
});
it('should detect technology from file extensions', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.tsx',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 1,
});
const result = await analyzer.analyze();
const component = result.components[0];
expect(component.technology).toMatch(/TypeScript/);
});
});
describe('Relationship Analysis', () => {
beforeEach(() => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
'/test/src/services/UserService.ts',
'/test/src/services/AuthService.ts',
]);
});
it('should analyze import relationships', async () => {
// Mock file contents with imports
mockFs.readFile.mockImplementation(async (filePath: any) => {
if (filePath.includes('UserController.ts')) {
return "import { UserService } from '../services/UserService';\n";
}
if (filePath.includes('AuthController.ts')) {
return "import { AuthService } from '../services/AuthService';\n";
}
return '';
});
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: true,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
expect(result.relationships.length).toBeGreaterThan(0);
expect(result.relationships[0]).toHaveProperty('sourceId');
expect(result.relationships[0]).toHaveProperty('targetId');
expect(result.relationships[0]).toHaveProperty('type');
expect(result.relationships[0].type).toBe('uses');
});
it('should skip external package imports', async () => {
mockFs.readFile.mockImplementation(async (filePath: any) => {
if (filePath.includes('UserController.ts')) {
return "import express from 'express';\nimport { UserService } from '../services/UserService';\n";
}
return '';
});
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: true,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
// Should only have internal relationships, not to 'express'
result.relationships.forEach(rel => {
expect(rel.sourceId).not.toContain('express');
expect(rel.targetId).not.toContain('express');
});
});
it('should handle require statements', async () => {
mockFs.readFile.mockImplementation(async (filePath: any) => {
if (filePath.includes('UserController.ts')) {
return "const { UserService } = require('../services/UserService');\n";
}
return '';
});
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: true,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
expect(result.relationships.length).toBeGreaterThan(0);
});
it('should prevent duplicate relationships', async () => {
mockFs.readFile.mockImplementation(async (filePath: any) => {
if (filePath.includes('UserController.ts')) {
// Multiple imports from same service
return "import { UserService } from '../services/UserService';\nimport { User } from '../services/UserService';\n";
}
return '';
});
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: true,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
// Should have unique relationships only
const relationshipKeys = result.relationships.map(r => `${r.sourceId}->${r.targetId}`);
const uniqueKeys = new Set(relationshipKeys);
expect(relationshipKeys.length).toBe(uniqueKeys.size);
});
});
describe('Component Metadata', () => {
it('should include component responsibilities for layers', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
const component = result.components[0];
expect(component.responsibilities).toBeDefined();
expect(component.responsibilities!.length).toBeGreaterThan(0);
});
it('should include file paths relative to rootDir', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
const component = result.components[0];
expect(component.files).toBeDefined();
component.files.forEach(file => {
expect(file).not.toContain('/test/src');
expect(file).toContain('controllers/');
});
});
it('should include analysis metadata', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
expect(result.metadata).toBeDefined();
expect(result.metadata.timestamp).toBeGreaterThan(0);
expect(result.metadata.durationMs).toBeGreaterThanOrEqual(0);
expect(result.metadata.filesAnalyzed).toBeGreaterThan(0);
});
});
describe('Configuration', () => {
it('should use custom exclude patterns', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
excludePatterns: ['**/*.test.ts', '**/*.custom.ts'],
});
await analyzer.analyze();
expect(mockFg).toHaveBeenCalledWith(
expect.any(Array),
expect.objectContaining({
ignore: expect.arrayContaining(['**/*.custom.ts']),
})
);
});
it('should respect maxDepth configuration', async () => {
mockFg.mockResolvedValue([
'/test/src/a/b/c/d/e/f/deep.ts',
'/test/src/a/b/c/d/e/f/deeper.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
maxDepth: 3,
minFilesPerComponent: 1,
});
const result = await analyzer.analyze();
// Should not detect components at depth > 3
expect(result.components.length).toBe(0);
});
});
describe('Edge Cases', () => {
it('should handle empty codebase', async () => {
mockFg.mockResolvedValue([]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir);
const result = await analyzer.analyze();
expect(result.components).toHaveLength(0);
expect(result.relationships).toHaveLength(0);
});
it('should handle file read errors gracefully', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
]);
mockFs.readFile.mockRejectedValue(new Error('Permission denied'));
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: true,
minFilesPerComponent: 2,
});
// Should not throw
await expect(analyzer.analyze()).resolves.toBeDefined();
});
it('should generate unique component IDs', async () => {
mockFg.mockResolvedValue([
'/test/src/controllers/UserController.ts',
'/test/src/controllers/AuthController.ts',
'/test/src/services/UserService.ts',
'/test/src/services/AuthService.ts',
]);
const analyzer = new ComponentBoundaryAnalyzer(rootDir, {
analyzeImports: false,
minFilesPerComponent: 2,
});
const result = await analyzer.analyze();
const ids = result.components.map(c => c.id);
const uniqueIds = new Set(ids);
expect(ids.length).toBe(uniqueIds.size);
});
});
});