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