mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: improve Qdrant connection error handling for external drives
- Add detailed error logging for external drive paths detection - Improve error messages for Qdrant connection failures - Add pre-connection check to validate Qdrant accessibility - Handle Internal Server Error (500) responses with specific messages - Add comprehensive test coverage for new error scenarios Fixes #6717
This commit is contained in:
parent
d90bab71ff
commit
2ec84d9bb0
2 changed files with 362 additions and 49 deletions
|
|
@ -28,6 +28,7 @@ vitest.mock("path", () => ({
|
|||
|
||||
const mockQdrantClientInstance = {
|
||||
getCollection: vitest.fn(),
|
||||
getCollections: vitest.fn(),
|
||||
createCollection: vitest.fn(),
|
||||
deleteCollection: vitest.fn(),
|
||||
createPayloadIndex: vitest.fn(),
|
||||
|
|
@ -64,9 +65,19 @@ describe("QdrantVectorStore", () => {
|
|||
// Mock getWorkspacePath
|
||||
;(getWorkspacePath as any).mockReturnValue(mockWorkspacePath)
|
||||
|
||||
// Mock console methods
|
||||
vitest.spyOn(console, "log").mockImplementation(() => {})
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
vectorStore = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, mockVectorSize, mockApiKey)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore all mocks
|
||||
vitest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should correctly initialize QdrantClient and collectionName in constructor", () => {
|
||||
expect(QdrantClient).toHaveBeenCalledTimes(1)
|
||||
expect(QdrantClient).toHaveBeenCalledWith({
|
||||
|
|
@ -509,6 +520,11 @@ describe("QdrantVectorStore", () => {
|
|||
})
|
||||
|
||||
describe("initialize", () => {
|
||||
beforeEach(() => {
|
||||
// Default mock for getCollections to simulate successful connection
|
||||
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
|
||||
})
|
||||
|
||||
it("should create a new collection if none exists and return true", async () => {
|
||||
// Mock getCollection to throw a 404-like error
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
|
|
@ -521,6 +537,7 @@ describe("QdrantVectorStore", () => {
|
|||
const result = await vectorStore.initialize()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
|
||||
|
|
@ -592,11 +609,11 @@ describe("QdrantVectorStore", () => {
|
|||
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
|
||||
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
|
||||
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
|
||||
|
||||
const result = await vectorStore.initialize()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2) // Once to check, once to verify deletion
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
|
||||
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
|
||||
|
|
@ -617,12 +634,10 @@ describe("QdrantVectorStore", () => {
|
|||
})
|
||||
}
|
||||
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
|
||||
;(console.warn as any).mockRestore() // Restore console.warn
|
||||
})
|
||||
it("should log warning for non-404 errors but still create collection", async () => {
|
||||
const genericError = new Error("Generic Qdrant Error")
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue(genericError)
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
|
||||
|
||||
const result = await vectorStore.initialize()
|
||||
|
||||
|
|
@ -633,9 +648,10 @@ describe("QdrantVectorStore", () => {
|
|||
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Warning during getCollectionInfo for "${expectedCollectionName}"`),
|
||||
genericError.message,
|
||||
expect.objectContaining({
|
||||
message: genericError.message,
|
||||
}),
|
||||
)
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
it("should re-throw error from createCollection when no collection initially exists", async () => {
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
|
|
@ -644,19 +660,17 @@ describe("QdrantVectorStore", () => {
|
|||
})
|
||||
const createError = new Error("Create Collection Failed")
|
||||
mockQdrantClientInstance.createCollection.mockRejectedValue(createError)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {}) // Suppress console.error
|
||||
|
||||
// The actual error message includes the URL and error details
|
||||
await expect(vectorStore.initialize()).rejects.toThrow(
|
||||
/Failed to connect to Qdrant vector database|vectorStore\.qdrantConnectionFailed/,
|
||||
)
|
||||
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled()
|
||||
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled() // Should not be called if createCollection fails
|
||||
expect(console.error).toHaveBeenCalledTimes(1) // Only the outer try/catch
|
||||
;(console.error as any).mockRestore()
|
||||
})
|
||||
it("should log but not fail if payload index creation errors occur", async () => {
|
||||
// Mock successful collection creation
|
||||
|
|
@ -669,7 +683,6 @@ describe("QdrantVectorStore", () => {
|
|||
// Mock payload index creation to fail
|
||||
const indexError = new Error("Index creation failed")
|
||||
mockQdrantClientInstance.createPayloadIndex.mockRejectedValue(indexError)
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
|
||||
|
||||
const result = await vectorStore.initialize()
|
||||
|
||||
|
|
@ -688,8 +701,6 @@ describe("QdrantVectorStore", () => {
|
|||
indexError.message,
|
||||
)
|
||||
}
|
||||
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
|
||||
it("should throw vectorDimensionMismatch error when deleteCollection fails during recreation", async () => {
|
||||
|
|
@ -706,8 +717,6 @@ describe("QdrantVectorStore", () => {
|
|||
|
||||
const deleteError = new Error("Delete Collection Failed")
|
||||
mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
// The error should have a cause property set to the original error
|
||||
let caughtError: any
|
||||
|
|
@ -721,6 +730,7 @@ describe("QdrantVectorStore", () => {
|
|||
expect(caughtError.message).toContain("Failed to update vector index for new model")
|
||||
expect(caughtError.cause).toBe(deleteError)
|
||||
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.createCollection).not.toHaveBeenCalled()
|
||||
|
|
@ -728,8 +738,6 @@ describe("QdrantVectorStore", () => {
|
|||
// Should log both the warning and the critical error
|
||||
expect(console.warn).toHaveBeenCalledTimes(1)
|
||||
expect(console.error).toHaveBeenCalledTimes(2) // One for the critical error, one for the outer catch
|
||||
;(console.error as any).mockRestore()
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
|
||||
it("should throw vectorDimensionMismatch error when createCollection fails during recreation", async () => {
|
||||
|
|
@ -754,8 +762,6 @@ describe("QdrantVectorStore", () => {
|
|||
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
|
||||
const createError = new Error("Create Collection Failed")
|
||||
mockQdrantClientInstance.createCollection.mockRejectedValue(createError)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
// Should throw an error with cause property set to the original error
|
||||
let caughtError: any
|
||||
|
|
@ -769,6 +775,7 @@ describe("QdrantVectorStore", () => {
|
|||
expect(caughtError.message).toContain("Failed to update vector index for new model")
|
||||
expect(caughtError.cause).toBe(createError)
|
||||
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
|
||||
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
|
||||
|
|
@ -776,8 +783,6 @@ describe("QdrantVectorStore", () => {
|
|||
// Should log warning, critical error, and outer error
|
||||
expect(console.warn).toHaveBeenCalledTimes(1)
|
||||
expect(console.error).toHaveBeenCalledTimes(2)
|
||||
;(console.error as any).mockRestore()
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
|
||||
it("should verify collection deletion before proceeding with recreation", async () => {
|
||||
|
|
@ -801,17 +806,16 @@ describe("QdrantVectorStore", () => {
|
|||
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
|
||||
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
|
||||
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
const result = await vectorStore.initialize()
|
||||
|
||||
expect(result).toBe(true)
|
||||
// Should call getCollection twice: once to check existing, once to verify deletion
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
|
||||
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
|
||||
it("should throw error if collection still exists after deletion attempt", async () => {
|
||||
|
|
@ -838,8 +842,6 @@ describe("QdrantVectorStore", () => {
|
|||
} as any)
|
||||
|
||||
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
let caughtError: any
|
||||
try {
|
||||
|
|
@ -853,12 +855,11 @@ describe("QdrantVectorStore", () => {
|
|||
// The error message should contain the contextual error details
|
||||
expect(caughtError.message).toContain("Deleted existing collection but failed verification step")
|
||||
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
|
||||
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.createCollection).not.toHaveBeenCalled()
|
||||
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
|
||||
;(console.error as any).mockRestore()
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
|
||||
it("should handle dimension mismatch scenario from 2048 to 768 dimensions", async () => {
|
||||
|
|
@ -869,6 +870,10 @@ describe("QdrantVectorStore", () => {
|
|||
// Create a new vector store with the new dimension
|
||||
const newVectorStore = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, newVectorSize, mockApiKey)
|
||||
|
||||
// Reset mocks for new vector store
|
||||
vitest.clearAllMocks()
|
||||
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
|
||||
|
||||
mockQdrantClientInstance.getCollection
|
||||
.mockResolvedValueOnce({
|
||||
config: {
|
||||
|
|
@ -888,11 +893,11 @@ describe("QdrantVectorStore", () => {
|
|||
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
|
||||
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
|
||||
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
const result = await newVectorStore.initialize()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
|
||||
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(expectedCollectionName, {
|
||||
|
|
@ -902,7 +907,6 @@ describe("QdrantVectorStore", () => {
|
|||
},
|
||||
})
|
||||
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
|
||||
it("should provide detailed error context for different failure scenarios", async () => {
|
||||
|
|
@ -920,8 +924,6 @@ describe("QdrantVectorStore", () => {
|
|||
// Test deletion failure with specific error message
|
||||
const deleteError = new Error("Qdrant server unavailable")
|
||||
mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
let caughtError: any
|
||||
try {
|
||||
|
|
@ -936,8 +938,6 @@ describe("QdrantVectorStore", () => {
|
|||
expect(caughtError.message).toContain("Failed to delete existing collection with vector size")
|
||||
expect(caughtError.message).toContain("Qdrant server unavailable")
|
||||
expect(caughtError.cause).toBe(deleteError)
|
||||
;(console.error as any).mockRestore()
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -971,7 +971,6 @@ describe("QdrantVectorStore", () => {
|
|||
it("should return false and log warning for non-404 errors", async () => {
|
||||
const genericError = new Error("Network error")
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue(genericError)
|
||||
vitest.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
const result = await vectorStore.collectionExists()
|
||||
|
||||
|
|
@ -979,9 +978,10 @@ describe("QdrantVectorStore", () => {
|
|||
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Warning during getCollectionInfo for "${expectedCollectionName}"`),
|
||||
genericError.message,
|
||||
expect.objectContaining({
|
||||
message: genericError.message,
|
||||
}),
|
||||
)
|
||||
;(console.warn as any).mockRestore()
|
||||
})
|
||||
describe("deleteCollection", () => {
|
||||
it("should delete collection when it exists", async () => {
|
||||
|
|
@ -1010,7 +1010,6 @@ describe("QdrantVectorStore", () => {
|
|||
vitest.spyOn(vectorStore, "collectionExists").mockResolvedValue(true)
|
||||
const deleteError = new Error("Deletion failed")
|
||||
mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
await expect(vectorStore.deleteCollection()).rejects.toThrow(deleteError)
|
||||
|
||||
|
|
@ -1020,7 +1019,6 @@ describe("QdrantVectorStore", () => {
|
|||
`[QdrantVectorStore] Failed to delete collection ${expectedCollectionName}:`,
|
||||
deleteError,
|
||||
)
|
||||
;(console.error as any).mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1193,13 +1191,11 @@ describe("QdrantVectorStore", () => {
|
|||
|
||||
const upsertError = new Error("Upsert failed")
|
||||
mockQdrantClientInstance.upsert.mockRejectedValue(upsertError)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
await expect(vectorStore.upsertPoints(mockPoints)).rejects.toThrow(upsertError)
|
||||
|
||||
expect(mockQdrantClientInstance.upsert).toHaveBeenCalledTimes(1)
|
||||
expect(console.error).toHaveBeenCalledWith("Failed to upsert points:", upsertError)
|
||||
;(console.error as any).mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1505,13 +1501,11 @@ describe("QdrantVectorStore", () => {
|
|||
const queryVector = [0.1, 0.2, 0.3]
|
||||
const queryError = new Error("Query failed")
|
||||
mockQdrantClientInstance.query.mockRejectedValue(queryError)
|
||||
vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
await expect(vectorStore.search(queryVector)).rejects.toThrow(queryError)
|
||||
|
||||
expect(mockQdrantClientInstance.query).toHaveBeenCalledTimes(1)
|
||||
expect(console.error).toHaveBeenCalledWith("Failed to search points:", queryError)
|
||||
;(console.error as any).mockRestore()
|
||||
})
|
||||
|
||||
it("should use constants DEFAULT_MAX_SEARCH_RESULTS and DEFAULT_SEARCH_MIN_SCORE correctly", async () => {
|
||||
|
|
@ -1527,4 +1521,207 @@ describe("QdrantVectorStore", () => {
|
|||
expect(callArgs.score_threshold).toBe(DEFAULT_SEARCH_MIN_SCORE)
|
||||
})
|
||||
})
|
||||
|
||||
describe("External Drive Detection and Logging", () => {
|
||||
it("should detect and log macOS external drive paths", () => {
|
||||
const externalDrivePath = "/Volumes/External 1/Projects/Justus/School/mayicreatorz"
|
||||
const externalVectorStore = new QdrantVectorStore(externalDrivePath, mockQdrantUrl, mockVectorSize)
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
`[QdrantVectorStore] Initializing with workspace path: "${externalDrivePath}"`,
|
||||
)
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
`[QdrantVectorStore] Detected external drive path: "${externalDrivePath}"`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should detect and log Windows external drive paths", () => {
|
||||
const externalDrivePath = "D:\\Projects\\MyProject"
|
||||
const externalVectorStore = new QdrantVectorStore(externalDrivePath, mockQdrantUrl, mockVectorSize)
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
`[QdrantVectorStore] Detected external drive path: "${externalDrivePath}"`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should detect and log Linux mounted drive paths", () => {
|
||||
const mountedDrivePath = "/mnt/external/projects"
|
||||
const externalVectorStore = new QdrantVectorStore(mountedDrivePath, mockQdrantUrl, mockVectorSize)
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
`[QdrantVectorStore] Detected external drive path: "${mountedDrivePath}"`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should not log external drive detection for regular paths", () => {
|
||||
const regularPath = "/home/user/projects"
|
||||
const regularVectorStore = new QdrantVectorStore(regularPath, mockQdrantUrl, mockVectorSize)
|
||||
|
||||
expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining("Detected external drive path"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("Enhanced Error Handling", () => {
|
||||
it("should check Qdrant connectivity before collection operations", async () => {
|
||||
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
response: { status: 404 },
|
||||
message: "Not found",
|
||||
})
|
||||
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
|
||||
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
expect(mockQdrantClientInstance.getCollections).toHaveBeenCalledTimes(1)
|
||||
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("Successfully connected to Qdrant"))
|
||||
})
|
||||
|
||||
it("should handle connection errors with detailed logging", async () => {
|
||||
const connectionError = {
|
||||
message: "ECONNREFUSED",
|
||||
code: "ECONNREFUSED",
|
||||
statusCode: undefined,
|
||||
}
|
||||
mockQdrantClientInstance.getCollections.mockRejectedValue(connectionError)
|
||||
|
||||
await expect(vectorStore.initialize()).rejects.toThrow(/Unable to connect to Qdrant/)
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to connect to Qdrant"),
|
||||
expect.objectContaining({
|
||||
error: "ECONNREFUSED",
|
||||
code: "ECONNREFUSED",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle Qdrant Internal Server Error (500) with specific message", async () => {
|
||||
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
|
||||
const internalServerError = {
|
||||
message: "Internal Server Error",
|
||||
statusCode: 500,
|
||||
response: {
|
||||
status: 500,
|
||||
data: {
|
||||
status: {
|
||||
error: "Invalid collection configuration",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
// First call returns the error, second call (for creation) should also fail
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue(internalServerError)
|
||||
mockQdrantClientInstance.createCollection.mockRejectedValue(internalServerError)
|
||||
|
||||
await expect(vectorStore.initialize()).rejects.toThrow(
|
||||
/Qdrant returned Internal Server Error.*Invalid collection configuration/,
|
||||
)
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Qdrant returned Internal Server Error (500)"),
|
||||
expect.objectContaining({
|
||||
collectionName: expectedCollectionName,
|
||||
workspacePath: mockWorkspacePath,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should log detailed error information for initialization failures", async () => {
|
||||
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
|
||||
const genericError = new Error("Some generic error")
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue(genericError)
|
||||
mockQdrantClientInstance.createCollection.mockRejectedValue(genericError)
|
||||
|
||||
await expect(vectorStore.initialize()).rejects.toThrow()
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to initialize Qdrant collection"),
|
||||
expect.objectContaining({
|
||||
message: "Some generic error",
|
||||
collectionName: expectedCollectionName,
|
||||
qdrantUrl: mockQdrantUrl,
|
||||
workspacePath: mockWorkspacePath,
|
||||
vectorSize: mockVectorSize,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle getCollectionInfo errors with proper status code detection", async () => {
|
||||
const error404 = {
|
||||
message: "Not found",
|
||||
statusCode: 404,
|
||||
}
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue(error404)
|
||||
|
||||
const result = await (vectorStore as any).getCollectionInfo()
|
||||
|
||||
expect(result).toBeNull()
|
||||
// Should not log warning for 404 errors
|
||||
expect(console.warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should log warnings for non-404 getCollectionInfo errors", async () => {
|
||||
const error500 = {
|
||||
message: "Internal error",
|
||||
statusCode: 500,
|
||||
response: {
|
||||
status: 500,
|
||||
data: { error: "Database error" },
|
||||
},
|
||||
}
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue(error500)
|
||||
|
||||
const result = await (vectorStore as any).getCollectionInfo()
|
||||
|
||||
expect(result).toBeNull()
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Warning during getCollectionInfo"),
|
||||
expect.objectContaining({
|
||||
message: "Internal error",
|
||||
statusCode: 500,
|
||||
collectionName: expectedCollectionName,
|
||||
qdrantUrl: mockQdrantUrl,
|
||||
workspacePath: mockWorkspacePath,
|
||||
responseData: { error: "Database error" },
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Client Configuration Logging", () => {
|
||||
it("should log Qdrant client configuration details", () => {
|
||||
const testUrl = "https://qdrant.example.com:6334/api/v1"
|
||||
const testVectorStore = new QdrantVectorStore(mockWorkspacePath, testUrl, mockVectorSize, mockApiKey)
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
`[QdrantVectorStore] Creating Qdrant client with config:`,
|
||||
expect.objectContaining({
|
||||
host: "qdrant.example.com",
|
||||
port: 6334,
|
||||
https: true,
|
||||
hasApiKey: true,
|
||||
prefix: "/api/v1",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should log error when URL parsing fails", () => {
|
||||
// Force URL parsing to fail by mocking URL constructor
|
||||
const originalURL = global.URL
|
||||
global.URL = vitest.fn().mockImplementation(() => {
|
||||
throw new Error("Invalid URL")
|
||||
}) as any
|
||||
|
||||
const invalidUrlStore = new QdrantVectorStore(mockWorkspacePath, "invalid://url", mockVectorSize)
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse URL"),
|
||||
expect.any(Error),
|
||||
)
|
||||
|
||||
// Restore original URL
|
||||
global.URL = originalURL
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,13 +23,32 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
* @param workspacePath Path to the workspace
|
||||
* @param url Optional URL to the Qdrant server
|
||||
*/
|
||||
constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) {
|
||||
constructor(
|
||||
private readonly workspacePath: string,
|
||||
url: string,
|
||||
vectorSize: number,
|
||||
apiKey?: string,
|
||||
) {
|
||||
// Parse the URL to determine the appropriate QdrantClient configuration
|
||||
const parsedUrl = this.parseQdrantUrl(url)
|
||||
|
||||
// Store the resolved URL for our property
|
||||
this.qdrantUrl = parsedUrl
|
||||
|
||||
// Log workspace path for debugging external drive issues
|
||||
console.log(`[QdrantVectorStore] Initializing with workspace path: "${workspacePath}"`)
|
||||
|
||||
// Check if workspace path is on an external drive (common patterns)
|
||||
const isExternalDrive =
|
||||
workspacePath.includes("/Volumes/") || // macOS external drives
|
||||
(workspacePath.match(/^[A-Z]:\\/i) && !workspacePath.match(/^C:\\/i)) || // Windows non-C drives
|
||||
workspacePath.includes("/mnt/") || // Linux mounted drives
|
||||
workspacePath.includes("/media/") // Linux media drives
|
||||
|
||||
if (isExternalDrive) {
|
||||
console.log(`[QdrantVectorStore] Detected external drive path: "${workspacePath}"`)
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(parsedUrl)
|
||||
|
||||
|
|
@ -53,7 +72,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
}
|
||||
}
|
||||
|
||||
this.client = new QdrantClient({
|
||||
const clientConfig = {
|
||||
host: urlObj.hostname,
|
||||
https: useHttps,
|
||||
port: port,
|
||||
|
|
@ -62,8 +81,19 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
headers: {
|
||||
"User-Agent": "Roo-Code",
|
||||
},
|
||||
}
|
||||
|
||||
console.log(`[QdrantVectorStore] Creating Qdrant client with config:`, {
|
||||
host: clientConfig.host,
|
||||
port: clientConfig.port,
|
||||
https: clientConfig.https,
|
||||
hasApiKey: !!clientConfig.apiKey,
|
||||
prefix: clientConfig.prefix,
|
||||
})
|
||||
|
||||
this.client = new QdrantClient(clientConfig)
|
||||
} catch (urlError) {
|
||||
console.error(`[QdrantVectorStore] Failed to parse URL "${parsedUrl}":`, 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({
|
||||
|
|
@ -79,6 +109,8 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
const hash = createHash("sha256").update(workspacePath).digest("hex")
|
||||
this.vectorSize = vectorSize
|
||||
this.collectionName = `ws-${hash.substring(0, 16)}`
|
||||
|
||||
console.log(`[QdrantVectorStore] Generated collection name: "${this.collectionName}" from workspace path`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -129,13 +161,27 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
try {
|
||||
const collectionInfo = await this.client.getCollection(this.collectionName)
|
||||
return collectionInfo
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.warn(
|
||||
`[QdrantVectorStore] Warning during getCollectionInfo for "${this.collectionName}". Collection may not exist or another error occurred:`,
|
||||
error.message,
|
||||
)
|
||||
} catch (error: any) {
|
||||
// Check if it's a 404 (collection not found) - this is expected for new collections
|
||||
const statusCode = error?.statusCode || error?.response?.status || error?.status
|
||||
if (statusCode === 404) {
|
||||
// This is normal - collection doesn't exist yet
|
||||
return null
|
||||
}
|
||||
|
||||
// For other errors, log more details
|
||||
// Log more details for debugging external drive issues
|
||||
console.warn(
|
||||
`[QdrantVectorStore] Warning during getCollectionInfo for "${this.collectionName}". Collection may not exist or another error occurred:`,
|
||||
{
|
||||
message: error?.message || String(error),
|
||||
statusCode: statusCode,
|
||||
collectionName: this.collectionName,
|
||||
qdrantUrl: this.qdrantUrl,
|
||||
workspacePath: this.workspacePath,
|
||||
responseData: error?.response?.data,
|
||||
},
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -147,10 +193,30 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
async initialize(): Promise<boolean> {
|
||||
let created = false
|
||||
try {
|
||||
console.log(`[QdrantVectorStore] Starting initialization for collection "${this.collectionName}"`)
|
||||
|
||||
// First, try to check if Qdrant is accessible
|
||||
try {
|
||||
const collections = await this.client.getCollections()
|
||||
console.log(
|
||||
`[QdrantVectorStore] Successfully connected to Qdrant. Found ${collections.collections?.length || 0} collections`,
|
||||
)
|
||||
} catch (connectionError: any) {
|
||||
console.error(`[QdrantVectorStore] Failed to connect to Qdrant at ${this.qdrantUrl}:`, {
|
||||
error: connectionError.message,
|
||||
code: connectionError.code,
|
||||
statusCode: connectionError.statusCode || connectionError.response?.status,
|
||||
})
|
||||
throw connectionError
|
||||
}
|
||||
|
||||
const collectionInfo = await this.getCollectionInfo()
|
||||
|
||||
if (collectionInfo === null) {
|
||||
// Collection info not retrieved (assume not found or inaccessible), create it
|
||||
console.log(
|
||||
`[QdrantVectorStore] Collection "${this.collectionName}" not found, creating new collection`,
|
||||
)
|
||||
await this.client.createCollection(this.collectionName, {
|
||||
vectors: {
|
||||
size: this.vectorSize,
|
||||
|
|
@ -158,6 +224,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
},
|
||||
})
|
||||
created = true
|
||||
console.log(`[QdrantVectorStore] Successfully created collection "${this.collectionName}"`)
|
||||
} else {
|
||||
// Collection exists, check vector size
|
||||
const vectorsConfig = collectionInfo.config?.params?.vectors
|
||||
|
|
@ -189,9 +256,19 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
return created
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || error
|
||||
const errorDetails = {
|
||||
message: errorMessage,
|
||||
collectionName: this.collectionName,
|
||||
qdrantUrl: this.qdrantUrl,
|
||||
workspacePath: this.workspacePath,
|
||||
vectorSize: this.vectorSize,
|
||||
statusCode: error?.statusCode || error?.response?.status,
|
||||
responseData: error?.response?.data,
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[QdrantVectorStore] Failed to initialize Qdrant collection "${this.collectionName}":`,
|
||||
errorMessage,
|
||||
errorDetails,
|
||||
)
|
||||
|
||||
// If this is already a vector dimension mismatch error (identified by cause), re-throw it as-is
|
||||
|
|
@ -199,6 +276,45 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
throw error
|
||||
}
|
||||
|
||||
// Check if this is a connection error
|
||||
const isConnectionError =
|
||||
errorMessage?.toLowerCase().includes("econnrefused") ||
|
||||
errorMessage?.toLowerCase().includes("connect") ||
|
||||
errorMessage?.toLowerCase().includes("network") ||
|
||||
error?.code === "ECONNREFUSED"
|
||||
|
||||
// Check for specific Qdrant errors
|
||||
const statusCode = error?.statusCode || error?.response?.status || error?.status
|
||||
const responseBody = error?.response?.data || error?.data || error?.body
|
||||
|
||||
// Handle specific status codes
|
||||
if (statusCode === 500 || errorMessage?.toLowerCase().includes("internal server error")) {
|
||||
// This is the error from the issue - Qdrant returned 500
|
||||
const detailedMessage = responseBody?.status?.error || responseBody?.error || errorMessage
|
||||
console.error(`[QdrantVectorStore] Qdrant returned Internal Server Error (500):`, {
|
||||
responseBody,
|
||||
collectionName: this.collectionName,
|
||||
workspacePath: this.workspacePath,
|
||||
})
|
||||
|
||||
throw new Error(
|
||||
t("embeddings:vectorStore.qdrantConnectionFailed", {
|
||||
qdrantUrl: this.qdrantUrl,
|
||||
errorMessage: `Qdrant returned Internal Server Error. This may be due to invalid collection name or configuration. Details: ${detailedMessage}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Provide more specific error messages based on the error type
|
||||
if (isConnectionError) {
|
||||
throw new Error(
|
||||
t("embeddings:vectorStore.qdrantConnectionFailed", {
|
||||
qdrantUrl: this.qdrantUrl,
|
||||
errorMessage: `Unable to connect to Qdrant. Please ensure Qdrant is running and accessible at ${this.qdrantUrl}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Otherwise, provide a more user-friendly error message that includes the original error
|
||||
throw new Error(
|
||||
t("embeddings:vectorStore.qdrantConnectionFailed", { qdrantUrl: this.qdrantUrl, errorMessage }),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue