qdrant-client.spec.ts 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  1. import { QdrantClient } from "@qdrant/js-client-rest"
  2. import { createHash } from "crypto"
  3. import { QdrantVectorStore } from "../qdrant-client"
  4. import { getWorkspacePath } from "../../../../utils/path"
  5. import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../../constants"
  6. // Mocks
  7. vitest.mock("@qdrant/js-client-rest")
  8. vitest.mock("crypto")
  9. vitest.mock("../../../../utils/path")
  10. vitest.mock("../../../../i18n", () => ({
  11. t: (key: string) => key, // Just return the key for testing
  12. }))
  13. vitest.mock("path", () => ({
  14. ...vitest.importActual("path"),
  15. sep: "/",
  16. }))
  17. const mockQdrantClientInstance = {
  18. getCollection: vitest.fn(),
  19. createCollection: vitest.fn(),
  20. deleteCollection: vitest.fn(),
  21. createPayloadIndex: vitest.fn(),
  22. upsert: vitest.fn(),
  23. query: vitest.fn(),
  24. delete: vitest.fn(),
  25. }
  26. const mockCreateHashInstance = {
  27. update: vitest.fn().mockReturnThis(),
  28. digest: vitest.fn(),
  29. }
  30. describe("QdrantVectorStore", () => {
  31. let vectorStore: QdrantVectorStore
  32. const mockWorkspacePath = "/test/workspace"
  33. const mockQdrantUrl = "http://mock-qdrant:6333"
  34. const mockApiKey = "test-api-key"
  35. const mockVectorSize = 1536
  36. const mockHashedPath = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" // Needs to be long enough
  37. const expectedCollectionName = `ws-${mockHashedPath.substring(0, 16)}`
  38. beforeEach(() => {
  39. vitest.clearAllMocks()
  40. // Mock QdrantClient constructor
  41. ;(QdrantClient as any).mockImplementation(() => mockQdrantClientInstance)
  42. // Mock crypto.createHash
  43. ;(createHash as any).mockReturnValue(mockCreateHashInstance)
  44. mockCreateHashInstance.update.mockReturnValue(mockCreateHashInstance) // Ensure it returns 'this'
  45. mockCreateHashInstance.digest.mockReturnValue(mockHashedPath)
  46. // Mock getWorkspacePath
  47. ;(getWorkspacePath as any).mockReturnValue(mockWorkspacePath)
  48. vectorStore = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, mockVectorSize, mockApiKey)
  49. })
  50. it("should correctly initialize QdrantClient and collectionName in constructor", () => {
  51. expect(QdrantClient).toHaveBeenCalledTimes(1)
  52. expect(QdrantClient).toHaveBeenCalledWith({
  53. host: "mock-qdrant",
  54. https: false,
  55. port: 6333,
  56. apiKey: mockApiKey,
  57. headers: {
  58. "User-Agent": "Roo-Code",
  59. },
  60. })
  61. expect(createHash).toHaveBeenCalledWith("sha256")
  62. expect(mockCreateHashInstance.update).toHaveBeenCalledWith(mockWorkspacePath)
  63. expect(mockCreateHashInstance.digest).toHaveBeenCalledWith("hex")
  64. // Access private member for testing constructor logic (not ideal, but necessary here)
  65. expect((vectorStore as any).collectionName).toBe(expectedCollectionName)
  66. expect((vectorStore as any).vectorSize).toBe(mockVectorSize)
  67. })
  68. it("should handle constructor with default URL when none provided", () => {
  69. const vectorStoreWithDefaults = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize)
  70. expect(QdrantClient).toHaveBeenLastCalledWith({
  71. host: "localhost",
  72. https: false,
  73. port: 6333,
  74. apiKey: undefined,
  75. headers: {
  76. "User-Agent": "Roo-Code",
  77. },
  78. })
  79. })
  80. it("should handle constructor without API key", () => {
  81. const vectorStoreWithoutKey = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, mockVectorSize)
  82. expect(QdrantClient).toHaveBeenLastCalledWith({
  83. host: "mock-qdrant",
  84. https: false,
  85. port: 6333,
  86. apiKey: undefined,
  87. headers: {
  88. "User-Agent": "Roo-Code",
  89. },
  90. })
  91. })
  92. describe("URL Parsing and Explicit Port Handling", () => {
  93. describe("HTTPS URL handling", () => {
  94. it("should use explicit port 443 for HTTPS URLs without port (fixes the main bug)", () => {
  95. const vectorStore = new QdrantVectorStore(
  96. mockWorkspacePath,
  97. "https://qdrant.ashbyfam.com",
  98. mockVectorSize,
  99. )
  100. expect(QdrantClient).toHaveBeenLastCalledWith({
  101. host: "qdrant.ashbyfam.com",
  102. https: true,
  103. port: 443,
  104. prefix: undefined, // No prefix for root path
  105. apiKey: undefined,
  106. headers: {
  107. "User-Agent": "Roo-Code",
  108. },
  109. })
  110. expect((vectorStore as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com")
  111. })
  112. it("should use explicit port for HTTPS URLs with explicit port", () => {
  113. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "https://example.com:9000", mockVectorSize)
  114. expect(QdrantClient).toHaveBeenLastCalledWith({
  115. host: "example.com",
  116. https: true,
  117. port: 9000,
  118. prefix: undefined, // No prefix for root path
  119. apiKey: undefined,
  120. headers: {
  121. "User-Agent": "Roo-Code",
  122. },
  123. })
  124. expect((vectorStore as any).qdrantUrl).toBe("https://example.com:9000")
  125. })
  126. it("should use port 443 for HTTPS URLs with paths and query parameters", () => {
  127. const vectorStore = new QdrantVectorStore(
  128. mockWorkspacePath,
  129. "https://example.com/api/v1?key=value",
  130. mockVectorSize,
  131. )
  132. expect(QdrantClient).toHaveBeenLastCalledWith({
  133. host: "example.com",
  134. https: true,
  135. port: 443,
  136. prefix: "/api/v1", // Should have prefix
  137. apiKey: undefined,
  138. headers: {
  139. "User-Agent": "Roo-Code",
  140. },
  141. })
  142. expect((vectorStore as any).qdrantUrl).toBe("https://example.com/api/v1?key=value")
  143. })
  144. })
  145. describe("HTTP URL handling", () => {
  146. it("should use explicit port 80 for HTTP URLs without port", () => {
  147. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://example.com", mockVectorSize)
  148. expect(QdrantClient).toHaveBeenLastCalledWith({
  149. host: "example.com",
  150. https: false,
  151. port: 80,
  152. prefix: undefined, // No prefix for root path
  153. apiKey: undefined,
  154. headers: {
  155. "User-Agent": "Roo-Code",
  156. },
  157. })
  158. expect((vectorStore as any).qdrantUrl).toBe("http://example.com")
  159. })
  160. it("should use explicit port for HTTP URLs with explicit port", () => {
  161. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:8080", mockVectorSize)
  162. expect(QdrantClient).toHaveBeenLastCalledWith({
  163. host: "localhost",
  164. https: false,
  165. port: 8080,
  166. prefix: undefined, // No prefix for root path
  167. apiKey: undefined,
  168. headers: {
  169. "User-Agent": "Roo-Code",
  170. },
  171. })
  172. expect((vectorStore as any).qdrantUrl).toBe("http://localhost:8080")
  173. })
  174. it("should use port 80 for HTTP URLs while preserving paths and query parameters", () => {
  175. const vectorStore = new QdrantVectorStore(
  176. mockWorkspacePath,
  177. "http://example.com/api/v1?key=value",
  178. mockVectorSize,
  179. )
  180. expect(QdrantClient).toHaveBeenLastCalledWith({
  181. host: "example.com",
  182. https: false,
  183. port: 80,
  184. prefix: "/api/v1", // Should have prefix
  185. apiKey: undefined,
  186. headers: {
  187. "User-Agent": "Roo-Code",
  188. },
  189. })
  190. expect((vectorStore as any).qdrantUrl).toBe("http://example.com/api/v1?key=value")
  191. })
  192. })
  193. describe("Hostname handling", () => {
  194. it("should convert hostname to http with port 80", () => {
  195. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "qdrant.example.com", mockVectorSize)
  196. expect(QdrantClient).toHaveBeenLastCalledWith({
  197. host: "qdrant.example.com",
  198. https: false,
  199. port: 80,
  200. apiKey: undefined,
  201. headers: {
  202. "User-Agent": "Roo-Code",
  203. },
  204. })
  205. expect((vectorStore as any).qdrantUrl).toBe("http://qdrant.example.com")
  206. })
  207. it("should handle hostname:port format with explicit port", () => {
  208. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "localhost:6333", mockVectorSize)
  209. expect(QdrantClient).toHaveBeenLastCalledWith({
  210. host: "localhost",
  211. https: false,
  212. port: 6333,
  213. apiKey: undefined,
  214. headers: {
  215. "User-Agent": "Roo-Code",
  216. },
  217. })
  218. expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
  219. })
  220. it("should handle explicit HTTP URLs correctly", () => {
  221. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:9000", mockVectorSize)
  222. expect(QdrantClient).toHaveBeenLastCalledWith({
  223. host: "localhost",
  224. https: false,
  225. port: 9000,
  226. apiKey: undefined,
  227. headers: {
  228. "User-Agent": "Roo-Code",
  229. },
  230. })
  231. expect((vectorStore as any).qdrantUrl).toBe("http://localhost:9000")
  232. })
  233. })
  234. describe("IP address handling", () => {
  235. it("should convert IP address to http with port 80", () => {
  236. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100", mockVectorSize)
  237. expect(QdrantClient).toHaveBeenLastCalledWith({
  238. host: "192.168.1.100",
  239. https: false,
  240. port: 80,
  241. apiKey: undefined,
  242. headers: {
  243. "User-Agent": "Roo-Code",
  244. },
  245. })
  246. expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100")
  247. })
  248. it("should handle IP:port format with explicit port", () => {
  249. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100:6333", mockVectorSize)
  250. expect(QdrantClient).toHaveBeenLastCalledWith({
  251. host: "192.168.1.100",
  252. https: false,
  253. port: 6333,
  254. apiKey: undefined,
  255. headers: {
  256. "User-Agent": "Roo-Code",
  257. },
  258. })
  259. expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100:6333")
  260. })
  261. })
  262. describe("Edge cases", () => {
  263. it("should handle undefined URL with host-based config", () => {
  264. const vectorStore = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize)
  265. expect(QdrantClient).toHaveBeenLastCalledWith({
  266. host: "localhost",
  267. https: false,
  268. port: 6333,
  269. apiKey: undefined,
  270. headers: {
  271. "User-Agent": "Roo-Code",
  272. },
  273. })
  274. expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
  275. })
  276. it("should handle empty string URL with host-based config", () => {
  277. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "", mockVectorSize)
  278. expect(QdrantClient).toHaveBeenLastCalledWith({
  279. host: "localhost",
  280. https: false,
  281. port: 6333,
  282. apiKey: undefined,
  283. headers: {
  284. "User-Agent": "Roo-Code",
  285. },
  286. })
  287. expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
  288. })
  289. it("should handle whitespace-only URL with host-based config", () => {
  290. const vectorStore = new QdrantVectorStore(mockWorkspacePath, " ", mockVectorSize)
  291. expect(QdrantClient).toHaveBeenLastCalledWith({
  292. host: "localhost",
  293. https: false,
  294. port: 6333,
  295. apiKey: undefined,
  296. headers: {
  297. "User-Agent": "Roo-Code",
  298. },
  299. })
  300. expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
  301. })
  302. })
  303. describe("Invalid URL fallback", () => {
  304. it("should treat invalid URLs as hostnames with port 80", () => {
  305. const vectorStore = new QdrantVectorStore(mockWorkspacePath, "invalid-url-format", mockVectorSize)
  306. expect(QdrantClient).toHaveBeenLastCalledWith({
  307. host: "invalid-url-format",
  308. https: false,
  309. port: 80,
  310. apiKey: undefined,
  311. headers: {
  312. "User-Agent": "Roo-Code",
  313. },
  314. })
  315. expect((vectorStore as any).qdrantUrl).toBe("http://invalid-url-format")
  316. })
  317. })
  318. })
  319. describe("URL Prefix Handling", () => {
  320. it("should pass the URL pathname as prefix to QdrantClient if not root", () => {
  321. const vectorStoreWithPrefix = new QdrantVectorStore(
  322. mockWorkspacePath,
  323. "http://localhost:6333/some/path",
  324. mockVectorSize,
  325. )
  326. expect(QdrantClient).toHaveBeenLastCalledWith({
  327. host: "localhost",
  328. https: false,
  329. port: 6333,
  330. prefix: "/some/path",
  331. apiKey: undefined,
  332. headers: {
  333. "User-Agent": "Roo-Code",
  334. },
  335. })
  336. expect((vectorStoreWithPrefix as any).qdrantUrl).toBe("http://localhost:6333/some/path")
  337. })
  338. it("should not pass prefix if the URL pathname is root ('/')", () => {
  339. const vectorStoreWithoutPrefix = new QdrantVectorStore(
  340. mockWorkspacePath,
  341. "http://localhost:6333/",
  342. mockVectorSize,
  343. )
  344. expect(QdrantClient).toHaveBeenLastCalledWith({
  345. host: "localhost",
  346. https: false,
  347. port: 6333,
  348. prefix: undefined,
  349. apiKey: undefined,
  350. headers: {
  351. "User-Agent": "Roo-Code",
  352. },
  353. })
  354. expect((vectorStoreWithoutPrefix as any).qdrantUrl).toBe("http://localhost:6333/")
  355. })
  356. it("should handle HTTPS URL with path as prefix", () => {
  357. const vectorStoreWithHttpsPrefix = new QdrantVectorStore(
  358. mockWorkspacePath,
  359. "https://qdrant.ashbyfam.com/api",
  360. mockVectorSize,
  361. )
  362. expect(QdrantClient).toHaveBeenLastCalledWith({
  363. host: "qdrant.ashbyfam.com",
  364. https: true,
  365. port: 443,
  366. prefix: "/api",
  367. apiKey: undefined,
  368. headers: {
  369. "User-Agent": "Roo-Code",
  370. },
  371. })
  372. expect((vectorStoreWithHttpsPrefix as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com/api")
  373. })
  374. it("should normalize URL pathname by removing trailing slash for prefix", () => {
  375. const vectorStoreWithTrailingSlash = new QdrantVectorStore(
  376. mockWorkspacePath,
  377. "http://localhost:6333/api/",
  378. mockVectorSize,
  379. )
  380. expect(QdrantClient).toHaveBeenLastCalledWith({
  381. host: "localhost",
  382. https: false,
  383. port: 6333,
  384. prefix: "/api", // Trailing slash should be removed
  385. apiKey: undefined,
  386. headers: {
  387. "User-Agent": "Roo-Code",
  388. },
  389. })
  390. expect((vectorStoreWithTrailingSlash as any).qdrantUrl).toBe("http://localhost:6333/api/")
  391. })
  392. it("should normalize URL pathname by removing multiple trailing slashes for prefix", () => {
  393. const vectorStoreWithMultipleTrailingSlashes = new QdrantVectorStore(
  394. mockWorkspacePath,
  395. "http://localhost:6333/api///",
  396. mockVectorSize,
  397. )
  398. expect(QdrantClient).toHaveBeenLastCalledWith({
  399. host: "localhost",
  400. https: false,
  401. port: 6333,
  402. prefix: "/api", // All trailing slashes should be removed
  403. apiKey: undefined,
  404. headers: {
  405. "User-Agent": "Roo-Code",
  406. },
  407. })
  408. expect((vectorStoreWithMultipleTrailingSlashes as any).qdrantUrl).toBe("http://localhost:6333/api///")
  409. })
  410. it("should handle multiple path segments correctly for prefix", () => {
  411. const vectorStoreWithMultiSegment = new QdrantVectorStore(
  412. mockWorkspacePath,
  413. "http://localhost:6333/api/v1/qdrant",
  414. mockVectorSize,
  415. )
  416. expect(QdrantClient).toHaveBeenLastCalledWith({
  417. host: "localhost",
  418. https: false,
  419. port: 6333,
  420. prefix: "/api/v1/qdrant",
  421. apiKey: undefined,
  422. headers: {
  423. "User-Agent": "Roo-Code",
  424. },
  425. })
  426. expect((vectorStoreWithMultiSegment as any).qdrantUrl).toBe("http://localhost:6333/api/v1/qdrant")
  427. })
  428. it("should handle complex URL with multiple segments, multiple trailing slashes, query params, and fragment", () => {
  429. const complexUrl = "https://example.com/ollama/api/v1///?key=value#pos"
  430. const vectorStoreComplex = new QdrantVectorStore(mockWorkspacePath, complexUrl, mockVectorSize)
  431. expect(QdrantClient).toHaveBeenLastCalledWith({
  432. host: "example.com",
  433. https: true,
  434. port: 443,
  435. prefix: "/ollama/api/v1", // Trailing slash removed, query/fragment ignored
  436. apiKey: undefined,
  437. headers: {
  438. "User-Agent": "Roo-Code",
  439. },
  440. })
  441. expect((vectorStoreComplex as any).qdrantUrl).toBe(complexUrl)
  442. })
  443. it("should ignore query parameters and fragments when determining prefix", () => {
  444. const vectorStoreWithQueryParams = new QdrantVectorStore(
  445. mockWorkspacePath,
  446. "http://localhost:6333/api/path?key=value#fragment",
  447. mockVectorSize,
  448. )
  449. expect(QdrantClient).toHaveBeenLastCalledWith({
  450. host: "localhost",
  451. https: false,
  452. port: 6333,
  453. prefix: "/api/path", // Query params and fragment should be ignored
  454. apiKey: undefined,
  455. headers: {
  456. "User-Agent": "Roo-Code",
  457. },
  458. })
  459. expect((vectorStoreWithQueryParams as any).qdrantUrl).toBe(
  460. "http://localhost:6333/api/path?key=value#fragment",
  461. )
  462. })
  463. })
  464. describe("initialize", () => {
  465. it("should create a new collection if none exists and return true", async () => {
  466. // Mock getCollection to throw a 404-like error
  467. mockQdrantClientInstance.getCollection.mockRejectedValue({
  468. response: { status: 404 },
  469. message: "Not found",
  470. })
  471. mockQdrantClientInstance.createCollection.mockResolvedValue(true as any) // Cast to any to satisfy QdrantClient types if strict
  472. mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any) // Mock successful index creation
  473. const result = await vectorStore.initialize()
  474. expect(result).toBe(true)
  475. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  476. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
  477. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
  478. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(expectedCollectionName, {
  479. vectors: {
  480. size: mockVectorSize,
  481. distance: "Cosine", // Assuming 'Cosine' is the DISTANCE_METRIC
  482. },
  483. })
  484. expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled()
  485. // Verify payload index creation
  486. for (let i = 0; i <= 4; i++) {
  487. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledWith(expectedCollectionName, {
  488. field_name: `pathSegments.${i}`,
  489. field_schema: "keyword",
  490. })
  491. }
  492. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
  493. })
  494. it("should not create a new collection if one exists with matching vectorSize and return false", async () => {
  495. // Mock getCollection to return existing collection info with matching vector size
  496. mockQdrantClientInstance.getCollection.mockResolvedValue({
  497. config: {
  498. params: {
  499. vectors: {
  500. size: mockVectorSize, // Matching vector size
  501. },
  502. },
  503. },
  504. } as any) // Cast to any to satisfy QdrantClient types
  505. mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
  506. const result = await vectorStore.initialize()
  507. expect(result).toBe(false)
  508. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  509. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
  510. expect(mockQdrantClientInstance.createCollection).not.toHaveBeenCalled()
  511. expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled()
  512. // Verify payload index creation still happens
  513. for (let i = 0; i <= 4; i++) {
  514. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledWith(expectedCollectionName, {
  515. field_name: `pathSegments.${i}`,
  516. field_schema: "keyword",
  517. })
  518. }
  519. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
  520. })
  521. it("should recreate collection if it exists but vectorSize mismatches and return true", async () => {
  522. const differentVectorSize = 768
  523. // Mock getCollection to return existing collection info with different vector size
  524. mockQdrantClientInstance.getCollection.mockResolvedValue({
  525. config: {
  526. params: {
  527. vectors: {
  528. size: differentVectorSize, // Mismatching vector size
  529. },
  530. },
  531. },
  532. } as any)
  533. mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
  534. mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
  535. mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
  536. vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
  537. const result = await vectorStore.initialize()
  538. expect(result).toBe(true)
  539. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  540. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
  541. expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
  542. expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledWith(expectedCollectionName)
  543. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
  544. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(expectedCollectionName, {
  545. vectors: {
  546. size: mockVectorSize, // Should use the new, correct vector size
  547. distance: "Cosine",
  548. },
  549. })
  550. // Verify payload index creation
  551. for (let i = 0; i <= 4; i++) {
  552. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledWith(expectedCollectionName, {
  553. field_name: `pathSegments.${i}`,
  554. field_schema: "keyword",
  555. })
  556. }
  557. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
  558. ;(console.warn as any).mockRestore() // Restore console.warn
  559. })
  560. it("should log warning for non-404 errors but still create collection", async () => {
  561. const genericError = new Error("Generic Qdrant Error")
  562. mockQdrantClientInstance.getCollection.mockRejectedValue(genericError)
  563. vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
  564. const result = await vectorStore.initialize()
  565. expect(result).toBe(true) // Collection was created
  566. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  567. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
  568. expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled()
  569. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
  570. expect(console.warn).toHaveBeenCalledWith(
  571. expect.stringContaining(`Warning during getCollectionInfo for "${expectedCollectionName}"`),
  572. genericError.message,
  573. )
  574. ;(console.warn as any).mockRestore()
  575. })
  576. it("should re-throw error from createCollection when no collection initially exists", async () => {
  577. mockQdrantClientInstance.getCollection.mockRejectedValue({
  578. response: { status: 404 },
  579. message: "Not found",
  580. })
  581. const createError = new Error("Create Collection Failed")
  582. mockQdrantClientInstance.createCollection.mockRejectedValue(createError)
  583. vitest.spyOn(console, "error").mockImplementation(() => {}) // Suppress console.error
  584. // The actual error message includes the URL and error details
  585. await expect(vectorStore.initialize()).rejects.toThrow(
  586. /Failed to connect to Qdrant vector database|vectorStore\.qdrantConnectionFailed/,
  587. )
  588. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  589. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
  590. expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled()
  591. expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled() // Should not be called if createCollection fails
  592. expect(console.error).toHaveBeenCalledTimes(1) // Only the outer try/catch
  593. ;(console.error as any).mockRestore()
  594. })
  595. it("should log but not fail if payload index creation errors occur", async () => {
  596. // Mock successful collection creation
  597. mockQdrantClientInstance.getCollection.mockRejectedValue({
  598. response: { status: 404 },
  599. message: "Not found",
  600. })
  601. mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
  602. // Mock payload index creation to fail
  603. const indexError = new Error("Index creation failed")
  604. mockQdrantClientInstance.createPayloadIndex.mockRejectedValue(indexError)
  605. vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
  606. const result = await vectorStore.initialize()
  607. // Should still return true since main collection setup succeeded
  608. expect(result).toBe(true)
  609. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
  610. // Verify all payload index creations were attempted
  611. expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
  612. // Verify warnings were logged for each failed index
  613. expect(console.warn).toHaveBeenCalledTimes(5)
  614. for (let i = 0; i <= 4; i++) {
  615. expect(console.warn).toHaveBeenCalledWith(
  616. expect.stringContaining(`Could not create payload index for pathSegments.${i}`),
  617. indexError.message,
  618. )
  619. }
  620. ;(console.warn as any).mockRestore()
  621. })
  622. it("should throw vectorDimensionMismatch error when deleteCollection fails during recreation", async () => {
  623. const differentVectorSize = 768
  624. mockQdrantClientInstance.getCollection.mockResolvedValue({
  625. config: {
  626. params: {
  627. vectors: {
  628. size: differentVectorSize,
  629. },
  630. },
  631. },
  632. } as any)
  633. const deleteError = new Error("Delete Collection Failed")
  634. mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
  635. vitest.spyOn(console, "error").mockImplementation(() => {})
  636. vitest.spyOn(console, "warn").mockImplementation(() => {})
  637. // The error should have a cause property set to the original error
  638. let caughtError: any
  639. try {
  640. await vectorStore.initialize()
  641. } catch (error: any) {
  642. caughtError = error
  643. }
  644. expect(caughtError).toBeDefined()
  645. expect(caughtError.message).toContain("embeddings:vectorStore.vectorDimensionMismatch")
  646. expect(caughtError.cause).toBe(deleteError)
  647. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  648. expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
  649. expect(mockQdrantClientInstance.createCollection).not.toHaveBeenCalled()
  650. expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
  651. // Should log both the warning and the critical error
  652. expect(console.warn).toHaveBeenCalledTimes(1)
  653. expect(console.error).toHaveBeenCalledTimes(2) // One for the critical error, one for the outer catch
  654. ;(console.error as any).mockRestore()
  655. ;(console.warn as any).mockRestore()
  656. })
  657. it("should throw vectorDimensionMismatch error when createCollection fails during recreation", async () => {
  658. const differentVectorSize = 768
  659. mockQdrantClientInstance.getCollection.mockResolvedValue({
  660. config: {
  661. params: {
  662. vectors: {
  663. size: differentVectorSize,
  664. },
  665. },
  666. },
  667. } as any)
  668. // Delete succeeds but create fails
  669. mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
  670. const createError = new Error("Create Collection Failed")
  671. mockQdrantClientInstance.createCollection.mockRejectedValue(createError)
  672. vitest.spyOn(console, "error").mockImplementation(() => {})
  673. vitest.spyOn(console, "warn").mockImplementation(() => {})
  674. // Should throw an error with cause property set to the original error
  675. let caughtError: any
  676. try {
  677. await vectorStore.initialize()
  678. } catch (error: any) {
  679. caughtError = error
  680. }
  681. expect(caughtError).toBeDefined()
  682. expect(caughtError.message).toContain("embeddings:vectorStore.vectorDimensionMismatch")
  683. expect(caughtError.cause).toBe(createError)
  684. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  685. expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
  686. expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
  687. expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
  688. // Should log warning, critical error, and outer error
  689. expect(console.warn).toHaveBeenCalledTimes(1)
  690. expect(console.error).toHaveBeenCalledTimes(2)
  691. ;(console.error as any).mockRestore()
  692. ;(console.warn as any).mockRestore()
  693. })
  694. })
  695. it("should return true when collection exists", async () => {
  696. mockQdrantClientInstance.getCollection.mockResolvedValue({
  697. config: {
  698. /* collection data */
  699. },
  700. } as any)
  701. const result = await vectorStore.collectionExists()
  702. expect(result).toBe(true)
  703. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  704. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
  705. })
  706. it("should return false when collection does not exist (404 error)", async () => {
  707. mockQdrantClientInstance.getCollection.mockRejectedValue({
  708. response: { status: 404 },
  709. message: "Not found",
  710. })
  711. const result = await vectorStore.collectionExists()
  712. expect(result).toBe(false)
  713. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  714. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
  715. })
  716. it("should return false and log warning for non-404 errors", async () => {
  717. const genericError = new Error("Network error")
  718. mockQdrantClientInstance.getCollection.mockRejectedValue(genericError)
  719. vitest.spyOn(console, "warn").mockImplementation(() => {})
  720. const result = await vectorStore.collectionExists()
  721. expect(result).toBe(false)
  722. expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
  723. expect(console.warn).toHaveBeenCalledWith(
  724. expect.stringContaining(`Warning during getCollectionInfo for "${expectedCollectionName}"`),
  725. genericError.message,
  726. )
  727. ;(console.warn as any).mockRestore()
  728. })
  729. describe("deleteCollection", () => {
  730. it("should delete collection when it exists", async () => {
  731. // Mock collectionExists to return true
  732. vitest.spyOn(vectorStore, "collectionExists").mockResolvedValue(true)
  733. mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
  734. await vectorStore.deleteCollection()
  735. expect(vectorStore.collectionExists).toHaveBeenCalledTimes(1)
  736. expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
  737. expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledWith(expectedCollectionName)
  738. })
  739. it("should not attempt to delete collection when it does not exist", async () => {
  740. // Mock collectionExists to return false
  741. vitest.spyOn(vectorStore, "collectionExists").mockResolvedValue(false)
  742. await vectorStore.deleteCollection()
  743. expect(vectorStore.collectionExists).toHaveBeenCalledTimes(1)
  744. expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled()
  745. })
  746. it("should log and re-throw error when deletion fails", async () => {
  747. vitest.spyOn(vectorStore, "collectionExists").mockResolvedValue(true)
  748. const deleteError = new Error("Deletion failed")
  749. mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
  750. vitest.spyOn(console, "error").mockImplementation(() => {})
  751. await expect(vectorStore.deleteCollection()).rejects.toThrow(deleteError)
  752. expect(vectorStore.collectionExists).toHaveBeenCalledTimes(1)
  753. expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
  754. expect(console.error).toHaveBeenCalledWith(
  755. `[QdrantVectorStore] Failed to delete collection ${expectedCollectionName}:`,
  756. deleteError,
  757. )
  758. ;(console.error as any).mockRestore()
  759. })
  760. })
  761. describe("upsertPoints", () => {
  762. it("should correctly call qdrantClient.upsert with processed points", async () => {
  763. const mockPoints = [
  764. {
  765. id: "test-id-1",
  766. vector: [0.1, 0.2, 0.3],
  767. payload: {
  768. filePath: "src/components/Button.tsx",
  769. content: "export const Button = () => {}",
  770. startLine: 1,
  771. endLine: 3,
  772. },
  773. },
  774. {
  775. id: "test-id-2",
  776. vector: [0.4, 0.5, 0.6],
  777. payload: {
  778. filePath: "src/utils/helpers.ts",
  779. content: "export function helper() {}",
  780. startLine: 5,
  781. endLine: 7,
  782. },
  783. },
  784. ]
  785. mockQdrantClientInstance.upsert.mockResolvedValue({} as any)
  786. await vectorStore.upsertPoints(mockPoints)
  787. expect(mockQdrantClientInstance.upsert).toHaveBeenCalledTimes(1)
  788. expect(mockQdrantClientInstance.upsert).toHaveBeenCalledWith(expectedCollectionName, {
  789. points: [
  790. {
  791. id: "test-id-1",
  792. vector: [0.1, 0.2, 0.3],
  793. payload: {
  794. filePath: "src/components/Button.tsx",
  795. content: "export const Button = () => {}",
  796. startLine: 1,
  797. endLine: 3,
  798. pathSegments: {
  799. "0": "src",
  800. "1": "components",
  801. "2": "Button.tsx",
  802. },
  803. },
  804. },
  805. {
  806. id: "test-id-2",
  807. vector: [0.4, 0.5, 0.6],
  808. payload: {
  809. filePath: "src/utils/helpers.ts",
  810. content: "export function helper() {}",
  811. startLine: 5,
  812. endLine: 7,
  813. pathSegments: {
  814. "0": "src",
  815. "1": "utils",
  816. "2": "helpers.ts",
  817. },
  818. },
  819. },
  820. ],
  821. wait: true,
  822. })
  823. })
  824. it("should handle points without filePath in payload", async () => {
  825. const mockPoints = [
  826. {
  827. id: "test-id-1",
  828. vector: [0.1, 0.2, 0.3],
  829. payload: {
  830. content: "some content without filePath",
  831. startLine: 1,
  832. endLine: 3,
  833. },
  834. },
  835. ]
  836. mockQdrantClientInstance.upsert.mockResolvedValue({} as any)
  837. await vectorStore.upsertPoints(mockPoints)
  838. expect(mockQdrantClientInstance.upsert).toHaveBeenCalledWith(expectedCollectionName, {
  839. points: [
  840. {
  841. id: "test-id-1",
  842. vector: [0.1, 0.2, 0.3],
  843. payload: {
  844. content: "some content without filePath",
  845. startLine: 1,
  846. endLine: 3,
  847. },
  848. },
  849. ],
  850. wait: true,
  851. })
  852. })
  853. it("should handle empty input arrays", async () => {
  854. mockQdrantClientInstance.upsert.mockResolvedValue({} as any)
  855. await vectorStore.upsertPoints([])
  856. expect(mockQdrantClientInstance.upsert).toHaveBeenCalledWith(expectedCollectionName, {
  857. points: [],
  858. wait: true,
  859. })
  860. })
  861. it("should correctly process pathSegments for nested file paths", async () => {
  862. const mockPoints = [
  863. {
  864. id: "test-id-1",
  865. vector: [0.1, 0.2, 0.3],
  866. payload: {
  867. filePath: "src/components/ui/forms/InputField.tsx",
  868. content: "export const InputField = () => {}",
  869. startLine: 1,
  870. endLine: 3,
  871. },
  872. },
  873. ]
  874. mockQdrantClientInstance.upsert.mockResolvedValue({} as any)
  875. await vectorStore.upsertPoints(mockPoints)
  876. expect(mockQdrantClientInstance.upsert).toHaveBeenCalledWith(expectedCollectionName, {
  877. points: [
  878. {
  879. id: "test-id-1",
  880. vector: [0.1, 0.2, 0.3],
  881. payload: {
  882. filePath: "src/components/ui/forms/InputField.tsx",
  883. content: "export const InputField = () => {}",
  884. startLine: 1,
  885. endLine: 3,
  886. pathSegments: {
  887. "0": "src",
  888. "1": "components",
  889. "2": "ui",
  890. "3": "forms",
  891. "4": "InputField.tsx",
  892. },
  893. },
  894. },
  895. ],
  896. wait: true,
  897. })
  898. })
  899. it("should handle error scenarios when qdrantClient.upsert fails", async () => {
  900. const mockPoints = [
  901. {
  902. id: "test-id-1",
  903. vector: [0.1, 0.2, 0.3],
  904. payload: {
  905. filePath: "src/test.ts",
  906. content: "test content",
  907. startLine: 1,
  908. endLine: 1,
  909. },
  910. },
  911. ]
  912. const upsertError = new Error("Upsert failed")
  913. mockQdrantClientInstance.upsert.mockRejectedValue(upsertError)
  914. vitest.spyOn(console, "error").mockImplementation(() => {})
  915. await expect(vectorStore.upsertPoints(mockPoints)).rejects.toThrow(upsertError)
  916. expect(mockQdrantClientInstance.upsert).toHaveBeenCalledTimes(1)
  917. expect(console.error).toHaveBeenCalledWith("Failed to upsert points:", upsertError)
  918. ;(console.error as any).mockRestore()
  919. })
  920. })
  921. describe("search", () => {
  922. it("should correctly call qdrantClient.query and transform results", async () => {
  923. const queryVector = [0.1, 0.2, 0.3]
  924. const mockQdrantResults = {
  925. points: [
  926. {
  927. id: "test-id-1",
  928. score: 0.85,
  929. payload: {
  930. filePath: "src/test.ts",
  931. codeChunk: "test code",
  932. startLine: 1,
  933. endLine: 5,
  934. pathSegments: { "0": "src", "1": "test.ts" },
  935. },
  936. },
  937. {
  938. id: "test-id-2",
  939. score: 0.75,
  940. payload: {
  941. filePath: "src/utils.ts",
  942. codeChunk: "utility code",
  943. startLine: 10,
  944. endLine: 15,
  945. pathSegments: { "0": "src", "1": "utils.ts" },
  946. },
  947. },
  948. ],
  949. }
  950. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  951. const results = await vectorStore.search(queryVector)
  952. expect(mockQdrantClientInstance.query).toHaveBeenCalledTimes(1)
  953. expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
  954. query: queryVector,
  955. filter: undefined,
  956. score_threshold: DEFAULT_SEARCH_MIN_SCORE,
  957. limit: DEFAULT_MAX_SEARCH_RESULTS,
  958. params: {
  959. hnsw_ef: 128,
  960. exact: false,
  961. },
  962. with_payload: {
  963. include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
  964. },
  965. })
  966. expect(results).toEqual(mockQdrantResults.points)
  967. })
  968. it("should apply filePathPrefix filter correctly", async () => {
  969. const queryVector = [0.1, 0.2, 0.3]
  970. const directoryPrefix = "src/components"
  971. const mockQdrantResults = {
  972. points: [
  973. {
  974. id: "test-id-1",
  975. score: 0.85,
  976. payload: {
  977. filePath: "src/components/Button.tsx",
  978. codeChunk: "button code",
  979. startLine: 1,
  980. endLine: 5,
  981. pathSegments: { "0": "src", "1": "components", "2": "Button.tsx" },
  982. },
  983. },
  984. ],
  985. }
  986. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  987. const results = await vectorStore.search(queryVector, directoryPrefix)
  988. expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
  989. query: queryVector,
  990. filter: {
  991. must: [
  992. {
  993. key: "pathSegments.0",
  994. match: { value: "src" },
  995. },
  996. {
  997. key: "pathSegments.1",
  998. match: { value: "components" },
  999. },
  1000. ],
  1001. },
  1002. score_threshold: DEFAULT_SEARCH_MIN_SCORE,
  1003. limit: DEFAULT_MAX_SEARCH_RESULTS,
  1004. params: {
  1005. hnsw_ef: 128,
  1006. exact: false,
  1007. },
  1008. with_payload: {
  1009. include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
  1010. },
  1011. })
  1012. expect(results).toEqual(mockQdrantResults.points)
  1013. })
  1014. it("should use custom minScore when provided", async () => {
  1015. const queryVector = [0.1, 0.2, 0.3]
  1016. const customMinScore = 0.8
  1017. const mockQdrantResults = { points: [] }
  1018. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  1019. await vectorStore.search(queryVector, undefined, customMinScore)
  1020. expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
  1021. query: queryVector,
  1022. filter: undefined,
  1023. score_threshold: customMinScore,
  1024. limit: DEFAULT_MAX_SEARCH_RESULTS,
  1025. params: {
  1026. hnsw_ef: 128,
  1027. exact: false,
  1028. },
  1029. with_payload: {
  1030. include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
  1031. },
  1032. })
  1033. })
  1034. it("should use custom maxResults when provided", async () => {
  1035. const queryVector = [0.1, 0.2, 0.3]
  1036. const customMaxResults = 100
  1037. const mockQdrantResults = { points: [] }
  1038. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  1039. await vectorStore.search(queryVector, undefined, undefined, customMaxResults)
  1040. expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
  1041. query: queryVector,
  1042. filter: undefined,
  1043. score_threshold: DEFAULT_SEARCH_MIN_SCORE,
  1044. limit: customMaxResults,
  1045. params: {
  1046. hnsw_ef: 128,
  1047. exact: false,
  1048. },
  1049. with_payload: {
  1050. include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
  1051. },
  1052. })
  1053. })
  1054. it("should filter out results with invalid payloads", async () => {
  1055. const queryVector = [0.1, 0.2, 0.3]
  1056. const mockQdrantResults = {
  1057. points: [
  1058. {
  1059. id: "valid-result",
  1060. score: 0.85,
  1061. payload: {
  1062. filePath: "src/test.ts",
  1063. codeChunk: "test code",
  1064. startLine: 1,
  1065. endLine: 5,
  1066. },
  1067. },
  1068. {
  1069. id: "invalid-result-1",
  1070. score: 0.75,
  1071. payload: {
  1072. // Missing required fields
  1073. filePath: "src/invalid.ts",
  1074. },
  1075. },
  1076. {
  1077. id: "valid-result-2",
  1078. score: 0.55,
  1079. payload: {
  1080. filePath: "src/test2.ts",
  1081. codeChunk: "test code 2",
  1082. startLine: 10,
  1083. endLine: 15,
  1084. },
  1085. },
  1086. ],
  1087. }
  1088. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  1089. const results = await vectorStore.search(queryVector)
  1090. // Should only return results with valid payloads
  1091. expect(results).toHaveLength(2)
  1092. expect(results[0].id).toBe("valid-result")
  1093. expect(results[1].id).toBe("valid-result-2")
  1094. })
  1095. it("should filter out results with null or undefined payloads", async () => {
  1096. const queryVector = [0.1, 0.2, 0.3]
  1097. const mockQdrantResults = {
  1098. points: [
  1099. {
  1100. id: "valid-result",
  1101. score: 0.85,
  1102. payload: {
  1103. filePath: "src/test.ts",
  1104. codeChunk: "test code",
  1105. startLine: 1,
  1106. endLine: 5,
  1107. },
  1108. },
  1109. {
  1110. id: "null-payload-result",
  1111. score: 0.75,
  1112. payload: null,
  1113. },
  1114. {
  1115. id: "undefined-payload-result",
  1116. score: 0.65,
  1117. payload: undefined,
  1118. },
  1119. {
  1120. id: "valid-result-2",
  1121. score: 0.55,
  1122. payload: {
  1123. filePath: "src/test2.ts",
  1124. codeChunk: "test code 2",
  1125. startLine: 10,
  1126. endLine: 15,
  1127. },
  1128. },
  1129. ],
  1130. }
  1131. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  1132. const results = await vectorStore.search(queryVector)
  1133. // Should only return results with valid payloads, filtering out null and undefined
  1134. expect(results).toHaveLength(2)
  1135. expect(results[0].id).toBe("valid-result")
  1136. expect(results[1].id).toBe("valid-result-2")
  1137. })
  1138. it("should handle scenarios where no results are found", async () => {
  1139. const queryVector = [0.1, 0.2, 0.3]
  1140. const mockQdrantResults = { points: [] }
  1141. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  1142. const results = await vectorStore.search(queryVector)
  1143. expect(mockQdrantClientInstance.query).toHaveBeenCalledTimes(1)
  1144. expect(results).toEqual([])
  1145. })
  1146. it("should handle complex directory prefix with multiple segments", async () => {
  1147. const queryVector = [0.1, 0.2, 0.3]
  1148. const directoryPrefix = "src/components/ui/forms"
  1149. const mockQdrantResults = { points: [] }
  1150. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  1151. await vectorStore.search(queryVector, directoryPrefix)
  1152. expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
  1153. query: queryVector,
  1154. filter: {
  1155. must: [
  1156. {
  1157. key: "pathSegments.0",
  1158. match: { value: "src" },
  1159. },
  1160. {
  1161. key: "pathSegments.1",
  1162. match: { value: "components" },
  1163. },
  1164. {
  1165. key: "pathSegments.2",
  1166. match: { value: "ui" },
  1167. },
  1168. {
  1169. key: "pathSegments.3",
  1170. match: { value: "forms" },
  1171. },
  1172. ],
  1173. },
  1174. score_threshold: DEFAULT_SEARCH_MIN_SCORE,
  1175. limit: DEFAULT_MAX_SEARCH_RESULTS,
  1176. params: {
  1177. hnsw_ef: 128,
  1178. exact: false,
  1179. },
  1180. with_payload: {
  1181. include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
  1182. },
  1183. })
  1184. })
  1185. it("should handle error scenarios when qdrantClient.query fails", async () => {
  1186. const queryVector = [0.1, 0.2, 0.3]
  1187. const queryError = new Error("Query failed")
  1188. mockQdrantClientInstance.query.mockRejectedValue(queryError)
  1189. vitest.spyOn(console, "error").mockImplementation(() => {})
  1190. await expect(vectorStore.search(queryVector)).rejects.toThrow(queryError)
  1191. expect(mockQdrantClientInstance.query).toHaveBeenCalledTimes(1)
  1192. expect(console.error).toHaveBeenCalledWith("Failed to search points:", queryError)
  1193. ;(console.error as any).mockRestore()
  1194. })
  1195. it("should use constants DEFAULT_MAX_SEARCH_RESULTS and DEFAULT_SEARCH_MIN_SCORE correctly", async () => {
  1196. const queryVector = [0.1, 0.2, 0.3]
  1197. const mockQdrantResults = { points: [] }
  1198. mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
  1199. await vectorStore.search(queryVector)
  1200. const callArgs = mockQdrantClientInstance.query.mock.calls[0][1]
  1201. expect(callArgs.limit).toBe(DEFAULT_MAX_SEARCH_RESULTS)
  1202. expect(callArgs.score_threshold).toBe(DEFAULT_SEARCH_MIN_SCORE)
  1203. })
  1204. })
  1205. })