Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

413 lines
13 KiB
TypeScript

/**
* Experience Capture Service Tests
* Phase 4: Self-Learning Features
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
ExperienceCaptureService,
createExperienceCaptureService,
DEFAULT_EXPERIENCE_CONFIG,
type TaskExperience,
type ExperienceCaptureConfig,
} from '../../src/learning/experience-capture.js';
import { type IPatternStore } from '../../src/learning/pattern-store.js';
import { type MemoryBackend, type EventBus, type Result } from '../../src/kernel/interfaces.js';
// Mock implementations
function createMockMemory(): MemoryBackend {
const store = new Map<string, unknown>();
return {
get: vi.fn(async <T>(key: string) => store.get(key) as T | undefined),
set: vi.fn(async (key: string, value: unknown) => {
store.set(key, value);
}),
delete: vi.fn(async (key: string) => {
store.delete(key);
}),
has: vi.fn(async (key: string) => store.has(key)),
keys: vi.fn(async () => Array.from(store.keys())),
search: vi.fn(async () => []),
stats: vi.fn(async () => ({ totalEntries: store.size, namespaces: {} })),
clear: vi.fn(async () => store.clear()),
close: vi.fn(async () => {}),
} as unknown as MemoryBackend;
}
function createMockPatternStore(): IPatternStore {
const patterns = new Map<string, unknown>();
return {
initialize: vi.fn(async () => {}),
store: vi.fn(async (pattern: unknown) => {
const p = pattern as { id: string };
patterns.set(p.id, pattern);
return { success: true, value: p.id } as Result<string>;
}),
create: vi.fn(async () => ({ success: false, error: new Error('Not implemented') } as Result<unknown>)),
get: vi.fn(async () => null),
search: vi.fn(async () => ({ success: true, value: [] } as Result<unknown[]>)),
recordUsage: vi.fn(async () => ({ success: true, value: undefined } as Result<void>)),
promote: vi.fn(async () => ({ success: true, value: undefined } as Result<void>)),
delete: vi.fn(async () => ({ success: true, value: undefined } as Result<void>)),
getStats: vi.fn(async () => ({
totalPatterns: patterns.size,
byDomain: {},
byType: {},
avgConfidence: 0.8,
avgUsageCount: 1,
recentlyUsed: [],
})),
dispose: vi.fn(async () => {}),
} as unknown as IPatternStore;
}
function createMockEventBus(): EventBus {
return {
publish: vi.fn(async () => {}),
subscribe: vi.fn(() => () => {}),
unsubscribe: vi.fn(),
emit: vi.fn(async () => {}),
} as unknown as EventBus;
}
describe('ExperienceCaptureService', () => {
let service: ExperienceCaptureService;
let mockMemory: MemoryBackend;
let mockPatternStore: IPatternStore;
let mockEventBus: EventBus;
beforeEach(async () => {
mockMemory = createMockMemory();
mockPatternStore = createMockPatternStore();
mockEventBus = createMockEventBus();
service = new ExperienceCaptureService(
mockMemory,
mockPatternStore,
mockEventBus,
DEFAULT_EXPERIENCE_CONFIG
);
await service.initialize();
});
afterEach(async () => {
await service.dispose();
});
describe('startCapture', () => {
it('should create a new experience with unique ID', () => {
const experienceId = service.startCapture('Generate unit tests', {
domain: 'test-generation',
agent: 'qe-test-architect',
});
expect(experienceId).toBeTruthy();
expect(typeof experienceId).toBe('string');
expect(experienceId.length).toBeGreaterThan(0);
});
it('should track active experiences', () => {
const id1 = service.startCapture('Task 1', { domain: 'test-generation' });
const id2 = service.startCapture('Task 2', { domain: 'coverage-analysis' });
// Active experiences can be retrieved
expect(service.getActiveExperience(id1)).toBeDefined();
expect(service.getActiveExperience(id2)).toBeDefined();
});
it('should use default domain when not specified', () => {
const id = service.startCapture('Task without domain');
expect(id).toBeTruthy();
});
});
describe('recordStep', () => {
it('should record steps for active experience', () => {
const experienceId = service.startCapture('Test task', { domain: 'test-generation' });
service.recordStep(experienceId, {
action: 'analyze',
result: 'found coverage: 85%',
quality: 0.85,
});
service.recordStep(experienceId, {
action: 'generate',
result: 'generated 5 tests',
quality: 0.9,
});
// Steps recorded internally - verified on completion
expect(true).toBe(true);
});
it('should handle non-existent experience gracefully', () => {
// Should not throw
service.recordStep('non-existent-id', {
action: 'test',
result: 'test result',
});
});
});
describe('completeCapture', () => {
it('should complete experience with success outcome', async () => {
const experienceId = service.startCapture('Test task', {
domain: 'test-generation',
agent: 'qe-test-architect',
});
service.recordStep(experienceId, {
action: 'generate',
result: 'generated 10 tests',
quality: 0.9,
});
const result = await service.completeCapture(experienceId, {
success: true,
quality: 0.95,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.success).toBe(true);
expect(result.value.quality).toBe(0.95);
expect(result.value.completedAt).toBeDefined();
}
});
it('should complete experience with failure outcome', async () => {
const experienceId = service.startCapture('Failing task', {
domain: 'test-execution',
});
const result = await service.completeCapture(experienceId, {
success: false,
quality: 0.2,
feedback: 'Test execution failed',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.success).toBe(false);
expect(result.value.feedback).toBe('Test execution failed');
}
});
it('should persist experience to memory', async () => {
const experienceId = service.startCapture('Persisted task', {
domain: 'coverage-analysis',
});
await service.completeCapture(experienceId, {
success: true,
quality: 0.9,
});
expect(mockMemory.set).toHaveBeenCalled();
});
it('should return error for non-existent experience', async () => {
const result = await service.completeCapture('non-existent', {
success: true,
quality: 1.0,
});
expect(result.success).toBe(false);
});
it('should remove experience from active after completion', async () => {
const id = service.startCapture('Task', { domain: 'test-generation' });
expect(service.getActiveExperience(id)).toBeDefined();
await service.completeCapture(id, { success: true, quality: 0.8 });
expect(service.getActiveExperience(id)).toBeUndefined();
});
});
describe('extractPattern', () => {
it('should try to create pattern from successful experience', async () => {
const now = Date.now();
const experience: TaskExperience = {
id: 'exp-1',
task: 'Generate unit tests for UserService',
domain: 'test-generation',
agent: 'qe-test-architect',
startedAt: now - 60000,
completedAt: now,
durationMs: 60000,
steps: [
{
action: 'analyze',
result: 'found 5 functions',
quality: 0.9,
timestamp: now - 30000,
},
{
action: 'generate',
result: 'generated 5 tests with 95% coverage',
quality: 0.95,
timestamp: now,
},
],
success: true,
quality: 0.95,
};
const result = await service.extractPattern(experience);
// Result structure has newPattern, reinforced, promoted
expect(result).toHaveProperty('newPattern');
expect(result).toHaveProperty('reinforced');
expect(result).toHaveProperty('promoted');
});
it('should return default result when no pattern store', async () => {
// Service without pattern store
const serviceWithoutStore = new ExperienceCaptureService(
mockMemory,
undefined as unknown as IPatternStore,
mockEventBus,
DEFAULT_EXPERIENCE_CONFIG
);
await serviceWithoutStore.initialize();
const now = Date.now();
const experience: TaskExperience = {
id: 'exp-2',
task: 'Task without store',
domain: 'test-generation',
startedAt: now,
completedAt: now,
durationMs: 0,
steps: [],
success: true,
quality: 0.9,
};
const result = await serviceWithoutStore.extractPattern(experience);
expect(result.newPattern).toBe(false);
expect(result.reinforced).toBe(false);
expect(result.promoted).toBe(false);
await serviceWithoutStore.dispose();
});
it('should search for similar patterns', async () => {
const now = Date.now();
const experience: TaskExperience = {
id: 'exp-3',
task: 'Generate tests',
domain: 'test-generation',
startedAt: now,
completedAt: now,
durationMs: 0,
steps: [],
success: true,
quality: 0.8,
};
await service.extractPattern(experience);
// Should call search on pattern store
expect(mockPatternStore.search).toHaveBeenCalled();
});
});
describe('getExperience', () => {
it('should retrieve persisted experience', async () => {
const experienceId = service.startCapture('Retrievable task', {
domain: 'test-generation',
});
await service.completeCapture(experienceId, {
success: true,
quality: 0.85,
});
// Mock the memory.get to return the experience
const now = Date.now();
const mockGet = mockMemory.get as ReturnType<typeof vi.fn>;
mockGet.mockResolvedValueOnce({
id: experienceId,
task: 'Retrievable task',
domain: 'test-generation',
startedAt: now,
completedAt: now,
durationMs: 0,
steps: [],
success: true,
quality: 0.85,
});
const experience = await service.getExperience(experienceId);
expect(experience).not.toBeNull();
});
it('should return null for non-existent experience', async () => {
const experience = await service.getExperience('non-existent');
expect(experience).toBeNull();
});
});
describe('getStats', () => {
it('should return accurate statistics', async () => {
// Start and complete some experiences
const id1 = service.startCapture('Task 1', { domain: 'test-generation' });
await service.completeCapture(id1, { success: true, quality: 0.9 });
const id2 = service.startCapture('Task 2', { domain: 'test-generation' });
await service.completeCapture(id2, { success: false, quality: 0.3 });
const stats = await service.getStats();
expect(stats.totalExperiences).toBe(2);
expect(stats.successRate).toBeCloseTo(0.5, 1);
expect(stats.patternsExtracted).toBeGreaterThanOrEqual(0);
expect(stats.patternsPromoted).toBeGreaterThanOrEqual(0);
});
});
describe('factory function', () => {
it('should create service with default config', () => {
const service = createExperienceCaptureService(
mockMemory,
mockPatternStore,
mockEventBus
);
expect(service).toBeInstanceOf(ExperienceCaptureService);
});
it('should create service with custom config', () => {
const customConfig: ExperienceCaptureConfig = {
...DEFAULT_EXPERIENCE_CONFIG,
minQualityForPattern: 0.9,
promotionThreshold: 5,
};
const service = createExperienceCaptureService(
mockMemory,
mockPatternStore,
mockEventBus,
customConfig
);
expect(service).toBeInstanceOf(ExperienceCaptureService);
});
});
});
describe('DEFAULT_EXPERIENCE_CONFIG', () => {
it('should have reasonable default values', () => {
expect(DEFAULT_EXPERIENCE_CONFIG.minQualityForPatternExtraction).toBe(0.7);
expect(DEFAULT_EXPERIENCE_CONFIG.promotionThreshold).toBe(3);
expect(DEFAULT_EXPERIENCE_CONFIG.similarityThreshold).toBe(0.85);
expect(DEFAULT_EXPERIENCE_CONFIG.maxExperiencesPerDomain).toBe(1000);
expect(DEFAULT_EXPERIENCE_CONFIG.enableCrossDomainSharing).toBe(true);
expect(DEFAULT_EXPERIENCE_CONFIG.autoCleanup).toBe(true);
});
});