diff --git a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts index 144571c28c..7ed9afb179 100644 --- a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts +++ b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts @@ -113,6 +113,7 @@ describe("QdrantVectorStore", () => { host: "qdrant.ashbyfam.com", https: true, port: 443, + prefix: undefined, // No prefix for root path apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -127,6 +128,7 @@ describe("QdrantVectorStore", () => { host: "example.com", https: true, port: 9000, + prefix: undefined, // No prefix for root path apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -145,6 +147,7 @@ describe("QdrantVectorStore", () => { host: "example.com", https: true, port: 443, + prefix: "/api/v1", // Should have prefix apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -161,6 +164,7 @@ describe("QdrantVectorStore", () => { host: "example.com", https: false, port: 80, + prefix: undefined, // No prefix for root path apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -175,6 +179,7 @@ describe("QdrantVectorStore", () => { host: "localhost", https: false, port: 8080, + prefix: undefined, // No prefix for root path apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -193,6 +198,7 @@ describe("QdrantVectorStore", () => { host: "example.com", https: false, port: 80, + prefix: "/api/v1", // Should have prefix apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -337,6 +343,159 @@ describe("QdrantVectorStore", () => { }) }) + describe("URL Prefix Handling", () => { + it("should pass the URL pathname as prefix to QdrantClient if not root", () => { + const vectorStoreWithPrefix = new QdrantVectorStore( + mockWorkspacePath, + "http://localhost:6333/some/path", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + prefix: "/some/path", + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreWithPrefix as any).qdrantUrl).toBe("http://localhost:6333/some/path") + }) + + it("should not pass prefix if the URL pathname is root ('/')", () => { + const vectorStoreWithoutPrefix = new QdrantVectorStore( + mockWorkspacePath, + "http://localhost:6333/", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + prefix: undefined, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreWithoutPrefix as any).qdrantUrl).toBe("http://localhost:6333/") + }) + + it("should handle HTTPS URL with path as prefix", () => { + const vectorStoreWithHttpsPrefix = new QdrantVectorStore( + mockWorkspacePath, + "https://qdrant.ashbyfam.com/api", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "qdrant.ashbyfam.com", + https: true, + port: 443, + prefix: "/api", + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreWithHttpsPrefix as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com/api") + }) + + it("should normalize URL pathname by removing trailing slash for prefix", () => { + const vectorStoreWithTrailingSlash = new QdrantVectorStore( + mockWorkspacePath, + "http://localhost:6333/api/", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + prefix: "/api", // Trailing slash should be removed + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreWithTrailingSlash as any).qdrantUrl).toBe("http://localhost:6333/api/") + }) + + it("should normalize URL pathname by removing multiple trailing slashes for prefix", () => { + const vectorStoreWithMultipleTrailingSlashes = new QdrantVectorStore( + mockWorkspacePath, + "http://localhost:6333/api///", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + prefix: "/api", // All trailing slashes should be removed + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreWithMultipleTrailingSlashes as any).qdrantUrl).toBe("http://localhost:6333/api///") + }) + + it("should handle multiple path segments correctly for prefix", () => { + const vectorStoreWithMultiSegment = new QdrantVectorStore( + mockWorkspacePath, + "http://localhost:6333/api/v1/qdrant", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + prefix: "/api/v1/qdrant", + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreWithMultiSegment as any).qdrantUrl).toBe("http://localhost:6333/api/v1/qdrant") + }) + + it("should handle complex URL with multiple segments, multiple trailing slashes, query params, and fragment", () => { + const complexUrl = "https://example.com/ollama/api/v1///?key=value#pos" + const vectorStoreComplex = new QdrantVectorStore(mockWorkspacePath, complexUrl, mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "example.com", + https: true, + port: 443, + prefix: "/ollama/api/v1", // Trailing slash removed, query/fragment ignored + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreComplex as any).qdrantUrl).toBe(complexUrl) + }) + + it("should ignore query parameters and fragments when determining prefix", () => { + const vectorStoreWithQueryParams = new QdrantVectorStore( + mockWorkspacePath, + "http://localhost:6333/api/path?key=value#fragment", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + prefix: "/api/path", // Query params and fragment should be ignored + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStoreWithQueryParams as any).qdrantUrl).toBe( + "http://localhost:6333/api/path?key=value#fragment", + ) + }) + }) + describe("initialize", () => { it("should create a new collection if none exists and return true", async () => { // Mock getCollection to throw a 404-like error diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 7f4a04ed44..c0f19af28b 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -57,6 +57,7 @@ export class QdrantVectorStore implements IVectorStore { host: urlObj.hostname, https: useHttps, port: port, + prefix: urlObj.pathname === "/" ? undefined : urlObj.pathname.replace(/\/+$/, ""), apiKey, headers: { "User-Agent": "Roo-Code", @@ -64,6 +65,7 @@ export class QdrantVectorStore implements IVectorStore { }) } catch (urlError) { // If URL parsing fails, fall back to URL-based config + // Note: This fallback won't correctly handle prefixes, but it's a last resort for malformed URLs. this.client = new QdrantClient({ url: parsedUrl, apiKey,