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 e2df39b7cbe..144571c28c5 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 @@ -58,7 +58,9 @@ describe("QdrantVectorStore", () => { it("should correctly initialize QdrantClient and collectionName in constructor", () => { expect(QdrantClient).toHaveBeenCalledTimes(1) expect(QdrantClient).toHaveBeenCalledWith({ - url: mockQdrantUrl, + host: "mock-qdrant", + https: false, + port: 6333, apiKey: mockApiKey, headers: { "User-Agent": "Roo-Code", @@ -75,7 +77,9 @@ describe("QdrantVectorStore", () => { const vectorStoreWithDefaults = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize) expect(QdrantClient).toHaveBeenLastCalledWith({ - url: "http://localhost:6333", // Should use default QDRANT_URL + host: "localhost", + https: false, + port: 6333, apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -87,7 +91,9 @@ describe("QdrantVectorStore", () => { const vectorStoreWithoutKey = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, mockVectorSize) expect(QdrantClient).toHaveBeenLastCalledWith({ - url: mockQdrantUrl, + host: "mock-qdrant", + https: false, + port: 6333, apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -95,6 +101,242 @@ describe("QdrantVectorStore", () => { }) }) + describe("URL Parsing and Explicit Port Handling", () => { + describe("HTTPS URL handling", () => { + it("should use explicit port 443 for HTTPS URLs without port (fixes the main bug)", () => { + const vectorStore = new QdrantVectorStore( + mockWorkspacePath, + "https://qdrant.ashbyfam.com", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "qdrant.ashbyfam.com", + https: true, + port: 443, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com") + }) + + it("should use explicit port for HTTPS URLs with explicit port", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "https://example.com:9000", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "example.com", + https: true, + port: 9000, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("https://example.com:9000") + }) + + it("should use port 443 for HTTPS URLs with paths and query parameters", () => { + const vectorStore = new QdrantVectorStore( + mockWorkspacePath, + "https://example.com/api/v1?key=value", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "example.com", + https: true, + port: 443, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("https://example.com/api/v1?key=value") + }) + }) + + describe("HTTP URL handling", () => { + it("should use explicit port 80 for HTTP URLs without port", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://example.com", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "example.com", + https: false, + port: 80, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://example.com") + }) + + it("should use explicit port for HTTP URLs with explicit port", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:8080", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 8080, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://localhost:8080") + }) + + it("should use port 80 for HTTP URLs while preserving paths and query parameters", () => { + const vectorStore = new QdrantVectorStore( + mockWorkspacePath, + "http://example.com/api/v1?key=value", + mockVectorSize, + ) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "example.com", + https: false, + port: 80, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://example.com/api/v1?key=value") + }) + }) + + describe("Hostname handling", () => { + it("should convert hostname to http with port 80", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "qdrant.example.com", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "qdrant.example.com", + https: false, + port: 80, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://qdrant.example.com") + }) + + it("should handle hostname:port format with explicit port", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "localhost:6333", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") + }) + + it("should handle explicit HTTP URLs correctly", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:9000", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 9000, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://localhost:9000") + }) + }) + + describe("IP address handling", () => { + it("should convert IP address to http with port 80", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "192.168.1.100", + https: false, + port: 80, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100") + }) + + it("should handle IP:port format with explicit port", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100:6333", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "192.168.1.100", + https: false, + port: 6333, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100:6333") + }) + }) + + describe("Edge cases", () => { + it("should handle undefined URL with host-based config", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") + }) + + it("should handle empty string URL with host-based config", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") + }) + + it("should handle whitespace-only URL with host-based config", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, " ", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "localhost", + https: false, + port: 6333, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") + }) + }) + + describe("Invalid URL fallback", () => { + it("should treat invalid URLs as hostnames with port 80", () => { + const vectorStore = new QdrantVectorStore(mockWorkspacePath, "invalid-url-format", mockVectorSize) + expect(QdrantClient).toHaveBeenLastCalledWith({ + host: "invalid-url-format", + https: false, + port: 80, + apiKey: undefined, + headers: { + "User-Agent": "Roo-Code", + }, + }) + expect((vectorStore as any).qdrantUrl).toBe("http://invalid-url-format") + }) + }) + }) + 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 d4ae2e4588d..7f4a04ed44f 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -24,14 +24,54 @@ export class QdrantVectorStore implements IVectorStore { * @param url Optional URL to the Qdrant server */ constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) { - this.qdrantUrl = url || "http://localhost:6333" - this.client = new QdrantClient({ - url: this.qdrantUrl, - apiKey, - headers: { - "User-Agent": "Roo-Code", - }, - }) + // Parse the URL to determine the appropriate QdrantClient configuration + const parsedUrl = this.parseQdrantUrl(url) + + // Store the resolved URL for our property + this.qdrantUrl = parsedUrl + + try { + const urlObj = new URL(parsedUrl) + + // Always use host-based configuration with explicit ports to avoid QdrantClient defaults + let port: number + let useHttps: boolean + + if (urlObj.port) { + // Explicit port specified - use it and determine protocol + port = Number(urlObj.port) + useHttps = urlObj.protocol === "https:" + } else { + // No explicit port - use protocol defaults + if (urlObj.protocol === "https:") { + port = 443 + useHttps = true + } else { + // http: or other protocols default to port 80 + port = 80 + useHttps = false + } + } + + this.client = new QdrantClient({ + host: urlObj.hostname, + https: useHttps, + port: port, + apiKey, + headers: { + "User-Agent": "Roo-Code", + }, + }) + } catch (urlError) { + // If URL parsing fails, fall back to URL-based config + this.client = new QdrantClient({ + url: parsedUrl, + apiKey, + headers: { + "User-Agent": "Roo-Code", + }, + }) + } // Generate collection name from workspace path const hash = createHash("sha256").update(workspacePath).digest("hex") @@ -39,6 +79,50 @@ export class QdrantVectorStore implements IVectorStore { this.collectionName = `ws-${hash.substring(0, 16)}` } + /** + * Parses and normalizes Qdrant server URLs to handle various input formats + * @param url Raw URL input from user + * @returns Properly formatted URL for QdrantClient + */ + private parseQdrantUrl(url: string | undefined): string { + // Handle undefined/null/empty cases + if (!url || url.trim() === "") { + return "http://localhost:6333" + } + + const trimmedUrl = url.trim() + + // Check if it starts with a protocol + if (!trimmedUrl.startsWith("http://") && !trimmedUrl.startsWith("https://") && !trimmedUrl.includes("://")) { + // No protocol - treat as hostname + return this.parseHostname(trimmedUrl) + } + + try { + // Attempt to parse as complete URL - return as-is, let constructor handle ports + const parsedUrl = new URL(trimmedUrl) + return trimmedUrl + } catch { + // Failed to parse as URL - treat as hostname + return this.parseHostname(trimmedUrl) + } + } + + /** + * Handles hostname-only inputs + * @param hostname Raw hostname input + * @returns Properly formatted URL with http:// prefix + */ + private parseHostname(hostname: string): string { + if (hostname.includes(":")) { + // Has port - add http:// prefix if missing + return hostname.startsWith("http") ? hostname : `http://${hostname}` + } else { + // No port - add http:// prefix without port (let constructor handle port assignment) + return `http://${hostname}` + } + } + private async getCollectionInfo(): Promise { try { const collectionInfo = await this.client.getCollection(this.collectionName)