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>
353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
/**
|
|
* Unit Tests for Metric Collectors
|
|
* ADR-024: Self-Optimization Engine
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import {
|
|
SearchLatencyCollector,
|
|
RoutingAccuracyCollector,
|
|
PatternQualityCollector,
|
|
TestMaintainabilityCollector,
|
|
MetricCollectorRegistry,
|
|
createDefaultCollectorRegistry,
|
|
} from '../../../src/optimization/index.js';
|
|
|
|
describe('SearchLatencyCollector', () => {
|
|
let collector: SearchLatencyCollector;
|
|
|
|
beforeEach(() => {
|
|
collector = new SearchLatencyCollector();
|
|
});
|
|
|
|
it('should have correct id and metric name', () => {
|
|
expect(collector.id).toBe('search-latency');
|
|
expect(collector.metricName).toBe('search_latency_ms');
|
|
});
|
|
|
|
it('should collect with no latencies recorded', async () => {
|
|
const sample = await collector.collect();
|
|
expect(sample.name).toBe('search_latency_ms');
|
|
expect(sample.value).toBe(0);
|
|
expect(sample.timestamp).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should collect average of recorded latencies', async () => {
|
|
collector.recordLatency(10);
|
|
collector.recordLatency(20);
|
|
collector.recordLatency(30);
|
|
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBe(20); // Average of 10, 20, 30
|
|
});
|
|
|
|
it('should clear pending latencies after collect', async () => {
|
|
collector.recordLatency(100);
|
|
await collector.collect();
|
|
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBe(0); // No pending latencies
|
|
});
|
|
|
|
it('should calculate stats over time', async () => {
|
|
// Add multiple samples
|
|
for (let i = 0; i < 10; i++) {
|
|
collector.recordLatency(10 + i * 2);
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000); // 1 hour
|
|
expect(stats.count).toBe(10);
|
|
expect(stats.min).toBe(10);
|
|
expect(stats.max).toBe(28);
|
|
expect(stats.mean).toBeCloseTo(19, 0);
|
|
});
|
|
|
|
it('should return empty stats when no samples', async () => {
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
expect(stats.count).toBe(0);
|
|
expect(stats.trend).toBe('stable');
|
|
});
|
|
});
|
|
|
|
describe('RoutingAccuracyCollector', () => {
|
|
let collector: RoutingAccuracyCollector;
|
|
|
|
beforeEach(() => {
|
|
collector = new RoutingAccuracyCollector();
|
|
});
|
|
|
|
it('should have correct id and metric name', () => {
|
|
expect(collector.id).toBe('routing-accuracy');
|
|
expect(collector.metricName).toBe('routing_accuracy');
|
|
});
|
|
|
|
it('should calculate accuracy when following recommendations', async () => {
|
|
collector.recordOutcome(true, true); // Followed, successful
|
|
collector.recordOutcome(true, true); // Followed, successful
|
|
collector.recordOutcome(true, false); // Followed, failed
|
|
collector.recordOutcome(false, true); // Not followed (ignored for accuracy)
|
|
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBeCloseTo(0.667, 2); // 2/3 success when followed
|
|
});
|
|
|
|
it('should return 0 when no recommendations followed', async () => {
|
|
collector.recordOutcome(false, true);
|
|
collector.recordOutcome(false, false);
|
|
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBe(0);
|
|
});
|
|
|
|
it('should clear outcomes after collect', async () => {
|
|
collector.recordOutcome(true, true);
|
|
await collector.collect();
|
|
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('PatternQualityCollector', () => {
|
|
let collector: PatternQualityCollector;
|
|
|
|
beforeEach(() => {
|
|
collector = new PatternQualityCollector();
|
|
});
|
|
|
|
it('should have correct id and metric name', () => {
|
|
expect(collector.id).toBe('pattern-quality');
|
|
expect(collector.metricName).toBe('pattern_quality_score');
|
|
});
|
|
|
|
it('should calculate average quality', async () => {
|
|
collector.recordQuality(0.8);
|
|
collector.recordQuality(0.9);
|
|
collector.recordQuality(0.7);
|
|
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBeCloseTo(0.8, 2);
|
|
});
|
|
|
|
it('should return 0 when no quality recorded', async () => {
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('TestMaintainabilityCollector', () => {
|
|
let collector: TestMaintainabilityCollector;
|
|
|
|
beforeEach(() => {
|
|
collector = new TestMaintainabilityCollector();
|
|
});
|
|
|
|
it('should have correct id and metric name', () => {
|
|
expect(collector.id).toBe('test-maintainability');
|
|
expect(collector.metricName).toBe('test_maintainability');
|
|
});
|
|
|
|
it('should calculate average maintainability', async () => {
|
|
collector.recordMaintainability(0.85);
|
|
collector.recordMaintainability(0.75);
|
|
collector.recordMaintainability(0.90);
|
|
|
|
const sample = await collector.collect();
|
|
expect(sample.value).toBeCloseTo(0.833, 2);
|
|
});
|
|
});
|
|
|
|
describe('MetricCollectorRegistry', () => {
|
|
let registry: MetricCollectorRegistry;
|
|
|
|
beforeEach(() => {
|
|
registry = new MetricCollectorRegistry();
|
|
});
|
|
|
|
it('should register and retrieve collectors', () => {
|
|
const collector = new SearchLatencyCollector();
|
|
registry.register(collector);
|
|
|
|
const retrieved = registry.get('search_latency_ms');
|
|
expect(retrieved).toBe(collector);
|
|
});
|
|
|
|
it('should return undefined for unknown metric', () => {
|
|
const retrieved = registry.get('unknown_metric');
|
|
expect(retrieved).toBeUndefined();
|
|
});
|
|
|
|
it('should get all collectors', () => {
|
|
registry.register(new SearchLatencyCollector());
|
|
registry.register(new RoutingAccuracyCollector());
|
|
|
|
const all = registry.getAll();
|
|
expect(all.length).toBe(2);
|
|
});
|
|
|
|
it('should collect all metrics', async () => {
|
|
registry.register(new SearchLatencyCollector());
|
|
registry.register(new PatternQualityCollector());
|
|
|
|
const samples = await registry.collectAll();
|
|
expect(samples.length).toBe(2);
|
|
expect(samples.map(s => s.name)).toContain('search_latency_ms');
|
|
expect(samples.map(s => s.name)).toContain('pattern_quality_score');
|
|
});
|
|
|
|
it('should get stats for all metrics', async () => {
|
|
const latencyCollector = new SearchLatencyCollector();
|
|
latencyCollector.recordLatency(10);
|
|
await latencyCollector.collect();
|
|
|
|
registry.register(latencyCollector);
|
|
registry.register(new PatternQualityCollector());
|
|
|
|
const allStats = await registry.getAllStats(60 * 60 * 1000);
|
|
expect(allStats.size).toBe(2);
|
|
expect(allStats.get('search_latency_ms')?.count).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('createDefaultCollectorRegistry', () => {
|
|
it('should create registry with all default collectors', () => {
|
|
const registry = createDefaultCollectorRegistry();
|
|
const collectors = registry.getAll();
|
|
|
|
expect(collectors.length).toBe(4);
|
|
|
|
const metricNames = collectors.map(c => c.metricName);
|
|
expect(metricNames).toContain('search_latency_ms');
|
|
expect(metricNames).toContain('routing_accuracy');
|
|
expect(metricNames).toContain('pattern_quality_score');
|
|
expect(metricNames).toContain('test_maintainability');
|
|
});
|
|
});
|
|
|
|
describe('MetricStats Calculations', () => {
|
|
it('should calculate percentiles correctly', async () => {
|
|
const collector = new SearchLatencyCollector();
|
|
|
|
// Add 100 samples with values 1-100
|
|
for (let i = 1; i <= 100; i++) {
|
|
collector.recordLatency(i);
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
expect(stats.count).toBe(100);
|
|
expect(stats.median).toBeCloseTo(50.5, 0);
|
|
expect(stats.p95).toBeCloseTo(95, 1);
|
|
expect(stats.p99).toBeCloseTo(99, 1);
|
|
});
|
|
|
|
it('should calculate standard deviation', async () => {
|
|
const collector = new SearchLatencyCollector();
|
|
|
|
// Add samples with known variance
|
|
const values = [2, 4, 4, 4, 5, 5, 7, 9];
|
|
for (const v of values) {
|
|
collector.recordLatency(v);
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
// Mean = 5, StdDev = 2
|
|
expect(stats.mean).toBe(5);
|
|
expect(stats.stdDev).toBeCloseTo(2, 0);
|
|
});
|
|
|
|
it('should detect improving trend', async () => {
|
|
const collector = new PatternQualityCollector();
|
|
|
|
// Add samples that improve over time (higher is better for quality)
|
|
for (let i = 0; i < 20; i++) {
|
|
collector.recordQuality(0.5 + i * 0.02); // 0.5 to 0.88
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
expect(stats.trend).toBe('improving');
|
|
});
|
|
|
|
it('should detect degrading trend', async () => {
|
|
const collector = new PatternQualityCollector();
|
|
|
|
// Add samples that degrade over time
|
|
for (let i = 0; i < 20; i++) {
|
|
collector.recordQuality(0.9 - i * 0.02); // 0.9 to 0.52
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
expect(stats.trend).toBe('degrading');
|
|
});
|
|
|
|
it('should detect stable trend', async () => {
|
|
const collector = new PatternQualityCollector();
|
|
|
|
// Add samples that stay roughly the same
|
|
for (let i = 0; i < 20; i++) {
|
|
collector.recordQuality(0.75 + (Math.random() - 0.5) * 0.02);
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
expect(stats.trend).toBe('stable');
|
|
});
|
|
});
|
|
|
|
describe('Division by Zero Edge Cases', () => {
|
|
it('should handle zero values in latency trend calculation', async () => {
|
|
const collector = new SearchLatencyCollector();
|
|
|
|
// First half all zeros
|
|
for (let i = 0; i < 10; i++) {
|
|
collector.recordLatency(0);
|
|
await collector.collect();
|
|
}
|
|
|
|
// Second half has some values
|
|
for (let i = 0; i < 10; i++) {
|
|
collector.recordLatency(5);
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
// Should not throw, and should detect degrading (latency went up from 0)
|
|
expect(stats.trend).toBe('degrading');
|
|
});
|
|
|
|
it('should handle all zero values without crashing', async () => {
|
|
const collector = new SearchLatencyCollector();
|
|
|
|
// All zeros
|
|
for (let i = 0; i < 20; i++) {
|
|
collector.recordLatency(0);
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
// Should not throw, should be stable
|
|
expect(stats.trend).toBe('stable');
|
|
});
|
|
|
|
it('should produce valid stats with zero mean', async () => {
|
|
const collector = new SearchLatencyCollector();
|
|
|
|
for (let i = 0; i < 15; i++) {
|
|
collector.recordLatency(0);
|
|
await collector.collect();
|
|
}
|
|
|
|
const stats = await collector.getStats(60 * 60 * 1000);
|
|
|
|
// All values should be valid numbers, not NaN or Infinity
|
|
expect(Number.isFinite(stats.mean)).toBe(true);
|
|
expect(Number.isFinite(stats.stdDev)).toBe(true);
|
|
expect(Number.isFinite(stats.min)).toBe(true);
|
|
expect(Number.isFinite(stats.max)).toBe(true);
|
|
});
|
|
});
|