23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
572 lines
19 KiB
TypeScript
572 lines
19 KiB
TypeScript
/**
|
|
* Tests for RvfEmbeddingCache and RvfEmbeddingService
|
|
*
|
|
* Covers CRUD, LRU eviction, TTL expiry, binary persistence,
|
|
* dimension validation, deterministic hashing, L2 normalization,
|
|
* caching, batch operations, interface conformance, and shutdown.
|
|
*/
|
|
|
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
import { RvfEmbeddingCache } from '../v3/@claude-flow/embeddings/src/rvf-embedding-cache.js';
|
|
import { RvfEmbeddingService } from '../v3/@claude-flow/embeddings/src/rvf-embedding-service.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let tmpDir: string;
|
|
|
|
function makeTmpDir(): string {
|
|
return mkdtempSync(join(tmpdir(), 'rvf-test-'));
|
|
}
|
|
|
|
function makeEmbedding(dims: number, fill = 0.5): Float32Array {
|
|
const arr = new Float32Array(dims);
|
|
arr.fill(fill);
|
|
return arr;
|
|
}
|
|
|
|
function l2Norm(v: Float32Array): number {
|
|
let sum = 0;
|
|
for (let i = 0; i < v.length; i++) sum += v[i] * v[i];
|
|
return Math.sqrt(sum);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RvfEmbeddingCache
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('RvfEmbeddingCache', () => {
|
|
beforeEach(() => {
|
|
tmpDir = makeTmpDir();
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// -- CRUD ----------------------------------------------------------------
|
|
|
|
describe('CRUD operations', () => {
|
|
it('set/get stores and retrieves an embedding', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
dimensions: 4,
|
|
});
|
|
const emb = makeEmbedding(4, 0.25);
|
|
await cache.set('hello', emb);
|
|
|
|
const result = await cache.get('hello');
|
|
assert.ok(result, 'Expected non-null result');
|
|
assert.equal(result.length, 4);
|
|
assert.ok(Math.abs(result[0] - 0.25) < 1e-6);
|
|
await cache.close();
|
|
});
|
|
|
|
it('has returns true for existing keys and false for missing keys', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
dimensions: 4,
|
|
});
|
|
await cache.set('exists', makeEmbedding(4));
|
|
|
|
assert.equal(await cache.has('exists'), true);
|
|
assert.equal(await cache.has('missing'), false);
|
|
await cache.close();
|
|
});
|
|
|
|
it('delete removes an entry and returns correct boolean', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
dimensions: 4,
|
|
});
|
|
await cache.set('key1', makeEmbedding(4));
|
|
|
|
assert.equal(await cache.delete('key1'), true);
|
|
assert.equal(await cache.has('key1'), false);
|
|
assert.equal(await cache.delete('key1'), false);
|
|
await cache.close();
|
|
});
|
|
|
|
it('clear removes all entries', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
dimensions: 4,
|
|
});
|
|
await cache.set('a', makeEmbedding(4));
|
|
await cache.set('b', makeEmbedding(4));
|
|
assert.equal(await cache.size(), 2);
|
|
|
|
await cache.clear();
|
|
assert.equal(await cache.size(), 0);
|
|
assert.equal(await cache.has('a'), false);
|
|
await cache.close();
|
|
});
|
|
|
|
it('size returns the number of entries', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
dimensions: 4,
|
|
});
|
|
assert.equal(await cache.size(), 0);
|
|
await cache.set('x', makeEmbedding(4));
|
|
assert.equal(await cache.size(), 1);
|
|
await cache.set('y', makeEmbedding(4));
|
|
assert.equal(await cache.size(), 2);
|
|
await cache.close();
|
|
});
|
|
});
|
|
|
|
// -- LRU Eviction --------------------------------------------------------
|
|
|
|
describe('LRU eviction', () => {
|
|
it('evicts oldest entries when maxSize is exceeded', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
maxSize: 5,
|
|
dimensions: 4,
|
|
});
|
|
|
|
// Insert 6 entries to exceed maxSize of 5
|
|
for (let i = 0; i < 6; i++) {
|
|
await cache.set(`key-${i}`, makeEmbedding(4, i * 0.1));
|
|
}
|
|
|
|
// After eviction to 90% of 5 = 4, size should be <= 5
|
|
const sz = await cache.size();
|
|
assert.ok(sz <= 5, `Expected size <= 5, got ${sz}`);
|
|
await cache.close();
|
|
});
|
|
});
|
|
|
|
// -- TTL Expiry ----------------------------------------------------------
|
|
|
|
describe('TTL expiry', () => {
|
|
it('returns null for expired entries', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
ttlMs: 1, // 1ms TTL - will expire almost instantly
|
|
dimensions: 4,
|
|
});
|
|
await cache.set('ephemeral', makeEmbedding(4));
|
|
|
|
// Wait for the entry to expire
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
const result = await cache.get('ephemeral');
|
|
assert.equal(result, null, 'Expected expired entry to return null');
|
|
await cache.close();
|
|
});
|
|
|
|
it('has returns false for expired entries', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'cache.rvec'),
|
|
ttlMs: 1,
|
|
dimensions: 4,
|
|
});
|
|
await cache.set('temp', makeEmbedding(4));
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
assert.equal(await cache.has('temp'), false);
|
|
await cache.close();
|
|
});
|
|
});
|
|
|
|
// -- Binary Persistence --------------------------------------------------
|
|
|
|
describe('binary persistence', () => {
|
|
it('data survives close and reinit cycle', async () => {
|
|
const cachePath = join(tmpDir, 'persist.rvec');
|
|
const emb = makeEmbedding(4, 0.42);
|
|
|
|
// Write
|
|
const cache1 = new RvfEmbeddingCache({
|
|
cachePath,
|
|
dimensions: 4,
|
|
});
|
|
await cache1.set('persist-test', emb);
|
|
await cache1.close();
|
|
|
|
// Reopen and read
|
|
const cache2 = new RvfEmbeddingCache({
|
|
cachePath,
|
|
dimensions: 4,
|
|
});
|
|
const loaded = await cache2.get('persist-test');
|
|
assert.ok(loaded, 'Expected persisted entry to be loaded');
|
|
assert.equal(loaded.length, 4);
|
|
// Float32 precision: values should be very close
|
|
assert.ok(Math.abs(loaded[0] - 0.42) < 1e-5);
|
|
await cache2.close();
|
|
});
|
|
});
|
|
|
|
// -- Binary Format -------------------------------------------------------
|
|
|
|
describe('binary format', () => {
|
|
it('file starts with RVEC magic bytes (0x52 0x56 0x45 0x43)', async () => {
|
|
const cachePath = join(tmpDir, 'magic.rvec');
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath,
|
|
dimensions: 4,
|
|
});
|
|
await cache.set('magic-check', makeEmbedding(4));
|
|
await cache.close();
|
|
|
|
assert.ok(existsSync(cachePath), 'Cache file should exist');
|
|
const buf = readFileSync(cachePath);
|
|
assert.equal(buf[0], 0x52, 'R');
|
|
assert.equal(buf[1], 0x56, 'V');
|
|
assert.equal(buf[2], 0x45, 'E');
|
|
assert.equal(buf[3], 0x43, 'C');
|
|
});
|
|
});
|
|
|
|
// -- Dimension Validation ------------------------------------------------
|
|
|
|
describe('dimension validation', () => {
|
|
it('throws error when embedding dimensions do not match configured dimensions', async () => {
|
|
const cache = new RvfEmbeddingCache({
|
|
cachePath: join(tmpDir, 'dim.rvec'),
|
|
dimensions: 4,
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => cache.set('wrong-dims', makeEmbedding(8)),
|
|
(err: Error) => {
|
|
assert.ok(err.message.includes('Dimension mismatch'));
|
|
assert.ok(err.message.includes('expected 4'));
|
|
assert.ok(err.message.includes('got 8'));
|
|
return true;
|
|
}
|
|
);
|
|
await cache.close();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RvfEmbeddingService
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('RvfEmbeddingService', () => {
|
|
beforeEach(() => {
|
|
tmpDir = makeTmpDir();
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// -- embed() -------------------------------------------------------------
|
|
|
|
describe('embed()', () => {
|
|
it('returns a Float32Array of correct dimensions', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 64 });
|
|
const result = await svc.embed('test input');
|
|
|
|
assert.ok(result.embedding instanceof Float32Array);
|
|
assert.equal(result.embedding.length, 64);
|
|
assert.equal(typeof result.latencyMs, 'number');
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('uses default 384 dimensions when not specified', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf' });
|
|
const result = await svc.embed('default dims');
|
|
|
|
assert.equal(result.embedding.length, 384);
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('is deterministic - same input produces same output', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 32 });
|
|
|
|
// Clear cache between calls to force regeneration
|
|
const r1 = await svc.embed('determinism check');
|
|
svc.clearCache();
|
|
const r2 = await svc.embed('determinism check');
|
|
|
|
assert.deepEqual(
|
|
Array.from(r1.embedding as Float32Array),
|
|
Array.from(r2.embedding as Float32Array),
|
|
'Embeddings for the same input must be identical'
|
|
);
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('produces different embeddings for different inputs', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 32 });
|
|
const r1 = await svc.embed('apple');
|
|
const r2 = await svc.embed('banana');
|
|
|
|
const a = r1.embedding as Float32Array;
|
|
const b = r2.embedding as Float32Array;
|
|
let same = true;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (Math.abs(a[i] - b[i]) > 1e-9) { same = false; break; }
|
|
}
|
|
assert.equal(same, false, 'Different inputs should yield different embeddings');
|
|
await svc.shutdown();
|
|
});
|
|
});
|
|
|
|
// -- L2 Normalization ----------------------------------------------------
|
|
|
|
describe('L2 normalization', () => {
|
|
it('output vectors are unit length (norm approximately 1.0)', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 128 });
|
|
const result = await svc.embed('normalization test');
|
|
const norm = l2Norm(result.embedding as Float32Array);
|
|
|
|
assert.ok(
|
|
Math.abs(norm - 1.0) < 1e-5,
|
|
`Expected norm ~1.0, got ${norm}`
|
|
);
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('multiple different texts all produce unit vectors', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 64 });
|
|
const texts = ['alpha', 'bravo', 'charlie', 'delta', 'echo'];
|
|
|
|
for (const text of texts) {
|
|
const result = await svc.embed(text);
|
|
const norm = l2Norm(result.embedding as Float32Array);
|
|
assert.ok(
|
|
Math.abs(norm - 1.0) < 1e-5,
|
|
`Expected unit vector for "${text}", got norm ${norm}`
|
|
);
|
|
}
|
|
await svc.shutdown();
|
|
});
|
|
});
|
|
|
|
// -- embedBatch() --------------------------------------------------------
|
|
|
|
describe('embedBatch()', () => {
|
|
it('returns the correct number of embeddings', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 32 });
|
|
const texts = ['one', 'two', 'three', 'four'];
|
|
const result = await svc.embedBatch(texts);
|
|
|
|
assert.equal(result.embeddings.length, 4);
|
|
assert.equal(typeof result.totalLatencyMs, 'number');
|
|
assert.equal(typeof result.avgLatencyMs, 'number');
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('reports cache hits for previously embedded texts', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 32 });
|
|
|
|
// Pre-embed some texts
|
|
await svc.embed('cached-a');
|
|
await svc.embed('cached-b');
|
|
|
|
// Batch with mix of cached and new
|
|
const result = await svc.embedBatch(['cached-a', 'cached-b', 'new-c']);
|
|
|
|
assert.ok(result.cacheStats, 'Expected cacheStats to be defined');
|
|
assert.equal(result.cacheStats!.hits, 2);
|
|
assert.equal(result.cacheStats!.misses, 1);
|
|
await svc.shutdown();
|
|
});
|
|
});
|
|
|
|
// -- Caching -------------------------------------------------------------
|
|
|
|
describe('caching', () => {
|
|
it('second call for same text returns cached result', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 32 });
|
|
|
|
const r1 = await svc.embed('cache me');
|
|
assert.ok(!r1.cached, 'First call should not be cached');
|
|
|
|
const r2 = await svc.embed('cache me');
|
|
assert.equal(r2.cached, true, 'Second call should be cached');
|
|
assert.equal(r2.latencyMs, 0, 'Cached result should report 0 latency');
|
|
await svc.shutdown();
|
|
});
|
|
});
|
|
|
|
// -- Persistent Cache Integration ----------------------------------------
|
|
|
|
describe('persistent cache integration', () => {
|
|
it('with cachePath, data persists across service instances', async () => {
|
|
const cachePath = join(tmpDir, 'svc-persist.rvec');
|
|
|
|
// First instance: embed and shutdown
|
|
const svc1 = new RvfEmbeddingService({
|
|
provider: 'rvf',
|
|
dimensions: 32,
|
|
cachePath,
|
|
});
|
|
const r1 = await svc1.embed('persistent text');
|
|
await svc1.shutdown();
|
|
|
|
// Second instance: should find the persistent entry
|
|
const svc2 = new RvfEmbeddingService({
|
|
provider: 'rvf',
|
|
dimensions: 32,
|
|
cachePath,
|
|
});
|
|
const r2 = await svc2.embed('persistent text');
|
|
|
|
assert.equal(r2.cached, true, 'Should be a cache hit from persistent store');
|
|
assert.equal(r2.persistentCached, true, 'Should flag persistent cache hit');
|
|
|
|
// Verify the embeddings match
|
|
const a = r1.embedding as Float32Array;
|
|
const b = r2.embedding as Float32Array;
|
|
for (let i = 0; i < a.length; i++) {
|
|
assert.ok(
|
|
Math.abs(a[i] - b[i]) < 1e-5,
|
|
`Mismatch at index ${i}: ${a[i]} vs ${b[i]}`
|
|
);
|
|
}
|
|
await svc2.shutdown();
|
|
});
|
|
});
|
|
|
|
// -- IEmbeddingService Interface -----------------------------------------
|
|
|
|
describe('IEmbeddingService interface', () => {
|
|
it('provider property is "rvf"', () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf' });
|
|
assert.equal(svc.provider, 'rvf');
|
|
});
|
|
|
|
it('implements all required methods', () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf' });
|
|
assert.equal(typeof svc.embed, 'function');
|
|
assert.equal(typeof svc.embedBatch, 'function');
|
|
assert.equal(typeof svc.clearCache, 'function');
|
|
assert.equal(typeof svc.getCacheStats, 'function');
|
|
assert.equal(typeof svc.shutdown, 'function');
|
|
});
|
|
});
|
|
|
|
// -- getCacheStats() -----------------------------------------------------
|
|
|
|
describe('getCacheStats()', () => {
|
|
it('returns correct hit/miss structure after operations', async () => {
|
|
const svc = new RvfEmbeddingService({
|
|
provider: 'rvf',
|
|
dimensions: 16,
|
|
cacheSize: 100,
|
|
});
|
|
|
|
// Initial state
|
|
const stats0 = svc.getCacheStats();
|
|
assert.equal(stats0.size, 0);
|
|
assert.equal(stats0.maxSize, 100);
|
|
assert.equal(stats0.hitRate, 0);
|
|
|
|
// Generate some embeddings
|
|
await svc.embed('stat-a');
|
|
await svc.embed('stat-b');
|
|
await svc.embed('stat-a'); // cache hit
|
|
|
|
const stats1 = svc.getCacheStats();
|
|
assert.equal(stats1.size, 2);
|
|
assert.ok(stats1.hitRate > 0, 'Hit rate should be > 0 after cache hit');
|
|
await svc.shutdown();
|
|
});
|
|
});
|
|
|
|
// -- shutdown() ----------------------------------------------------------
|
|
|
|
describe('shutdown()', () => {
|
|
it('clears in-memory cache', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 16 });
|
|
await svc.embed('before-shutdown');
|
|
|
|
const statsBefore = svc.getCacheStats();
|
|
assert.equal(statsBefore.size, 1);
|
|
|
|
await svc.shutdown();
|
|
|
|
const statsAfter = svc.getCacheStats();
|
|
assert.equal(statsAfter.size, 0);
|
|
});
|
|
|
|
it('closes persistent cache without error', async () => {
|
|
const svc = new RvfEmbeddingService({
|
|
provider: 'rvf',
|
|
dimensions: 16,
|
|
cachePath: join(tmpDir, 'shutdown.rvec'),
|
|
});
|
|
await svc.embed('shutdown-persist');
|
|
|
|
// Should not throw
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('removes event listeners on shutdown', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 16 });
|
|
let called = false;
|
|
svc.addEventListener(() => { called = true; });
|
|
await svc.embed('pre-shutdown');
|
|
assert.equal(called, true, 'Listener should fire before shutdown');
|
|
|
|
await svc.shutdown();
|
|
called = false;
|
|
|
|
// After shutdown, listeners should be cleared, but embed still works
|
|
// (the in-memory cache is cleared, so it regenerates)
|
|
await svc.embed('post-shutdown');
|
|
assert.equal(called, false, 'Listener should not fire after shutdown');
|
|
});
|
|
});
|
|
|
|
// -- Event system --------------------------------------------------------
|
|
|
|
describe('event system', () => {
|
|
it('fires embed_start and embed_complete for new embeddings', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 16 });
|
|
const events: string[] = [];
|
|
svc.addEventListener((e) => events.push(e.type));
|
|
|
|
await svc.embed('event-test');
|
|
|
|
assert.ok(events.includes('embed_start'));
|
|
assert.ok(events.includes('embed_complete'));
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('fires cache_hit for cached embeddings', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 16 });
|
|
const events: string[] = [];
|
|
await svc.embed('hit-test'); // first call (miss)
|
|
|
|
svc.addEventListener((e) => events.push(e.type));
|
|
await svc.embed('hit-test'); // second call (hit)
|
|
|
|
assert.ok(events.includes('cache_hit'));
|
|
await svc.shutdown();
|
|
});
|
|
|
|
it('removeEventListener stops delivering events', async () => {
|
|
const svc = new RvfEmbeddingService({ provider: 'rvf', dimensions: 16 });
|
|
let count = 0;
|
|
const listener = () => { count++; };
|
|
|
|
svc.addEventListener(listener);
|
|
await svc.embed('before-remove');
|
|
const countBefore = count;
|
|
|
|
svc.removeEventListener(listener);
|
|
count = 0;
|
|
await svc.embed('after-remove');
|
|
assert.equal(count, 0, 'Listener should not fire after removal');
|
|
assert.ok(countBefore > 0, 'Listener should have fired before removal');
|
|
await svc.shutdown();
|
|
});
|
|
});
|
|
});
|