@claude-flow/embeddings
Version:
V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking
185 lines • 7.38 kB
JavaScript
/**
* ADR-121 Phase 21 — embed cache tests.
*/
import { describe, it, expect, vi } from 'vitest';
import { EmbedCache, wrapWithCache, canonicalizeText } from '../embed-cache.js';
function vec(values) {
return new Float32Array(values);
}
describe('canonicalizeText', () => {
it('lowercases', () => {
expect(canonicalizeText('HELLO World')).toBe('hello world');
});
it('trims', () => {
expect(canonicalizeText(' hello ')).toBe('hello');
});
it('collapses internal whitespace', () => {
expect(canonicalizeText('hello world')).toBe('hello world');
});
it('produces identical output for case + spacing variants', () => {
expect(canonicalizeText('How does AUTH work?')).toBe(canonicalizeText(' how does auth work? '));
});
});
describe('EmbedCache — basic shape', () => {
it('throws on bad maxEntries', () => {
expect(() => new EmbedCache({ maxEntries: 0 })).toThrow();
expect(() => new EmbedCache({ maxEntries: -1 })).toThrow();
});
it('miss before any set', () => {
const c = new EmbedCache();
expect(c.get('hello')).toBeUndefined();
expect(c.stats().misses).toBe(1);
expect(c.stats().hits).toBe(0);
});
it('set then get returns the same vector', () => {
const c = new EmbedCache();
c.set('hello', vec([1, 2, 3]));
const got = c.get('hello');
expect(got).toBeDefined();
expect(Array.from(got)).toEqual([1, 2, 3]);
});
it('hit increments hits counter', () => {
const c = new EmbedCache();
c.set('hello', vec([1, 2, 3]));
c.get('hello');
c.get('hello');
expect(c.stats().hits).toBe(2);
expect(c.stats().misses).toBe(0);
expect(c.stats().hitRate).toBeCloseTo(1, 6);
});
it('case + spacing variants hit the same entry', () => {
const c = new EmbedCache();
c.set('Hello World', vec([1, 2, 3]));
expect(c.get('hello world')).toBeDefined();
expect(c.get(' HELLO WORLD ')).toBeDefined();
});
it('different dims throw on set after the first', () => {
const c = new EmbedCache();
c.set('a', vec([1, 2, 3]));
expect(() => c.set('b', vec([1, 2]))).toThrow(/dim/);
});
it('has() reports presence correctly', () => {
const c = new EmbedCache();
expect(c.has('x')).toBe(false);
c.set('x', vec([1]));
expect(c.has('x')).toBe(true);
expect(c.has('y')).toBe(false);
});
it('reset() clears entries + stats', () => {
const c = new EmbedCache();
c.set('a', vec([1, 2]));
c.get('a');
c.reset();
expect(c.stats().entries).toBe(0);
expect(c.stats().hits).toBe(0);
expect(c.stats().misses).toBe(0);
});
});
describe('EmbedCache — LRU eviction', () => {
it('evicts least-recently-used when over capacity', () => {
const c = new EmbedCache({ maxEntries: 2 });
c.set('a', vec([1]));
c.set('b', vec([2]));
c.set('c', vec([3])); // should evict 'a' (oldest)
expect(c.has('a')).toBe(false);
expect(c.has('b')).toBe(true);
expect(c.has('c')).toBe(true);
});
it('get() bumps an entry to most-recent', () => {
const c = new EmbedCache({ maxEntries: 2 });
c.set('a', vec([1]));
c.set('b', vec([2]));
c.get('a'); // bump 'a' to MRU
c.set('c', vec([3])); // evict 'b' (now oldest), keep 'a'
expect(c.has('a')).toBe(true);
expect(c.has('b')).toBe(false);
expect(c.has('c')).toBe(true);
});
it('re-setting an existing key bumps to MRU', () => {
const c = new EmbedCache({ maxEntries: 2 });
c.set('a', vec([1]));
c.set('b', vec([2]));
c.set('a', vec([99])); // overwrite + bump
c.set('c', vec([3])); // evict 'b'
expect(c.has('a')).toBe(true);
expect(c.has('b')).toBe(false);
expect(c.has('c')).toBe(true);
expect(Array.from(c.get('a'))).toEqual([99]);
});
it('stats track entries + hit rate', () => {
const c = new EmbedCache({ maxEntries: 5 });
c.set('a', vec([1, 2, 3]));
c.set('b', vec([4, 5, 6]));
c.get('a');
c.get('a');
c.get('missing');
const s = c.stats();
expect(s.entries).toBe(2);
expect(s.hits).toBe(2);
expect(s.misses).toBe(1);
expect(s.hitRate).toBeCloseTo(2 / 3, 6);
expect(s.approxByteSize).toBe(2 * 3 * 4); // 2 vecs × dim 3 × 4 bytes
});
});
describe('EmbedCache — sha256 keying', () => {
it('produces a 64-char hex key', () => {
const c = new EmbedCache();
expect(c.keyFor('hello', 384)).toMatch(/^[0-9a-f]{64}$/);
});
it('different dims for same text produce different keys', () => {
const c = new EmbedCache();
expect(c.keyFor('hello', 128)).not.toBe(c.keyFor('hello', 384));
});
it('different text produces different keys', () => {
const c = new EmbedCache();
expect(c.keyFor('hello', 384)).not.toBe(c.keyFor('world', 384));
});
});
describe('wrapWithCache', () => {
it('calls the wrapped embedder on miss', async () => {
const embedSpy = vi.fn(async (_text) => vec([1, 2, 3]));
const cache = new EmbedCache();
const wrapped = wrapWithCache(embedSpy, cache);
const result = await wrapped('hello');
expect(Array.from(result)).toEqual([1, 2, 3]);
expect(embedSpy).toHaveBeenCalledTimes(1);
});
it('does NOT call the wrapped embedder on hit', async () => {
const embedSpy = vi.fn(async (_text) => vec([1, 2, 3]));
const cache = new EmbedCache();
const wrapped = wrapWithCache(embedSpy, cache);
await wrapped('hello'); // miss → calls embedder
await wrapped('hello'); // hit → does NOT call embedder
expect(embedSpy).toHaveBeenCalledTimes(1);
});
it('cache hit rate reflects repeated calls', async () => {
const embedSpy = vi.fn(async (text) => vec([text.length]));
const cache = new EmbedCache();
const wrapped = wrapWithCache(embedSpy, cache);
await wrapped('a');
await wrapped('a');
await wrapped('a');
await wrapped('b');
expect(embedSpy).toHaveBeenCalledTimes(2); // 'a' once, 'b' once
expect(cache.stats().hits).toBe(2);
expect(cache.stats().misses).toBe(2);
expect(cache.stats().hitRate).toBe(0.5);
});
it('case + spacing variants hit the same cached vector', async () => {
const embedSpy = vi.fn(async (text) => vec([text.length]));
const cache = new EmbedCache();
const wrapped = wrapWithCache(embedSpy, cache);
await wrapped('Hello World');
await wrapped(' HELLO WORLD ');
expect(embedSpy).toHaveBeenCalledTimes(1);
});
it('accepts number[] from the embedder and converts to Float32Array', async () => {
const embedSpy = vi.fn(async (_text) => [1, 2, 3]);
const cache = new EmbedCache();
const wrapped = wrapWithCache(embedSpy, cache);
const result = await wrapped('hello');
expect(result).toBeInstanceOf(Float32Array);
expect(Array.from(result)).toEqual([1, 2, 3]);
});
});
//# sourceMappingURL=embed-cache.test.js.map