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

392 lines
11 KiB
TypeScript

/**
* Agentic QE v3 - LLM Cache Unit Tests
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import {
LLMCache,
LLMResponseCache,
DEFAULT_CACHE_CONFIG,
} from '../../../../src/shared/llm/cache';
import type { LLMResponse, EmbeddingResponse } from '../../../../src/shared/llm/interfaces';
describe('LLMCache', () => {
let cache: LLMCache<string>;
beforeEach(() => {
cache = new LLMCache<string>({
maxSize: 100,
defaultTtlMs: 60000,
enableLRU: true,
});
});
describe('basic operations', () => {
it('should store and retrieve values', () => {
cache.set('key1', 'value1');
expect(cache.get('key1')).toBe('value1');
});
it('should return undefined for missing keys', () => {
expect(cache.get('nonexistent')).toBeUndefined();
});
it('should check if key exists', () => {
cache.set('key1', 'value1');
expect(cache.has('key1')).toBe(true);
expect(cache.has('nonexistent')).toBe(false);
});
it('should delete values', () => {
cache.set('key1', 'value1');
expect(cache.delete('key1')).toBe(true);
expect(cache.get('key1')).toBeUndefined();
expect(cache.delete('key1')).toBe(false);
});
it('should clear all values', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.clear();
expect(cache.get('key1')).toBeUndefined();
expect(cache.get('key2')).toBeUndefined();
expect(cache.getStats().size).toBe(0);
});
});
describe('TTL expiration', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should expire entries after TTL', () => {
cache.set('key1', 'value1', 1000); // 1 second TTL
expect(cache.get('key1')).toBe('value1');
vi.advanceTimersByTime(1001);
expect(cache.get('key1')).toBeUndefined();
});
it('should not expire entries with 0 TTL', () => {
cache.set('key1', 'value1', 0); // No expiry
vi.advanceTimersByTime(999999);
expect(cache.get('key1')).toBe('value1');
});
it('should prune expired entries', () => {
cache.set('key1', 'value1', 1000);
cache.set('key2', 'value2', 2000);
cache.set('key3', 'value3', 3000);
vi.advanceTimersByTime(1500);
const pruned = cache.pruneExpired();
expect(pruned).toBe(1);
expect(cache.has('key1')).toBe(false);
expect(cache.has('key2')).toBe(true);
expect(cache.has('key3')).toBe(true);
});
});
describe('LRU eviction', () => {
it('should evict least recently used when full', () => {
const smallCache = new LLMCache<string>({ maxSize: 3, enableLRU: true });
smallCache.set('key1', 'value1');
smallCache.set('key2', 'value2');
smallCache.set('key3', 'value3');
// Access key1 to make it recently used
smallCache.get('key1');
// Add key4, should evict key2 (least recently used after key1 access)
smallCache.set('key4', 'value4');
expect(smallCache.has('key1')).toBe(true);
expect(smallCache.has('key2')).toBe(false); // Evicted
expect(smallCache.has('key3')).toBe(true);
expect(smallCache.has('key4')).toBe(true);
});
it('should track eviction count', () => {
const smallCache = new LLMCache<string>({ maxSize: 2, enableLRU: true });
smallCache.set('key1', 'value1');
smallCache.set('key2', 'value2');
smallCache.set('key3', 'value3'); // Triggers eviction
expect(smallCache.getStats().evictions).toBe(1);
});
});
describe('statistics', () => {
it('should track hits and misses', () => {
cache.set('key1', 'value1');
cache.get('key1'); // Hit
cache.get('key1'); // Hit
cache.get('missing'); // Miss
const stats = cache.getStats();
expect(stats.hits).toBe(2);
expect(stats.misses).toBe(1);
expect(stats.hitRate).toBeCloseTo(0.667, 2);
});
it('should track size', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
expect(cache.getStats().size).toBe(2);
});
it('should estimate memory usage', () => {
cache.set('key1', 'a'.repeat(100));
cache.set('key2', 'b'.repeat(200));
const stats = cache.getStats();
expect(stats.memoryUsageBytes).toBeGreaterThan(0);
});
});
describe('export/import', () => {
it('should export entries', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
const entries = cache.entries();
expect(entries.length).toBe(2);
});
it('should import entries', () => {
const sourceCache = new LLMCache<string>();
sourceCache.set('key1', 'value1');
sourceCache.set('key2', 'value2');
const entries = sourceCache.entries();
const targetCache = new LLMCache<string>();
targetCache.import(entries);
expect(targetCache.get('key1')).toBe('value1');
expect(targetCache.get('key2')).toBe('value2');
});
it('should respect max size on import', () => {
const sourceCache = new LLMCache<string>();
for (let i = 0; i < 10; i++) {
sourceCache.set(`key${i}`, `value${i}`);
}
const smallCache = new LLMCache<string>({ maxSize: 3 });
smallCache.import(sourceCache.entries());
expect(smallCache.getStats().size).toBeLessThanOrEqual(3);
});
});
describe('key generation', () => {
it('should generate consistent keys', () => {
const key1 = LLMCache.generateKey('generation', 'test input', {
model: 'gpt-4',
temperature: 0.7,
});
const key2 = LLMCache.generateKey('generation', 'test input', {
model: 'gpt-4',
temperature: 0.7,
});
expect(key1).toBe(key2);
});
it('should generate different keys for different inputs', () => {
const key1 = LLMCache.generateKey('generation', 'input A');
const key2 = LLMCache.generateKey('generation', 'input B');
expect(key1).not.toBe(key2);
});
it('should generate different keys for different options', () => {
const key1 = LLMCache.generateKey('generation', 'test', { model: 'gpt-4' });
const key2 = LLMCache.generateKey('generation', 'test', { model: 'claude' });
expect(key1).not.toBe(key2);
});
it('should generate different keys for different types', () => {
const key1 = LLMCache.generateKey('generation', 'test');
const key2 = LLMCache.generateKey('embedding', 'test');
expect(key1).not.toBe(key2);
});
});
});
describe('LLMResponseCache', () => {
let cache: LLMResponseCache;
const mockResponse: LLMResponse = {
content: 'Test response',
model: 'claude-3',
provider: 'claude',
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
cost: { inputCost: 0.01, outputCost: 0.02, totalCost: 0.03, currency: 'USD' },
latencyMs: 500,
finishReason: 'stop',
cached: false,
requestId: 'test-1',
};
const mockEmbedding: EmbeddingResponse = {
embedding: [0.1, 0.2, 0.3],
model: 'text-embedding-3-small',
provider: 'openai',
tokenCount: 5,
latencyMs: 100,
cached: false,
};
beforeEach(() => {
cache = new LLMResponseCache({
maxSize: 100,
cacheGenerations: true,
cacheEmbeddings: true,
cacheCompletions: true,
});
});
describe('generation caching', () => {
it('should cache and retrieve generations', () => {
cache.setGeneration('test prompt', mockResponse);
const cached = cache.getGeneration('test prompt');
expect(cached).toEqual(mockResponse);
});
it('should cache with options', () => {
cache.setGeneration('test prompt', mockResponse, {
model: 'gpt-4',
temperature: 0.7,
});
const cached = cache.getGeneration('test prompt', {
model: 'gpt-4',
temperature: 0.7,
});
expect(cached).toEqual(mockResponse);
});
it('should not find cache with different options', () => {
cache.setGeneration('test prompt', mockResponse, { model: 'gpt-4' });
const cached = cache.getGeneration('test prompt', { model: 'claude-3' });
expect(cached).toBeUndefined();
});
it('should respect cache disabled setting', () => {
const disabledCache = new LLMResponseCache({ cacheGenerations: false });
disabledCache.setGeneration('test prompt', mockResponse);
const cached = disabledCache.getGeneration('test prompt');
expect(cached).toBeUndefined();
});
});
describe('embedding caching', () => {
it('should cache and retrieve embeddings', () => {
cache.setEmbedding('test text', mockEmbedding);
const cached = cache.getEmbedding('test text');
expect(cached).toEqual(mockEmbedding);
});
it('should cache with model option', () => {
cache.setEmbedding('test text', mockEmbedding, {
model: 'text-embedding-3-small',
});
const cached = cache.getEmbedding('test text', {
model: 'text-embedding-3-small',
});
expect(cached).toEqual(mockEmbedding);
});
});
describe('completion caching', () => {
it('should cache and retrieve completions', () => {
const completion = {
completion: 'completed text',
model: 'gpt-4',
provider: 'openai' as const,
usage: { promptTokens: 5, completionTokens: 10, totalTokens: 15 },
latencyMs: 200,
cached: false,
};
cache.setCompletion('partial', completion);
const cached = cache.getCompletion('partial');
expect(cached).toEqual(completion);
});
});
describe('statistics', () => {
it('should provide combined stats', () => {
cache.setGeneration('gen1', mockResponse);
cache.setEmbedding('emb1', mockEmbedding);
cache.getGeneration('gen1'); // Hit
cache.getGeneration('missing'); // Miss
cache.getEmbedding('emb1'); // Hit
const stats = cache.getStats();
expect(stats.generation.hits).toBe(1);
expect(stats.generation.misses).toBe(1);
expect(stats.embedding.hits).toBe(1);
expect(stats.total.hits).toBe(2);
expect(stats.total.misses).toBe(1);
});
});
describe('maintenance', () => {
it('should clear all caches', () => {
cache.setGeneration('gen1', mockResponse);
cache.setEmbedding('emb1', mockEmbedding);
cache.clear();
expect(cache.getGeneration('gen1')).toBeUndefined();
expect(cache.getEmbedding('emb1')).toBeUndefined();
});
it('should prune expired from all caches', () => {
vi.useFakeTimers();
const shortTtlCache = new LLMResponseCache({ defaultTtlMs: 1000 });
shortTtlCache.setGeneration('gen1', mockResponse);
shortTtlCache.setEmbedding('emb1', mockEmbedding);
vi.advanceTimersByTime(1500);
const pruned = shortTtlCache.pruneExpired();
expect(pruned).toBe(2);
vi.useRealTimers();
});
});
});