Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
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>
2026-03-01 11:35:41 +00:00

341 lines
11 KiB
TypeScript

/**
* Agentic QE v3 - Diff Risk Classifier Unit Tests
*/
import { describe, it, expect, beforeEach } 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());
});
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);
});
});
});