Files
socraticode/tests/integration/embeddings.test.ts
T
2026-02-28 17:06:21 +00:00

152 lines
5.2 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { beforeAll, describe, expect, it } from "vitest";
import { ensureQdrantReady } from "../../src/services/docker.js";
import { getEmbeddingConfig } from "../../src/services/embedding-config.js";
import {
generateEmbeddings,
generateQueryEmbedding,
prepareDocumentText,
} from "../../src/services/embeddings.js";
import { ensureOllamaReady } from "../../src/services/ollama.js";
import { isDockerAvailable } from "../helpers/fixtures.js";
import { waitForOllama } from "../helpers/setup.js";
const dockerAvailable = isDockerAvailable();
describe.skipIf(!dockerAvailable)("embeddings service", () => {
const config = getEmbeddingConfig();
beforeAll(async () => {
await ensureQdrantReady();
await ensureOllamaReady();
await waitForOllama();
});
describe("prepareDocumentText", () => {
it("combines file metadata with content", () => {
const text = prepareDocumentText(
"export function greet() { return 'hello'; }",
"src/utils.ts",
);
expect(text).toContain("src/utils.ts");
expect(text).toContain("greet");
});
it("handles empty content", () => {
const text = prepareDocumentText("", "empty.ts");
expect(text).toContain("empty.ts");
});
});
describe("generateEmbeddings", () => {
it("generates embeddings for a batch of texts", async () => {
const texts = [
"function add(a, b) { return a + b; }",
"class UserService { constructor() {} }",
"const PI = 3.14159;",
];
const embeddings = await generateEmbeddings(texts);
expect(embeddings).toHaveLength(3);
for (const emb of embeddings) {
expect(emb).toHaveLength(config.embeddingDimensions);
}
});
it("generates consistent embeddings for the same text", async () => {
const text = "function add(a, b) { return a + b; }";
const [emb1] = await generateEmbeddings([text]);
const [emb2] = await generateEmbeddings([text]);
// Embeddings should be very close (not perfectly identical due to floating point)
expect(emb1).toHaveLength(config.embeddingDimensions);
expect(emb2).toHaveLength(config.embeddingDimensions);
// Cosine similarity should be very high
let dot = 0, norm1 = 0, norm2 = 0;
for (let i = 0; i < emb1.length; i++) {
dot += emb1[i] * emb2[i];
norm1 += emb1[i] * emb1[i];
norm2 += emb2[i] * emb2[i];
}
const similarity = dot / (Math.sqrt(norm1) * Math.sqrt(norm2));
expect(similarity).toBeGreaterThan(0.99);
});
it("generates different embeddings for different content", async () => {
const embeddings = await generateEmbeddings([
"authentication middleware with JWT token validation",
"fibonacci sequence mathematical algorithm calculation",
]);
expect(embeddings).toHaveLength(2);
// Calculate cosine similarity - should be < 1.0
let dot = 0, norm1 = 0, norm2 = 0;
for (let i = 0; i < embeddings[0].length; i++) {
dot += embeddings[0][i] * embeddings[1][i];
norm1 += embeddings[0][i] * embeddings[0][i];
norm2 += embeddings[1][i] * embeddings[1][i];
}
const similarity = dot / (Math.sqrt(norm1) * Math.sqrt(norm2));
// Different texts should have substantially different embeddings
expect(similarity).toBeLessThan(0.95);
});
it("handles a single text", async () => {
const embeddings = await generateEmbeddings(["single text"]);
expect(embeddings).toHaveLength(1);
expect(embeddings[0]).toHaveLength(config.embeddingDimensions);
});
it("handles empty array", async () => {
const embeddings = await generateEmbeddings([]);
expect(embeddings).toHaveLength(0);
});
});
describe("generateQueryEmbedding", () => {
it("generates a query embedding", async () => {
const embedding = await generateQueryEmbedding("search for authentication code");
expect(embedding).toHaveLength(config.embeddingDimensions);
for (const val of embedding) {
expect(Number.isFinite(val)).toBe(true);
}
});
it("produces a semantically relevant embedding", async () => {
// Generate embeddings for documents
const [authEmb] = await generateEmbeddings([
"function authenticateUser(token) { return validateJWT(token); }",
]);
const [mathEmb] = await generateEmbeddings([
"function fibonacci(n) { return n <= 1 ? n : fibonacci(n-1) + fibonacci(n-2); }",
]);
// Generate query embedding
const queryEmb = await generateQueryEmbedding("user authentication JWT");
// The query should be more similar to auth code than math code
function cosineSim(a: number[], b: number[]): number {
let dot = 0, n1 = 0, n2 = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
n1 += a[i] * a[i];
n2 += b[i] * b[i];
}
return dot / (Math.sqrt(n1) * Math.sqrt(n2));
}
const authSim = cosineSim(queryEmb, authEmb);
const mathSim = cosineSim(queryEmb, mathEmb);
expect(authSim).toBeGreaterThan(mathSim);
});
});
});