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>
473 lines
15 KiB
TypeScript
473 lines
15 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Defect Predictor Service Unit Tests
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
DefectPredictorService,
|
|
PredictionFeedback,
|
|
} from '../../../../src/domains/defect-intelligence/services/defect-predictor';
|
|
import { MemoryBackend, StoreOptions, VectorSearchResult } from '../../../../src/kernel/interfaces';
|
|
import { GitAnalyzer } from '../../../../src/shared/git';
|
|
import { FileReader } from '../../../../src/shared/io';
|
|
import { TypeScriptParser } from '../../../../src/shared/parsers';
|
|
|
|
/**
|
|
* Mock MemoryBackend implementation for testing
|
|
*/
|
|
function createMockMemoryBackend(): MemoryBackend {
|
|
const storage = new Map<string, unknown>();
|
|
const vectors = new Map<string, { embedding: number[]; metadata: unknown }>();
|
|
|
|
return {
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
set: vi.fn().mockImplementation(async <T>(key: string, value: T, _options?: StoreOptions) => {
|
|
storage.set(key, value);
|
|
}),
|
|
get: vi.fn().mockImplementation(async <T>(key: string): Promise<T | undefined> => {
|
|
return storage.get(key) as T | undefined;
|
|
}),
|
|
delete: vi.fn().mockImplementation(async (key: string) => {
|
|
return storage.delete(key);
|
|
}),
|
|
has: vi.fn().mockImplementation(async (key: string) => {
|
|
return storage.has(key);
|
|
}),
|
|
search: vi.fn().mockImplementation(async (pattern: string, limit?: number) => {
|
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
const keys = Array.from(storage.keys()).filter((k) => regex.test(k));
|
|
return keys.slice(0, limit);
|
|
}),
|
|
vectorSearch: vi.fn().mockResolvedValue([]),
|
|
storeVector: vi.fn().mockImplementation(async (key: string, embedding: number[], metadata?: unknown) => {
|
|
vectors.set(key, { embedding, metadata });
|
|
}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mock GitAnalyzer for testing
|
|
*/
|
|
function createMockGitAnalyzer(): Partial<GitAnalyzer> {
|
|
return {
|
|
getBlameInfo: vi.fn().mockResolvedValue({
|
|
'file.ts': [
|
|
{
|
|
commitHash: 'abc123',
|
|
author: 'test@example.com',
|
|
timestamp: Date.now(),
|
|
lineCount: 10,
|
|
},
|
|
],
|
|
}),
|
|
getFileHistory: vi.fn().mockResolvedValue({
|
|
filePath: 'file.ts',
|
|
totalCommits: 5,
|
|
uniqueAuthors: 2,
|
|
firstCommit: new Date(Date.now() - 86400000),
|
|
lastCommit: new Date(),
|
|
changeFrequency: 5,
|
|
isRecentlyModified: true,
|
|
bugFixCommits: 1,
|
|
}),
|
|
getChangeFrequency: vi.fn().mockResolvedValue(0.5),
|
|
getDeveloperExperience: vi.fn().mockResolvedValue(0.5),
|
|
getCodeAge: vi.fn().mockResolvedValue(0.4),
|
|
getBugHistory: vi.fn().mockResolvedValue(0.2),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mock FileReader for testing
|
|
*/
|
|
function createMockFileReader(): Partial<FileReader> {
|
|
return {
|
|
readFile: vi.fn().mockResolvedValue({
|
|
success: true,
|
|
value: `
|
|
export class TestClass {
|
|
private value: number;
|
|
|
|
constructor() {
|
|
this.value = 0;
|
|
}
|
|
|
|
public method(): void {
|
|
this.value++;
|
|
}
|
|
}
|
|
`,
|
|
}),
|
|
fileExists: vi.fn().mockResolvedValue(true),
|
|
getFileStats: vi.fn().mockResolvedValue({
|
|
size: 1000,
|
|
lines: 50,
|
|
functions: 2,
|
|
classes: 1,
|
|
}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mock TypeScriptParser for testing
|
|
*/
|
|
function createMockTypeScriptParser(): Partial<TypeScriptParser> {
|
|
const mockAst = {} as unknown; // Empty mock AST
|
|
|
|
return {
|
|
parseFile: vi.fn().mockReturnValue(mockAst),
|
|
extractFunctions: vi.fn().mockReturnValue([
|
|
{ name: 'method', isAsync: false },
|
|
]),
|
|
extractClasses: vi.fn().mockReturnValue([
|
|
{ name: 'TestClass', methods: [{ name: 'method' }] },
|
|
]),
|
|
extractImports: vi.fn().mockReturnValue([]),
|
|
getComplexity: vi.fn().mockResolvedValue(5),
|
|
getDependencies: vi.fn().mockResolvedValue([]),
|
|
};
|
|
}
|
|
|
|
describe('DefectPredictorService', () => {
|
|
let service: DefectPredictorService;
|
|
let mockMemory: MemoryBackend;
|
|
let mockGit: Partial<GitAnalyzer>;
|
|
let mockReader: Partial<FileReader>;
|
|
let mockParser: Partial<TypeScriptParser>;
|
|
|
|
beforeEach(() => {
|
|
mockMemory = createMockMemoryBackend();
|
|
mockGit = createMockGitAnalyzer();
|
|
mockReader = createMockFileReader();
|
|
mockParser = createMockTypeScriptParser();
|
|
service = new DefectPredictorService(
|
|
mockMemory,
|
|
{},
|
|
mockGit as GitAnalyzer,
|
|
mockReader as FileReader,
|
|
mockParser as TypeScriptParser
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('predictDefects', () => {
|
|
it('should predict defects for valid files', async () => {
|
|
const result = await service.predictDefects({
|
|
files: ['src/service/user-service.ts', 'src/controller/user-controller.ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.predictions).toHaveLength(2);
|
|
expect(result.value.predictions[0].file).toBe('src/service/user-service.ts');
|
|
expect(result.value.predictions[0].probability).toBeGreaterThanOrEqual(0);
|
|
expect(result.value.predictions[0].probability).toBeLessThanOrEqual(1);
|
|
expect(result.value.modelConfidence).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should return error for empty files array', async () => {
|
|
const result = await service.predictDefects({ files: [] });
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.message).toBe('No files provided for prediction');
|
|
}
|
|
});
|
|
|
|
it('should return error when exceeding max predictions per batch', async () => {
|
|
const files = Array.from({ length: 150 }, (_, i) => `file${i}.ts`);
|
|
const result = await service.predictDefects({ files });
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.message).toContain('Too many files');
|
|
}
|
|
});
|
|
|
|
it('should respect custom threshold for risk level calculation', async () => {
|
|
const result = await service.predictDefects({
|
|
files: ['src/utils/helper.ts'],
|
|
threshold: 0.1, // Low threshold makes more things high risk
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
const prediction = result.value.predictions[0];
|
|
expect(prediction.riskLevel).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('should use custom feature weights when provided', async () => {
|
|
const result = await service.predictDefects({
|
|
files: ['src/service.ts'],
|
|
features: [
|
|
{ name: 'codeComplexity', weight: 0.5 },
|
|
{ name: 'testCoverage', weight: 0.5 },
|
|
],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.predictions[0].factors).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('should include recommendations in predictions', async () => {
|
|
const result = await service.predictDefects({
|
|
files: ['src/controller/complex-controller.ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
const prediction = result.value.predictions[0];
|
|
expect(prediction.recommendations).toBeInstanceOf(Array);
|
|
}
|
|
});
|
|
|
|
it('should calculate model confidence based on predictions', async () => {
|
|
const result = await service.predictDefects({
|
|
files: ['file1.ts', 'file2.ts', 'file3.ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.modelConfidence).toBeGreaterThanOrEqual(0);
|
|
expect(result.value.modelConfidence).toBeLessThanOrEqual(1);
|
|
}
|
|
});
|
|
|
|
it('should store prediction in memory for feedback', async () => {
|
|
await service.predictDefects({
|
|
files: ['src/test.ts'],
|
|
});
|
|
|
|
expect(mockMemory.set).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('analyzeRegressionRisk', () => {
|
|
it('should analyze regression risk for valid changeset', async () => {
|
|
const result = await service.analyzeRegressionRisk({
|
|
changeset: ['src/api/endpoint.ts', 'src/service/data-service.ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.overallRisk).toBeGreaterThanOrEqual(0);
|
|
expect(result.value.overallRisk).toBeLessThanOrEqual(1);
|
|
expect(result.value.riskLevel).toBeDefined();
|
|
expect(result.value.impactedAreas).toBeInstanceOf(Array);
|
|
expect(result.value.recommendedTests).toBeInstanceOf(Array);
|
|
expect(result.value.confidence).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should return error for empty changeset', async () => {
|
|
const result = await service.analyzeRegressionRisk({ changeset: [] });
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.message).toBe('No files in changeset');
|
|
}
|
|
});
|
|
|
|
it('should increase confidence for deep analysis', async () => {
|
|
const shallowResult = await service.analyzeRegressionRisk({
|
|
changeset: ['src/file.ts'],
|
|
depth: 'shallow',
|
|
});
|
|
|
|
const deepResult = await service.analyzeRegressionRisk({
|
|
changeset: ['src/file.ts'],
|
|
depth: 'deep',
|
|
});
|
|
|
|
expect(shallowResult.success).toBe(true);
|
|
expect(deepResult.success).toBe(true);
|
|
if (shallowResult.success && deepResult.success) {
|
|
expect(deepResult.value.confidence).toBeGreaterThan(shallowResult.value.confidence);
|
|
}
|
|
});
|
|
|
|
it('should categorize files into correct areas', async () => {
|
|
const result = await service.analyzeRegressionRisk({
|
|
changeset: [
|
|
'src/controller/api-controller.ts',
|
|
'src/service/business-service.ts',
|
|
'src/repository/data-repository.ts',
|
|
],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
const areas = result.value.impactedAreas.map((a) => a.area);
|
|
expect(areas).toContain('API Layer');
|
|
expect(areas).toContain('Business Logic');
|
|
expect(areas).toContain('Data Access');
|
|
}
|
|
});
|
|
|
|
it('should generate test recommendations based on impacted areas', async () => {
|
|
const result = await service.analyzeRegressionRisk({
|
|
changeset: ['src/controller/user-controller.ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.recommendedTests.length).toBeGreaterThan(0);
|
|
expect(result.value.recommendedTests.some((t) => t.includes('API'))).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('updateModel', () => {
|
|
it('should store feedback successfully', async () => {
|
|
const feedback: PredictionFeedback = {
|
|
predictionId: 'pred-123',
|
|
file: 'src/test.ts',
|
|
predictedProbability: 0.7,
|
|
actualDefect: true,
|
|
defectType: 'null-pointer',
|
|
};
|
|
|
|
const result = await service.updateModel(feedback);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockMemory.set).toHaveBeenCalledWith(
|
|
expect.stringContaining('feedback'),
|
|
expect.objectContaining({ predictionId: 'pred-123' }),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
it('should update model metrics after true positive feedback', async () => {
|
|
const feedback: PredictionFeedback = {
|
|
predictionId: 'pred-456',
|
|
file: 'src/buggy.ts',
|
|
predictedProbability: 0.8,
|
|
actualDefect: true,
|
|
};
|
|
|
|
await service.updateModel(feedback);
|
|
const metrics = await service.getModelMetrics();
|
|
|
|
expect(metrics.totalPredictions).toBeDefined();
|
|
expect(metrics.accuracy).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should update model metrics after false positive feedback', async () => {
|
|
const feedback: PredictionFeedback = {
|
|
predictionId: 'pred-789',
|
|
file: 'src/clean.ts',
|
|
predictedProbability: 0.6,
|
|
actualDefect: false,
|
|
};
|
|
|
|
await service.updateModel(feedback);
|
|
const metrics = await service.getModelMetrics();
|
|
|
|
expect(metrics.precision).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('getModelMetrics', () => {
|
|
it('should return initial model metrics', async () => {
|
|
const metrics = await service.getModelMetrics();
|
|
|
|
expect(metrics.accuracy).toBeGreaterThan(0);
|
|
expect(metrics.precision).toBeGreaterThan(0);
|
|
expect(metrics.recall).toBeGreaterThan(0);
|
|
expect(metrics.f1Score).toBeGreaterThan(0);
|
|
expect(metrics.totalPredictions).toBeDefined();
|
|
expect(metrics.lastUpdated).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should load persisted metrics from memory', async () => {
|
|
const storedMetrics = {
|
|
accuracy: 0.9,
|
|
precision: 0.88,
|
|
recall: 0.92,
|
|
f1Score: 0.90,
|
|
totalPredictions: 100,
|
|
lastUpdated: new Date().toISOString(),
|
|
};
|
|
|
|
(mockMemory.get as ReturnType<typeof vi.fn>).mockImplementation(async (key: string) => {
|
|
if (key.includes('metrics')) {
|
|
return storedMetrics;
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
const metrics = await service.getModelMetrics();
|
|
|
|
expect(metrics.accuracy).toBe(0.9);
|
|
expect(metrics.totalPredictions).toBe(100);
|
|
});
|
|
|
|
it('should increment total predictions after predictDefects', async () => {
|
|
const initialMetrics = await service.getModelMetrics();
|
|
const initialCount = initialMetrics.totalPredictions;
|
|
|
|
await service.predictDefects({
|
|
files: ['file1.ts', 'file2.ts'],
|
|
});
|
|
|
|
const updatedMetrics = await service.getModelMetrics();
|
|
expect(updatedMetrics.totalPredictions).toBe(initialCount + 2);
|
|
});
|
|
});
|
|
|
|
describe('edge cases and error handling', () => {
|
|
it('should handle files with special characters in path', async () => {
|
|
const result = await service.predictDefects({
|
|
files: ['src/utils/string-helpers[v2].ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('should handle memory backend errors gracefully', async () => {
|
|
(mockMemory.set as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Storage error'));
|
|
|
|
const result = await service.predictDefects({
|
|
files: ['src/test.ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.message).toContain('Storage error');
|
|
}
|
|
});
|
|
|
|
it('should handle service files with higher complexity', async () => {
|
|
const result = await service.predictDefects({
|
|
files: ['src/service/complex-service.ts'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
const prediction = result.value.predictions[0];
|
|
// Service files should have higher base complexity
|
|
expect(prediction.probability).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should cache file metrics to avoid recomputation', async () => {
|
|
// First prediction
|
|
await service.predictDefects({ files: ['src/cached.ts'] });
|
|
|
|
// Second prediction for same file
|
|
await service.predictDefects({ files: ['src/cached.ts'] });
|
|
|
|
// Memory.set should be called to cache metrics
|
|
expect(mockMemory.set).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|