mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
808 lines
26 KiB
TypeScript
808 lines
26 KiB
TypeScript
/**
|
|
* Project Analyzer Tests
|
|
* ADR-025: Enhanced Init with Self-Configuration
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { join } from 'path';
|
|
import {
|
|
ProjectAnalyzer,
|
|
createProjectAnalyzer,
|
|
} from '../../../src/init/project-analyzer.js';
|
|
import type { ProjectAnalysis } from '../../../src/init/types.js';
|
|
|
|
// Mock fs module
|
|
vi.mock('fs', async () => {
|
|
const actual = await vi.importActual('fs');
|
|
return {
|
|
...actual,
|
|
existsSync: vi.fn(),
|
|
readFileSync: vi.fn(),
|
|
statSync: vi.fn(),
|
|
readdirSync: vi.fn(),
|
|
};
|
|
});
|
|
|
|
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
|
|
|
|
const mockExistsSync = existsSync as ReturnType<typeof vi.fn>;
|
|
const mockReadFileSync = readFileSync as ReturnType<typeof vi.fn>;
|
|
const mockStatSync = statSync as ReturnType<typeof vi.fn>;
|
|
const mockReaddirSync = readdirSync as ReturnType<typeof vi.fn>;
|
|
|
|
describe('ProjectAnalyzer', () => {
|
|
const testProjectRoot = '/test/project';
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Default mocks
|
|
mockExistsSync.mockReturnValue(false);
|
|
mockReadFileSync.mockReturnValue('');
|
|
mockStatSync.mockReturnValue({ isDirectory: () => false, isFile: () => true });
|
|
mockReaddirSync.mockReturnValue([]);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('createProjectAnalyzer', () => {
|
|
it('should create a project analyzer', () => {
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
expect(analyzer).toBeInstanceOf(ProjectAnalyzer);
|
|
});
|
|
});
|
|
|
|
describe('Framework Detection', () => {
|
|
it('should detect Jest framework from config file', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return path === join(testProjectRoot, 'jest.config.js');
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({
|
|
name: 'test-project',
|
|
devDependencies: { jest: '^29.0.0' },
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const frameworks = await analyzer.detectFrameworks();
|
|
|
|
expect(frameworks).toHaveLength(1);
|
|
expect(frameworks[0].name).toBe('jest');
|
|
expect(frameworks[0].confidence).toBe(1.0);
|
|
});
|
|
|
|
it('should detect Vitest framework', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return path === join(testProjectRoot, 'vitest.config.ts');
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({
|
|
name: 'test-project',
|
|
devDependencies: { vitest: '^1.0.0' },
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const frameworks = await analyzer.detectFrameworks();
|
|
|
|
expect(frameworks).toHaveLength(1);
|
|
expect(frameworks[0].name).toBe('vitest');
|
|
});
|
|
|
|
it('should detect multiple frameworks', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return (
|
|
path === join(testProjectRoot, 'jest.config.js') ||
|
|
path === join(testProjectRoot, 'playwright.config.ts')
|
|
);
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({
|
|
name: 'test-project',
|
|
devDependencies: {
|
|
jest: '^29.0.0',
|
|
'@playwright/test': '^1.40.0',
|
|
},
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const frameworks = await analyzer.detectFrameworks();
|
|
|
|
expect(frameworks).toHaveLength(2);
|
|
const names = frameworks.map((f) => f.name);
|
|
expect(names).toContain('jest');
|
|
expect(names).toContain('playwright');
|
|
});
|
|
|
|
it('should detect pytest from pyproject.toml', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return path === join(testProjectRoot, 'pyproject.toml');
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'pyproject.toml')) {
|
|
return '[tool.pytest]\ntestpaths = ["tests"]';
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const frameworks = await analyzer.detectFrameworks();
|
|
|
|
expect(frameworks).toHaveLength(1);
|
|
expect(frameworks[0].name).toBe('pytest');
|
|
});
|
|
});
|
|
|
|
describe('Language Detection', () => {
|
|
it('should detect TypeScript files', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['index.ts', 'utils.ts', 'types.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const isDir = path === testProjectRoot || path === join(testProjectRoot, 'src');
|
|
return {
|
|
isDirectory: () => isDir,
|
|
isFile: () => !isDir,
|
|
};
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const languages = await analyzer.detectLanguages();
|
|
|
|
expect(languages).toHaveLength(1);
|
|
expect(languages[0].name).toBe('typescript');
|
|
expect(languages[0].fileCount).toBe(3);
|
|
});
|
|
|
|
it('should detect multiple languages', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src', 'scripts'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['index.ts', 'utils.ts'];
|
|
if (dir === join(testProjectRoot, 'scripts')) return ['build.py', 'deploy.py'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src'), join(testProjectRoot, 'scripts')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const languages = await analyzer.detectLanguages();
|
|
|
|
expect(languages).toHaveLength(2);
|
|
const names = languages.map((l) => l.name);
|
|
expect(names).toContain('typescript');
|
|
expect(names).toContain('python');
|
|
});
|
|
});
|
|
|
|
describe('Test Detection', () => {
|
|
it('should detect test files', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['tests'];
|
|
if (dir === join(testProjectRoot, 'tests')) {
|
|
return ['unit.test.ts', 'integration.test.ts', 'e2e.spec.ts'];
|
|
}
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'tests')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const tests = await analyzer.detectExistingTests();
|
|
|
|
expect(tests.totalCount).toBe(3);
|
|
});
|
|
|
|
it('should categorize test types', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['tests'];
|
|
if (dir === join(testProjectRoot, 'tests')) return ['unit', 'integration', 'e2e'];
|
|
if (dir === join(testProjectRoot, 'tests', 'unit')) return ['foo.test.ts'];
|
|
if (dir === join(testProjectRoot, 'tests', 'integration')) return ['bar.test.ts'];
|
|
if (dir === join(testProjectRoot, 'tests', 'e2e')) return ['baz.test.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [
|
|
testProjectRoot,
|
|
join(testProjectRoot, 'tests'),
|
|
join(testProjectRoot, 'tests', 'unit'),
|
|
join(testProjectRoot, 'tests', 'integration'),
|
|
join(testProjectRoot, 'tests', 'e2e'),
|
|
];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const tests = await analyzer.detectExistingTests();
|
|
|
|
expect(tests.byType.unit).toBe(1);
|
|
expect(tests.byType.integration).toBe(1);
|
|
expect(tests.byType.e2e).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Project Type Detection', () => {
|
|
it('should detect monorepo from lerna.json', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return (
|
|
path === join(testProjectRoot, 'lerna.json') ||
|
|
path === join(testProjectRoot, 'package.json')
|
|
);
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({ name: 'monorepo' });
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.projectType).toBe('monorepo');
|
|
});
|
|
|
|
it('should detect monorepo from workspaces', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return path === join(testProjectRoot, 'package.json');
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({
|
|
name: 'monorepo',
|
|
workspaces: ['packages/*'],
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.projectType).toBe('monorepo');
|
|
});
|
|
|
|
it('should detect library from package.json exports', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return path === join(testProjectRoot, 'package.json');
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({
|
|
name: 'my-library',
|
|
main: 'dist/index.js',
|
|
exports: { '.': './dist/index.js' },
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.projectType).toBe('library');
|
|
});
|
|
});
|
|
|
|
describe('Package Manager Detection', () => {
|
|
it('should detect npm from package-lock.json', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return (
|
|
path === join(testProjectRoot, 'package-lock.json') ||
|
|
path === join(testProjectRoot, 'package.json')
|
|
);
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({ name: 'test' });
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.packageManager).toBe('npm');
|
|
});
|
|
|
|
it('should detect pnpm from pnpm-lock.yaml', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return (
|
|
path === join(testProjectRoot, 'pnpm-lock.yaml') ||
|
|
path === join(testProjectRoot, 'package.json')
|
|
);
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({ name: 'test' });
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.packageManager).toBe('pnpm');
|
|
});
|
|
});
|
|
|
|
describe('CI Detection', () => {
|
|
it('should detect GitHub Actions', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return (
|
|
path === join(testProjectRoot, '.github/workflows') ||
|
|
path === join(testProjectRoot, 'package.json')
|
|
);
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({ name: 'test' });
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.hasCIConfig).toBe(true);
|
|
expect(analysis.ciProvider).toBe('github-actions');
|
|
});
|
|
|
|
it('should detect GitLab CI', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return (
|
|
path === join(testProjectRoot, '.gitlab-ci.yml') ||
|
|
path === join(testProjectRoot, 'package.json')
|
|
);
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({ name: 'test' });
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.hasCIConfig).toBe(true);
|
|
expect(analysis.ciProvider).toBe('gitlab-ci');
|
|
});
|
|
});
|
|
|
|
describe('Complete Analysis', () => {
|
|
it('should perform complete analysis', async () => {
|
|
mockExistsSync.mockImplementation((path: string) => {
|
|
return (
|
|
path === join(testProjectRoot, 'package.json') ||
|
|
path === join(testProjectRoot, 'tsconfig.json') ||
|
|
path === join(testProjectRoot, 'vitest.config.ts') ||
|
|
path === join(testProjectRoot, 'package-lock.json') ||
|
|
path === join(testProjectRoot, '.github/workflows')
|
|
);
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path === join(testProjectRoot, 'package.json')) {
|
|
return JSON.stringify({
|
|
name: 'test-project',
|
|
devDependencies: { vitest: '^1.0.0' },
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
mockReaddirSync.mockReturnValue([]);
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const analysis = await analyzer.analyze();
|
|
|
|
expect(analysis.projectName).toBe('test-project');
|
|
expect(analysis.hasTypeScript).toBe(true);
|
|
expect(analysis.hasCIConfig).toBe(true);
|
|
expect(analysis.ciProvider).toBe('github-actions');
|
|
expect(analysis.packageManager).toBe('npm');
|
|
expect(analysis.analysisTimestamp).toBeInstanceOf(Date);
|
|
expect(analysis.analysisDurationMs).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
describe('Cyclomatic Complexity Analysis', () => {
|
|
it('should calculate complexity 1 for empty function', async () => {
|
|
// Create a mock TypeScript file with a simple function
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['simple.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('simple.ts')) {
|
|
return `
|
|
function empty() {
|
|
return 42;
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// Empty function = base complexity of 1
|
|
expect(complexity.averageCyclomatic).toBe(1);
|
|
expect(complexity.maxCyclomatic).toBe(1);
|
|
});
|
|
|
|
it('should count if statements as decision points', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['conditionals.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('conditionals.ts')) {
|
|
return `
|
|
function checkValue(x: number) {
|
|
if (x > 0) {
|
|
return 'positive';
|
|
} else if (x < 0) {
|
|
return 'negative';
|
|
} else {
|
|
return 'zero';
|
|
}
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// 1 (base) + 1 (if) + 1 (else if) = 3
|
|
expect(complexity.averageCyclomatic).toBe(3);
|
|
});
|
|
|
|
it('should count logical operators as decision points', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['logical.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('logical.ts')) {
|
|
return `
|
|
function validate(a: boolean, b: boolean, c: boolean) {
|
|
if (a && b || c) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// 1 (base) + 1 (if) + 1 (&&) + 1 (||) = 4
|
|
expect(complexity.averageCyclomatic).toBe(4);
|
|
});
|
|
|
|
it('should count loops as decision points', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['loops.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('loops.ts')) {
|
|
return `
|
|
function processItems(items: string[]) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
console.log(items[i]);
|
|
}
|
|
|
|
while (items.length > 0) {
|
|
items.pop();
|
|
}
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// 1 (base) + 1 (for) + 1 (while) = 3
|
|
expect(complexity.averageCyclomatic).toBe(3);
|
|
});
|
|
|
|
it('should count switch cases as decision points', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['switch.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('switch.ts')) {
|
|
return `
|
|
function getDay(num: number) {
|
|
switch (num) {
|
|
case 1:
|
|
return 'Monday';
|
|
case 2:
|
|
return 'Tuesday';
|
|
case 3:
|
|
return 'Wednesday';
|
|
default:
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// 1 (base) + 3 (cases) = 4
|
|
expect(complexity.averageCyclomatic).toBe(4);
|
|
});
|
|
|
|
it('should count ternary operators as decision points', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['ternary.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('ternary.ts')) {
|
|
return `
|
|
function getLabel(isActive: boolean) {
|
|
return isActive ? 'Active' : 'Inactive';
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// 1 (base) + 1 (ternary) = 2
|
|
expect(complexity.averageCyclomatic).toBe(2);
|
|
});
|
|
|
|
it('should ignore keywords in comments and strings', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['comments.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('comments.ts')) {
|
|
return `
|
|
function demo() {
|
|
// This comment has if (x) and for (i) and while (true)
|
|
const str = "if (x > 0) { for (i=0) { while (true) } }";
|
|
/* Multi-line comment
|
|
if (shouldNotCount) {
|
|
for (let i = 0; i < 10; i++) {}
|
|
}
|
|
*/
|
|
return str;
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// Should only count base complexity, not keywords in comments/strings
|
|
expect(complexity.averageCyclomatic).toBe(1);
|
|
});
|
|
|
|
it('should count catch blocks as decision points', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['tryCatch.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('tryCatch.ts')) {
|
|
return `
|
|
function riskyOperation() {
|
|
try {
|
|
doSomething();
|
|
} catch (error) {
|
|
handleError(error);
|
|
}
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// 1 (base) + 1 (catch) = 2
|
|
expect(complexity.averageCyclomatic).toBe(2);
|
|
});
|
|
|
|
it('should flag complex files correctly', async () => {
|
|
mockReaddirSync.mockImplementation((dir: string) => {
|
|
if (dir === testProjectRoot) return ['src'];
|
|
if (dir === join(testProjectRoot, 'src')) return ['complex.ts'];
|
|
return [];
|
|
});
|
|
|
|
mockStatSync.mockImplementation((path: string) => {
|
|
const dirs = [testProjectRoot, join(testProjectRoot, 'src')];
|
|
return {
|
|
isDirectory: () => dirs.includes(path),
|
|
isFile: () => !dirs.includes(path),
|
|
};
|
|
});
|
|
|
|
mockReadFileSync.mockImplementation((path: string) => {
|
|
if (path.includes('complex.ts')) {
|
|
// Create a complex function with many decision points
|
|
return `
|
|
function veryComplex(a: number, b: number, c: boolean) {
|
|
if (a > 0) {
|
|
if (b > 0) {
|
|
if (c) {
|
|
for (let i = 0; i < a; i++) {
|
|
while (i < b) {
|
|
if (i % 2 === 0 && i % 3 === 0) {
|
|
switch (i % 4) {
|
|
case 0: return 'zero';
|
|
case 1: return 'one';
|
|
case 2: return 'two';
|
|
default: return 'other';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return c ? 'yes' : 'no';
|
|
}
|
|
`;
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const analyzer = createProjectAnalyzer(testProjectRoot);
|
|
const complexity = await analyzer.analyzeComplexity();
|
|
|
|
// This should flag as complex (> 10)
|
|
expect(complexity.maxCyclomatic).toBeGreaterThan(10);
|
|
expect(complexity.complexFiles).toContain('src/complex.ts');
|
|
});
|
|
});
|
|
});
|