mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
b91627f282
Phase 1: Test Determinism - Create SeededRandom utility with Mulberry32 PRNG (src/utils/SeededRandom.ts) - Migrate 258+ Math.random() calls to SeededRandom across 65+ test files - Add timer test utilities (tests/helpers/timerTestUtils.ts) - Add jest.useFakeTimers() pattern to 13 test files Phase 2: Type Safety (63.5% reduction) - Reduce `any` types from ~1,800 to 656 - Add 50+ new interfaces across agents, core, mcp, cli modules - Fix all TypeScript errors (0 errors, build passes) - Replace Record<string, any> with Record<string, unknown> - Add proper type guards for hook methods (PostTaskData, TaskErrorData) Key improvements: - agents/: 105 → 0 any types (100% reduction) - mcp/: 226 → 102 any types (55% reduction) - cli/: 277 → 168 any types (39% reduction) - core/: 199 → 175 any types (12% reduction) Closes #149 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
/**
|
|
* Test Prioritizer Test Suite - O(log n) Algorithm Validation
|
|
* Tests the sublinear test prioritization and binary heap implementation
|
|
*/
|
|
|
|
import { TestPrioritizer, TestCase, PriorityScore } from './testPrioritizer';
|
|
import { createSeededRandom } from '../../src/utils/SeededRandom';
|
|
|
|
describe('TestPrioritizer', () => {
|
|
let prioritizer: TestPrioritizer;
|
|
|
|
beforeEach(() => {
|
|
prioritizer = new TestPrioritizer(50); // Max 50 tests
|
|
});
|
|
|
|
describe('O(log n) prioritization algorithm', () => {
|
|
it('should prioritize tests using sublinear algorithm', () => {
|
|
const tests: TestCase[] = generateTestSuite(100);
|
|
const startTime = Date.now();
|
|
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
const executionTime = Date.now() - startTime;
|
|
|
|
expect(prioritized).toBeDefined();
|
|
expect(prioritized.length).toBeLessThanOrEqual(50);
|
|
expect(executionTime).toBeLessThan(100); // Should be very fast
|
|
|
|
// Verify scores are in descending order
|
|
for (let i = 1; i < prioritized.length; i++) {
|
|
expect(prioritized[i].score).toBeLessThanOrEqual(prioritized[i - 1].score);
|
|
}
|
|
});
|
|
|
|
it('should scale logarithmically with input size', () => {
|
|
const sizes = [100, 200, 400, 800];
|
|
const executionTimes: number[] = [];
|
|
|
|
sizes.forEach(size => {
|
|
const tests = generateTestSuite(size);
|
|
const startTime = Date.now();
|
|
prioritizer.prioritizeTests(tests);
|
|
const executionTime = Date.now() - startTime;
|
|
executionTimes.push(executionTime);
|
|
});
|
|
|
|
// Verify sublinear growth
|
|
for (let i = 1; i < executionTimes.length; i++) {
|
|
const growthRatio = executionTimes[i] / executionTimes[i - 1];
|
|
expect(growthRatio).toBeLessThan(1.5); // Much less than linear (2x) growth
|
|
}
|
|
});
|
|
|
|
it('should prioritize critical path tests highest', () => {
|
|
const tests: TestCase[] = [
|
|
{
|
|
id: 'critical-1',
|
|
path: 'src/payment/PaymentProcessor.test.js',
|
|
complexity: 3,
|
|
coverage: 15,
|
|
criticalPath: true,
|
|
dependencies: [],
|
|
executionTime: 500
|
|
},
|
|
{
|
|
id: 'normal-1',
|
|
path: 'src/utils/Helper.test.js',
|
|
complexity: 2,
|
|
coverage: 25,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 200
|
|
},
|
|
{
|
|
id: 'critical-2',
|
|
path: 'src/auth/AuthService.test.js',
|
|
complexity: 5,
|
|
coverage: 10,
|
|
criticalPath: true,
|
|
dependencies: [],
|
|
executionTime: 800
|
|
}
|
|
];
|
|
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
|
|
// Critical path tests should be at the top
|
|
expect(prioritized[0].testId).toMatch(/critical-/);
|
|
expect(prioritized[1].testId).toMatch(/critical-/);
|
|
expect(prioritized[2].testId).toBe('normal-1');
|
|
});
|
|
|
|
it('should handle empty test suites gracefully', () => {
|
|
const prioritized = prioritizer.prioritizeTests([]);
|
|
expect(prioritized).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('priority scoring algorithm', () => {
|
|
it('should calculate scores based on multiple factors', () => {
|
|
const test: TestCase = {
|
|
id: 'test-1',
|
|
path: 'src/module/Class.test.js',
|
|
complexity: 3,
|
|
coverage: 20,
|
|
criticalPath: false,
|
|
dependencies: ['test-0'],
|
|
executionTime: 1000
|
|
};
|
|
|
|
const tests = [test];
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
|
|
expect(prioritized[0].score).toBeGreaterThan(0);
|
|
expect(prioritized[0].reasons).toBeDefined();
|
|
expect(prioritized[0].reasons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should penalize high complexity tests', () => {
|
|
const lowComplexity: TestCase = {
|
|
id: 'low-complexity',
|
|
path: 'test.js',
|
|
complexity: 1,
|
|
coverage: 10,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 100
|
|
};
|
|
|
|
const highComplexity: TestCase = {
|
|
id: 'high-complexity',
|
|
path: 'test.js',
|
|
complexity: 10,
|
|
coverage: 10,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 100
|
|
};
|
|
|
|
const lowResult = prioritizer.prioritizeTests([lowComplexity]);
|
|
const highResult = prioritizer.prioritizeTests([highComplexity]);
|
|
|
|
expect(lowResult[0].score).toBeGreaterThan(highResult[0].score);
|
|
});
|
|
|
|
it('should prefer fast-executing tests', () => {
|
|
const fastTest: TestCase = {
|
|
id: 'fast-test',
|
|
path: 'test.js',
|
|
complexity: 2,
|
|
coverage: 10,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 50
|
|
};
|
|
|
|
const slowTest: TestCase = {
|
|
id: 'slow-test',
|
|
path: 'test.js',
|
|
complexity: 2,
|
|
coverage: 10,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 5000
|
|
};
|
|
|
|
const fastResult = prioritizer.prioritizeTests([fastTest]);
|
|
const slowResult = prioritizer.prioritizeTests([slowTest]);
|
|
|
|
expect(fastResult[0].score).toBeGreaterThan(slowResult[0].score);
|
|
});
|
|
|
|
it('should penalize tests with many dependencies', () => {
|
|
const independent: TestCase = {
|
|
id: 'independent',
|
|
path: 'test.js',
|
|
complexity: 2,
|
|
coverage: 10,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 100
|
|
};
|
|
|
|
const dependent: TestCase = {
|
|
id: 'dependent',
|
|
path: 'test.js',
|
|
complexity: 2,
|
|
coverage: 10,
|
|
criticalPath: false,
|
|
dependencies: ['test-1', 'test-2', 'test-3'],
|
|
executionTime: 100
|
|
};
|
|
|
|
const independentResult = prioritizer.prioritizeTests([independent]);
|
|
const dependentResult = prioritizer.prioritizeTests([dependent]);
|
|
|
|
expect(independentResult[0].score).toBeGreaterThan(dependentResult[0].score);
|
|
});
|
|
});
|
|
|
|
describe('binary heap implementation', () => {
|
|
it('should maintain heap property during insertions', () => {
|
|
const tests = generateTestSuite(20);
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
|
|
// Verify that each parent has higher or equal priority than children
|
|
for (let i = 0; i < Math.floor(prioritized.length / 2); i++) {
|
|
const leftChild = 2 * i + 1;
|
|
const rightChild = 2 * i + 2;
|
|
|
|
if (leftChild < prioritized.length) {
|
|
expect(prioritized[i].score).toBeGreaterThanOrEqual(prioritized[leftChild].score);
|
|
}
|
|
|
|
if (rightChild < prioritized.length) {
|
|
expect(prioritized[i].score).toBeGreaterThanOrEqual(prioritized[rightChild].score);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should extract maximum elements correctly', () => {
|
|
const tests = generateTestSuite(10);
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
|
|
// Should be in descending order (max-heap extraction)
|
|
for (let i = 1; i < prioritized.length; i++) {
|
|
expect(prioritized[i - 1].score).toBeGreaterThanOrEqual(prioritized[i].score);
|
|
}
|
|
});
|
|
|
|
it('should handle duplicate scores correctly', () => {
|
|
const tests: TestCase[] = Array.from({ length: 5 }, (_, i) => ({
|
|
id: `test-${i}`,
|
|
path: 'test.js',
|
|
complexity: 2,
|
|
coverage: 10, // Same coverage for all
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 100
|
|
}));
|
|
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
expect(prioritized).toHaveLength(5);
|
|
|
|
// All should have similar scores
|
|
const scores = prioritized.map(p => p.score);
|
|
const maxScore = Math.max(...scores);
|
|
const minScore = Math.min(...scores);
|
|
expect(maxScore - minScore).toBeLessThan(5); // Small variation due to rounding
|
|
});
|
|
});
|
|
|
|
describe('coverage matrix optimization', () => {
|
|
it('should build coverage matrix efficiently', () => {
|
|
const tests = generateTestSuite(100);
|
|
const startTime = Date.now();
|
|
|
|
prioritizer.prioritizeTests(tests);
|
|
const executionTime = Date.now() - startTime;
|
|
|
|
expect(executionTime).toBeLessThan(50); // Should be very fast
|
|
});
|
|
|
|
it('should identify overlapping coverage areas', () => {
|
|
const tests: TestCase[] = [
|
|
{
|
|
id: 'test-1',
|
|
path: 'src/module/ClassA.test.js',
|
|
complexity: 2,
|
|
coverage: 15,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 100
|
|
},
|
|
{
|
|
id: 'test-2',
|
|
path: 'src/module/ClassA.integration.test.js',
|
|
complexity: 3,
|
|
coverage: 12,
|
|
criticalPath: false,
|
|
dependencies: [],
|
|
executionTime: 200
|
|
}
|
|
];
|
|
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
expect(prioritized).toHaveLength(2);
|
|
|
|
// Should prefer unit test over integration test for same module
|
|
// (lower complexity, faster execution)
|
|
expect(prioritized[0].testId).toBe('test-1');
|
|
});
|
|
});
|
|
|
|
describe('memory efficiency', () => {
|
|
it('should handle large test suites without memory issues', () => {
|
|
const largeTestSuite = generateTestSuite(10000);
|
|
|
|
expect(() => {
|
|
const prioritized = prioritizer.prioritizeTests(largeTestSuite);
|
|
expect(prioritized.length).toBeLessThanOrEqual(50);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should limit output size regardless of input size', () => {
|
|
const prioritizerSmall = new TestPrioritizer(10);
|
|
const tests = generateTestSuite(1000);
|
|
|
|
const prioritized = prioritizerSmall.prioritizeTests(tests);
|
|
expect(prioritized.length).toBeLessThanOrEqual(10);
|
|
});
|
|
});
|
|
|
|
describe('algorithm correctness', () => {
|
|
it('should select optimal test subset for maximum coverage', () => {
|
|
const tests: TestCase[] = [
|
|
// High coverage, critical path
|
|
{ id: 'optimal-1', path: 'critical.test.js', complexity: 2, coverage: 30, criticalPath: true, dependencies: [], executionTime: 100 },
|
|
// High coverage, fast
|
|
{ id: 'optimal-2', path: 'fast.test.js', complexity: 1, coverage: 25, criticalPath: false, dependencies: [], executionTime: 50 },
|
|
// Low coverage, slow
|
|
{ id: 'suboptimal-1', path: 'slow.test.js', complexity: 5, coverage: 5, criticalPath: false, dependencies: ['dep1', 'dep2'], executionTime: 2000 },
|
|
// Medium coverage, medium complexity
|
|
{ id: 'medium-1', path: 'medium.test.js', complexity: 3, coverage: 15, criticalPath: false, dependencies: [], executionTime: 300 }
|
|
];
|
|
|
|
const prioritized = prioritizer.prioritizeTests(tests);
|
|
|
|
// Optimal tests should be at the top
|
|
expect(prioritized[0].testId).toBe('optimal-1');
|
|
expect(prioritized[1].testId).toBe('optimal-2');
|
|
expect(prioritized[2].testId).toBe('medium-1');
|
|
expect(prioritized[3].testId).toBe('suboptimal-1');
|
|
});
|
|
|
|
it('should provide meaningful reasoning for each score', () => {
|
|
const test: TestCase = {
|
|
id: 'test-with-reasons',
|
|
path: 'src/complex/Module.test.js',
|
|
complexity: 8,
|
|
coverage: 35,
|
|
criticalPath: true,
|
|
dependencies: ['dep1'],
|
|
executionTime: 1500
|
|
};
|
|
|
|
const prioritized = prioritizer.prioritizeTests([test]);
|
|
const reasons = prioritized[0].reasons;
|
|
|
|
expect(reasons).toContain('Critical path coverage');
|
|
expect(reasons.some(r => r.includes('Coverage impact'))).toBe(true);
|
|
expect(reasons.some(r => r.includes('complexity penalty'))).toBe(true);
|
|
expect(reasons.some(r => r.includes('execution penalty'))).toBe(true);
|
|
expect(reasons.some(r => r.includes('Dependencies'))).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Helper function to generate test suite
|
|
function generateTestSuite(size: number, seed: number = 600001): TestCase[] {
|
|
const rng = createSeededRandom(seed);
|
|
return Array.from({ length: size }, (_, i) => ({
|
|
id: `test-${i}`,
|
|
path: `src/module${Math.floor(i / 10)}/Class${i % 10}.test.js`,
|
|
complexity: Math.floor(rng.random() * 10) + 1,
|
|
coverage: rng.random() * 50 + 5, // 5-55% coverage
|
|
criticalPath: rng.random() < 0.1, // 10% critical path
|
|
dependencies: Array.from(
|
|
{ length: Math.floor(rng.random() * 3) },
|
|
(_, j) => `dep-${i}-${j}`
|
|
),
|
|
executionTime: rng.random() * 2000 + 50 // 50-2050ms
|
|
}));
|
|
} |