@claude-flow/embeddings
Version:
V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking
84 lines • 4.43 kB
JavaScript
/**
* ADR-121 Phase 3b — `@ruvector/attention`-backed Poincaré op tests.
*
* Tests run with the optional peer dep MOCKED ABSENT so we exercise
* the fallback path deterministically. The "real backend reachable"
* path is exercised in the CI smoke against the actual native
* binding (when CI's OS/arch has it).
*
* Acceptance:
* - All async ops return Float32Array of correct dimension
* - hyperbolicAttentionAvailable() returns boolean (no throw)
* - Fallback path produces values consistent with hand-rolled
* hyperbolic.ts (within numerical tolerance)
* - Default curvature = 1.0 when omitted
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Force fallback path so tests don't depend on the native binding
// shipping correctly for the test runner's OS.
vi.mock('@ruvector/attention', () => ({}));
import {
// The source module exports under the simple names; the package's
// public index.ts re-exports with an `Async` suffix to distinguish
// from the hand-rolled `hyperbolic.ts` sync versions. We import
// direct here to test the source.
projectToPoincareBall as projectToPoincareBallAsync, poincareDistance as poincareDistanceAsync, expMap as expMapAsync, logMap as logMapAsync, mobiusAddition as mobiusAdditionAsync, hyperbolicAttentionAvailable, __resetHyperbolicAttentionCache, } from '../hyperbolic-attention.js';
beforeEach(() => {
__resetHyperbolicAttentionCache();
});
describe('hyperbolic-attention — fallback path (no peer dep)', () => {
it('hyperbolicAttentionAvailable returns false when peer dep absent', async () => {
expect(await hyperbolicAttentionAvailable()).toBe(false);
});
it('projectToPoincareBallAsync returns a Float32Array of correct dim', async () => {
const r = await projectToPoincareBallAsync(new Float32Array([0.3, 0.0, 0.0]));
expect(r).toBeInstanceOf(Float32Array);
expect(r.length).toBe(3);
});
it('projectToPoincareBallAsync accepts number[] input', async () => {
const r = await projectToPoincareBallAsync([0.3, 0.0, 0.0]);
expect(r).toBeInstanceOf(Float32Array);
expect(r.length).toBe(3);
});
it('projectToPoincareBallAsync keeps small Euclidean vectors inside the ball', async () => {
const r = await projectToPoincareBallAsync(new Float32Array([0.3, 0.0, 0.0]));
let normSq = 0;
for (let i = 0; i < r.length; i++)
normSq += r[i] * r[i];
expect(Math.sqrt(normSq)).toBeLessThan(1);
});
it('poincareDistanceAsync returns a finite non-negative number', async () => {
const d = await poincareDistanceAsync(new Float32Array([0.2, 0.0]), new Float32Array([0.0, 0.2]));
expect(typeof d).toBe('number');
expect(d).toBeGreaterThanOrEqual(0);
expect(Number.isFinite(d)).toBe(true);
});
it('poincareDistanceAsync gives 0 (or near-0) for identical points', async () => {
const d = await poincareDistanceAsync(new Float32Array([0.1, 0.1, 0.0]), new Float32Array([0.1, 0.1, 0.0]));
expect(d).toBeLessThan(1e-6);
});
it('mobiusAdditionAsync returns a Float32Array', async () => {
const r = await mobiusAdditionAsync(new Float32Array([0.1, 0.0]), new Float32Array([0.0, 0.1]));
expect(r).toBeInstanceOf(Float32Array);
expect(r.length).toBe(2);
});
it('expMapAsync and logMapAsync return Float32Arrays of correct dim', async () => {
const a = new Float32Array([0.2, 0.0, 0.0]);
const tangent = new Float32Array([0.05, 0.05, 0.0]);
const exp = await expMapAsync(a, tangent);
expect(exp).toBeInstanceOf(Float32Array);
expect(exp.length).toBe(3);
const b = new Float32Array([0.3, 0.1, 0.0]);
const log = await logMapAsync(a, b);
expect(log).toBeInstanceOf(Float32Array);
expect(log.length).toBe(3);
});
it('honors a custom curvature option', async () => {
const d1 = await poincareDistanceAsync(new Float32Array([0.2, 0.0]), new Float32Array([0.0, 0.2]), { curvature: 1.0 });
const d2 = await poincareDistanceAsync(new Float32Array([0.2, 0.0]), new Float32Array([0.0, 0.2]), { curvature: 0.5 });
// Different curvatures produce different distances — exact values
// depend on the backend, but they must differ.
expect(d1).not.toBe(d2);
});
});
//# sourceMappingURL=hyperbolic-attention.test.js.map