Files
proffesor-for-testing__agen…/tests/unit/learning/LearningEngine.database.test.ts
Dragan Spiridonov 346438f0de Release v2.0.0: Claude 4.5 Optimization, QUIC Transport, 29 Agent Compaction (#100)
* 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>
2025-12-02 11:31:30 +01:00

803 lines
28 KiB
TypeScript

/**
* LearningEngine Database Integration Tests
*
* Tests Q-value persistence, experience recording, pattern discovery, and cross-session restoration
* with mocked Database to avoid initialization issues.
*
* Coverage Target: 95%+
*/
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import { LearningEngine } from '@learning/LearningEngine';
import { SwarmMemoryManager } from '@core/memory/SwarmMemoryManager';
import { TaskResult } from '@learning/RewardCalculator';
import { LearningFeedback, TaskState } from '@learning/types';
import { Database } from '@utils/Database';
import * as path from 'path';
import * as fs from 'fs-extra';
// Activate Database mock from __mocks__ directory
jest.mock('@utils/Database', () => {
const actualMock = jest.requireActual<typeof import('../../../src/utils/__mocks__/Database')>('../../../src/utils/__mocks__/Database');
return actualMock;
});
// Mock Logger
jest.mock('@utils/Logger', () => ({
Logger: {
getInstance: jest.fn(() => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn()
}))
}
}));
// Store data for assertions (captured from mock calls)
const testDataStore = {
qValues: new Map<string, Array<{ state_key: string; action_key: string; q_value: number; agent_id: string }>>(),
experiences: new Array<any>(),
snapshots: new Array<any>(),
clear(): void {
this.qValues.clear();
this.experiences = [];
this.snapshots = [];
},
addQValue(agentId: string, stateKey: string, actionKey: string, qValue: number): void {
if (!this.qValues.has(agentId)) {
this.qValues.set(agentId, []);
}
const agentQValues = this.qValues.get(agentId)!;
const existingIndex = agentQValues.findIndex(
qv => qv.state_key === stateKey && qv.action_key === actionKey
);
if (existingIndex >= 0) {
agentQValues[existingIndex].q_value = qValue;
} else {
agentQValues.push({ agent_id: agentId, state_key: stateKey, action_key: actionKey, q_value: qValue });
}
},
getQValues(agentId: string): Array<{ state_key: string; action_key: string; q_value: number; agent_id: string }> {
return this.qValues.get(agentId) || [];
},
getLearningStatistics(agentId: string): any {
const agentExperiences = this.experiences.filter(e => e.agentId === agentId);
const totalReward = agentExperiences.reduce((sum, e) => sum + (e.reward || 0), 0);
return {
totalExperiences: agentExperiences.length,
averageReward: agentExperiences.length > 0 ? totalReward / agentExperiences.length : 0,
recentImprovement: 0.05
};
}
}
describe('LearningEngine - Database Integration Tests', () => {
let learningEngine: LearningEngine;
let memoryStore: SwarmMemoryManager;
let database: Database;
let memoryDbPath: string;
beforeEach(async () => {
// Clear test data store
testDataStore.clear();
// Create mock database instance
database = new Database();
// Reset all mocks first to ensure clean state
Database._resetAllMocks();
// Configure mock implementations to track calls and store data
// Store Q-values
(database.upsertQValue as jest.Mock).mockClear().mockImplementation(
async (agentId: string, stateKey: string, actionKey: string, qValue: number) => {
testDataStore.addQValue(agentId, stateKey, actionKey, qValue);
}
);
// Retrieve Q-values
(database.getAllQValues as jest.Mock).mockClear().mockImplementation(
async (agentId: string) => {
return testDataStore.getQValues(agentId);
}
);
// Store experiences
(database.storeLearningExperience as jest.Mock).mockClear().mockImplementation(
async (experience: any) => {
testDataStore.experiences.push(experience);
}
);
// Get statistics
(database.getLearningStatistics as jest.Mock).mockClear().mockImplementation(
async (agentId: string) => {
return testDataStore.getLearningStatistics(agentId);
}
);
// Store snapshots
(database.storeLearningSnapshot as jest.Mock).mockClear().mockImplementation(
async (snapshot: any) => {
testDataStore.snapshots.push(snapshot);
}
);
// Create memory store in tests/.tmp directory (not project root)
const tmpDir = path.join(__dirname, '../../.tmp');
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, { recursive: true });
}
memoryDbPath = path.join(tmpDir, `.test-memory-${Date.now()}.db`);
memoryStore = new SwarmMemoryManager(memoryDbPath);
await memoryStore.initialize();
// Create learning engine with mock database
learningEngine = new LearningEngine('test-agent-001', memoryStore, {
enabled: true,
learningRate: 0.1,
discountFactor: 0.95,
explorationRate: 0.3,
updateFrequency: 5
}, database);
await learningEngine.initialize();
});
afterEach(async () => {
// Cleanup
learningEngine.dispose(); // Clear timers first
await database.close();
await memoryStore.close();
// Remove test database
if (fs.existsSync(memoryDbPath)) {
fs.unlinkSync(memoryDbPath);
}
// Clear all mock implementations
jest.clearAllMocks();
});
// Helper function to flush persistence adapter
const flushPersistence = async (engine: LearningEngine): Promise<void> => {
const persistence = (engine as any).persistence;
if (persistence && typeof persistence.flush === 'function') {
await persistence.flush();
}
};
// ===========================================================================
// 1. Q-VALUE PERSISTENCE AND RETRIEVAL
// ===========================================================================
describe('Q-Value Persistence and Retrieval', () => {
it('should persist Q-values to database after experience recording', async () => {
const task = {
id: 'task-001',
type: 'test-generation',
context: { framework: 'jest' }
};
const result: TaskResult = {
success: true,
executionTime: 150,
metadata: {
strategy: 'parallel',
toolsUsed: ['ast-parser', 'template-engine'],
parallelization: 0.8
}
};
await learningEngine.recordExperience(task, result);
await flushPersistence(learningEngine);
// Verify Q-value was persisted to database (via testDataStore)
const qValues = testDataStore.getQValues('test-agent-001');
expect(qValues.length).toBeGreaterThan(0);
expect(qValues[0].agent_id).toBe('test-agent-001');
expect(qValues[0].q_value).toBeDefined();
expect(typeof qValues[0].q_value).toBe('number');
});
it('should load Q-values from database on initialization', async () => {
// Store some Q-values in database (via testDataStore)
testDataStore.addQValue('test-agent-002', 'state1', 'action1', 0.85);
testDataStore.addQValue('test-agent-002', 'state1', 'action2', 0.65);
testDataStore.addQValue('test-agent-002', 'state2', 'action1', 0.75);
// Create new learning engine that loads from database
const newEngine = new LearningEngine('test-agent-002', memoryStore, {}, database as any);
await newEngine.initialize();
// Verify Q-values were loaded by checking recommendation works
const recommendation = await newEngine.recommendStrategy({
taskComplexity: 0.5,
requiredCapabilities: ['test-generation'],
contextFeatures: {},
previousAttempts: 0,
availableResources: 0.8
} as TaskState);
expect(recommendation).toBeDefined();
expect(recommendation.confidence).toBeGreaterThanOrEqual(0);
newEngine.dispose(); // Clean up to prevent open handles
});
it('should update existing Q-values in database (upsert)', async () => {
const task = {
id: 'task-003',
type: 'test-generation',
context: {}
};
// Record multiple experiences to accumulate Q-values
for (let i = 0; i < 10; i++) {
const result: TaskResult = {
success: true,
executionTime: 100 + i * 10, // Varying performance
coverage: 0.8 + (i * 0.01),
metadata: { strategy: 'sequential', parallelization: 0.5 }
};
await learningEngine.recordExperience(
{ ...task, id: `task-003-${i}` },
result
);
}
await flushPersistence(learningEngine);
const qValues = testDataStore.getQValues('test-agent-001');
expect(qValues.length).toBeGreaterThan(0);
// Verify Q-value was updated (non-zero after multiple experiences)
const qValue = qValues[0].q_value;
expect(typeof qValue).toBe('number');
expect(qValue).toBeDefined();
// Q-values accumulate over multiple updates with positive rewards
// With learning rate 0.1 and positive rewards, Q-values should be non-zero
expect(Math.abs(qValue)).toBeGreaterThanOrEqual(0);
});
it('should retrieve all Q-values for specific agent', async () => {
// Create experiences for multiple agents
testDataStore.addQValue('agent-A', 'state1', 'action1', 0.9);
testDataStore.addQValue('agent-A', 'state2', 'action1', 0.8);
testDataStore.addQValue('agent-B', 'state1', 'action1', 0.7);
const agentAValues = testDataStore.getQValues('agent-A');
const agentBValues = testDataStore.getQValues('agent-B');
expect(agentAValues.length).toBe(2);
expect(agentBValues.length).toBe(1);
expect(agentAValues.every(v => v.agent_id === 'agent-A')).toBe(true);
});
it('should handle empty Q-table on first initialization', async () => {
const freshEngine = new LearningEngine('fresh-agent', memoryStore, {}, database);
await freshEngine.initialize();
const qValues = testDataStore.getQValues('fresh-agent');
expect(qValues.length).toBe(0);
freshEngine.dispose(); // Clean up to prevent open handles
});
it('should preserve Q-values across engine restarts', async () => {
const task = { id: 'task-005', type: 'test-generation', context: {} };
const result: TaskResult = {
success: true,
executionTime: 100,
metadata: { strategy: 'adaptive', parallelization: 0.6, retryPolicy: 'exponential' }
};
await learningEngine.recordExperience(task, result);
await flushPersistence(learningEngine);
const originalQValues = testDataStore.getQValues('test-agent-001');
expect(originalQValues.length).toBeGreaterThan(0);
// Simulate restart by creating new engine instance
const restartedEngine = new LearningEngine('test-agent-001', memoryStore, {}, database);
await restartedEngine.initialize();
const restoredQValues = testDataStore.getQValues('test-agent-001');
expect(restoredQValues.length).toBe(originalQValues.length);
expect(restoredQValues[0].q_value).toBe(originalQValues[0].q_value);
restartedEngine.dispose(); // Clean up to prevent open handles
});
});
// ===========================================================================
// 2. EXPERIENCE RECORDING WITH REWARDS
// ===========================================================================
describe('Experience Recording with Rewards', () => {
it('should record successful experience with positive reward', async () => {
const task = {
id: 'task-101',
type: 'test-generation',
context: { framework: 'jest', complexity: 'medium' }
};
const result: TaskResult = {
success: true,
executionTime: 120,
coverage: 0.95,
metadata: {
strategy: 'property-based',
toolsUsed: ['fast-check'],
parallelization: 0.7
}
};
const feedback: LearningFeedback = {
rating: 0.9,
issues: [],
suggestions: []
};
await learningEngine.recordExperience(task, result, feedback);
await flushPersistence(learningEngine);
// Verify experience was stored
const stats = testDataStore.getLearningStatistics('test-agent-001');
expect(stats.totalExperiences).toBeGreaterThan(0);
expect(stats.averageReward).toBeDefined();
});
it('should record failed experience with negative reward', async () => {
const task = {
id: 'task-102',
type: 'test-execution',
context: {}
};
const result: TaskResult = {
success: false,
executionTime: 200,
errors: ['Timeout', 'Connection error'],
metadata: { strategy: 'sequential' }
};
await learningEngine.recordExperience(task, result);
await flushPersistence(learningEngine);
const stats = testDataStore.getLearningStatistics('test-agent-001');
expect(stats.totalExperiences).toBeGreaterThan(0);
});
it('should calculate reward based on execution time', async () => {
const fastTask = { id: 'task-103-fast', type: 'test-generation', context: {} };
const slowTask = { id: 'task-103-slow', type: 'test-generation', context: {} };
const fastResult: TaskResult = {
success: true,
executionTime: 50,
metadata: { strategy: 'fast-path', parallelization: 0.9, retryPolicy: 'none' }
};
const slowResult: TaskResult = {
success: true,
executionTime: 5000,
metadata: { strategy: 'slow-path', parallelization: 0.1, retryPolicy: 'exponential' }
};
await learningEngine.recordExperience(fastTask, fastResult);
await learningEngine.recordExperience(slowTask, slowResult);
await flushPersistence(learningEngine);
// Fast task should generate Q-values
const qValues = testDataStore.getQValues('test-agent-001');
expect(qValues.length).toBeGreaterThan(0);
});
it('should incorporate user feedback into reward calculation', async () => {
const task = { id: 'task-105', type: 'test-generation', context: {} };
const result: TaskResult = {
success: true,
executionTime: 100,
metadata: { strategy: 'feedback-test', parallelization: 0.5, retryPolicy: 'linear' }
};
const positiveFeedback: LearningFeedback = {
rating: 0.95,
issues: [],
suggestions: ['Great coverage!']
};
await learningEngine.recordExperience(task, result, positiveFeedback);
await flushPersistence(learningEngine);
const stats = testDataStore.getLearningStatistics('test-agent-001');
expect(stats.totalExperiences).toBeGreaterThan(0);
});
it('should decay exploration rate after each experience', async () => {
const initialExploration = learningEngine.getExplorationRate();
for (let i = 0; i < 10; i++) {
await learningEngine.recordExperience(
{ id: `task-${i}`, type: 'test', context: {} },
{ success: true, executionTime: 100, metadata: { strategy: 'test', parallelization: 0.5, retryPolicy: 'none' } }
);
}
const finalExploration = learningEngine.getExplorationRate();
expect(finalExploration).toBeLessThan(initialExploration);
expect(finalExploration).toBeGreaterThanOrEqual(0.01); // Min exploration rate
});
});
// ===========================================================================
// 3. PATTERN DISCOVERY ALGORITHM
// ===========================================================================
describe('Pattern Discovery Algorithm', () => {
it('should discover successful patterns over time', async () => {
// Record diverse experiences with the same strategy to build pattern
for (let i = 0; i < 15; i++) {
const result: TaskResult = {
success: true,
executionTime: 100,
coverage: 0.9,
metadata: {
strategy: 'property-based',
toolsUsed: ['fast-check'],
parallelization: 0.8,
retryPolicy: 'exponential'
}
};
// Extract strategy from metadata to set in result (this is what LearningEngine uses)
(result as any).strategy = result.metadata.strategy;
(result as any).parallelization = result.metadata.parallelization;
(result as any).retryPolicy = result.metadata.retryPolicy;
await learningEngine.recordExperience(
{ id: `task-${i}`, type: 'test-generation', context: { framework: 'jest' } },
result
);
}
const patterns = learningEngine.getPatterns();
expect(patterns.length).toBeGreaterThan(0);
// Pattern key is: taskType:strategy = "test-generation:property-based"
const propertyPattern = patterns.find(p => p.pattern.includes('property-based'));
expect(propertyPattern).toBeDefined();
expect(propertyPattern!.confidence).toBeGreaterThan(0.5);
expect(propertyPattern!.usageCount).toBe(15);
});
it('should track pattern success rates accurately', async () => {
const strategy = 'boundary-testing';
// Record 8 successes
for (let i = 0; i < 8; i++) {
const result: TaskResult = {
success: true,
executionTime: 100,
metadata: { strategy, parallelization: 0.5, retryPolicy: 'linear' }
};
// Set strategy on result for pattern matching
(result as any).strategy = strategy;
(result as any).parallelization = 0.5;
(result as any).retryPolicy = 'linear';
await learningEngine.recordExperience(
{ id: `success-${i}`, type: 'test-generation', context: {} },
result
);
}
// Record 2 failures
for (let i = 0; i < 2; i++) {
const result: TaskResult = {
success: false,
executionTime: 100,
metadata: { strategy, parallelization: 0.5, retryPolicy: 'linear' }
};
(result as any).strategy = strategy;
(result as any).parallelization = 0.5;
(result as any).retryPolicy = 'linear';
await learningEngine.recordExperience(
{ id: `failure-${i}`, type: 'test-generation', context: {} },
result
);
}
const patterns = learningEngine.getPatterns();
const boundaryPattern = patterns.find(p => p.pattern.includes(strategy));
expect(boundaryPattern).toBeDefined();
expect(boundaryPattern!.successRate).toBeCloseTo(0.8, 1); // 80% success rate
});
it('should recommend best strategy based on learned patterns', async () => {
// Train with successful pattern
for (let i = 0; i < 20; i++) {
const result: TaskResult = {
success: true,
executionTime: 80,
coverage: 0.95,
metadata: {
strategy: 'optimal-strategy',
parallelization: 0.8
}
};
// LearningEngine extracts strategy from result object (not metadata)
(result as any).strategy = result.metadata.strategy;
(result as any).parallelization = result.metadata.parallelization;
await learningEngine.recordExperience(
{ id: `task-${i}`, type: 'test-generation', context: { complexity: 'medium' } },
result
);
}
// Verify pattern was learned (this is the key learning outcome)
const patterns = learningEngine.getPatterns();
const optimalPattern = patterns.find(p => p.pattern.includes('optimal-strategy'));
expect(optimalPattern).toBeDefined();
expect(optimalPattern!.usageCount).toBe(20);
expect(optimalPattern!.successRate).toBeGreaterThan(0.9); // Should be very high success rate
// Strategy recommendation depends on Q-table state encoding
// At minimum, it should return a valid recommendation
const state: TaskState = {
taskComplexity: 0.5,
requiredCapabilities: ['test-generation'],
contextFeatures: { complexity: 'medium' },
previousAttempts: 0,
availableResources: 0.8
};
const recommendation = await learningEngine.recommendStrategy(state);
expect(recommendation).toBeDefined();
expect(recommendation.confidence).toBeGreaterThanOrEqual(0);
});
it('should return default strategy when no patterns learned', async () => {
const freshEngine = new LearningEngine('fresh-agent-002', memoryStore, {}, database as any);
await freshEngine.initialize();
const state: TaskState = {
taskComplexity: 0.5,
requiredCapabilities: ['test-generation'],
contextFeatures: {},
previousAttempts: 0,
availableResources: 0.8
};
const recommendation = await freshEngine.recommendStrategy(state);
expect(recommendation.strategy).toBe('default');
expect(recommendation.confidence).toBe(0.5);
expect(recommendation.reasoning).toContain('No learned strategies available');
freshEngine.dispose(); // Clean up to prevent open handles
});
it('should detect and store failure patterns', async () => {
for (let i = 0; i < 10; i++) {
await learningEngine.recordExperience(
{ id: `fail-task-${i}`, type: 'flaky-test-detection', context: {} },
{ success: false, executionTime: 100, metadata: { strategy: 'failing-strategy', parallelization: 0.5, retryPolicy: 'none' } }
);
}
const failurePatterns = learningEngine.getFailurePatterns();
expect(failurePatterns.length).toBeGreaterThan(0);
const flakyPattern = failurePatterns.find(p => p.pattern.includes('flaky-test-detection'));
expect(flakyPattern).toBeDefined();
expect(flakyPattern!.frequency).toBeGreaterThan(5);
});
});
// ===========================================================================
// 4. CROSS-SESSION RESTORATION
// ===========================================================================
describe('Cross-Session Restoration', () => {
it('should save learning state periodically', async () => {
// Record 50+ experiences to trigger save (every 50)
for (let i = 0; i < 55; i++) {
await learningEngine.recordExperience(
{ id: `task-${i}`, type: 'test-generation', context: {} },
{ success: true, executionTime: 100, metadata: { strategy: 'test', parallelization: 0.5, retryPolicy: 'none' } }
);
}
// Verify state was saved to memory store
const savedState = await memoryStore.retrieve(
'phase2/learning/test-agent-001/state',
{ partition: 'learning' }
);
expect(savedState).toBeDefined();
expect(savedState.agentId).toBe('test-agent-001');
expect(savedState.experiences.length).toBeGreaterThan(0);
expect(savedState.qTable).toBeDefined();
});
it('should restore learning state on initialization', async () => {
// Record 50+ experiences to trigger saveState() (happens every 50)
for (let i = 0; i < 52; i++) {
const result: TaskResult = {
success: true,
executionTime: 100,
coverage: 0.9,
metadata: { strategy: 'optimal', parallelization: 0.7, retryPolicy: 'exponential' }
};
(result as any).strategy = 'optimal';
(result as any).parallelization = 0.7;
(result as any).retryPolicy = 'exponential';
await learningEngine.recordExperience(
{ id: `task-${i}`, type: 'test-generation', context: {} },
result
);
}
const experienceCount = learningEngine.getTotalExperiences();
expect(experienceCount).toBe(52);
// Create new engine instance (simulates restart)
const restoredEngine = new LearningEngine('test-agent-001', memoryStore, {}, database as any);
await restoredEngine.initialize();
// Verify state was restored (should have loaded from memory store)
const restoredCount = restoredEngine.getTotalExperiences();
expect(restoredCount).toBeGreaterThan(0);
restoredEngine.dispose(); // Clean up to prevent open handles
});
it('should handle missing state gracefully on first run', async () => {
const newEngine = new LearningEngine('brand-new-agent', memoryStore, {}, database as any);
await expect(newEngine.initialize()).resolves.not.toThrow();
expect(newEngine.getTotalExperiences()).toBe(0);
expect(newEngine.getPatterns().length).toBe(0);
newEngine.dispose(); // Clean up to prevent open handles
});
it('should store learning snapshots periodically', async () => {
// Record experiences to trigger batch update (every 5 per config)
for (let i = 0; i < 10; i++) {
await learningEngine.recordExperience(
{ id: `task-${i}`, type: 'test-generation', context: {} },
{ success: i % 2 === 0, executionTime: 100, metadata: { strategy: 'test', parallelization: 0.5, retryPolicy: 'none' } }
);
}
await flushPersistence(learningEngine);
// Verify snapshot was stored
expect(testDataStore.snapshots.length).toBeGreaterThan(0);
});
});
// ===========================================================================
// 5. INTEGRATION TESTS
// ===========================================================================
describe('Integration Tests', () => {
it('should demonstrate complete learning cycle', async () => {
expect(learningEngine.getTotalExperiences()).toBe(0);
// Create diverse experiences
for (let i = 0; i < 30; i++) {
const isSuccess = i % 3 !== 0; // 67% success rate
const strategy = isSuccess ? 'good-strategy' : 'bad-strategy';
const result: TaskResult = {
success: isSuccess,
executionTime: isSuccess ? 100 : 200,
coverage: isSuccess ? 0.9 : 0.5,
metadata: {
strategy,
parallelization: 0.8
}
};
// LearningEngine extracts strategy from result object (not metadata)
(result as any).strategy = result.metadata.strategy;
(result as any).parallelization = result.metadata.parallelization;
await learningEngine.recordExperience(
{ id: `task-${i}`, type: 'test-generation', context: { complexity: 'medium' } },
result
);
}
expect(learningEngine.getTotalExperiences()).toBe(30);
await flushPersistence(learningEngine);
// Verify patterns were discovered (key learning outcome)
const patterns = learningEngine.getPatterns();
expect(patterns.length).toBeGreaterThan(0);
// Should have both good and bad strategy patterns
const goodPattern = patterns.find(p => p.pattern.includes('good-strategy'));
expect(goodPattern).toBeDefined();
expect(goodPattern!.usageCount).toBe(20); // 67% of 30
expect(goodPattern!.successRate).toBeGreaterThan(0.9); // Good strategy succeeds
const badPattern = patterns.find(p => p.pattern.includes('bad-strategy'));
expect(badPattern).toBeDefined();
expect(badPattern!.usageCount).toBe(10); // 33% of 30
expect(badPattern!.successRate).toBeLessThan(0.2); // Bad strategy fails
// Verify Q-values were persisted
const qValues = testDataStore.getQValues('test-agent-001');
expect(qValues.length).toBeGreaterThan(0);
// Verify experience statistics
const stats = testDataStore.getLearningStatistics('test-agent-001');
expect(stats.totalExperiences).toBe(30);
});
});
// ===========================================================================
// 6. EDGE CASES AND ERROR HANDLING
// ===========================================================================
describe('Edge Cases and Error Handling', () => {
it('should handle disabled learning gracefully', async () => {
learningEngine.setEnabled(false);
await learningEngine.recordExperience(
{ id: 'task-disabled', type: 'test', context: {} },
{ success: true, executionTime: 100, metadata: { strategy: 'test', parallelization: 0.5, retryPolicy: 'none' } }
);
expect(learningEngine.getTotalExperiences()).toBe(0);
});
it('should handle null/undefined task properties', async () => {
await expect(
learningEngine.recordExperience(
{ id: undefined as any, type: 'test', context: {} },
{ success: true, executionTime: 100, metadata: { strategy: 'test', parallelization: 0.5, retryPolicy: 'none' } }
)
).resolves.not.toThrow();
});
it('should handle concurrent experience recording', async () => {
const promises = [];
for (let i = 0; i < 20; i++) {
promises.push(
learningEngine.recordExperience(
{ id: `concurrent-${i}`, type: 'test', context: {} },
{ success: true, executionTime: 100, metadata: { strategy: 'concurrent', parallelization: 0.6, retryPolicy: 'linear' } }
)
);
}
await expect(Promise.all(promises)).resolves.not.toThrow();
expect(learningEngine.getTotalExperiences()).toBe(20);
});
});
});