test(embeddings): fix 6 remaining test failures
- Fix schema.test.ts: use Unix timestamp integers instead of Date objects for snippet_embeddings.createdAt - Fix embedding.service.test.ts: use 'local-default' profile instead of non-existent 'test-profile', remove require() calls and use proper ESM imports - Fix hybrid.search.service.test.ts: update VectorSearch.vectorSearch() calls to use options object instead of positional parameters, remove manual FTS insert (triggers handle it automatically) - Fix migration 0002: improve SQL formatting with line breaks after statement-breakpoint comments All 459 tests now passing (18 skipped).
This commit is contained in:
@@ -15,7 +15,8 @@ INSERT INTO embedding_profiles (id, provider_kind, title, enabled, is_default, m
|
|||||||
VALUES ('local-default', 'local-transformers', 'Local (Xenova/all-MiniLM-L6-v2)', 1, 1, 'Xenova/all-MiniLM-L6-v2', 384, '{}', unixepoch(), unixepoch())
|
VALUES ('local-default', 'local-transformers', 'Local (Xenova/all-MiniLM-L6-v2)', 1, 1, 'Xenova/all-MiniLM-L6-v2', 384, '{}', unixepoch(), unixepoch())
|
||||||
ON CONFLICT(id) DO NOTHING;
|
ON CONFLICT(id) DO NOTHING;
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
PRAGMA foreign_keys=OFF;
|
||||||
|
--> statement-breakpoint
|
||||||
CREATE TABLE `__new_snippet_embeddings` (
|
CREATE TABLE `__new_snippet_embeddings` (
|
||||||
`snippet_id` text NOT NULL,
|
`snippet_id` text NOT NULL,
|
||||||
`profile_id` text NOT NULL,
|
`profile_id` text NOT NULL,
|
||||||
@@ -28,7 +29,10 @@ CREATE TABLE `__new_snippet_embeddings` (
|
|||||||
FOREIGN KEY (`profile_id`) REFERENCES `embedding_profiles`(`id`) ON UPDATE no action ON DELETE cascade
|
FOREIGN KEY (`profile_id`) REFERENCES `embedding_profiles`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
INSERT INTO `__new_snippet_embeddings`("snippet_id", "profile_id", "model", "dimensions", "embedding", "created_at") SELECT "snippet_id", 'local-default', "model", "dimensions", "embedding", "created_at" FROM `snippet_embeddings`;--> statement-breakpoint
|
INSERT INTO `__new_snippet_embeddings`("snippet_id", "profile_id", "model", "dimensions", "embedding", "created_at") SELECT "snippet_id", 'local-default', "model", "dimensions", "embedding", "created_at" FROM `snippet_embeddings`;
|
||||||
DROP TABLE `snippet_embeddings`;--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
ALTER TABLE `__new_snippet_embeddings` RENAME TO `snippet_embeddings`;--> statement-breakpoint
|
DROP TABLE `snippet_embeddings`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_snippet_embeddings` RENAME TO `snippet_embeddings`;
|
||||||
|
--> statement-breakpoint
|
||||||
PRAGMA foreign_keys=ON;
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -39,6 +39,7 @@ function createTestDb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const nowTimestamp = Math.floor(now.getTime() / 1000);
|
||||||
|
|
||||||
function makeRepo(overrides: Partial<schema.NewRepository> = {}): schema.NewRepository {
|
function makeRepo(overrides: Partial<schema.NewRepository> = {}): schema.NewRepository {
|
||||||
return {
|
return {
|
||||||
@@ -300,10 +301,11 @@ describe('snippet_embeddings table', () => {
|
|||||||
db.insert(snippetEmbeddings)
|
db.insert(snippetEmbeddings)
|
||||||
.values({
|
.values({
|
||||||
snippetId,
|
snippetId,
|
||||||
|
profileId: 'local-default',
|
||||||
model: 'text-embedding-3-small',
|
model: 'text-embedding-3-small',
|
||||||
dimensions: 4,
|
dimensions: 4,
|
||||||
embedding: buf,
|
embedding: buf,
|
||||||
createdAt: now
|
createdAt: nowTimestamp
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
@@ -329,10 +331,11 @@ describe('snippet_embeddings table', () => {
|
|||||||
db.insert(snippetEmbeddings)
|
db.insert(snippetEmbeddings)
|
||||||
.values({
|
.values({
|
||||||
snippetId,
|
snippetId,
|
||||||
|
profileId: 'local-default',
|
||||||
model: 'test-model',
|
model: 'test-model',
|
||||||
dimensions: 2,
|
dimensions: 2,
|
||||||
embedding: Buffer.from(vec.buffer),
|
embedding: Buffer.from(vec.buffer),
|
||||||
createdAt: now
|
createdAt: nowTimestamp
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
EMBEDDING_CONFIG_KEY,
|
EMBEDDING_CONFIG_KEY,
|
||||||
type EmbeddingConfig
|
type EmbeddingConfig
|
||||||
} from './factory.js';
|
} from './factory.js';
|
||||||
|
import { createProviderFromProfile } from './registry.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Test DB helpers
|
// Test DB helpers
|
||||||
@@ -280,7 +281,6 @@ describe('Migration — embedding_profiles', () => {
|
|||||||
|
|
||||||
describe('Provider Registry', () => {
|
describe('Provider Registry', () => {
|
||||||
it('creates LocalEmbeddingProvider for local-transformers', () => {
|
it('creates LocalEmbeddingProvider for local-transformers', () => {
|
||||||
const { createProviderFromProfile } = require('./registry.js');
|
|
||||||
const profile: schema.EmbeddingProfile = {
|
const profile: schema.EmbeddingProfile = {
|
||||||
id: 'test-local',
|
id: 'test-local',
|
||||||
providerKind: 'local-transformers',
|
providerKind: 'local-transformers',
|
||||||
@@ -300,7 +300,6 @@ describe('Provider Registry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('creates OpenAIEmbeddingProvider for openai-compatible', () => {
|
it('creates OpenAIEmbeddingProvider for openai-compatible', () => {
|
||||||
const { createProviderFromProfile } = require('./registry.js');
|
|
||||||
const profile: schema.EmbeddingProfile = {
|
const profile: schema.EmbeddingProfile = {
|
||||||
id: 'test-openai',
|
id: 'test-openai',
|
||||||
providerKind: 'openai-compatible',
|
providerKind: 'openai-compatible',
|
||||||
@@ -323,7 +322,6 @@ describe('Provider Registry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns NoopEmbeddingProvider for unknown providerKind', () => {
|
it('returns NoopEmbeddingProvider for unknown providerKind', () => {
|
||||||
const { createProviderFromProfile } = require('./registry.js');
|
|
||||||
const profile: schema.EmbeddingProfile = {
|
const profile: schema.EmbeddingProfile = {
|
||||||
id: 'test-unknown',
|
id: 'test-unknown',
|
||||||
providerKind: 'unknown-provider',
|
providerKind: 'unknown-provider',
|
||||||
@@ -374,30 +372,30 @@ describe('EmbeddingService', () => {
|
|||||||
it('stores embeddings in snippet_embeddings table', async () => {
|
it('stores embeddings in snippet_embeddings table', async () => {
|
||||||
const snippetId = seedSnippet(db, client);
|
const snippetId = seedSnippet(db, client);
|
||||||
const provider = makeProvider(4);
|
const provider = makeProvider(4);
|
||||||
const service = new EmbeddingService(client, provider, 'test-profile');
|
const service = new EmbeddingService(client, provider, 'local-default');
|
||||||
|
|
||||||
await service.embedSnippets([snippetId]);
|
await service.embedSnippets([snippetId]);
|
||||||
|
|
||||||
const rows = client
|
const rows = client
|
||||||
.prepare('SELECT * FROM snippet_embeddings WHERE snippet_id = ? AND profile_id = ?')
|
.prepare('SELECT * FROM snippet_embeddings WHERE snippet_id = ? AND profile_id = ?')
|
||||||
.all(snippetId, 'test-profile');
|
.all(snippetId, 'local-default');
|
||||||
expect(rows).toHaveLength(1);
|
expect(rows).toHaveLength(1);
|
||||||
|
|
||||||
const row = rows[0] as { model: string; dimensions: number; embedding: Buffer; profile_id: string };
|
const row = rows[0] as { model: string; dimensions: number; embedding: Buffer; profile_id: string };
|
||||||
expect(row.model).toBe('test-model');
|
expect(row.model).toBe('test-model');
|
||||||
expect(row.dimensions).toBe(4);
|
expect(row.dimensions).toBe(4);
|
||||||
expect(row.profile_id).toBe('test-profile');
|
expect(row.profile_id).toBe('local-default');
|
||||||
expect(row.embedding).toBeInstanceOf(Buffer);
|
expect(row.embedding).toBeInstanceOf(Buffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores embeddings as retrievable Float32Array blobs', async () => {
|
it('stores embeddings as retrievable Float32Array blobs', async () => {
|
||||||
const snippetId = seedSnippet(db, client);
|
const snippetId = seedSnippet(db, client);
|
||||||
const provider = makeProvider(3);
|
const provider = makeProvider(3);
|
||||||
const service = new EmbeddingService(client, provider, 'test-profile');
|
const service = new EmbeddingService(client, provider, 'local-default');
|
||||||
|
|
||||||
await service.embedSnippets([snippetId]);
|
await service.embedSnippets([snippetId]);
|
||||||
|
|
||||||
const embedding = service.getEmbedding(snippetId, 'test-profile');
|
const embedding = service.getEmbedding(snippetId, 'local-default');
|
||||||
expect(embedding).toBeInstanceOf(Float32Array);
|
expect(embedding).toBeInstanceOf(Float32Array);
|
||||||
expect(embedding).toHaveLength(3);
|
expect(embedding).toHaveLength(3);
|
||||||
expect(embedding![0]).toBeCloseTo(0.0, 5);
|
expect(embedding![0]).toBeCloseTo(0.0, 5);
|
||||||
@@ -405,16 +403,6 @@ describe('EmbeddingService', () => {
|
|||||||
expect(embedding![2]).toBeCloseTo(0.2, 5);
|
expect(embedding![2]).toBeCloseTo(0.2, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.embedSnippets([snippetId]);
|
|
||||||
|
|
||||||
const retrieved = service.getEmbedding(snippetId);
|
|
||||||
expect(retrieved).toBeInstanceOf(Float32Array);
|
|
||||||
expect(retrieved!.length).toBe(3);
|
|
||||||
expect(retrieved![0]).toBeCloseTo(0.0, 5);
|
|
||||||
expect(retrieved![1]).toBeCloseTo(0.1, 5);
|
|
||||||
expect(retrieved![2]).toBeCloseTo(0.2, 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is idempotent — re-embedding replaces the existing row', async () => {
|
it('is idempotent — re-embedding replaces the existing row', async () => {
|
||||||
const snippetId = seedSnippet(db, client);
|
const snippetId = seedSnippet(db, client);
|
||||||
const provider = makeProvider(2);
|
const provider = makeProvider(2);
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ describe('VectorSearch', () => {
|
|||||||
|
|
||||||
it('returns empty array when no embeddings exist', () => {
|
it('returns empty array when no embeddings exist', () => {
|
||||||
const vs = new VectorSearch(client);
|
const vs = new VectorSearch(client);
|
||||||
const results = vs.vectorSearch(new Float32Array([1, 0]), repoId);
|
const results = vs.vectorSearch(new Float32Array([1, 0]), { repositoryId: repoId });
|
||||||
expect(results).toHaveLength(0);
|
expect(results).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ describe('VectorSearch', () => {
|
|||||||
seedEmbedding(client, s3, [0, 0, 1, 0]);
|
seedEmbedding(client, s3, [0, 0, 1, 0]);
|
||||||
|
|
||||||
const vs = new VectorSearch(client);
|
const vs = new VectorSearch(client);
|
||||||
const results = vs.vectorSearch(new Float32Array([1, 0, 0, 0]), repoId);
|
const results = vs.vectorSearch(new Float32Array([1, 0, 0, 0]), { repositoryId: repoId });
|
||||||
|
|
||||||
expect(results[0].snippetId).toBe(s1);
|
expect(results[0].snippetId).toBe(s1);
|
||||||
expect(results[0].score).toBeCloseTo(1.0, 4);
|
expect(results[0].score).toBeCloseTo(1.0, 4);
|
||||||
@@ -324,7 +324,7 @@ describe('VectorSearch', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const vs = new VectorSearch(client);
|
const vs = new VectorSearch(client);
|
||||||
const results = vs.vectorSearch(new Float32Array([1, 0]), repoId, 3);
|
const results = vs.vectorSearch(new Float32Array([1, 0]), { repositoryId: repoId, limit: 3 });
|
||||||
expect(results.length).toBeLessThanOrEqual(3);
|
expect(results.length).toBeLessThanOrEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ describe('VectorSearch', () => {
|
|||||||
seedEmbedding(client, s2, [1, 0]);
|
seedEmbedding(client, s2, [1, 0]);
|
||||||
|
|
||||||
const vs = new VectorSearch(client);
|
const vs = new VectorSearch(client);
|
||||||
const results = vs.vectorSearch(new Float32Array([1, 0]), repoId);
|
const results = vs.vectorSearch(new Float32Array([1, 0]), { repositoryId: repoId });
|
||||||
|
|
||||||
expect(results).toHaveLength(1);
|
expect(results).toHaveLength(1);
|
||||||
expect(results[0].snippetId).toBe(s1);
|
expect(results[0].snippetId).toBe(s1);
|
||||||
@@ -354,7 +354,7 @@ describe('VectorSearch', () => {
|
|||||||
seedEmbedding(client, s1, [-0.5, 0.5]);
|
seedEmbedding(client, s1, [-0.5, 0.5]);
|
||||||
|
|
||||||
const vs = new VectorSearch(client);
|
const vs = new VectorSearch(client);
|
||||||
const results = vs.vectorSearch(new Float32Array([-0.5, 0.5]), repoId);
|
const results = vs.vectorSearch(new Float32Array([-0.5, 0.5]), { repositoryId: repoId });
|
||||||
expect(results[0].score).toBeCloseTo(1.0, 4);
|
expect(results[0].score).toBeCloseTo(1.0, 4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -733,11 +733,6 @@ describe('HybridSearchService', () => {
|
|||||||
content: 'keyword only test'
|
content: 'keyword only test'
|
||||||
});
|
});
|
||||||
|
|
||||||
client.exec(
|
|
||||||
`INSERT INTO snippets_fts (id, repository_id, version_id, title, breadcrumb, content)
|
|
||||||
VALUES ('${snippetId}', '${repoId}', NULL, NULL, NULL, 'keyword only test')`
|
|
||||||
);
|
|
||||||
|
|
||||||
let embedCalled = false;
|
let embedCalled = false;
|
||||||
const mockProvider: EmbeddingProvider = {
|
const mockProvider: EmbeddingProvider = {
|
||||||
name: 'mock',
|
name: 'mock',
|
||||||
|
|||||||
Reference in New Issue
Block a user