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

266 lines
8.6 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { ensureQdrantReady } from "../../src/services/docker.js";
import { ensureOllamaReady } from "../../src/services/ollama.js";
import {
deleteCollection,
deleteFileChunks,
deleteProjectMetadata,
ensureCollection,
getCollectionInfo,
getProjectMetadata,
listCodebaseCollections,
loadProjectHashes,
saveProjectMetadata,
searchChunks,
upsertChunks,
} from "../../src/services/qdrant.js";
import type { FileChunk } from "../../src/types.js";
import { isDockerAvailable } from "../helpers/fixtures.js";
import { deleteTestCollection, waitForOllama, waitForQdrant } from "../helpers/setup.js";
const dockerAvailable = isDockerAvailable();
const TEST_COLLECTION = "codebase_test_qdrant_integration";
describe.skipIf(!dockerAvailable)("qdrant service", () => {
beforeAll(async () => {
await ensureQdrantReady();
await ensureOllamaReady();
await waitForQdrant();
await waitForOllama();
// Clean up from any previous test run
await deleteTestCollection(TEST_COLLECTION);
});
afterAll(async () => {
await deleteTestCollection(TEST_COLLECTION);
});
describe("collection management", () => {
it("creates a collection with correct dimensions", async () => {
await ensureCollection(TEST_COLLECTION);
const info = await getCollectionInfo(TEST_COLLECTION);
expect(info).toBeDefined();
expect(info?.status).toBe("green");
});
it("is idempotent — creating an existing collection does not error", async () => {
await expect(ensureCollection(TEST_COLLECTION)).resolves.not.toThrow();
});
it("lists collections including the test collection", async () => {
const collections = await listCodebaseCollections();
expect(collections).toContain(TEST_COLLECTION);
});
it("returns collection info", async () => {
const info = await getCollectionInfo(TEST_COLLECTION);
expect(info).toBeDefined();
expect(info?.pointsCount).toBe(0); // no data yet
});
it("returns null info for non-existent collection", async () => {
const info = await getCollectionInfo("nonexistent_collection_xyz");
expect(info).toBeNull();
});
});
describe("chunk upsert and search with real embeddings", () => {
const chunks: FileChunk[] = [
{
id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
filePath: "/project/src/auth.ts",
relativePath: "src/auth.ts",
content:
"export function authenticateUser(token: string): boolean {\n // Validate JWT token and check permissions\n const parts = token.split('.');\n return parts.length === 3;\n}",
startLine: 1,
endLine: 5,
language: "typescript",
type: "code",
},
{
id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
filePath: "/project/src/math.ts",
relativePath: "src/math.ts",
content:
"export function fibonacci(n: number): number {\n if (n <= 0) return 0;\n if (n === 1) return 1;\n let prev = 0, curr = 1;\n for (let i = 2; i <= n; i++) {\n [prev, curr] = [curr, prev + curr];\n }\n return curr;\n}",
startLine: 1,
endLine: 9,
language: "typescript",
type: "code",
},
{
id: "cccccccc-cccc-cccc-cccc-cccccccccccc",
filePath: "/project/lib/data.py",
relativePath: "lib/data.py",
content:
'def load_json_file(filepath: str) -> dict:\n """Load and parse a JSON file from disk."""\n with open(filepath, "r") as f:\n return json.load(f)',
startLine: 1,
endLine: 4,
language: "python",
type: "code",
},
];
const _embeddings: number[][] = [];
it("upserts chunks with real embeddings (generated internally)", async () => {
await upsertChunks(TEST_COLLECTION, chunks, "test-content-hash");
// Verify points were created
const info = await getCollectionInfo(TEST_COLLECTION);
expect(info).toBeDefined();
expect(info?.pointsCount).toBe(3);
});
it("searches for authentication-related code", async () => {
const results = await searchChunks(
TEST_COLLECTION,
"user authentication with JWT tokens",
5,
);
expect(results.length).toBeGreaterThan(0);
// The auth.ts chunk should rank highest for auth-related queries
expect(results[0].relativePath).toBe("src/auth.ts");
expect(results[0].score).toBeGreaterThan(0);
expect(results[0].content).toContain("authenticateUser");
});
it("searches for mathematical algorithms", async () => {
const results = await searchChunks(
TEST_COLLECTION,
"fibonacci sequence calculation algorithm",
5,
);
expect(results.length).toBeGreaterThan(0);
// The math.ts chunk should rank highest for math queries
expect(results[0].relativePath).toBe("src/math.ts");
expect(results[0].content).toContain("fibonacci");
});
it("searches for data loading utilities", async () => {
const results = await searchChunks(
TEST_COLLECTION,
"load and parse JSON data from file",
5,
);
expect(results.length).toBeGreaterThan(0);
// The data.py chunk should rank highest
expect(results[0].relativePath).toBe("lib/data.py");
});
it("supports limit parameter", async () => {
const results = await searchChunks(TEST_COLLECTION, "code", 2);
expect(results.length).toBeLessThanOrEqual(2);
});
it("supports file filter", async () => {
const results = await searchChunks(
TEST_COLLECTION,
"function",
10,
"auth",
);
// Only auth.ts should match the file filter
for (const r of results) {
expect(r.relativePath).toContain("auth");
}
});
it("supports language filter", async () => {
const results = await searchChunks(
TEST_COLLECTION,
"function",
10,
undefined,
"python",
);
for (const r of results) {
expect(r.language).toBe("python");
}
});
it("returns empty results for unrelated queries in filtered search", async () => {
const results = await searchChunks(
TEST_COLLECTION,
"authentication",
10,
undefined,
"python",
);
// Should return python results (only data.py), even though
// the query is more related to auth.ts
for (const r of results) {
expect(r.language).toBe("python");
}
});
});
describe("delete file chunks", () => {
it("deletes chunks for a specific file", async () => {
await deleteFileChunks(TEST_COLLECTION, "/project/lib/data.py");
const info = await getCollectionInfo(TEST_COLLECTION);
expect(info?.pointsCount).toBe(2); // 2 remaining
});
});
describe("metadata collection", () => {
const metadataCollection = TEST_COLLECTION; // reuse the test collection for metadata
const projectPath = "/test/project/path";
it("saves project metadata", async () => {
const fileHashes = new Map<string, string>([
["src/auth.ts", "hash-auth"],
["src/math.ts", "hash-math"],
]);
await saveProjectMetadata(metadataCollection, projectPath, 42, 2, fileHashes, "completed");
const metadata = await getProjectMetadata(metadataCollection);
expect(metadata).toBeDefined();
expect(metadata?.projectPath).toBe(projectPath);
expect(metadata?.filesTotal).toBe(42);
expect(metadata?.filesIndexed).toBe(2);
});
it("loads project hashes", async () => {
const hashes = await loadProjectHashes(metadataCollection);
// Initially might be empty or contain entries based on implementation
expect(hashes).toBeDefined();
expect(typeof hashes).toBe("object");
});
it("can delete project metadata", async () => {
await deleteProjectMetadata(metadataCollection);
// After deletion, metadata should be gone
const metadata = await getProjectMetadata(metadataCollection);
expect(metadata).toBeNull();
});
});
describe("collection deletion", () => {
it("deletes the test collection", async () => {
await deleteCollection(TEST_COLLECTION);
const info = await getCollectionInfo(TEST_COLLECTION);
expect(info).toBeNull();
});
it("does not error when deleting non-existent collection", async () => {
await expect(
deleteCollection("nonexistent_collection_xyz_test"),
).resolves.not.toThrow();
});
});
});