mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
c6b3142f09
Mirrors the qe-reasoning-bank split: the original 622-line file ran
~43 tests in a single fork, most of which spun up a full
AQELearningEngine (transformer + HNSW + WASM coherence) via
beforeEach. Distributing across three sequential forks (pool=forks,
maxForks=1, fileParallelism=false) caps per-fork heap.
- _aqe-engine-test-helpers.ts (NEW, not a test file)
Shared createMockMemoryBackend() and createMockEventBus() factories
pulled out of the original so the splits don't duplicate. Underscore
prefix keeps it out of vitest's test-file include glob.
- aqe-learning-engine-factory.test.ts (NEW, 4 tests, no init)
Factory Functions + DEFAULT_ENGINE_CONFIG. Tests only call the
constructor — no engine.initialize() — so transformer / HNSW /
WASM aren't loaded. Safe to include in test:unit:fast.
- aqe-learning-engine-patterns.test.ts (NEW, 15 tests, full engine)
The three heaviest sub-describes: Pattern Learning, Task Tracking,
Experience Capture Integration. Each test runs a full engine init.
Excluded from test:unit:fast until step 3's dispose hooks are
proven in CI.
- aqe-learning-engine.test.ts (SLIMMED, ~21 tests, full engine)
Init / Status / Task Routing / Model Routing / Codebase Analysis /
Guidance / Dispose. Header comment updated to reference the two
companion files. Inline mock factories replaced with imports from
the shared helper.
Coverage preserved: ~43 tests still run under test:ci, across three
forks instead of one.
Issue: #448
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
327 lines
11 KiB
TypeScript
327 lines
11 KiB
TypeScript
/**
|
|
* Unit Tests for AQE Learning Engine (core suite)
|
|
*
|
|
* Unified learning engine with graceful degradation. Covers initialization,
|
|
* status, routing (task + model), codebase analysis, guidance, and dispose.
|
|
* The heaviest sub-suites live in companion files (issue #448, step 2):
|
|
*
|
|
* - aqe-learning-engine-patterns.test.ts — Pattern Learning + Task
|
|
* Tracking + Experience Capture Integration.
|
|
* - aqe-learning-engine-factory.test.ts — Factory functions +
|
|
* DEFAULT_ENGINE_CONFIG (no `engine.initialize()` overhead).
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
|
|
import { setRuVectorFeatureFlags, resetRuVectorFeatureFlags } from '../../../src/integrations/ruvector/feature-flags.js';
|
|
import { clearEmbeddingCache, resetInitialization } from '../../../src/learning/real-embeddings';
|
|
import { _resetWitnessChainForTests } from '../../../src/audit/witness-chain';
|
|
|
|
// Ensure these tests exercise the in-memory PatternStore, not the RVF variant
|
|
beforeEach(() => { setRuVectorFeatureFlags({ useRVFPatternStore: false }); });
|
|
afterEach(() => { resetRuVectorFeatureFlags(); });
|
|
|
|
// Issue #448 step 3: release module-level singletons between tests so per-test
|
|
// memory growth doesn't compound across the file.
|
|
afterEach(() => {
|
|
clearEmbeddingCache();
|
|
_resetWitnessChainForTests();
|
|
});
|
|
afterAll(() => {
|
|
resetInitialization();
|
|
});
|
|
|
|
import {
|
|
AQELearningEngine,
|
|
createAQELearningEngine,
|
|
createDefaultLearningEngine,
|
|
DEFAULT_ENGINE_CONFIG,
|
|
type AQELearningEngineConfig,
|
|
} from '../../../src/learning/aqe-learning-engine.js';
|
|
import type { MemoryBackend, EventBus } from '../../../src/kernel/interfaces.js';
|
|
import { createMockMemoryBackend, createMockEventBus } from './_aqe-engine-test-helpers';
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('AQELearningEngine', () => {
|
|
let memory: MemoryBackend;
|
|
let eventBus: EventBus;
|
|
let engine: AQELearningEngine;
|
|
|
|
beforeEach(async () => {
|
|
memory = createMockMemoryBackend();
|
|
eventBus = createMockEventBus();
|
|
engine = createAQELearningEngine(
|
|
memory,
|
|
{ projectRoot: '/test/project', enableClaudeFlow: false },
|
|
eventBus
|
|
);
|
|
await engine.initialize();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.clearAllMocks();
|
|
await engine.dispose();
|
|
});
|
|
|
|
describe('Initialization', () => {
|
|
it('should initialize with default config', async () => {
|
|
const newEngine = createDefaultLearningEngine(memory, '/test/project');
|
|
await newEngine.initialize();
|
|
|
|
const status = newEngine.getStatus();
|
|
expect(status.initialized).toBe(true);
|
|
expect(status.features.patternLearning).toBe(true);
|
|
expect(status.features.vectorSearch).toBe(true);
|
|
|
|
await newEngine.dispose();
|
|
});
|
|
|
|
it('should initialize with custom config', async () => {
|
|
const customConfig: AQELearningEngineConfig = {
|
|
projectRoot: '/custom/project',
|
|
enableClaudeFlow: false,
|
|
enableExperienceCapture: true,
|
|
enablePatternPromotion: true,
|
|
promotionThreshold: 5,
|
|
};
|
|
|
|
const customEngine = createAQELearningEngine(memory, customConfig);
|
|
await customEngine.initialize();
|
|
|
|
const status = customEngine.getStatus();
|
|
expect(status.initialized).toBe(true);
|
|
|
|
await customEngine.dispose();
|
|
});
|
|
|
|
it('should skip re-initialization', async () => {
|
|
const status1 = engine.getStatus();
|
|
expect(status1.initialized).toBe(true);
|
|
|
|
// Initialize again
|
|
await engine.initialize();
|
|
|
|
const status2 = engine.getStatus();
|
|
expect(status2.initialized).toBe(true);
|
|
});
|
|
|
|
it('should handle Claude Flow unavailability gracefully', async () => {
|
|
const cfEngine = createAQELearningEngine(memory, {
|
|
projectRoot: '/test/project',
|
|
enableClaudeFlow: true, // Try to enable, but should fail gracefully
|
|
});
|
|
await cfEngine.initialize();
|
|
|
|
const status = cfEngine.getStatus();
|
|
// Should be initialized even if CF is not available
|
|
expect(status.initialized).toBe(true);
|
|
// Core features should still work
|
|
expect(status.features.patternLearning).toBe(true);
|
|
expect(status.features.taskRouting).toBe(true);
|
|
|
|
await cfEngine.dispose();
|
|
});
|
|
});
|
|
|
|
describe('Status and Statistics', () => {
|
|
it('should return correct status', () => {
|
|
const status = engine.getStatus();
|
|
|
|
expect(status.initialized).toBe(true);
|
|
expect(status.claudeFlowAvailable).toBe(false);
|
|
expect(status.features.patternLearning).toBe(true);
|
|
expect(status.features.vectorSearch).toBe(true);
|
|
expect(status.features.taskRouting).toBe(true);
|
|
// CF features should be false when CF not available
|
|
expect(status.features.trajectories).toBe(false);
|
|
expect(status.features.modelRouting).toBe(false);
|
|
});
|
|
|
|
it('should return statistics', async () => {
|
|
const stats = await engine.getStats();
|
|
|
|
expect(stats.activeTasks).toBe(0);
|
|
expect(stats.completedTasks).toBe(0);
|
|
expect(stats.claudeFlowErrors).toBe(0);
|
|
});
|
|
|
|
it('should throw when getting stats before initialization', async () => {
|
|
const uninitEngine = createAQELearningEngine(memory, {
|
|
projectRoot: '/test',
|
|
});
|
|
|
|
await expect(uninitEngine.getStats()).rejects.toThrow('not initialized');
|
|
});
|
|
});
|
|
|
|
describe('Task Routing', () => {
|
|
it('should route task to optimal agent', async () => {
|
|
const result = await engine.routeTask({
|
|
task: 'Generate unit tests for UserService',
|
|
taskType: 'test-generation',
|
|
context: { framework: 'vitest', language: 'typescript' },
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.value?.recommendedAgent).toBeDefined();
|
|
expect(result.value?.confidence).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should provide convenience route method', async () => {
|
|
const result = await engine.route('Analyze coverage for auth module', {
|
|
framework: 'vitest',
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.recommendedAgent).toBeDefined();
|
|
});
|
|
|
|
it('should return null for failed routing', async () => {
|
|
// Create engine without proper initialization
|
|
const failEngine = createAQELearningEngine(memory, {
|
|
projectRoot: '/test',
|
|
});
|
|
|
|
const result = await failEngine.route('Test task');
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Model Routing', () => {
|
|
it('should recommend haiku for simple tasks', async () => {
|
|
const result = await engine.recommendModel('Fix typo in README');
|
|
|
|
expect(result.model).toBe('haiku');
|
|
expect(result.confidence).toBeGreaterThan(0);
|
|
expect(result.reasoning).toContain('speed');
|
|
});
|
|
|
|
it('should recommend opus for complex tasks', async () => {
|
|
const result = await engine.recommendModel(
|
|
'Design distributed architecture with security considerations for multi-tenant system'
|
|
);
|
|
|
|
expect(result.model).toBe('opus');
|
|
expect(result.reasoning).toContain('capability');
|
|
});
|
|
|
|
it('should recommend sonnet for medium tasks', async () => {
|
|
// Use a longer task description that doesn't match low/high complexity patterns
|
|
const result = await engine.recommendModel(
|
|
'Implement user profile update functionality with validation logic'
|
|
);
|
|
|
|
expect(result.model).toBe('sonnet');
|
|
expect(result.reasoning).toContain('balance');
|
|
});
|
|
|
|
it('should use task length heuristic', async () => {
|
|
// Very short task
|
|
const shortResult = await engine.recommendModel('Fix bug');
|
|
expect(shortResult.model).toBe('haiku');
|
|
|
|
// Very long task (simulate with repeated text)
|
|
const longTask = 'A '.repeat(300) + 'complex task with many requirements';
|
|
const longResult = await engine.recommendModel(longTask);
|
|
expect(longResult.model).toBe('opus');
|
|
});
|
|
|
|
it('should record model outcome', async () => {
|
|
// Should not throw
|
|
await engine.recordModelOutcome(
|
|
'Test task',
|
|
'sonnet',
|
|
'success'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Codebase Analysis', () => {
|
|
it('should analyze codebase (local fallback)', async () => {
|
|
const result = await engine.analyzeCodebase('/test/project', 'shallow');
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.repositoryPath).toBe('/test/project');
|
|
expect(result.depth).toBe('shallow');
|
|
});
|
|
|
|
it('should generate agent configs', async () => {
|
|
const configs = await engine.generateAgentConfigs('yaml');
|
|
|
|
expect(configs).toBeDefined();
|
|
expect(Array.isArray(configs)).toBe(true);
|
|
expect(configs.length).toBeGreaterThan(0);
|
|
|
|
// Should include default QE agents
|
|
expect(configs.some((c: any) => c.name === 'qe-test-architect')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Guidance', () => {
|
|
it('should get guidance for domain', () => {
|
|
const guidance = engine.getGuidance('test-generation');
|
|
|
|
expect(guidance).toBeDefined();
|
|
expect(guidance.bestPractices.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should generate context for domain', () => {
|
|
const context = engine.generateContext('test-generation', {
|
|
framework: 'vitest',
|
|
language: 'typescript',
|
|
});
|
|
|
|
expect(context).toContain('QE Guidance');
|
|
expect(context).toContain('vitest');
|
|
});
|
|
|
|
it('should check anti-patterns', () => {
|
|
// God Test detection requires 5 consecutive expects (pattern: expect.*expect.*expect.*expect.*expect)
|
|
const testCode = `
|
|
it('should do everything', () => {
|
|
expect(a).toBe(1);expect(b).toBe(2);expect(c).toBe(3);expect(d).toBe(4);expect(e).toBe(5);
|
|
});
|
|
`;
|
|
|
|
const detected = engine.checkAntiPatterns('test-generation', testCode);
|
|
|
|
expect(detected.length).toBeGreaterThan(0);
|
|
expect(detected.some((ap) => ap.name === 'God Test')).toBe(true);
|
|
});
|
|
|
|
it('should throw when getting guidance before initialization', () => {
|
|
const uninitEngine = createAQELearningEngine(memory, {
|
|
projectRoot: '/test',
|
|
});
|
|
|
|
expect(() => uninitEngine.getGuidance('test-generation')).toThrow(
|
|
'not initialized'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Dispose', () => {
|
|
it('should dispose all components', async () => {
|
|
const taskId = await engine.startTask('Pre-dispose task');
|
|
|
|
await engine.dispose();
|
|
|
|
// After dispose, status should show not initialized
|
|
const status = engine.getStatus();
|
|
expect(status.initialized).toBe(false);
|
|
|
|
// Active tasks should be cleared
|
|
const task = engine.getTask(taskId);
|
|
expect(task).toBeUndefined();
|
|
});
|
|
|
|
it('should be safe to dispose multiple times', async () => {
|
|
await engine.dispose();
|
|
await engine.dispose(); // Should not throw
|
|
});
|
|
});
|
|
});
|