mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
00e4d2cfa6
125 test files had beforeEach/beforeAll but no afterEach/afterAll, leaking state between tests and causing order-dependent failures. Added cleanup to 104 files (skipped 4 archived, 16 import-only): - 2 critical DB handle leaks (witness-chain): added db.close() - 7 high-risk service files: added vi.restoreAllMocks() - 7 beforeAll files: added matching afterAll - 88 medium/low risk: added afterEach with appropriate cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Diff Risk Classifier Unit Tests
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
import {
|
|
createDiffRiskClassifier,
|
|
RuVectorDiffRiskClassifier,
|
|
FallbackDiffRiskClassifier,
|
|
} from '../../../../src/integrations/ruvector';
|
|
import type {
|
|
RuVectorConfig,
|
|
DiffContext,
|
|
FileChange,
|
|
} from '../../../../src/integrations/ruvector';
|
|
|
|
// Test fixtures
|
|
function createFileChange(overrides: Partial<FileChange> = {}): FileChange {
|
|
return {
|
|
filePath: 'src/services/user-service.ts',
|
|
status: 'modified',
|
|
additions: 10,
|
|
deletions: 5,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createDiffContext(overrides: Partial<DiffContext> = {}): DiffContext {
|
|
return {
|
|
files: [createFileChange()],
|
|
commitHash: 'abc123',
|
|
message: 'Update user service',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createConfig(overrides: Partial<RuVectorConfig> = {}): RuVectorConfig {
|
|
return {
|
|
enabled: true,
|
|
endpoint: 'http://localhost:8080',
|
|
fallbackEnabled: true,
|
|
cacheEnabled: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('Diff Risk Classifier', () => {
|
|
describe('Factory Function', () => {
|
|
it('should create RuVectorDiffRiskClassifier when enabled', async () => {
|
|
const classifier = await createDiffRiskClassifier(createConfig({ enabled: true }));
|
|
expect(classifier).toBeInstanceOf(RuVectorDiffRiskClassifier);
|
|
});
|
|
|
|
it('should create FallbackDiffRiskClassifier when disabled', async () => {
|
|
const classifier = await createDiffRiskClassifier(createConfig({ enabled: false }));
|
|
expect(classifier).toBeInstanceOf(FallbackDiffRiskClassifier);
|
|
});
|
|
});
|
|
|
|
describe('RuVectorDiffRiskClassifier', () => {
|
|
let classifier: RuVectorDiffRiskClassifier;
|
|
|
|
beforeEach(() => {
|
|
classifier = new RuVectorDiffRiskClassifier(createConfig());
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Reset state to prevent leaks between tests
|
|
});
|
|
|
|
describe('classifyDiff', () => {
|
|
it('should classify a simple diff', async () => {
|
|
const context = createDiffContext();
|
|
const result = await classifier.classifyDiff(context);
|
|
|
|
expect(result).toHaveProperty('level');
|
|
expect(result).toHaveProperty('score');
|
|
expect(result).toHaveProperty('factors');
|
|
expect(result).toHaveProperty('highRiskFiles');
|
|
expect(result).toHaveProperty('recommendedTests');
|
|
expect(result.usedFallback).toBe(false);
|
|
});
|
|
|
|
it('should return score between 0 and 1', async () => {
|
|
const context = createDiffContext();
|
|
const result = await classifier.classifyDiff(context);
|
|
|
|
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
expect(result.score).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
it('should return valid severity level', async () => {
|
|
const context = createDiffContext();
|
|
const result = await classifier.classifyDiff(context);
|
|
|
|
expect(['critical', 'high', 'medium', 'low', 'info']).toContain(result.level);
|
|
});
|
|
|
|
it('should identify high-risk files', async () => {
|
|
const context = createDiffContext({
|
|
files: [
|
|
createFileChange({ filePath: 'src/auth/token-service.ts', additions: 100 }),
|
|
createFileChange({ filePath: 'src/utils/helper.ts', additions: 5 }),
|
|
],
|
|
});
|
|
|
|
const result = await classifier.classifyDiff(context);
|
|
|
|
expect(Array.isArray(result.highRiskFiles)).toBe(true);
|
|
});
|
|
|
|
it('should recommend tests based on risk', async () => {
|
|
const context = createDiffContext({
|
|
files: [
|
|
createFileChange({ filePath: 'src/security/auth.ts', additions: 50 }),
|
|
],
|
|
});
|
|
|
|
const result = await classifier.classifyDiff(context);
|
|
|
|
expect(Array.isArray(result.recommendedTests)).toBe(true);
|
|
expect(result.recommendedTests.length).toBeGreaterThan(0);
|
|
|
|
for (const test of result.recommendedTests) {
|
|
expect(test).toHaveProperty('type');
|
|
expect(test).toHaveProperty('priority');
|
|
expect(test).toHaveProperty('reason');
|
|
}
|
|
});
|
|
|
|
it('should classify large changesets as higher risk', async () => {
|
|
const smallContext = createDiffContext({
|
|
files: [createFileChange({ additions: 10, deletions: 5 })],
|
|
});
|
|
|
|
const largeContext = createDiffContext({
|
|
files: [createFileChange({ additions: 500, deletions: 200 })],
|
|
});
|
|
|
|
const smallResult = await classifier.classifyDiff(smallContext);
|
|
const largeResult = await classifier.classifyDiff(largeContext);
|
|
|
|
expect(largeResult.score).toBeGreaterThanOrEqual(smallResult.score);
|
|
});
|
|
|
|
it('should classify security-related changes as higher risk', async () => {
|
|
const normalContext = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/utils/format.ts' })],
|
|
});
|
|
|
|
const securityContext = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/auth/password.ts' })],
|
|
});
|
|
|
|
const normalResult = await classifier.classifyDiff(normalContext);
|
|
const securityResult = await classifier.classifyDiff(securityContext);
|
|
|
|
expect(securityResult.score).toBeGreaterThanOrEqual(normalResult.score);
|
|
});
|
|
});
|
|
|
|
describe('rankFilesByRisk', () => {
|
|
it('should rank files by risk score', async () => {
|
|
const files: FileChange[] = [
|
|
createFileChange({ filePath: 'src/utils/helper.ts', additions: 5 }),
|
|
createFileChange({ filePath: 'src/auth/token.ts', additions: 50 }),
|
|
createFileChange({ filePath: 'src/services/user.ts', additions: 20 }),
|
|
];
|
|
|
|
const ranking = await classifier.rankFilesByRisk(files);
|
|
|
|
expect(ranking).toHaveLength(3);
|
|
|
|
// Should be sorted by risk score descending
|
|
for (let i = 1; i < ranking.length; i++) {
|
|
expect(ranking[i - 1].riskScore).toBeGreaterThanOrEqual(ranking[i].riskScore);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('requiresSecurityReview', () => {
|
|
it('should require security review for auth changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/auth/login.ts' })],
|
|
});
|
|
|
|
const result = await classifier.requiresSecurityReview(context);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should require security review for password changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/services/password-service.ts' })],
|
|
});
|
|
|
|
const result = await classifier.requiresSecurityReview(context);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should require security review for crypto changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/utils/encryption.ts' })],
|
|
});
|
|
|
|
const result = await classifier.requiresSecurityReview(context);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should not require security review for non-sensitive changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/components/button.tsx' })],
|
|
});
|
|
|
|
const result = await classifier.requiresSecurityReview(context);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getRecommendedReviewers', () => {
|
|
it('should recommend security team for security changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/security/auth.ts' })],
|
|
});
|
|
|
|
const reviewers = await classifier.getRecommendedReviewers(context);
|
|
|
|
expect(reviewers).toContain('security-team');
|
|
});
|
|
|
|
it('should recommend API owner for API changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/api/public-api.ts' })],
|
|
});
|
|
|
|
const reviewers = await classifier.getRecommendedReviewers(context);
|
|
|
|
expect(reviewers).toContain('api-owner');
|
|
});
|
|
|
|
it('should recommend tech lead for core changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: 'src/kernel/bootstrap.ts' })],
|
|
});
|
|
|
|
const reviewers = await classifier.getRecommendedReviewers(context);
|
|
|
|
expect(reviewers).toContain('tech-lead');
|
|
});
|
|
});
|
|
|
|
describe('predictDefects', () => {
|
|
it('should predict defects for risky changes', async () => {
|
|
const context = createDiffContext({
|
|
files: [
|
|
createFileChange({ filePath: 'src/auth/complex.ts', additions: 150 }),
|
|
],
|
|
});
|
|
|
|
const predictions = await classifier.predictDefects(context);
|
|
|
|
expect(Array.isArray(predictions)).toBe(true);
|
|
for (const pred of predictions) {
|
|
expect(pred).toHaveProperty('filePath');
|
|
expect(pred).toHaveProperty('probability');
|
|
expect(pred).toHaveProperty('type');
|
|
}
|
|
});
|
|
|
|
it('should predict new-code-defects for large additions', async () => {
|
|
const context = createDiffContext({
|
|
files: [
|
|
createFileChange({ additions: 200, deletions: 10 }),
|
|
],
|
|
});
|
|
|
|
const predictions = await classifier.predictDefects(context);
|
|
|
|
const newCodeDefects = predictions.filter((p) => p.type === 'new-code-defect');
|
|
expect(newCodeDefects.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should predict regressions for large deletions', async () => {
|
|
const context = createDiffContext({
|
|
files: [
|
|
createFileChange({ additions: 10, deletions: 100 }),
|
|
],
|
|
});
|
|
|
|
const predictions = await classifier.predictDefects(context);
|
|
|
|
const regressions = predictions.filter((p) => p.type === 'regression');
|
|
expect(regressions.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FallbackDiffRiskClassifier', () => {
|
|
let classifier: FallbackDiffRiskClassifier;
|
|
|
|
beforeEach(() => {
|
|
classifier = new FallbackDiffRiskClassifier();
|
|
});
|
|
|
|
it('should classify diffs using rules', async () => {
|
|
const context = createDiffContext();
|
|
const result = await classifier.classifyDiff(context);
|
|
|
|
expect(result.usedFallback).toBe(true);
|
|
expect(result).toHaveProperty('level');
|
|
expect(result).toHaveProperty('score');
|
|
});
|
|
|
|
it('should identify sensitive files', async () => {
|
|
const context = createDiffContext({
|
|
files: [createFileChange({ filePath: '.env.local' })],
|
|
});
|
|
|
|
const result = await classifier.classifyDiff(context);
|
|
|
|
expect(result.score).toBeGreaterThan(0.3);
|
|
});
|
|
});
|
|
|
|
describe('Integration', () => {
|
|
it('should fallback when RuVector disabled', async () => {
|
|
const classifier = await createDiffRiskClassifier(createConfig({ enabled: false }));
|
|
const result = await classifier.classifyDiff(createDiffContext());
|
|
|
|
expect(result.usedFallback).toBe(true);
|
|
});
|
|
|
|
it('should cache results when enabled', async () => {
|
|
const classifier = new RuVectorDiffRiskClassifier(
|
|
createConfig({ cacheEnabled: true, cacheTtl: 60000 })
|
|
);
|
|
const context = createDiffContext();
|
|
|
|
const result1 = await classifier.classifyDiff(context);
|
|
const result2 = await classifier.classifyDiff(context);
|
|
|
|
expect(result1.score).toBe(result2.score);
|
|
});
|
|
});
|
|
});
|