mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
346438f0de
* SwarmMemoryManager Refactoring Plan * fix: add missing directories to npm package (v1.9.3) Added templates/, .claude/helpers/, and docs/reference/ to package.json files array. These were missing from the npm distribution, causing `aqe init` to fail with "Template file not found" errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * checkpoint improvements * fix: critical memory/learning system fixes for v1.9.4 ## Critical Fixes - Vector embeddings now stored correctly (was storing NULL) - SQL parameter style fixed for agentdb's SqlJsDatabase wrapper - HNSW index schema mismatch resolved with pattern_id column - Hooks now save to correct database (.agentic-qe/agentdb.db) ## New Features - Added 5 SwarmMemoryManager methods for learning data retrieval: - getBestAction() - Q-learning best action selection - getRecentLearningExperiences() - Recent experience retrieval - getLearningExperiencesByTaskType() - Task-filtered experiences - getHighRewardExperiences() - Successful experience extraction - getLearningStats() - Aggregate learning statistics - Added comprehensive integration test for memory/learning loop - Added RuVector pattern store integration (experimental) ## Technical Details - Vector embeddings: 384 dimensions × 4 bytes = 1,536 bytes per pattern - All 12 integration tests pass - HNSW index: 150x faster vector search enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: move ARM64-only ruvector packages to optionalDependencies Fixes CI failures caused by platform-specific dependencies: - @ruvector/node-linux-arm64-gnu (ARM64 only) - ruvector-core-linux-arm64-gnu (ARM64 only) These packages caused npm ci to fail on x64 CI runners with: "EBADPLATFORM - wanted linux/arm64, current linux/x64" Solution: Move ARM64 packages to optionalDependencies and add x64 variants for cross-platform compatibility. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: regenerate package-lock.json with x64 ruvector packages Updates lockfile to include optional x64 platform packages for CI compatibility: - @ruvector/node-linux-x64-gnu@0.1.17 - ruvector-core-linux-x64-gnu@0.1.15 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: replace fake QUIC with proper implementation and clean up skills manifest QUIC Transport: - Remove fake HTTP/2-based QUICTransport.ts that pretended to be QUIC - Add proper QUIC implementation via Rust/WASM (@agentic-flow/quic-wasm) - Add WebSocket fallback transport for when WASM unavailable - Add dedicated type declarations for optional WASM module - Features: 0-RTT connection, stream multiplexing, TLS 1.3 built-in Skills Manifest: - Rewrite skills-manifest.json with only QE skills (41 skills) - Remove Claude Flow platform skills (agentdb-*, flow-nexus-*, github-*, etc.) - Organize into proper categories: qe-core, testing-methodologies, etc. Bug Fixes: - Fix UnifiedMemoryCoordinator.ts TypeScript errors - Use Logger.getInstance() instead of private constructor - Use swarmMemory.query() instead of non-existent list() method - Use correct close() method signatures for backends 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add QX Partner agent, compact subagents, fix CLAUDE.md dynamic counts - Add QX Partner agent for Quality Experience analysis combining QA + UX perspectives - Compact 9 subagent definitions to ~80 lines each (88% reduction) - Fix CLAUDE.md template to dynamically count agents (including subagents) and skills - Move CLAUDE.md creation after parallel phases for accurate counts - Remove 'Production-Ready' claim from README and CHANGELOG - Clean up backup files and move docs to proper directory Initialization now correctly reports: - 31 QE Agents (20 main + 11 subagents) - 53 QE Skills - 8 Slash Commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
576 lines
17 KiB
TypeScript
576 lines
17 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)', () => {
|
||
// Use tests/.tmp directory for test databases (not project root)
|
||
const tmpDir = path.join(__dirname, '../.tmp');
|
||
const testDbPath = path.join(tmpDir, '.test-learning-perf.db');
|
||
const memoryDbPath = path.join(tmpDir, '.test-memory-perf.db');
|
||
let database: Database;
|
||
let memoryManager: SwarmMemoryManager;
|
||
|
||
beforeEach(async () => {
|
||
// Ensure tmp directory exists
|
||
if (!fs.existsSync(tmpDir)) {
|
||
fs.mkdirSync(tmpDir, { recursive: true });
|
||
}
|
||
|
||
// 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';
|
||
let engineInMemory: LearningEngine | null = null;
|
||
let engineWithDB: LearningEngine | null = null;
|
||
|
||
try {
|
||
// Test 1: In-memory only (no database)
|
||
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;
|
||
|
||
// Cleanup in-memory engine
|
||
engineInMemory.dispose();
|
||
engineInMemory = null;
|
||
|
||
// Restore environment
|
||
if (originalPath) {
|
||
process.env.AQE_DB_PATH = originalPath;
|
||
} else {
|
||
delete process.env.AQE_DB_PATH;
|
||
}
|
||
|
||
// Test 2: With database persistence
|
||
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);
|
||
} finally {
|
||
// Cleanup
|
||
if (engineInMemory) {
|
||
engineInMemory.dispose();
|
||
}
|
||
if (engineWithDB) {
|
||
engineWithDB.dispose();
|
||
}
|
||
}
|
||
}, 30000); // 30 second timeout
|
||
|
||
it('should complete 1000 learning cycles in <5 seconds', async () => {
|
||
const agentId = 'perf-agent-throughput';
|
||
let engine: LearningEngine | null = null;
|
||
|
||
try {
|
||
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);
|
||
} finally {
|
||
if (engine) {
|
||
engine.dispose();
|
||
}
|
||
}
|
||
}, 10000); // 10 second timeout
|
||
});
|
||
|
||
describe('Memory Usage', () => {
|
||
it('should keep memory usage bounded during continuous learning', async () => {
|
||
const agentId = 'perf-agent-memory';
|
||
let engine: LearningEngine | null = null;
|
||
|
||
try {
|
||
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);
|
||
} finally {
|
||
if (engine) {
|
||
engine.dispose();
|
||
}
|
||
}
|
||
}, 30000); // 30 second timeout
|
||
});
|
||
|
||
describe('Batch Write Efficiency', () => {
|
||
it('should batch writes efficiently for high-volume learning', async () => {
|
||
const agentId = 'perf-agent-batch';
|
||
let engine: LearningEngine | null = null;
|
||
|
||
try {
|
||
// Small batch size for faster testing
|
||
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);
|
||
} finally {
|
||
if (engine) {
|
||
engine.dispose();
|
||
}
|
||
}
|
||
}, 15000); // 15 second timeout
|
||
});
|
||
|
||
describe('Concurrent Learning', () => {
|
||
it('should handle concurrent learning from multiple agents', async () => {
|
||
const agentCount = 5;
|
||
const cyclesPerAgent = 50;
|
||
let engines: LearningEngine[] = [];
|
||
|
||
try {
|
||
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);
|
||
}
|
||
} finally {
|
||
// Cleanup all engines
|
||
for (const engine of engines) {
|
||
if (engine) {
|
||
engine.dispose();
|
||
}
|
||
}
|
||
}
|
||
}, 15000); // 15 second timeout
|
||
});
|
||
|
||
describe('Database Query Performance', () => {
|
||
it('should retrieve Q-values quickly after learning', async () => {
|
||
const agentId = 'perf-agent-query';
|
||
let engine: LearningEngine | null = null;
|
||
|
||
try {
|
||
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);
|
||
} finally {
|
||
if (engine) {
|
||
engine.dispose();
|
||
}
|
||
}
|
||
});
|
||
|
||
it('should retrieve learning statistics quickly', async () => {
|
||
const agentId = 'perf-agent-stats';
|
||
let engine: LearningEngine | null = null;
|
||
|
||
try {
|
||
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);
|
||
} finally {
|
||
if (engine) {
|
||
engine.dispose();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
});
|