mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
a6202c39a3
* feat: implement ADR-062 (StrongDM all 3 tiers) and ADR-087 (RuVector 14/14 capabilities)
ADR-062 StrongDM Software Factory — all 6 actions across 3 tiers:
- Tier 1: Loop detection wired to MCP protocol-server (blocks on strike 3+),
token dashboard (pre-existing)
- Tier 2: Holdout testing (10% FNV-1a selection, filtered in test-execution),
gate ratcheting (monotonic 5-pass/2%/95% cap, persisted to MemoryBackend)
- Tier 3: Progressive context (TF-IDF prediction, reduces availableFiles on spawn),
meta-learning cycle (4 insight types, stored + published as domain events)
ADR-087 RuVector Advanced Capabilities — milestones 1-5:
- EWC++ recordOutcome + backgroundConsolidate wired to 3 domain coordinators
- R1: HDC fingerprinting (10K-bit hypervectors, wired to PatternStore.store())
- R3: Delta event sourcing (sparse encoding, wired to PatternStore.store())
- R13: Cognitive routing (predictive compression, wired to mailbox.send/receive)
- R14: Hyperbolic HNSW (Poincare ball, auto-indexed in PatternStore.store())
Also updates ADR statuses: 059, 060, 061 → Implemented (were already done),
062 → Implemented, 086 → In Progress, 087 → Implemented.
223 new tests across 11 test files, all passing. Zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: implement RVF cluster (ADR-065/066/067/071) with native benchmarks
Implements the RVF persistence cluster for persistent HNSW vector storage,
agent memory branching, and HNSW unification foundation.
Key changes:
- RvfPatternStore: IPatternStore backed by @ruvector/rvf-node native HNSW
(1.2ms cold-start, 0.19ms search p50, verified with real native binary)
- AgentMemoryBranch: per-agent COW isolation via derive(), ingest-log merge,
wired into Kernel.initialize() + DefaultAgentCoordinator
- HnswLegacyBridge: adapts old IHNSWIndex to unified IHnswIndexProvider,
wired into PatternStore when useUnifiedHnsw flag enabled
- Factory routing: createPatternStore() returns RvfPatternStore when flag
enabled + native available + data directory exists
- Dream engine: accepts external RVF adapter (no duplicate file handles),
RVCOWBranchManager enhanced with recall validation check
- @ruvector/gnn bumped 0.1.19→0.1.25, 3 new feature flags added
- IPatternStore interface extracted for polymorphic pattern store usage
- 50 new tests (16 RvfPatternStore + 17 AgentMemoryBranch + 12 bridge + 5 benchmarks)
- 1188 tests pass, 0 regressions
ADR status: 066 Implemented, 067 Implemented, 065 Implemented,
069/071 In Progress (honest assessment after brutal-honesty review)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(init): resolve skills/agents paths from package root, not relative to bundle depth
esbuild code-splitting puts installer code into dist/cli/chunks/ (3 levels
deep) instead of dist/init/ (2 levels deep), so the old ../../ traversals
landed at dist/ instead of the package root — causing "Source directory not
found" errors for all global installs.
Replace brittle relative-path resolution in all 4 installers with a shared
findPackageRoot() that walks up to the nearest package.json with
name=agentic-qe. Prioritise assets/ paths (canonical npm source) over
.claude/ paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: complete RVF cluster Phases 2-3 (ADR-067/069/071/072) with devil's advocate fixes
Phase 2A (ADR-067): Agent memory branching enabled — useAgentMemoryBranching=true,
BRANCH_DISCARD witness action, recordIngest callback exposed to agents via config.
Phase 2B (ADR-069): Dream RVF COW — useRvfFork driven by isRVFPatternStoreEnabled(),
child adapter stored + temp files cleaned up on merge/discard, 13 tests.
Phase 2C (ADR-071): HNSW unification — useUnifiedHnsw=true, HNSWEmbeddingIndex routes
through HnswAdapter, UnifiedMemory vectorIndex uses UnifiedHnswIndex, HnswLegacyBridge
delegates to adapter string-ID APIs (no duplicate ID maps), addEmbeddingsBatch routed
correctly, saveIndex/loadIndex safe under unified mode.
Phase 3 (ADR-072): RVF primary migration scaffolding — RvfMigrationAdapter (5-stage
routing with write latency tracking), RvfConsistencyValidator (rolling divergence +
compaction), RvfStageGate (go/no-go with witness chain), rvfMigrationStage=1 default,
wired into shared-rvf-dual-writer at stage >= 2, 32 tests.
Cross-cutting fixes from devil's advocate review:
- Shared RVF adapter singleton eliminates dual file handles (kernel + dream engine)
- RvfPatternStore reports honest nativeAvailable=false + rvfInitError on failure
- ESM-safe dynamic import() replaces require() in dream-engine and branch-manager
- quality_assess defaults to src/ instead of cwd to prevent full-project scan hang
- discoverSourceFiles skips .agentic-qe/.claude/.cache dirs, adds maxFiles=5000 cap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: complete RVF cluster plan — Phase 2C decommission, Phase 3 integration, fix 7 pre-existing test failures
Phase 2C (ADR-071) — HNSW decommission:
- Remove RuvectorFlatIndex fallback from unified-memory.ts; always use UnifiedHnswIndex
- Hardcode isUnifiedHnswActive()=true in HNSWEmbeddingIndex (hnswlib-node legacy path is dead code)
- Create HnswShadowValidator for brute-force vs HnswAdapter divergence validation
- Benchmark alias (rvf-pattern-store.bench.ts) for plan compliance
Phase 3 (ADR-072) — RVF Primary Migration integration:
- RvfMigrationCoordinator: wires adapter + validator + gate with real SQLite/RVF handles
- MCP tools: migration_status, migration_check, migration_promote (persistence category)
- Kernel boot wiring: coordinator initializes when rvfMigrationStage >= 2
- RvfMigrationAdapter.setStage(): propagate promotions to routing logic
- Promote default rvfMigrationStage from 1 (hybrid) to 2 (dual-write-sqlite-primary)
- Integration test: 13 tests covering full lifecycle (stage 1→2→3→4), write/read routing,
consistency checks, gate enforcement, fallback, metrics
Test fixes (7 pre-existing failures → 1 remaining):
- code-intelligence-metrics-wiring: spy console.info not console.log (logger routes to console.info)
- mcp-integration: disable ADR-062 loop detection (repeated identical calls are by design)
- dream-cycle-tool (2 tests): RvfPatternStore empty-query branch returns all patterns from SQLite
- coherence-integration (4 tests): RvfPatternStore vector search fallback computes brute-force
cosine over SQLite embeddings when RVF adapter unavailable; getStats() falls back to SQLite count;
auto-attach SQLitePatternStore in initialize() when not explicitly wired
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore(release): bump version to v3.9.1
- Version bump in package.json and skills-manifest.json
- Fix "Dynamic require of X is not supported" in ESM bundles by adding
createRequire banner to both CLI and MCP build scripts — every chunk
now has a working require() for Node.js builtins
- Add CHANGELOG.md entry and release notes for v3.9.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): regenerate package-lock.json for npm ci compatibility
The lock file was out of sync with package.json — new transitive
dependencies added on main were missing. All 15 CI checks failed
at `npm ci` with "Missing from lock file" errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): pin Node to 24.13.0 and restore working lock file
Node 24.14.1 ships npm 11.11.0 which enforces stricter lock file
validation than 11.6.x (what 24.13.0 ships). The existing lock file
passes under 11.6.x but fails under 11.11.0 with "Missing from lock
file" for transitive dependencies of private-registry packages.
Pin all 5 workflow files to node-version: '24.13.0' and restore the
lock file from commit f9b76968 (last CI-passing state) with only the
version bump to 3.9.1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): regenerate package-lock.json with npm 10 for stable npm ci
npm 11 (Node 24) produces internally inconsistent lock files that npm ci
rejects — each npm install resolves transitive deps differently. Generated
with npm 10 (Node 22 LTS) which produces stable lock files compatible with
both npm 10 and npm 11.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
611 lines
20 KiB
TypeScript
611 lines
20 KiB
TypeScript
/**
|
|
* Unit Tests for AQE Learning Engine
|
|
* Unified learning engine with graceful degradation
|
|
*
|
|
* Tests pattern storage, task routing, model recommendations,
|
|
* and Claude Flow integration with fallbacks.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { setRuVectorFeatureFlags, resetRuVectorFeatureFlags } from '../../../src/integrations/ruvector/feature-flags.js';
|
|
|
|
// Ensure these tests exercise the in-memory PatternStore, not the RVF variant
|
|
beforeEach(() => { setRuVectorFeatureFlags({ useRVFPatternStore: false }); });
|
|
afterEach(() => { resetRuVectorFeatureFlags(); });
|
|
|
|
import {
|
|
AQELearningEngine,
|
|
createAQELearningEngine,
|
|
createDefaultLearningEngine,
|
|
DEFAULT_ENGINE_CONFIG,
|
|
type AQELearningEngineConfig,
|
|
type AQELearningEngineStatus,
|
|
} from '../../../src/learning/aqe-learning-engine.js';
|
|
import type { MemoryBackend, EventBus } from '../../../src/kernel/interfaces.js';
|
|
import type { QEDomain } from '../../../src/learning/qe-patterns.js';
|
|
import { ok, err } from '../../../src/shared/types/index.js';
|
|
|
|
// ============================================================================
|
|
// Mock Factory
|
|
// ============================================================================
|
|
|
|
function createMockMemoryBackend(): MemoryBackend {
|
|
const storage = new Map<string, unknown>();
|
|
|
|
return {
|
|
get: vi.fn((key: string) => Promise.resolve(storage.get(key) ?? null)),
|
|
set: vi.fn((key: string, value: unknown) => {
|
|
storage.set(key, value);
|
|
return Promise.resolve();
|
|
}),
|
|
delete: vi.fn((key: string) => {
|
|
storage.delete(key);
|
|
return Promise.resolve();
|
|
}),
|
|
has: vi.fn((key: string) => Promise.resolve(storage.has(key))),
|
|
keys: vi.fn(() => Promise.resolve(Array.from(storage.keys()))),
|
|
search: vi.fn((pattern: string) => {
|
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
const matches = Array.from(storage.keys()).filter((k) => regex.test(k));
|
|
return Promise.resolve(matches);
|
|
}),
|
|
clear: vi.fn(() => {
|
|
storage.clear();
|
|
return Promise.resolve();
|
|
}),
|
|
size: vi.fn(() => Promise.resolve(storage.size)),
|
|
close: vi.fn(() => Promise.resolve()),
|
|
getState: vi.fn(() => ({ type: 'memory', ready: true })),
|
|
} as unknown as MemoryBackend;
|
|
}
|
|
|
|
function createMockEventBus(): EventBus {
|
|
return {
|
|
publish: vi.fn(),
|
|
subscribe: vi.fn().mockReturnValue(() => {}),
|
|
unsubscribe: vi.fn(),
|
|
emit: vi.fn(),
|
|
} as unknown as EventBus;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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('Pattern Learning', () => {
|
|
it('should store a pattern', async () => {
|
|
const result = await engine.storePattern({
|
|
patternType: 'test-template',
|
|
name: 'Unit Test Pattern',
|
|
description: 'Standard unit test pattern',
|
|
template: {
|
|
type: 'code',
|
|
content: 'describe("Test", () => { it("should work", () => {}); })',
|
|
variables: [],
|
|
},
|
|
context: {
|
|
tags: ['unit', 'typescript'],
|
|
framework: 'vitest',
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.value?.id).toBeDefined();
|
|
expect(result.value?.patternType).toBe('test-template');
|
|
});
|
|
|
|
it('should search patterns', async () => {
|
|
// Store a pattern first
|
|
await engine.storePattern({
|
|
patternType: 'test-template',
|
|
name: 'UserService Test',
|
|
description: 'Testing user service',
|
|
template: {
|
|
type: 'code',
|
|
content: 'describe("UserService", () => {})',
|
|
variables: [],
|
|
},
|
|
context: { tags: ['user', 'service'] },
|
|
});
|
|
|
|
const results = await engine.searchPatterns('user service');
|
|
|
|
expect(results.success).toBe(true);
|
|
});
|
|
|
|
it('should get pattern by ID', async () => {
|
|
const storeResult = await engine.storePattern({
|
|
patternType: 'mock-pattern',
|
|
name: 'Database Mock',
|
|
description: 'Mock database connection',
|
|
template: {
|
|
type: 'code',
|
|
content: 'vi.mock("./db")',
|
|
variables: [],
|
|
},
|
|
context: { tags: ['mock', 'database'] },
|
|
});
|
|
|
|
const pattern = await engine.getPattern(storeResult.value!.id);
|
|
|
|
expect(pattern).toBeDefined();
|
|
expect(pattern?.name).toBe('Database Mock');
|
|
});
|
|
|
|
it('should return null for non-existent pattern', async () => {
|
|
const pattern = await engine.getPattern('non-existent-id');
|
|
expect(pattern).toBeNull();
|
|
});
|
|
|
|
it('should record pattern outcome', async () => {
|
|
const storeResult = await engine.storePattern({
|
|
patternType: 'test-template',
|
|
name: 'Outcome Test Pattern',
|
|
description: 'Pattern for outcome testing',
|
|
template: { type: 'code', content: 'test', variables: [] },
|
|
context: { tags: ['test'] },
|
|
});
|
|
|
|
const outcomeResult = await engine.recordOutcome({
|
|
patternId: storeResult.value!.id,
|
|
success: true,
|
|
metrics: { testsPassed: 10, testsFailed: 0 },
|
|
});
|
|
|
|
expect(outcomeResult.success).toBe(true);
|
|
});
|
|
|
|
it('should return error when not initialized', async () => {
|
|
const uninitEngine = createAQELearningEngine(memory, {
|
|
projectRoot: '/test',
|
|
});
|
|
|
|
const result = await uninitEngine.storePattern({
|
|
patternType: 'test-template',
|
|
name: 'Test',
|
|
description: 'Test',
|
|
template: { type: 'code', content: '', variables: [] },
|
|
context: { tags: [] },
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error?.message).toContain('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('Task Tracking', () => {
|
|
it('should start and track a task', async () => {
|
|
const taskId = await engine.startTask(
|
|
'Generate tests for PaymentService',
|
|
'qe-test-architect',
|
|
'test-generation'
|
|
);
|
|
|
|
expect(taskId).toBeDefined();
|
|
|
|
const task = engine.getTask(taskId);
|
|
expect(task).toBeDefined();
|
|
expect(task?.task).toBe('Generate tests for PaymentService');
|
|
expect(task?.agent).toBe('qe-test-architect');
|
|
});
|
|
|
|
it('should record steps in task', async () => {
|
|
const taskId = await engine.startTask('Multi-step task');
|
|
|
|
await engine.recordStep(taskId, 'analyze', 'Found 5 files', 0.9);
|
|
await engine.recordStep(taskId, 'generate', 'Created tests', 0.85);
|
|
|
|
const task = engine.getTask(taskId);
|
|
expect(task?.steps).toHaveLength(2);
|
|
expect(task?.steps[0].action).toBe('analyze');
|
|
expect(task?.steps[1].quality).toBe(0.85);
|
|
});
|
|
|
|
it('should end task and return execution', async () => {
|
|
const taskId = await engine.startTask('Completable task');
|
|
|
|
await engine.recordStep(taskId, 'step1');
|
|
const execution = await engine.endTask(taskId, true, 'Great job!');
|
|
|
|
expect(execution).toBeDefined();
|
|
expect(execution?.task).toBe('Completable task');
|
|
expect(execution?.steps).toHaveLength(1);
|
|
|
|
// Task should be removed from active
|
|
const task = engine.getTask(taskId);
|
|
expect(task).toBeUndefined();
|
|
});
|
|
|
|
it('should track completed task count', async () => {
|
|
const taskId1 = await engine.startTask('Task 1');
|
|
const taskId2 = await engine.startTask('Task 2');
|
|
|
|
await engine.endTask(taskId1, true);
|
|
await engine.endTask(taskId2, true);
|
|
|
|
const stats = await engine.getStats();
|
|
expect(stats.completedTasks).toBe(2);
|
|
});
|
|
|
|
it('should handle ending non-existent task', async () => {
|
|
const result = await engine.endTask('non-existent-task', true);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
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('Experience Capture Integration', () => {
|
|
it('should capture experience on task completion', async () => {
|
|
const taskId = await engine.startTask(
|
|
'Experience capture task',
|
|
'qe-test-architect',
|
|
'test-generation'
|
|
);
|
|
|
|
await engine.recordStep(taskId, 'step1', 'Result', 0.9);
|
|
await engine.endTask(taskId, true);
|
|
|
|
// Experience should be captured (check via stats or event emission)
|
|
const stats = await engine.getStats();
|
|
expect(stats.experienceCapture.totalExperiences).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('should allow manual experience capture', () => {
|
|
const id = engine.startExperienceCapture('Manual capture task', {
|
|
agent: 'qe-test-architect',
|
|
domain: 'test-generation',
|
|
model: 'sonnet',
|
|
});
|
|
|
|
expect(id).toBeDefined();
|
|
});
|
|
|
|
it('should return experience capture service', () => {
|
|
const service = engine.getExperienceCaptureService();
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
it('should work without experience capture enabled', async () => {
|
|
const noExpEngine = createAQELearningEngine(memory, {
|
|
projectRoot: '/test',
|
|
enableExperienceCapture: false,
|
|
});
|
|
await noExpEngine.initialize();
|
|
|
|
const taskId = await noExpEngine.startTask('No experience task');
|
|
await noExpEngine.endTask(taskId, true);
|
|
|
|
// Should not throw
|
|
const service = noExpEngine.getExperienceCaptureService();
|
|
expect(service).toBeUndefined();
|
|
|
|
await noExpEngine.dispose();
|
|
});
|
|
});
|
|
|
|
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
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Factory Functions', () => {
|
|
let memory: MemoryBackend;
|
|
|
|
beforeEach(() => {
|
|
memory = createMockMemoryBackend();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should create engine with createAQELearningEngine', () => {
|
|
const engine = createAQELearningEngine(memory, {
|
|
projectRoot: '/test',
|
|
});
|
|
|
|
expect(engine).toBeInstanceOf(AQELearningEngine);
|
|
});
|
|
|
|
it('should create engine with createDefaultLearningEngine', () => {
|
|
const engine = createDefaultLearningEngine(memory, '/test');
|
|
|
|
expect(engine).toBeInstanceOf(AQELearningEngine);
|
|
});
|
|
|
|
it('should accept optional event bus', () => {
|
|
const eventBus = createMockEventBus();
|
|
const engine = createAQELearningEngine(
|
|
memory,
|
|
{ projectRoot: '/test' },
|
|
eventBus
|
|
);
|
|
|
|
expect(engine).toBeInstanceOf(AQELearningEngine);
|
|
});
|
|
});
|
|
|
|
describe('DEFAULT_ENGINE_CONFIG', () => {
|
|
it('should have expected defaults', () => {
|
|
expect(DEFAULT_ENGINE_CONFIG.enableClaudeFlow).toBe(true);
|
|
expect(DEFAULT_ENGINE_CONFIG.enableExperienceCapture).toBe(true);
|
|
expect(DEFAULT_ENGINE_CONFIG.enablePatternPromotion).toBe(true);
|
|
expect(DEFAULT_ENGINE_CONFIG.promotionThreshold).toBe(3);
|
|
});
|
|
});
|