Skip to content
212 changes: 208 additions & 4 deletions src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ vitest.mock("../../../../i18n", () => ({
return key // Just return the key for other cases
},
}))
vitest.mock("path", () => ({
...vitest.importActual("path"),
sep: "/",
}))
vitest.mock("path", async () => {
const actual = await vitest.importActual("path")
return {
...actual,
sep: "/",
posix: actual.posix,
}
})

const mockQdrantClientInstance = {
getCollection: vitest.fn(),
Expand Down Expand Up @@ -1526,5 +1530,205 @@ 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: "src" },
},
],
}, // Should normalize "./src" to "src"
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"],
},
})
})
})
})
})
27 changes: 20 additions & 7 deletions src/services/code-index/vector-store/qdrant-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,26 @@ 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 = path.posix.normalize(directoryPrefix.replace(/\\/g, "/"))
// Note: path.posix.normalize("") returns ".", and normalize("./") returns "./"
if (normalizedPrefix === "." || normalizedPrefix === "./") {
// Don't create a filter - search entire workspace
filter = undefined
} else {
// Remove leading "./" from paths like "./src" to normalize them
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) => ({
key: `pathSegments.${index}`,
match: { value: segment },
})),
}
}
}
}

Expand Down
Loading