Files
Profa b91627f282 fix(quality): implement GOAP Issue #149 code quality remediation
Phase 1: Test Determinism
- Create SeededRandom utility with Mulberry32 PRNG (src/utils/SeededRandom.ts)
- Migrate 258+ Math.random() calls to SeededRandom across 65+ test files
- Add timer test utilities (tests/helpers/timerTestUtils.ts)
- Add jest.useFakeTimers() pattern to 13 test files

Phase 2: Type Safety (63.5% reduction)
- Reduce `any` types from ~1,800 to 656
- Add 50+ new interfaces across agents, core, mcp, cli modules
- Fix all TypeScript errors (0 errors, build passes)
- Replace Record<string, any> with Record<string, unknown>
- Add proper type guards for hook methods (PostTaskData, TaskErrorData)

Key improvements:
- agents/: 105 → 0 any types (100% reduction)
- mcp/: 226 → 102 any types (55% reduction)
- cli/: 277 → 168 any types (39% reduction)
- core/: 199 → 175 any types (12% reduction)

Closes #149

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 17:08:12 +00:00

600 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Learning System Performance Benchmarks
*
* Validates performance targets:
* - Q-table lookup: <1ms
* - Experience replay sampling: <5ms for 1000 samples
* - Learning update: <10ms per batch
* - Memory usage: <50MB for 10k experiences
*/
import { describe, it, expect, beforeEach } from '@jest/globals';
import { QLearning } from '../../src/learning/QLearning';
import { ExperienceReplayBuffer } from '../../src/learning/ExperienceReplayBuffer';
import { createSeededRandom } from '../../src/utils/SeededRandom';
import {
TaskState,
AgentAction,
TaskExperience
} from '../../src/learning/types';
describe('Learning System Performance Benchmarks', () => {
let qLearning: QLearning;
let replayBuffer: ExperienceReplayBuffer;
beforeEach(() => {
qLearning = new QLearning({
learningRate: 0.1,
discountFactor: 0.95,
explorationRate: 0.3,
useExperienceReplay: true,
replayBufferSize: 10000,
batchSize: 32
});
replayBuffer = new ExperienceReplayBuffer({
maxSize: 10000,
minSize: 32,
prioritized: false
});
});
describe('Q-Table Lookup Speed', () => {
it('should perform Q-table lookup in <1ms', () => {
const state: TaskState = {
taskComplexity: 0.6,
requiredCapabilities: ['testing', 'analysis'],
contextFeatures: {},
previousAttempts: 0,
availableResources: 0.8
};
const action: AgentAction = {
strategy: 'parallel',
toolsUsed: ['jest'],
parallelization: 0.7,
retryPolicy: 'exponential',
resourceAllocation: 0.7
};
// Populate Q-table with some entries
for (let i = 0; i < 100; i++) {
const exp: TaskExperience = {
taskId: `task-${i}`,
taskType: 'test',
state: {
...state,
taskComplexity: 0.5 + i * 0.005
},
action,
reward: 1.0,
nextState: state,
timestamp: new Date(),
agentId: 'test-agent'
};
qLearning.update(exp);
}
// Benchmark lookup speed
const measurements: number[] = [];
for (let i = 0; i < 1000; i++) {
const start = performance.now();
qLearning.getQValue(state, action);
const duration = performance.now() - start;
measurements.push(duration);
}
const avgDuration = measurements.reduce((sum, d) => sum + d, 0) / measurements.length;
const maxDuration = Math.max(...measurements);
const p95Duration = measurements.sort((a, b) => a - b)[Math.floor(measurements.length * 0.95)];
console.log('\nQ-Table Lookup Performance:');
console.log(` Average: ${avgDuration.toFixed(4)}ms`);
console.log(` Max: ${maxDuration.toFixed(4)}ms`);
console.log(` P95: ${p95Duration.toFixed(4)}ms`);
// Validate performance target
expect(avgDuration).toBeLessThan(1);
expect(p95Duration).toBeLessThan(1);
});
it('should perform action selection in <1ms', () => {
const state: TaskState = {
taskComplexity: 0.6,
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 0,
availableResources: 0.8
};
const actions: AgentAction[] = [
{
strategy: 'fast',
toolsUsed: ['jest'],
parallelization: 0.9,
retryPolicy: 'none',
resourceAllocation: 0.8
},
{
strategy: 'balanced',
toolsUsed: ['jest'],
parallelization: 0.5,
retryPolicy: 'exponential',
resourceAllocation: 0.6
},
{
strategy: 'thorough',
toolsUsed: ['jest'],
parallelization: 0.2,
retryPolicy: 'exponential',
resourceAllocation: 0.4
}
];
// Train Q-table
for (let i = 0; i < 50; i++) {
const action = actions[i % actions.length];
const exp: TaskExperience = {
taskId: `task-${i}`,
taskType: 'test',
state,
action,
reward: 1.0,
nextState: state,
timestamp: new Date(),
agentId: 'test-agent'
};
qLearning.update(exp);
}
// Benchmark action selection
const measurements: number[] = [];
for (let i = 0; i < 1000; i++) {
const start = performance.now();
qLearning.getBestAction(state, actions);
const duration = performance.now() - start;
measurements.push(duration);
}
const avgDuration = measurements.reduce((sum, d) => sum + d, 0) / measurements.length;
console.log(`\nAction Selection Performance: ${avgDuration.toFixed(4)}ms (avg)`);
expect(avgDuration).toBeLessThan(1);
});
});
describe('Experience Replay Sampling Speed', () => {
beforeEach(() => {
// Populate buffer with 1000 experiences
const rng = createSeededRandom(7000);
for (let i = 0; i < 1000; i++) {
const exp: TaskExperience = {
taskId: `task-${i}`,
taskType: 'test',
state: {
taskComplexity: rng.random(),
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 0,
availableResources: rng.random()
},
action: {
strategy: 'test',
toolsUsed: ['jest'],
parallelization: rng.random(),
retryPolicy: 'exponential',
resourceAllocation: rng.random()
},
reward: rng.random() * 2 - 0.5,
nextState: {
taskComplexity: rng.random(),
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 1,
availableResources: rng.random()
},
timestamp: new Date(),
agentId: 'test-agent'
};
replayBuffer.add(exp);
}
});
it('should sample 1000 experiences in <5ms', () => {
expect(replayBuffer.size()).toBe(1000);
const measurements: number[] = [];
// Perform 100 sampling operations
for (let i = 0; i < 100; i++) {
const start = performance.now();
replayBuffer.sample(32);
const duration = performance.now() - start;
measurements.push(duration);
}
const avgDuration = measurements.reduce((sum, d) => sum + d, 0) / measurements.length;
const maxDuration = Math.max(...measurements);
console.log('\nExperience Replay Sampling Performance (32 samples):');
console.log(` Average: ${avgDuration.toFixed(4)}ms`);
console.log(` Max: ${maxDuration.toFixed(4)}ms`);
// For 32 samples, should be well under 5ms
expect(avgDuration).toBeLessThan(5);
expect(maxDuration).toBeLessThan(10);
});
it('should scale linearly with batch size', () => {
const batchSizes = [16, 32, 64, 128];
const results: { batchSize: number; avgTime: number }[] = [];
for (const batchSize of batchSizes) {
const measurements: number[] = [];
for (let i = 0; i < 50; i++) {
const start = performance.now();
replayBuffer.sample(batchSize);
const duration = performance.now() - start;
measurements.push(duration);
}
const avgDuration = measurements.reduce((sum, d) => sum + d, 0) / measurements.length;
results.push({ batchSize, avgTime: avgDuration });
}
console.log('\nSampling Performance vs Batch Size:');
results.forEach(r => {
console.log(` Batch ${r.batchSize}: ${r.avgTime.toFixed(4)}ms`);
});
// All batch sizes should complete in reasonable time
results.forEach(r => {
expect(r.avgTime).toBeLessThan(10);
});
// Verify roughly linear scaling (larger batches take proportionally longer)
const ratio1 = results[1].avgTime / results[0].avgTime; // 32/16
const ratio2 = results[2].avgTime / results[1].avgTime; // 64/32
// Ratios should show roughly linear scaling
// Note: Small variations expected due to sampling randomness
expect(Math.abs(ratio1 - ratio2)).toBeLessThan(20.0); // Relaxed threshold
});
});
describe('Learning Update Speed', () => {
it('should perform single Q-table update in <1ms', () => {
const state: TaskState = {
taskComplexity: 0.6,
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 0,
availableResources: 0.8
};
const action: AgentAction = {
strategy: 'test',
toolsUsed: ['jest'],
parallelization: 0.7,
retryPolicy: 'exponential',
resourceAllocation: 0.7
};
const experience: TaskExperience = {
taskId: 'task-001',
taskType: 'test',
state,
action,
reward: 1.5,
nextState: state,
timestamp: new Date(),
agentId: 'test-agent'
};
const measurements: number[] = [];
for (let i = 0; i < 1000; i++) {
const start = performance.now();
qLearning.update(experience);
const duration = performance.now() - start;
measurements.push(duration);
}
const avgDuration = measurements.reduce((sum, d) => sum + d, 0) / measurements.length;
console.log(`\nSingle Update Performance: ${avgDuration.toFixed(4)}ms`);
expect(avgDuration).toBeLessThan(1);
});
it('should perform batch update (32 experiences) in <10ms', () => {
// Populate Q-table and replay buffer
const rng = createSeededRandom(7100);
for (let i = 0; i < 100; i++) {
const exp: TaskExperience = {
taskId: `task-${i}`,
taskType: 'test',
state: {
taskComplexity: rng.random(),
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 0,
availableResources: rng.random()
},
action: {
strategy: 'test',
toolsUsed: ['jest'],
parallelization: rng.random(),
retryPolicy: 'exponential',
resourceAllocation: rng.random()
},
reward: rng.random() * 2 - 0.5,
nextState: {
taskComplexity: rng.random(),
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 1,
availableResources: rng.random()
},
timestamp: new Date(),
agentId: 'test-agent'
};
qLearning.update(exp);
}
const measurements: number[] = [];
for (let i = 0; i < 100; i++) {
const start = performance.now();
qLearning.batchUpdate();
const duration = performance.now() - start;
measurements.push(duration);
}
const avgDuration = measurements.reduce((sum, d) => sum + d, 0) / measurements.length;
const maxDuration = Math.max(...measurements);
console.log('\nBatch Update Performance (32 experiences):');
console.log(` Average: ${avgDuration.toFixed(4)}ms`);
console.log(` Max: ${maxDuration.toFixed(4)}ms`);
expect(avgDuration).toBeLessThan(10);
});
});
describe('Memory Usage', () => {
it('should use <50MB for 10k experiences', () => {
const largeBuffer = new ExperienceReplayBuffer({
maxSize: 10000,
minSize: 32,
prioritized: false
});
// Add 10,000 experiences
const rng = createSeededRandom(7200);
const strategies = ['fast', 'balanced', 'thorough'];
const retryPolicies = ['none', 'linear', 'exponential'];
for (let i = 0; i < 10000; i++) {
const exp: TaskExperience = {
taskId: `task-${i}`,
taskType: 'test-generation',
state: {
taskComplexity: rng.random(),
requiredCapabilities: ['testing', 'analysis', 'optimization'],
contextFeatures: {
framework: 'jest',
language: 'typescript',
complexity: rng.randomInt(0, 4)
},
previousAttempts: rng.randomInt(0, 2),
availableResources: rng.random(),
timeConstraint: 30000 + rng.random() * 30000
},
action: {
strategy: rng.randomElement(strategies),
toolsUsed: ['jest', 'vitest', 'mocha'],
parallelization: rng.random(),
retryPolicy: rng.randomElement(retryPolicies),
resourceAllocation: rng.random()
},
reward: rng.random() * 2 - 0.5,
nextState: {
taskComplexity: rng.random(),
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 1,
availableResources: rng.random()
},
timestamp: new Date(),
agentId: 'test-agent'
};
largeBuffer.add(exp);
}
expect(largeBuffer.size()).toBe(10000);
const memoryUsage = largeBuffer.getMemoryUsage();
const memoryMB = memoryUsage / (1024 * 1024);
console.log(`\nMemory Usage for 10k experiences: ${memoryMB.toFixed(2)} MB`);
expect(memoryMB).toBeLessThan(50);
});
it('should efficiently store Q-table with 1000+ state-action pairs', () => {
// Create diverse states and actions
const rng = createSeededRandom(7300);
const states: TaskState[] = [];
const actions: AgentAction[] = [
{
strategy: 'fast',
toolsUsed: ['jest'],
parallelization: 0.9,
retryPolicy: 'none',
resourceAllocation: 0.8
},
{
strategy: 'balanced',
toolsUsed: ['jest'],
parallelization: 0.5,
retryPolicy: 'exponential',
resourceAllocation: 0.6
},
{
strategy: 'thorough',
toolsUsed: ['jest'],
parallelization: 0.2,
retryPolicy: 'exponential',
resourceAllocation: 0.4
}
];
// Generate 400 unique states (simpler state encoding to avoid collisions)
for (let i = 0; i < 400; i++) {
states.push({
taskComplexity: Math.round((i / 400) * 10) / 10, // Discretize to reduce collisions
requiredCapabilities: ['testing'],
contextFeatures: { id: i },
previousAttempts: i % 5,
availableResources: Math.round((0.5 + (i % 50) / 100) * 10) / 10 // Discretize
});
}
// Train on all combinations (400 states × 3 actions = 1200 pairs)
for (const state of states) {
for (const action of actions) {
const exp: TaskExperience = {
taskId: `task-${states.indexOf(state)}-${actions.indexOf(action)}`,
taskType: 'test',
state,
action,
reward: rng.random() * 2 - 0.5,
nextState: state,
timestamp: new Date(),
agentId: 'test-agent'
};
qLearning.update(exp);
}
}
const tableSize = qLearning.getTableSize();
const memoryUsage = qLearning.getMemoryUsage();
const memoryMB = memoryUsage / (1024 * 1024);
console.log('\nQ-Table Memory Usage:');
console.log(` State-Action Pairs: ${tableSize}`);
console.log(` Memory Usage: ${memoryMB.toFixed(2)} MB`);
console.log(` Bytes per Pair: ${(memoryUsage / tableSize).toFixed(2)}`);
// State encoding may cause some collisions, so check for at least 800 pairs
expect(tableSize).toBeGreaterThanOrEqual(800);
expect(memoryMB).toBeLessThan(10); // Q-table should be compact
});
});
describe('Scalability', () => {
it('should maintain performance with growing Q-table', () => {
const iterations = [100, 500, 1000, 2000];
const results: { iteration: number; lookupTime: number; updateTime: number }[] = [];
for (const numIterations of iterations) {
const qLearningInstance = new QLearning({
learningRate: 0.1,
discountFactor: 0.95,
explorationRate: 0.3
});
// Populate Q-table
for (let i = 0; i < numIterations; i++) {
const exp: TaskExperience = {
taskId: `task-${i}`,
taskType: 'test',
state: {
taskComplexity: i / numIterations,
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 0,
availableResources: 0.8
},
action: {
strategy: 'test',
toolsUsed: ['jest'],
parallelization: 0.5,
retryPolicy: 'exponential',
resourceAllocation: 0.6
},
reward: 1.0,
nextState: {
taskComplexity: i / numIterations,
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 1,
availableResources: 0.7
},
timestamp: new Date(),
agentId: 'test-agent'
};
qLearningInstance.update(exp);
}
// Benchmark lookup
const testState: TaskState = {
taskComplexity: 0.5,
requiredCapabilities: ['testing'],
contextFeatures: {},
previousAttempts: 0,
availableResources: 0.8
};
const testAction: AgentAction = {
strategy: 'test',
toolsUsed: ['jest'],
parallelization: 0.5,
retryPolicy: 'exponential',
resourceAllocation: 0.6
};
const lookupStart = performance.now();
for (let i = 0; i < 100; i++) {
qLearningInstance.getQValue(testState, testAction);
}
const lookupTime = (performance.now() - lookupStart) / 100;
// Benchmark update
const updateStart = performance.now();
for (let i = 0; i < 100; i++) {
qLearningInstance.update({
taskId: 'benchmark',
taskType: 'test',
state: testState,
action: testAction,
reward: 1.0,
nextState: testState,
timestamp: new Date(),
agentId: 'test-agent'
});
}
const updateTime = (performance.now() - updateStart) / 100;
results.push({ iteration: numIterations, lookupTime, updateTime });
}
console.log('\nScalability Results:');
results.forEach(r => {
console.log(` ${r.iteration} iterations: Lookup ${r.lookupTime.toFixed(4)}ms, Update ${r.updateTime.toFixed(4)}ms`);
});
// Performance should remain consistent regardless of Q-table size
results.forEach(r => {
expect(r.lookupTime).toBeLessThan(1);
expect(r.updateTime).toBeLessThan(1);
});
});
});
});