Skip to content

Commit 6408277

Browse files
committed
Expanded test isolation for branches.
Adds comprehensive tests to verify branch isolation: Improves test coverage for separate collections per branch Ensures search results originate only from the active branch Verifies correct data persistence when switching branches Implements additional cleanup logic between tests These enhancements ensure reliable separation of vector indexes across development branches.
1 parent 9fee98a commit 6408277

File tree

1 file changed

+229
-2
lines changed

1 file changed

+229
-2
lines changed

src/services/code-index/vector-store/__tests__/qdrant-client.branch-isolation.spec.ts

Lines changed: 229 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ describe("QdrantVectorStore - Branch Isolation", () => {
2727
// Setup mock Qdrant client
2828
mockQdrantClient = {
2929
getCollections: vi.fn().mockResolvedValue({ collections: [] }),
30-
getCollection: vi.fn().mockResolvedValue(null),
30+
getCollection: vi.fn().mockResolvedValue({ vectors_count: 1 }),
3131
createCollection: vi.fn().mockResolvedValue(true),
3232
deleteCollection: vi.fn().mockResolvedValue(true),
3333
upsert: vi.fn().mockResolvedValue({ status: "completed" }),
3434
search: vi.fn().mockResolvedValue([]),
35+
query: vi.fn().mockResolvedValue({ points: [] }),
3536
delete: vi.fn().mockResolvedValue({ status: "completed" }),
3637
}
3738

@@ -93,6 +94,28 @@ describe("QdrantVectorStore - Branch Isolation", () => {
9394
})
9495

9596
describe("collection naming with branch isolation", () => {
97+
beforeEach(() => {
98+
// Clear all mocks before each test in this suite to prevent cache pollution
99+
vi.clearAllMocks()
100+
mockedGetCurrentBranch.mockClear()
101+
102+
// Ensure vectorStore is undefined to force new instance creation
103+
vectorStore = undefined as any
104+
105+
// Reset the mock Qdrant client to ensure clean state for each test
106+
mockQdrantClient = {
107+
getCollections: vi.fn().mockResolvedValue({ collections: [] }),
108+
getCollection: vi.fn().mockResolvedValue(null),
109+
createCollection: vi.fn().mockResolvedValue(true),
110+
deleteCollection: vi.fn().mockResolvedValue(true),
111+
upsert: vi.fn().mockResolvedValue({ status: "completed" }),
112+
search: vi.fn().mockResolvedValue([]),
113+
query: vi.fn().mockResolvedValue({ points: [] }),
114+
delete: vi.fn().mockResolvedValue({ status: "completed" }),
115+
}
116+
vi.mocked(QdrantClient).mockImplementation(() => mockQdrantClient)
117+
})
118+
96119
it("should create branch-specific collection name when branch is provided", async () => {
97120
mockedGetCurrentBranch.mockResolvedValue("feature-branch")
98121

@@ -167,6 +190,10 @@ describe("QdrantVectorStore - Branch Isolation", () => {
167190
})
168191

169192
it("should handle detached HEAD (undefined branch)", async () => {
193+
// Clear any cached branch from previous tests and reset mock
194+
mockedGetCurrentBranch.mockClear()
195+
mockedGetCurrentBranch.mockResolvedValue(undefined as any)
196+
170197
vectorStore = new QdrantVectorStore(
171198
testWorkspacePath,
172199
testQdrantUrl,
@@ -248,7 +275,7 @@ describe("QdrantVectorStore - Branch Isolation", () => {
248275
})
249276

250277
describe("getCurrentBranch method", () => {
251-
it("should return current branch when branch isolation is enabled", () => {
278+
it("should return current branch when branch isolation is enabled", async () => {
252279
vectorStore = new QdrantVectorStore(
253280
testWorkspacePath,
254281
testQdrantUrl,
@@ -258,6 +285,10 @@ describe("QdrantVectorStore - Branch Isolation", () => {
258285
"main",
259286
)
260287

288+
// Need to initialize to set currentBranch
289+
mockQdrantClient.getCollection.mockResolvedValue({ vectors_count: 0 })
290+
await vectorStore.initialize()
291+
261292
expect(vectorStore.getCurrentBranch()).toBe("main")
262293
})
263294

@@ -326,4 +357,200 @@ describe("QdrantVectorStore - Branch Isolation", () => {
326357
expect(mockedGetCurrentBranch).toHaveBeenCalledWith(testWorkspacePath)
327358
})
328359
})
360+
361+
describe("cross-branch search isolation", () => {
362+
it("should not return results from other branch collections when searching", async () => {
363+
// Setup: Create vector store on main branch
364+
mockedGetCurrentBranch.mockResolvedValue("main")
365+
vectorStore = new QdrantVectorStore(
366+
testWorkspacePath,
367+
testQdrantUrl,
368+
testVectorSize,
369+
undefined,
370+
true,
371+
"main",
372+
)
373+
374+
// Mock collection doesn't exist initially, then exists after creation
375+
mockQdrantClient.getCollection.mockResolvedValueOnce(null)
376+
mockQdrantClient.getCollection.mockResolvedValue({ vectors_count: 1 })
377+
await vectorStore.initialize()
378+
379+
// Capture the collection name used for main branch
380+
const mainCollectionCall = mockQdrantClient.createCollection.mock.calls[0]
381+
const mainCollectionName = mainCollectionCall[0]
382+
expect(mainCollectionName).toMatch(/^ws-[a-f0-9]+-br-main$/)
383+
384+
// Index documents on main branch
385+
const mainDocs = [
386+
{
387+
id: "main-doc-1",
388+
vector: [1, 0, 0],
389+
payload: { path: "main.ts", content: "main branch code" },
390+
},
391+
]
392+
await vectorStore.upsertPoints(mainDocs)
393+
394+
// Verify upsert was called with main collection
395+
expect(mockQdrantClient.upsert).toHaveBeenCalledWith(
396+
mainCollectionName,
397+
expect.objectContaining({
398+
points: expect.arrayContaining([
399+
expect.objectContaining({
400+
id: "main-doc-1",
401+
payload: expect.objectContaining({ path: "main.ts" }),
402+
}),
403+
]),
404+
}),
405+
)
406+
407+
// Switch to feature branch
408+
vi.clearAllMocks()
409+
vectorStore.invalidateBranchCache()
410+
mockedGetCurrentBranch.mockResolvedValue("feature-branch")
411+
412+
// Re-initialize for feature branch - collection doesn't exist, then exists
413+
mockQdrantClient.getCollection.mockResolvedValueOnce(null)
414+
mockQdrantClient.getCollection.mockResolvedValue({ vectors_count: 1 })
415+
await vectorStore.initialize()
416+
417+
// Capture the collection name used for feature branch
418+
const featureCollectionCall = mockQdrantClient.createCollection.mock.calls[0]
419+
const featureCollectionName = featureCollectionCall[0]
420+
expect(featureCollectionName).toMatch(/^ws-[a-f0-9]+-br-feature-branch$/)
421+
422+
// Verify different collection names
423+
expect(featureCollectionName).not.toBe(mainCollectionName)
424+
425+
// Index different documents on feature branch
426+
const featureDocs = [
427+
{
428+
id: "feature-doc-1",
429+
vector: [0, 1, 0],
430+
payload: { path: "feature.ts", content: "feature branch code" },
431+
},
432+
]
433+
await vectorStore.upsertPoints(featureDocs)
434+
435+
// Verify upsert was called with feature collection
436+
expect(mockQdrantClient.upsert).toHaveBeenCalledWith(
437+
featureCollectionName,
438+
expect.objectContaining({
439+
points: expect.arrayContaining([
440+
expect.objectContaining({
441+
id: "feature-doc-1",
442+
payload: expect.objectContaining({ path: "feature.ts" }),
443+
}),
444+
]),
445+
}),
446+
)
447+
448+
// Mock search results - feature branch should only return feature docs
449+
mockQdrantClient.query.mockResolvedValue({
450+
points: [
451+
{
452+
id: "feature-doc-1",
453+
score: 0.95,
454+
payload: {
455+
filePath: "feature.ts",
456+
codeChunk: "feature branch code",
457+
startLine: 1,
458+
endLine: 10,
459+
},
460+
},
461+
],
462+
})
463+
464+
// Search on feature branch
465+
const searchResults = await vectorStore.search([0, 1, 0])
466+
467+
// Verify search was called with feature collection, not main
468+
expect(mockQdrantClient.query).toHaveBeenCalledWith(
469+
featureCollectionName,
470+
expect.objectContaining({
471+
query: [0, 1, 0],
472+
}),
473+
)
474+
475+
// Verify results are from feature branch only
476+
expect(searchResults).toHaveLength(1)
477+
expect(searchResults[0].payload.filePath).toBe("feature.ts")
478+
expect(searchResults[0].payload.codeChunk).toBe("feature branch code")
479+
480+
// Verify main branch document is NOT in results
481+
expect(searchResults).not.toContainEqual(
482+
expect.objectContaining({
483+
payload: expect.objectContaining({ filePath: "main.ts" }),
484+
}),
485+
)
486+
})
487+
488+
it("should maintain separate indexes when switching back to previous branch", async () => {
489+
// Start on main branch
490+
mockedGetCurrentBranch.mockResolvedValue("main")
491+
vectorStore = new QdrantVectorStore(
492+
testWorkspacePath,
493+
testQdrantUrl,
494+
testVectorSize,
495+
undefined,
496+
true,
497+
"main",
498+
)
499+
500+
// Collection doesn't exist initially, then exists after creation
501+
mockQdrantClient.getCollection.mockResolvedValueOnce(null)
502+
mockQdrantClient.getCollection.mockResolvedValue({ vectors_count: 1 })
503+
await vectorStore.initialize()
504+
const mainCollectionName = mockQdrantClient.createCollection.mock.calls[0][0]
505+
506+
// Index on main
507+
await vectorStore.upsertPoints([{ id: "main-1", vector: [1, 0, 0], payload: { path: "main.ts" } }])
508+
509+
// Switch to feature branch
510+
vi.clearAllMocks()
511+
vectorStore.invalidateBranchCache()
512+
mockedGetCurrentBranch.mockResolvedValue("feature")
513+
// Collection doesn't exist initially, then exists after creation
514+
mockQdrantClient.getCollection.mockResolvedValueOnce(null)
515+
mockQdrantClient.getCollection.mockResolvedValue({ vectors_count: 1 })
516+
await vectorStore.initialize()
517+
const featureCollectionName = mockQdrantClient.createCollection.mock.calls[0][0]
518+
519+
// Index on feature
520+
await vectorStore.upsertPoints([{ id: "feature-1", vector: [0, 1, 0], payload: { path: "feature.ts" } }])
521+
522+
// Switch back to main
523+
vi.clearAllMocks()
524+
vectorStore.invalidateBranchCache()
525+
mockedGetCurrentBranch.mockResolvedValue("main")
526+
527+
// Mock that main collection already exists
528+
mockQdrantClient.getCollection.mockResolvedValue({ vectors_count: 1 })
529+
await vectorStore.initialize()
530+
531+
// Mock search returns main branch docs
532+
mockQdrantClient.query.mockResolvedValue({
533+
points: [
534+
{
535+
id: "main-1",
536+
score: 0.95,
537+
payload: {
538+
filePath: "main.ts",
539+
codeChunk: "main branch code",
540+
startLine: 1,
541+
endLine: 10,
542+
},
543+
},
544+
],
545+
})
546+
547+
const results = await vectorStore.search([1, 0, 0])
548+
549+
// Should search in main collection
550+
expect(mockQdrantClient.query).toHaveBeenCalledWith(mainCollectionName, expect.any(Object))
551+
552+
// Should get main branch results
553+
expect(results[0].payload.filePath).toBe("main.ts")
554+
})
555+
})
329556
})

0 commit comments

Comments
 (0)