From 12b94e7c85864398494d1ca8dd6b95f1b8e0e69b Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 31 Jul 2025 21:53:22 +0000 Subject: [PATCH 1/7] fix: handle current directory path "." correctly in codebase_search tool - Fix path filtering logic in QdrantVectorStore.search() to properly handle current directory representations - When directoryPrefix is ".", "./", "", or similar, set filter to undefined to search entire workspace - Add comprehensive tests covering various current directory path formats including cross-platform support - Resolves issue where codebase_search with path="." returned no results Fixes #6514 --- .../__tests__/qdrant-client.spec.ts | 204 ++++++++++++++++++ .../code-index/vector-store/qdrant-client.ts | 22 +- 2 files changed, 219 insertions(+), 7 deletions(-) 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 e539c2edde..cba585f0bd 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 @@ -1526,5 +1526,209 @@ describe("QdrantVectorStore", () => { expect(callArgs.limit).toBe(DEFAULT_MAX_SEARCH_RESULTS) expect(callArgs.score_threshold).toBe(DEFAULT_SEARCH_MIN_SCORE) }) + + describe("current directory path handling", () => { + it("should not apply filter when directoryPrefix is '.'", async () => { + const queryVector = [0.1, 0.2, 0.3] + const directoryPrefix = "." + const mockQdrantResults = { + points: [ + { + id: "test-id-1", + score: 0.85, + payload: { + filePath: "src/test.ts", + codeChunk: "test code", + startLine: 1, + endLine: 5, + pathSegments: { "0": "src", "1": "test.ts" }, + }, + }, + ], + } + + mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults) + + const results = await vectorStore.search(queryVector, directoryPrefix) + + expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, { + query: queryVector, + filter: undefined, // Should be undefined for current directory + score_threshold: DEFAULT_SEARCH_MIN_SCORE, + limit: DEFAULT_MAX_SEARCH_RESULTS, + params: { + hnsw_ef: 128, + exact: false, + }, + with_payload: { + include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + }, + }) + + expect(results).toEqual(mockQdrantResults.points) + }) + + it("should not apply filter when directoryPrefix is './'", async () => { + const queryVector = [0.1, 0.2, 0.3] + const directoryPrefix = "./" + const mockQdrantResults = { points: [] } + + mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults) + + await vectorStore.search(queryVector, directoryPrefix) + + expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, { + query: queryVector, + filter: undefined, // Should be undefined for current directory + score_threshold: DEFAULT_SEARCH_MIN_SCORE, + limit: DEFAULT_MAX_SEARCH_RESULTS, + params: { + hnsw_ef: 128, + exact: false, + }, + with_payload: { + include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + }, + }) + }) + + it("should not apply filter when directoryPrefix is empty string", async () => { + const queryVector = [0.1, 0.2, 0.3] + const directoryPrefix = "" + const mockQdrantResults = { points: [] } + + mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults) + + await vectorStore.search(queryVector, directoryPrefix) + + expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, { + query: queryVector, + filter: undefined, // Should be undefined for empty string + score_threshold: DEFAULT_SEARCH_MIN_SCORE, + limit: DEFAULT_MAX_SEARCH_RESULTS, + params: { + hnsw_ef: 128, + exact: false, + }, + with_payload: { + include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + }, + }) + }) + + it("should not apply filter when directoryPrefix is '.\\' (Windows style)", async () => { + const queryVector = [0.1, 0.2, 0.3] + const directoryPrefix = ".\\" + const mockQdrantResults = { points: [] } + + mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults) + + await vectorStore.search(queryVector, directoryPrefix) + + expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, { + query: queryVector, + filter: undefined, // Should be undefined for Windows current directory + score_threshold: DEFAULT_SEARCH_MIN_SCORE, + limit: DEFAULT_MAX_SEARCH_RESULTS, + params: { + hnsw_ef: 128, + exact: false, + }, + with_payload: { + include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + }, + }) + }) + + it("should not apply filter when directoryPrefix has trailing slashes", async () => { + const queryVector = [0.1, 0.2, 0.3] + const directoryPrefix = ".///" + const mockQdrantResults = { points: [] } + + mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults) + + await vectorStore.search(queryVector, directoryPrefix) + + expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, { + query: queryVector, + filter: undefined, // Should be undefined after normalizing trailing slashes + score_threshold: DEFAULT_SEARCH_MIN_SCORE, + limit: DEFAULT_MAX_SEARCH_RESULTS, + params: { + hnsw_ef: 128, + exact: false, + }, + with_payload: { + include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + }, + }) + }) + + it("should still apply filter for relative paths like './src'", async () => { + const queryVector = [0.1, 0.2, 0.3] + const directoryPrefix = "./src" + const mockQdrantResults = { points: [] } + + mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults) + + await vectorStore.search(queryVector, directoryPrefix) + + expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, { + query: queryVector, + filter: { + must: [ + { + key: "pathSegments.0", + match: { value: "." }, + }, + { + key: "pathSegments.1", + match: { value: "src" }, + }, + ], + }, // Should still create filter for actual relative paths + score_threshold: DEFAULT_SEARCH_MIN_SCORE, + limit: DEFAULT_MAX_SEARCH_RESULTS, + params: { + hnsw_ef: 128, + exact: false, + }, + with_payload: { + include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + }, + }) + }) + + it("should still apply filter for regular directory paths", async () => { + const queryVector = [0.1, 0.2, 0.3] + const directoryPrefix = "src" + const mockQdrantResults = { points: [] } + + mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults) + + await vectorStore.search(queryVector, directoryPrefix) + + expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, { + query: queryVector, + filter: { + must: [ + { + key: "pathSegments.0", + match: { value: "src" }, + }, + ], + }, // Should still create filter for regular paths + score_threshold: DEFAULT_SEARCH_MIN_SCORE, + limit: DEFAULT_MAX_SEARCH_RESULTS, + params: { + hnsw_ef: 128, + exact: false, + }, + with_payload: { + include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + }, + }) + }) + }) }) }) diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 0218e37295..96dc545815 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -375,13 +375,21 @@ export class QdrantVectorStore implements IVectorStore { let filter = undefined if (directoryPrefix) { - const segments = directoryPrefix.split(path.sep).filter(Boolean) - - filter = { - must: segments.map((segment, index) => ({ - key: `pathSegments.${index}`, - match: { value: segment }, - })), + // Check if the path represents current directory + const normalizedPrefix = directoryPrefix.replace(/\\/g, "/").replace(/\/+$/, "") + if (normalizedPrefix === "." || normalizedPrefix === "" || normalizedPrefix === "./") { + // Don't create a filter - search entire workspace + filter = undefined + } else { + const segments = directoryPrefix.split(path.sep).filter(Boolean) + if (segments.length > 0) { + filter = { + must: segments.map((segment, index) => ({ + key: `pathSegments.${index}`, + match: { value: segment }, + })), + } + } } } From 11413fbb7dc2e6ace6ca23b57d97cf67e928f554 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 1 Aug 2025 18:22:18 -0500 Subject: [PATCH 2/7] fix: normalize directory prefix handling in Qdrant vector store --- src/services/code-index/vector-store/qdrant-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 96dc545815..6195327938 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -376,12 +376,12 @@ export class QdrantVectorStore implements IVectorStore { if (directoryPrefix) { // Check if the path represents current directory - const normalizedPrefix = directoryPrefix.replace(/\\/g, "/").replace(/\/+$/, "") + const normalizedPrefix = directoryPrefix.toPosix() if (normalizedPrefix === "." || normalizedPrefix === "" || normalizedPrefix === "./") { // Don't create a filter - search entire workspace filter = undefined } else { - const segments = directoryPrefix.split(path.sep).filter(Boolean) + const segments = normalizedPrefix.split("/").filter(Boolean) if (segments.length > 0) { filter = { must: segments.map((segment, index) => ({ From a26a5b4b571373275315df40f5c03d4ff5b7e40e Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 1 Aug 2025 18:29:22 -0500 Subject: [PATCH 3/7] fix: normalize paths starting with './' and fix OS-dependency issue - Use forward slash for splitting after toPosix() conversion - Remove leading './' from paths like './src' to normalize them to 'src' - Update test expectations to match correct behavior --- .../code-index/vector-store/__tests__/qdrant-client.spec.ts | 6 +----- src/services/code-index/vector-store/qdrant-client.ts | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) 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 cba585f0bd..4ae7f38501 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 @@ -1679,14 +1679,10 @@ describe("QdrantVectorStore", () => { must: [ { key: "pathSegments.0", - match: { value: "." }, - }, - { - key: "pathSegments.1", match: { value: "src" }, }, ], - }, // Should still create filter for actual relative paths + }, // Should normalize "./src" to "src" score_threshold: DEFAULT_SEARCH_MIN_SCORE, limit: DEFAULT_MAX_SEARCH_RESULTS, params: { diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 6195327938..31b9c97fb7 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -381,7 +381,11 @@ export class QdrantVectorStore implements IVectorStore { // Don't create a filter - search entire workspace filter = undefined } else { - const segments = normalizedPrefix.split("/").filter(Boolean) + // Remove leading "./" from paths like "./src" to normalize them + const cleanedPrefix = normalizedPrefix.startsWith("./") + ? normalizedPrefix.slice(2) + : normalizedPrefix + const segments = cleanedPrefix.split("/").filter(Boolean) if (segments.length > 0) { filter = { must: segments.map((segment, index) => ({ From 8e42821a7fbb4fae743243006e73dca6d9c1a18f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 4 Aug 2025 15:01:30 +0000 Subject: [PATCH 4/7] refactor: use path.posix.normalize instead of custom toPosix method - Replaced directoryPrefix.toPosix() with path.posix.normalize() - Added proper handling of backslashes before normalization - Updated test mock to include posix.normalize method - All tests passing (381 tests in code-index service) --- .../vector-store/__tests__/qdrant-client.spec.ts | 12 ++++++++++++ .../code-index/vector-store/qdrant-client.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) 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 4ae7f38501..64f7d6115e 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 @@ -24,6 +24,18 @@ vitest.mock("../../../../i18n", () => ({ vitest.mock("path", () => ({ ...vitest.importActual("path"), sep: "/", + posix: { + normalize: (p: string) => { + // Simple implementation of posix.normalize for testing + // Remove redundant slashes and handle . and .. segments + return ( + p + .split("/") + .filter((segment) => segment !== "" && segment !== ".") + .join("/") || "." + ) + }, + }, })) const mockQdrantClientInstance = { diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 31b9c97fb7..7d6a14cea8 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -376,7 +376,7 @@ export class QdrantVectorStore implements IVectorStore { if (directoryPrefix) { // Check if the path represents current directory - const normalizedPrefix = directoryPrefix.toPosix() + const normalizedPrefix = path.posix.normalize(directoryPrefix.replace(/\\/g, "/")) if (normalizedPrefix === "." || normalizedPrefix === "" || normalizedPrefix === "./") { // Don't create a filter - search entire workspace filter = undefined From 85cec2e3955838928b970e5f717ca1f7bc7ca173 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Mon, 4 Aug 2025 12:08:40 -0500 Subject: [PATCH 5/7] refactor: address review comments - improve path normalization - Keep check for './' after normalization as path.posix.normalize('./') returns './' - Use actual Node.js path.posix implementation in tests instead of custom mock - Apply path.posix.normalize to cleanedPrefix for consistency All 381 code-index tests pass --- .../__tests__/qdrant-client.spec.ts | 24 +++++++------------ .../code-index/vector-store/qdrant-client.ts | 4 ++-- 2 files changed, 10 insertions(+), 18 deletions(-) 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 64f7d6115e..822832d17c 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 @@ -21,22 +21,14 @@ vitest.mock("../../../../i18n", () => ({ return key // Just return the key for other cases }, })) -vitest.mock("path", () => ({ - ...vitest.importActual("path"), - sep: "/", - posix: { - normalize: (p: string) => { - // Simple implementation of posix.normalize for testing - // Remove redundant slashes and handle . and .. segments - return ( - p - .split("/") - .filter((segment) => segment !== "" && segment !== ".") - .join("/") || "." - ) - }, - }, -})) +vitest.mock("path", async () => { + const actual = await vitest.importActual("path") + return { + ...actual, + sep: "/", + posix: actual.posix, + } +}) const mockQdrantClientInstance = { getCollection: vitest.fn(), diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 7d6a14cea8..0ca1653564 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -377,7 +377,7 @@ export class QdrantVectorStore implements IVectorStore { if (directoryPrefix) { // Check if the path represents current directory const normalizedPrefix = path.posix.normalize(directoryPrefix.replace(/\\/g, "/")) - if (normalizedPrefix === "." || normalizedPrefix === "" || normalizedPrefix === "./") { + if (normalizedPrefix === "." || normalizedPrefix === "./" || normalizedPrefix === "") { // Don't create a filter - search entire workspace filter = undefined } else { @@ -385,7 +385,7 @@ export class QdrantVectorStore implements IVectorStore { const cleanedPrefix = normalizedPrefix.startsWith("./") ? normalizedPrefix.slice(2) : normalizedPrefix - const segments = cleanedPrefix.split("/").filter(Boolean) + const segments = path.posix.normalize(cleanedPrefix).split("/").filter(Boolean) if (segments.length > 0) { filter = { must: segments.map((segment, index) => ({ From b6bfdb68c6c903ad8def0cd60bd160ed3ca9ed88 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Tue, 5 Aug 2025 16:40:29 -0700 Subject: [PATCH 6/7] fix: apply path.posix.normalize when cleaning prefix to avoid redundant normalization Addresses review comment from @mrubens to normalize the path at line 385 instead of normalizing twice --- src/services/code-index/vector-store/qdrant-client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 0ca1653564..bdade8576c 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -382,10 +382,10 @@ export class QdrantVectorStore implements IVectorStore { filter = undefined } else { // Remove leading "./" from paths like "./src" to normalize them - const cleanedPrefix = normalizedPrefix.startsWith("./") - ? normalizedPrefix.slice(2) - : normalizedPrefix - const segments = path.posix.normalize(cleanedPrefix).split("/").filter(Boolean) + const cleanedPrefix = path.posix.normalize( + normalizedPrefix.startsWith("./") ? normalizedPrefix.slice(2) : normalizedPrefix, + ) + const segments = cleanedPrefix.split("/").filter(Boolean) if (segments.length > 0) { filter = { must: segments.map((segment, index) => ({ From 22beb640d92f1af330707cd4edd242d3782d31e8 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Tue, 5 Aug 2025 16:54:38 -0700 Subject: [PATCH 7/7] fix: correct current directory detection logic The issue was that the condition checked for an empty string after normalization, but path.posix.normalize('') actually returns '.', not ''. This caused the current directory check to fail when an empty string was passed. Removed the redundant empty string check since normalize('') returns '.' which is already handled by the first condition. --- src/services/code-index/vector-store/qdrant-client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index bdade8576c..50f39666c4 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -377,7 +377,8 @@ export class QdrantVectorStore implements IVectorStore { if (directoryPrefix) { // Check if the path represents current directory const normalizedPrefix = path.posix.normalize(directoryPrefix.replace(/\\/g, "/")) - if (normalizedPrefix === "." || normalizedPrefix === "./" || normalizedPrefix === "") { + // Note: path.posix.normalize("") returns ".", and normalize("./") returns "./" + if (normalizedPrefix === "." || normalizedPrefix === "./") { // Don't create a filter - search entire workspace filter = undefined } else {