mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
152 lines
5.2 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|