mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
86ccaa6ee4
CRITICAL LEARNING SYSTEM FIXES (4 issues):
- Fixed Q-learning persistence: Database auto-initialization when not provided
- Fixed Database.initialize() call for auto-created instances
- Removed FK constraint on learning_experiences.task_id (architectural fix)
- Fixed SQL syntax: datetime('now', '-7 days') quotes
NEW FEATURES:
- LearningPersistenceAdapter pattern for flexible storage backends
- Auto-initialize Database in LearningEngine when learning enabled
COMPREHENSIVE TEST EXPANSION (44 MCP handler test files):
- Analysis handlers: +1,637 lines across 5 files
- Chaos handlers: +2,094 lines across 3 files
- Coordination handlers: +5,225 lines across 7 files
- Memory handlers: +6,415 lines across 10 files
- Prediction handlers: +3,440 lines across 5 files
- Test handlers: +1,628 lines across 4 files
NEW TEST SUITES (9 total):
- tests/integration/learning-persistence.test.ts (468 lines, 7 tests)
- tests/integration/learning-backward-compat.test.ts
- tests/unit/learning/LearningEngine.database.test.ts
- tests/unit/learning/LearningPersistenceAdapter.test.ts
- tests/core/memory/SwarmMemoryManager.test.ts
- tests/performance/learning-engine-refactor.perf.test.ts
- 3 test helper scripts for Q-learning verification
DOCUMENTATION:
- Updated CHANGELOG.md with learning fixes + test stats
- Added 16 comprehensive reports in docs/ (learning analysis, Q-learning evidence, fixes)
- Cleaned up 6 obsolete release files from root directory
FILES CHANGED:
- Modified: 48 source/test files
- Added: 25 new files (tests + docs)
- Deleted: 6 obsolete release files
- Total: ~22,000 lines added
IMPACT:
✅ Learning system now persists Q-values correctly
✅ Comprehensive MCP handler test coverage
✅ Zero breaking changes (backward compatible)
✅ Documentation verified: 18 QE agents, 34 QE skills, 8 commands
🤖 Generated with Claude Code
https://claude.com/claude-code
Co-Authored-By: Claude <noreply@anthropic.com>
508 lines
15 KiB
TypeScript
508 lines
15 KiB
TypeScript
/**
|
||
* Performance Tests: LearningEngine Refactor
|
||
*
|
||
* Verifies that the refactored LearningEngine with database persistence:
|
||
* 1. Has <10% overhead compared to in-memory only
|
||
* 2. Completes 1000 learning cycles in <5 seconds
|
||
* 3. Memory usage stays bounded
|
||
* 4. Batch writes are efficient
|
||
*/
|
||
|
||
// CRITICAL: Unmock Database to use real implementation for performance tests
|
||
jest.unmock('../../src/utils/Database');
|
||
|
||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||
import { LearningEngine } from '../../src/learning/LearningEngine';
|
||
import { SwarmMemoryManager } from '../../src/core/memory/SwarmMemoryManager';
|
||
import { Database } from '../../src/utils/Database';
|
||
import { TaskResult } from '../../src/learning/RewardCalculator';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
|
||
describe('LearningEngine Performance (Refactored)', () => {
|
||
const testDbPath = path.join(process.cwd(), '.test-learning-perf.db');
|
||
const memoryDbPath = path.join(process.cwd(), '.test-memory-perf.db');
|
||
let database: Database;
|
||
let memoryManager: SwarmMemoryManager;
|
||
|
||
beforeEach(async () => {
|
||
// Clean up test databases
|
||
[testDbPath, memoryDbPath].forEach(dbPath => {
|
||
if (fs.existsSync(dbPath)) {
|
||
fs.unlinkSync(dbPath);
|
||
}
|
||
});
|
||
|
||
// Create fresh database and memory manager
|
||
database = new Database(testDbPath);
|
||
await database.initialize();
|
||
|
||
memoryManager = new SwarmMemoryManager(memoryDbPath);
|
||
await memoryManager.initialize();
|
||
});
|
||
|
||
afterEach(async () => {
|
||
// Close database connections
|
||
try {
|
||
if (database) await database.close();
|
||
} catch (error) {
|
||
// Ignore close errors
|
||
}
|
||
|
||
try {
|
||
if (memoryManager && (memoryManager as any).db) {
|
||
(memoryManager as any).db.close();
|
||
}
|
||
} catch (error) {
|
||
// Ignore close errors
|
||
}
|
||
|
||
// Clean up test databases
|
||
[testDbPath, memoryDbPath].forEach(dbPath => {
|
||
if (fs.existsSync(dbPath)) {
|
||
fs.unlinkSync(dbPath);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('Persistence Overhead', () => {
|
||
it('should have <10% overhead compared to no persistence', async () => {
|
||
const agentId = 'perf-agent-overhead';
|
||
|
||
// Test 1: In-memory only (no database)
|
||
const engineInMemory = new LearningEngine(
|
||
agentId + '-memory',
|
||
memoryManager,
|
||
{ enabled: true }
|
||
// No database parameter
|
||
);
|
||
// Disable database auto-init by setting env var to empty
|
||
const originalPath = process.env.AQE_DB_PATH;
|
||
process.env.AQE_DB_PATH = '';
|
||
|
||
await engineInMemory.initialize();
|
||
|
||
const startMemory = Date.now();
|
||
for (let i = 0; i < 100; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: {},
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: true,
|
||
executionTime: 1000,
|
||
errors: [],
|
||
coverage: 0.85,
|
||
metadata: {
|
||
strategy: 'template',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
await engineInMemory.recordExperience(task, result);
|
||
}
|
||
const durationMemory = Date.now() - startMemory;
|
||
|
||
// Restore environment
|
||
if (originalPath) {
|
||
process.env.AQE_DB_PATH = originalPath;
|
||
} else {
|
||
delete process.env.AQE_DB_PATH;
|
||
}
|
||
|
||
// Test 2: With database persistence
|
||
const engineWithDB = new LearningEngine(
|
||
agentId + '-db',
|
||
memoryManager,
|
||
{ enabled: true },
|
||
database
|
||
);
|
||
await engineWithDB.initialize();
|
||
|
||
const startDB = Date.now();
|
||
for (let i = 0; i < 100; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: {},
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: true,
|
||
executionTime: 1000,
|
||
errors: [],
|
||
coverage: 0.85,
|
||
metadata: {
|
||
strategy: 'template',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
await engineWithDB.recordExperience(task, result);
|
||
}
|
||
const durationDB = Date.now() - startDB;
|
||
|
||
// Calculate overhead
|
||
const overhead = ((durationDB - durationMemory) / durationMemory) * 100;
|
||
|
||
console.log(`In-memory duration: ${durationMemory}ms`);
|
||
console.log(`With database duration: ${durationDB}ms`);
|
||
console.log(`Overhead: ${overhead.toFixed(2)}%`);
|
||
|
||
// Verify overhead is less than 10%
|
||
expect(overhead).toBeLessThan(10);
|
||
}, 30000); // 30 second timeout
|
||
|
||
it('should complete 1000 learning cycles in <5 seconds', async () => {
|
||
const agentId = 'perf-agent-throughput';
|
||
|
||
const engine = new LearningEngine(
|
||
agentId,
|
||
memoryManager,
|
||
{ enabled: true, batchSize: 50, updateFrequency: 100 },
|
||
database
|
||
);
|
||
await engine.initialize();
|
||
|
||
const start = Date.now();
|
||
|
||
// Run 1000 learning cycles
|
||
for (let i = 0; i < 1000; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: {},
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: Math.random() > 0.2,
|
||
executionTime: 1000 + Math.random() * 500,
|
||
errors: [],
|
||
coverage: 0.7 + Math.random() * 0.3,
|
||
metadata: {
|
||
strategy: i % 3 === 0 ? 'template' : i % 3 === 1 ? 'property' : 'mutation',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
await engine.recordExperience(task, result);
|
||
}
|
||
|
||
const duration = Date.now() - start;
|
||
|
||
console.log(`1000 learning cycles completed in ${duration}ms`);
|
||
console.log(`Average: ${(duration / 1000).toFixed(2)}ms per cycle`);
|
||
|
||
// Verify completion time
|
||
expect(duration).toBeLessThan(5000); // 5 seconds
|
||
|
||
// Verify all experiences were persisted
|
||
const stats = await database.getLearningStatistics(agentId);
|
||
expect(stats.totalExperiences).toBe(1000);
|
||
}, 10000); // 10 second timeout
|
||
});
|
||
|
||
describe('Memory Usage', () => {
|
||
it('should keep memory usage bounded during continuous learning', async () => {
|
||
const agentId = 'perf-agent-memory';
|
||
|
||
const engine = new LearningEngine(
|
||
agentId,
|
||
memoryManager,
|
||
{ enabled: true, maxMemorySize: 10 * 1024 * 1024 }, // 10MB limit
|
||
database
|
||
);
|
||
await engine.initialize();
|
||
|
||
// Force garbage collection before measurement
|
||
if (global.gc) {
|
||
global.gc();
|
||
}
|
||
|
||
const initialMemory = process.memoryUsage().heapUsed;
|
||
|
||
// Run 500 learning cycles
|
||
for (let i = 0; i < 500; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: { iteration: i },
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: true,
|
||
executionTime: 1000,
|
||
errors: [],
|
||
coverage: 0.85,
|
||
metadata: {
|
||
strategy: 'template',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
await engine.recordExperience(task, result);
|
||
|
||
// Periodic GC to prevent false positives
|
||
if (i % 100 === 0 && global.gc) {
|
||
global.gc();
|
||
}
|
||
}
|
||
|
||
// Force garbage collection after test
|
||
if (global.gc) {
|
||
global.gc();
|
||
}
|
||
|
||
const finalMemory = process.memoryUsage().heapUsed;
|
||
const memoryIncrease = (finalMemory - initialMemory) / (1024 * 1024); // MB
|
||
|
||
console.log(`Initial memory: ${(initialMemory / 1024 / 1024).toFixed(2)} MB`);
|
||
console.log(`Final memory: ${(finalMemory / 1024 / 1024).toFixed(2)} MB`);
|
||
console.log(`Memory increase: ${memoryIncrease.toFixed(2)} MB`);
|
||
|
||
// Memory increase should be reasonable (< 50MB)
|
||
expect(memoryIncrease).toBeLessThan(50);
|
||
}, 30000); // 30 second timeout
|
||
});
|
||
|
||
describe('Batch Write Efficiency', () => {
|
||
it('should batch writes efficiently for high-volume learning', async () => {
|
||
const agentId = 'perf-agent-batch';
|
||
|
||
// Small batch size for faster testing
|
||
const engine = new LearningEngine(
|
||
agentId,
|
||
memoryManager,
|
||
{ enabled: true, batchSize: 10, updateFrequency: 50 },
|
||
database
|
||
);
|
||
await engine.initialize();
|
||
|
||
const start = Date.now();
|
||
|
||
// Run 200 rapid learning cycles
|
||
const promises = [];
|
||
for (let i = 0; i < 200; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: {},
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: true,
|
||
executionTime: 1000,
|
||
errors: [],
|
||
coverage: 0.85,
|
||
metadata: {
|
||
strategy: 'template',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
// Don't await - queue them up to test batching
|
||
promises.push(engine.recordExperience(task, result));
|
||
}
|
||
|
||
// Wait for all to complete
|
||
await Promise.all(promises);
|
||
|
||
const duration = Date.now() - start;
|
||
|
||
console.log(`200 batched writes completed in ${duration}ms`);
|
||
console.log(`Average: ${(duration / 200).toFixed(2)}ms per write`);
|
||
|
||
// Batching should make this fast (< 2 seconds)
|
||
expect(duration).toBeLessThan(2000);
|
||
|
||
// Verify all experiences were persisted
|
||
const stats = await database.getLearningStatistics(agentId);
|
||
expect(stats.totalExperiences).toBe(200);
|
||
}, 15000); // 15 second timeout
|
||
});
|
||
|
||
describe('Concurrent Learning', () => {
|
||
it('should handle concurrent learning from multiple agents', async () => {
|
||
const agentCount = 5;
|
||
const cyclesPerAgent = 50;
|
||
|
||
const engines = await Promise.all(
|
||
Array.from({ length: agentCount }, async (_, i) => {
|
||
const engine = new LearningEngine(
|
||
`perf-agent-concurrent-${i}`,
|
||
memoryManager,
|
||
{ enabled: true },
|
||
database
|
||
);
|
||
await engine.initialize();
|
||
return engine;
|
||
})
|
||
);
|
||
|
||
const start = Date.now();
|
||
|
||
// Run concurrent learning
|
||
await Promise.all(
|
||
engines.map(async (engine, agentIdx) => {
|
||
for (let i = 0; i < cyclesPerAgent; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: { agent: agentIdx },
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: true,
|
||
executionTime: 1000,
|
||
errors: [],
|
||
coverage: 0.85,
|
||
metadata: {
|
||
strategy: 'template',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
await engine.recordExperience(task, result);
|
||
}
|
||
})
|
||
);
|
||
|
||
const duration = Date.now() - start;
|
||
|
||
console.log(`${agentCount} agents × ${cyclesPerAgent} cycles = ${agentCount * cyclesPerAgent} total cycles`);
|
||
console.log(`Concurrent learning completed in ${duration}ms`);
|
||
console.log(`Average: ${(duration / (agentCount * cyclesPerAgent)).toFixed(2)}ms per cycle`);
|
||
|
||
// Should complete in reasonable time (< 5 seconds)
|
||
expect(duration).toBeLessThan(5000);
|
||
|
||
// Verify all experiences were persisted
|
||
for (let i = 0; i < agentCount; i++) {
|
||
const stats = await database.getLearningStatistics(`perf-agent-concurrent-${i}`);
|
||
expect(stats.totalExperiences).toBe(cyclesPerAgent);
|
||
}
|
||
}, 15000); // 15 second timeout
|
||
});
|
||
|
||
describe('Database Query Performance', () => {
|
||
it('should retrieve Q-values quickly after learning', async () => {
|
||
const agentId = 'perf-agent-query';
|
||
|
||
const engine = new LearningEngine(
|
||
agentId,
|
||
memoryManager,
|
||
{ enabled: true },
|
||
database
|
||
);
|
||
await engine.initialize();
|
||
|
||
// Learn from 100 experiences
|
||
for (let i = 0; i < 100; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: {},
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: true,
|
||
executionTime: 1000,
|
||
errors: [],
|
||
coverage: 0.85,
|
||
metadata: {
|
||
strategy: i % 3 === 0 ? 'template' : i % 3 === 1 ? 'property' : 'mutation',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
await engine.recordExperience(task, result);
|
||
}
|
||
|
||
// Test query performance
|
||
const start = Date.now();
|
||
const qValues = await database.getAllQValues(agentId);
|
||
const duration = Date.now() - start;
|
||
|
||
console.log(`Retrieved ${qValues.length} Q-values in ${duration}ms`);
|
||
|
||
// Query should be fast (< 100ms)
|
||
expect(duration).toBeLessThan(100);
|
||
expect(qValues.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('should retrieve learning statistics quickly', async () => {
|
||
const agentId = 'perf-agent-stats';
|
||
|
||
const engine = new LearningEngine(
|
||
agentId,
|
||
memoryManager,
|
||
{ enabled: true },
|
||
database
|
||
);
|
||
await engine.initialize();
|
||
|
||
// Learn from 100 experiences
|
||
for (let i = 0; i < 100; i++) {
|
||
const task = {
|
||
type: 'test-generation',
|
||
context: {},
|
||
requirements: { capabilities: ['testing'] }
|
||
};
|
||
|
||
const result: TaskResult = {
|
||
success: Math.random() > 0.3,
|
||
executionTime: 1000,
|
||
errors: [],
|
||
coverage: 0.85,
|
||
metadata: {
|
||
strategy: 'template',
|
||
toolsUsed: ['jest'],
|
||
parallelization: 0.5,
|
||
retryPolicy: 'exponential',
|
||
resourceAllocation: 0.5
|
||
}
|
||
};
|
||
|
||
await engine.recordExperience(task, result);
|
||
}
|
||
|
||
// Test statistics query performance
|
||
const start = Date.now();
|
||
const stats = await database.getLearningStatistics(agentId);
|
||
const duration = Date.now() - start;
|
||
|
||
console.log(`Retrieved statistics in ${duration}ms`);
|
||
console.log(`Total experiences: ${stats.totalExperiences}`);
|
||
console.log(`Avg reward: ${stats.avgReward.toFixed(3)}`);
|
||
console.log(`Q-table size: ${stats.qTableSize}`);
|
||
|
||
// Statistics query should be fast (< 50ms)
|
||
expect(duration).toBeLessThan(50);
|
||
expect(stats.totalExperiences).toBe(100);
|
||
});
|
||
});
|
||
});
|