mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
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>
392 lines
11 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|