146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
/**
|
|
* createEmbeddingProvider tests — ported from
|
|
* hive-mind/packages/core/src/mind/embedding-provider.test.ts.
|
|
*
|
|
* Memory Sync Repair Step 2. Source: hive-mind file at HEAD c363257.
|
|
*
|
|
* Verbatim port — only the import path is adjusted from
|
|
* `./embedding-provider.js` to `../../src/mind/embedding-provider.js`.
|
|
*
|
|
* NOTE: waggle-os has additional `tests/embedding-provider-quota.test.ts`
|
|
* at the top level that exercises tier+quota enforcement (Waggle-only
|
|
* feature). That file is NOT a substitute for this port — they cover
|
|
* complementary surfaces: this file pins the generic mock fallback,
|
|
* dimension respect, deterministic-vector behavior, batch shape, and
|
|
* reprobe contract; the top-level file pins tier gating.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
createEmbeddingProvider,
|
|
capEmbedText,
|
|
maxEmbedCharsForModel,
|
|
reembedPerText,
|
|
} from '../../src/mind/embedding-provider.js';
|
|
import type { Embedder } from '../../src/mind/embeddings.js';
|
|
|
|
describe('createEmbeddingProvider (hive-mind port)', () => {
|
|
it('falls back to mock when provider=mock is requested explicitly', async () => {
|
|
const provider = await createEmbeddingProvider({ provider: 'mock' });
|
|
expect(provider.getActiveProvider()).toBe('mock');
|
|
const status = provider.getStatus();
|
|
expect(status.activeProvider).toBe('mock');
|
|
expect(status.availableProviders).toContain('mock');
|
|
expect(status.dimensions).toBe(1024);
|
|
expect(status.modelName).toBe('deterministic-mock');
|
|
});
|
|
|
|
it('respects targetDimensions when configured', async () => {
|
|
const provider = await createEmbeddingProvider({
|
|
provider: 'mock',
|
|
targetDimensions: 512,
|
|
});
|
|
expect(provider.dimensions).toBe(512);
|
|
const vec = await provider.embed('hello');
|
|
expect(vec).toBeInstanceOf(Float32Array);
|
|
expect(vec.length).toBe(512);
|
|
});
|
|
|
|
it('produces deterministic mock vectors for identical inputs', async () => {
|
|
const provider = await createEmbeddingProvider({ provider: 'mock' });
|
|
const a = await provider.embed('deterministic input');
|
|
const b = await provider.embed('deterministic input');
|
|
expect(a.length).toBe(1024);
|
|
expect(b.length).toBe(1024);
|
|
for (let i = 0; i < a.length; i++) {
|
|
expect(a[i]).toBe(b[i]);
|
|
}
|
|
});
|
|
|
|
it('returns empty array for embedBatch([])', async () => {
|
|
const provider = await createEmbeddingProvider({ provider: 'mock' });
|
|
const result = await provider.embedBatch([]);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('batch-embeds multiple inputs to the expected shape', async () => {
|
|
const provider = await createEmbeddingProvider({
|
|
provider: 'mock',
|
|
targetDimensions: 256,
|
|
});
|
|
const out = await provider.embedBatch(['a', 'b', 'c']);
|
|
expect(out).toHaveLength(3);
|
|
for (const vec of out) {
|
|
expect(vec).toBeInstanceOf(Float32Array);
|
|
expect(vec.length).toBe(256);
|
|
}
|
|
});
|
|
|
|
it('falls back to mock when an explicit non-mock provider fails to probe', async () => {
|
|
// litellm with an obviously-unroutable URL — probe should fail quickly and
|
|
// the factory should land on mock.
|
|
const provider = await createEmbeddingProvider({
|
|
provider: 'litellm',
|
|
litellm: { url: 'http://127.0.0.1:1' },
|
|
});
|
|
expect(provider.getActiveProvider()).toBe('mock');
|
|
const status = provider.getStatus();
|
|
expect(status.availableProviders).toEqual(['mock']);
|
|
});
|
|
|
|
it('reprobe() refreshes status and keeps mock available when nothing else is', async () => {
|
|
const provider = await createEmbeddingProvider({ provider: 'mock' });
|
|
const first = provider.getStatus().probeTimestamp;
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
const second = await provider.reprobe();
|
|
expect(second.availableProviders).toContain('mock');
|
|
expect(Date.parse(second.probeTimestamp)).toBeGreaterThanOrEqual(Date.parse(first));
|
|
});
|
|
});
|
|
|
|
// Reverse-ported from OSS hive-mind (oss-drift triage R5, 2026-06-11).
|
|
describe('embedding guards (oversized-frame truncation + skip-not-abort)', () => {
|
|
it('capEmbedText truncates only inputs over the limit', () => {
|
|
expect(capEmbedText('short', 6000)).toBe('short');
|
|
expect(capEmbedText('x'.repeat(6000), 6000)).toHaveLength(6000); // exactly at limit: unchanged
|
|
expect(capEmbedText('x'.repeat(20000), 6000)).toHaveLength(6000); // over limit: clamped
|
|
});
|
|
|
|
it('maxEmbedCharsForModel returns 8000 for 8k-named models and 6000 otherwise', () => {
|
|
// D1 probe (2026-06-12): the OSS 24k branch was unsafe — '-8k' named
|
|
// models can be architecture-capped at 2048 tokens (nomic-bert) and 400
|
|
// well below 24k chars, mock-poisoning every long frame. 8k chars ≈ the
|
|
// real 2048-token prose budget.
|
|
expect(maxEmbedCharsForModel('nomic-embed-text')).toBe(6000);
|
|
expect(maxEmbedCharsForModel('voyage-3-lite')).toBe(6000);
|
|
expect(maxEmbedCharsForModel('deterministic-mock')).toBe(6000);
|
|
expect(maxEmbedCharsForModel('nomic-embed-text-8k')).toBe(8000);
|
|
expect(maxEmbedCharsForModel('custom (num_ctx 8192)')).toBe(8000);
|
|
});
|
|
|
|
it('reembedPerText degrades ONLY the failing text, not the whole batch', async () => {
|
|
// The regression: the provider used to mock-poison the WHOLE batch when one
|
|
// text made the backend throw. Per-text re-embed keeps the good ones real.
|
|
const realFirstByte = (t: string): Float32Array => {
|
|
const v = new Float32Array(4);
|
|
v[0] = t.length; // a "real" marker the mock can't produce for these strings
|
|
return v;
|
|
};
|
|
const embedder: Embedder = {
|
|
dimensions: 4,
|
|
async embed(t: string) {
|
|
if (t === 'POISON') throw new Error('backend rejected this input');
|
|
return realFirstByte(t);
|
|
},
|
|
async embedBatch() {
|
|
throw new Error('batch path not used in this test');
|
|
},
|
|
};
|
|
|
|
const out = await reembedPerText(embedder, ['alpha', 'POISON', 'betas'], 4);
|
|
expect(out).toHaveLength(3);
|
|
expect(out[0][0]).toBe(5); // 'alpha' embedded for real
|
|
expect(out[2][0]).toBe(5); // 'betas' embedded for real
|
|
expect(out[1][0]).not.toBe(6); // 'POISON' degraded to mock, NOT a real length-6 vector
|
|
});
|
|
});
|