glancey 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +435 -0
  3. package/dist/__tests__/ast-chunker.test.d.ts +2 -0
  4. package/dist/__tests__/ast-chunker.test.d.ts.map +1 -0
  5. package/dist/__tests__/ast-chunker.test.js +307 -0
  6. package/dist/__tests__/ast-chunker.test.js.map +1 -0
  7. package/dist/__tests__/config.test.d.ts +2 -0
  8. package/dist/__tests__/config.test.d.ts.map +1 -0
  9. package/dist/__tests__/config.test.js +242 -0
  10. package/dist/__tests__/config.test.js.map +1 -0
  11. package/dist/__tests__/dashboard/beads.test.d.ts +2 -0
  12. package/dist/__tests__/dashboard/beads.test.d.ts.map +1 -0
  13. package/dist/__tests__/dashboard/beads.test.js +151 -0
  14. package/dist/__tests__/dashboard/beads.test.js.map +1 -0
  15. package/dist/__tests__/dashboard/index.test.d.ts +2 -0
  16. package/dist/__tests__/dashboard/index.test.d.ts.map +1 -0
  17. package/dist/__tests__/dashboard/index.test.js +116 -0
  18. package/dist/__tests__/dashboard/index.test.js.map +1 -0
  19. package/dist/__tests__/dashboard/routes.test.d.ts +2 -0
  20. package/dist/__tests__/dashboard/routes.test.d.ts.map +1 -0
  21. package/dist/__tests__/dashboard/routes.test.js +125 -0
  22. package/dist/__tests__/dashboard/routes.test.js.map +1 -0
  23. package/dist/__tests__/dashboard/server.test.d.ts +2 -0
  24. package/dist/__tests__/dashboard/server.test.d.ts.map +1 -0
  25. package/dist/__tests__/dashboard/server.test.js +75 -0
  26. package/dist/__tests__/dashboard/server.test.js.map +1 -0
  27. package/dist/__tests__/dashboard/state.test.d.ts +2 -0
  28. package/dist/__tests__/dashboard/state.test.d.ts.map +1 -0
  29. package/dist/__tests__/dashboard/state.test.js +124 -0
  30. package/dist/__tests__/dashboard/state.test.js.map +1 -0
  31. package/dist/__tests__/dashboard/token-tracking.test.d.ts +2 -0
  32. package/dist/__tests__/dashboard/token-tracking.test.d.ts.map +1 -0
  33. package/dist/__tests__/dashboard/token-tracking.test.js +315 -0
  34. package/dist/__tests__/dashboard/token-tracking.test.js.map +1 -0
  35. package/dist/__tests__/embeddings/factory.test.d.ts +2 -0
  36. package/dist/__tests__/embeddings/factory.test.d.ts.map +1 -0
  37. package/dist/__tests__/embeddings/factory.test.js +157 -0
  38. package/dist/__tests__/embeddings/factory.test.js.map +1 -0
  39. package/dist/__tests__/embeddings/gemini.test.d.ts +2 -0
  40. package/dist/__tests__/embeddings/gemini.test.d.ts.map +1 -0
  41. package/dist/__tests__/embeddings/gemini.test.js +178 -0
  42. package/dist/__tests__/embeddings/gemini.test.js.map +1 -0
  43. package/dist/__tests__/embeddings/ollama.test.d.ts +2 -0
  44. package/dist/__tests__/embeddings/ollama.test.d.ts.map +1 -0
  45. package/dist/__tests__/embeddings/ollama.test.js +273 -0
  46. package/dist/__tests__/embeddings/ollama.test.js.map +1 -0
  47. package/dist/__tests__/embeddings/rate-limiter.test.d.ts +2 -0
  48. package/dist/__tests__/embeddings/rate-limiter.test.d.ts.map +1 -0
  49. package/dist/__tests__/embeddings/rate-limiter.test.js +163 -0
  50. package/dist/__tests__/embeddings/rate-limiter.test.js.map +1 -0
  51. package/dist/__tests__/embeddings/retry.test.d.ts +2 -0
  52. package/dist/__tests__/embeddings/retry.test.d.ts.map +1 -0
  53. package/dist/__tests__/embeddings/retry.test.js +334 -0
  54. package/dist/__tests__/embeddings/retry.test.js.map +1 -0
  55. package/dist/__tests__/embeddings/types.test.d.ts +2 -0
  56. package/dist/__tests__/embeddings/types.test.d.ts.map +1 -0
  57. package/dist/__tests__/embeddings/types.test.js +31 -0
  58. package/dist/__tests__/embeddings/types.test.js.map +1 -0
  59. package/dist/__tests__/mocks/embedding-backend.mock.d.ts +10 -0
  60. package/dist/__tests__/mocks/embedding-backend.mock.d.ts.map +1 -0
  61. package/dist/__tests__/mocks/embedding-backend.mock.js +39 -0
  62. package/dist/__tests__/mocks/embedding-backend.mock.js.map +1 -0
  63. package/dist/__tests__/mocks/fetch.mock.d.ts +49 -0
  64. package/dist/__tests__/mocks/fetch.mock.d.ts.map +1 -0
  65. package/dist/__tests__/mocks/fetch.mock.js +102 -0
  66. package/dist/__tests__/mocks/fetch.mock.js.map +1 -0
  67. package/dist/__tests__/mocks/lancedb.mock.d.ts +38 -0
  68. package/dist/__tests__/mocks/lancedb.mock.d.ts.map +1 -0
  69. package/dist/__tests__/mocks/lancedb.mock.js +63 -0
  70. package/dist/__tests__/mocks/lancedb.mock.js.map +1 -0
  71. package/dist/__tests__/search/clustering.test.d.ts +2 -0
  72. package/dist/__tests__/search/clustering.test.d.ts.map +1 -0
  73. package/dist/__tests__/search/clustering.test.js +230 -0
  74. package/dist/__tests__/search/clustering.test.js.map +1 -0
  75. package/dist/__tests__/search/hybrid-search.test.d.ts +2 -0
  76. package/dist/__tests__/search/hybrid-search.test.d.ts.map +1 -0
  77. package/dist/__tests__/search/hybrid-search.test.js +279 -0
  78. package/dist/__tests__/search/hybrid-search.test.js.map +1 -0
  79. package/dist/__tests__/search/indexer.test.d.ts +2 -0
  80. package/dist/__tests__/search/indexer.test.d.ts.map +1 -0
  81. package/dist/__tests__/search/indexer.test.js +1305 -0
  82. package/dist/__tests__/search/indexer.test.js.map +1 -0
  83. package/dist/__tests__/search/tree-sitter-chunker.test.d.ts +2 -0
  84. package/dist/__tests__/search/tree-sitter-chunker.test.d.ts.map +1 -0
  85. package/dist/__tests__/search/tree-sitter-chunker.test.js +228 -0
  86. package/dist/__tests__/search/tree-sitter-chunker.test.js.map +1 -0
  87. package/dist/__tests__/setup.d.ts +2 -0
  88. package/dist/__tests__/setup.d.ts.map +1 -0
  89. package/dist/__tests__/setup.js +11 -0
  90. package/dist/__tests__/setup.js.map +1 -0
  91. package/dist/__tests__/tools/clustering-handlers.test.d.ts +2 -0
  92. package/dist/__tests__/tools/clustering-handlers.test.d.ts.map +1 -0
  93. package/dist/__tests__/tools/clustering-handlers.test.js +286 -0
  94. package/dist/__tests__/tools/clustering-handlers.test.js.map +1 -0
  95. package/dist/__tests__/tools/commit-handlers.test.d.ts +2 -0
  96. package/dist/__tests__/tools/commit-handlers.test.d.ts.map +1 -0
  97. package/dist/__tests__/tools/commit-handlers.test.js +153 -0
  98. package/dist/__tests__/tools/commit-handlers.test.js.map +1 -0
  99. package/dist/__tests__/tools/index-handlers.test.d.ts +2 -0
  100. package/dist/__tests__/tools/index-handlers.test.d.ts.map +1 -0
  101. package/dist/__tests__/tools/index-handlers.test.js +184 -0
  102. package/dist/__tests__/tools/index-handlers.test.js.map +1 -0
  103. package/dist/__tests__/tools/instructions-handlers.test.d.ts +2 -0
  104. package/dist/__tests__/tools/instructions-handlers.test.d.ts.map +1 -0
  105. package/dist/__tests__/tools/instructions-handlers.test.js +53 -0
  106. package/dist/__tests__/tools/instructions-handlers.test.js.map +1 -0
  107. package/dist/__tests__/tools/memory-handlers.test.d.ts +2 -0
  108. package/dist/__tests__/tools/memory-handlers.test.d.ts.map +1 -0
  109. package/dist/__tests__/tools/memory-handlers.test.js +175 -0
  110. package/dist/__tests__/tools/memory-handlers.test.js.map +1 -0
  111. package/dist/__tests__/tools/search-handlers.test.d.ts +2 -0
  112. package/dist/__tests__/tools/search-handlers.test.d.ts.map +1 -0
  113. package/dist/__tests__/tools/search-handlers.test.js +229 -0
  114. package/dist/__tests__/tools/search-handlers.test.js.map +1 -0
  115. package/dist/__tests__/tools/symbol-handlers.test.d.ts +2 -0
  116. package/dist/__tests__/tools/symbol-handlers.test.d.ts.map +1 -0
  117. package/dist/__tests__/tools/symbol-handlers.test.js +348 -0
  118. package/dist/__tests__/tools/symbol-handlers.test.js.map +1 -0
  119. package/dist/__tests__/tools/worktree-handlers.test.d.ts +2 -0
  120. package/dist/__tests__/tools/worktree-handlers.test.d.ts.map +1 -0
  121. package/dist/__tests__/tools/worktree-handlers.test.js +297 -0
  122. package/dist/__tests__/tools/worktree-handlers.test.js.map +1 -0
  123. package/dist/__tests__/utils/cache.test.d.ts +2 -0
  124. package/dist/__tests__/utils/cache.test.d.ts.map +1 -0
  125. package/dist/__tests__/utils/cache.test.js +147 -0
  126. package/dist/__tests__/utils/cache.test.js.map +1 -0
  127. package/dist/__tests__/utils/concurrency.test.d.ts +2 -0
  128. package/dist/__tests__/utils/concurrency.test.d.ts.map +1 -0
  129. package/dist/__tests__/utils/concurrency.test.js +83 -0
  130. package/dist/__tests__/utils/concurrency.test.js.map +1 -0
  131. package/dist/__tests__/utils/errors.test.d.ts +2 -0
  132. package/dist/__tests__/utils/errors.test.d.ts.map +1 -0
  133. package/dist/__tests__/utils/errors.test.js +136 -0
  134. package/dist/__tests__/utils/errors.test.js.map +1 -0
  135. package/dist/__tests__/utils/logger.test.d.ts +2 -0
  136. package/dist/__tests__/utils/logger.test.d.ts.map +1 -0
  137. package/dist/__tests__/utils/logger.test.js +131 -0
  138. package/dist/__tests__/utils/logger.test.js.map +1 -0
  139. package/dist/__tests__/utils/type-guards.test.d.ts +2 -0
  140. package/dist/__tests__/utils/type-guards.test.d.ts.map +1 -0
  141. package/dist/__tests__/utils/type-guards.test.js +80 -0
  142. package/dist/__tests__/utils/type-guards.test.js.map +1 -0
  143. package/dist/__tests__/worktree/worktree-manager.test.d.ts +2 -0
  144. package/dist/__tests__/worktree/worktree-manager.test.d.ts.map +1 -0
  145. package/dist/__tests__/worktree/worktree-manager.test.js +403 -0
  146. package/dist/__tests__/worktree/worktree-manager.test.js.map +1 -0
  147. package/dist/config.d.ts +191 -0
  148. package/dist/config.d.ts.map +1 -0
  149. package/dist/config.js +788 -0
  150. package/dist/config.js.map +1 -0
  151. package/dist/dashboard/beads.d.ts +35 -0
  152. package/dist/dashboard/beads.d.ts.map +1 -0
  153. package/dist/dashboard/beads.js +102 -0
  154. package/dist/dashboard/beads.js.map +1 -0
  155. package/dist/dashboard/events.d.ts +64 -0
  156. package/dist/dashboard/events.d.ts.map +1 -0
  157. package/dist/dashboard/events.js +211 -0
  158. package/dist/dashboard/events.js.map +1 -0
  159. package/dist/dashboard/index.d.ts +69 -0
  160. package/dist/dashboard/index.d.ts.map +1 -0
  161. package/dist/dashboard/index.js +93 -0
  162. package/dist/dashboard/index.js.map +1 -0
  163. package/dist/dashboard/routes.d.ts +6 -0
  164. package/dist/dashboard/routes.d.ts.map +1 -0
  165. package/dist/dashboard/routes.js +505 -0
  166. package/dist/dashboard/routes.js.map +1 -0
  167. package/dist/dashboard/server.d.ts +27 -0
  168. package/dist/dashboard/server.d.ts.map +1 -0
  169. package/dist/dashboard/server.js +72 -0
  170. package/dist/dashboard/server.js.map +1 -0
  171. package/dist/dashboard/state.d.ts +172 -0
  172. package/dist/dashboard/state.d.ts.map +1 -0
  173. package/dist/dashboard/state.js +362 -0
  174. package/dist/dashboard/state.js.map +1 -0
  175. package/dist/dashboard/token-tracking.d.ts +143 -0
  176. package/dist/dashboard/token-tracking.d.ts.map +1 -0
  177. package/dist/dashboard/token-tracking.js +363 -0
  178. package/dist/dashboard/token-tracking.js.map +1 -0
  179. package/dist/dashboard/ui.d.ts +6 -0
  180. package/dist/dashboard/ui.d.ts.map +1 -0
  181. package/dist/dashboard/ui.js +2710 -0
  182. package/dist/dashboard/ui.js.map +1 -0
  183. package/dist/embeddings/gemini.d.ts +28 -0
  184. package/dist/embeddings/gemini.d.ts.map +1 -0
  185. package/dist/embeddings/gemini.js +165 -0
  186. package/dist/embeddings/gemini.js.map +1 -0
  187. package/dist/embeddings/index.d.ts +34 -0
  188. package/dist/embeddings/index.d.ts.map +1 -0
  189. package/dist/embeddings/index.js +120 -0
  190. package/dist/embeddings/index.js.map +1 -0
  191. package/dist/embeddings/ollama.d.ts +22 -0
  192. package/dist/embeddings/ollama.d.ts.map +1 -0
  193. package/dist/embeddings/ollama.js +179 -0
  194. package/dist/embeddings/ollama.js.map +1 -0
  195. package/dist/embeddings/rate-limiter.d.ts +75 -0
  196. package/dist/embeddings/rate-limiter.d.ts.map +1 -0
  197. package/dist/embeddings/rate-limiter.js +145 -0
  198. package/dist/embeddings/rate-limiter.js.map +1 -0
  199. package/dist/embeddings/retry.d.ts +20 -0
  200. package/dist/embeddings/retry.d.ts.map +1 -0
  201. package/dist/embeddings/retry.js +227 -0
  202. package/dist/embeddings/retry.js.map +1 -0
  203. package/dist/embeddings/types.d.ts +103 -0
  204. package/dist/embeddings/types.d.ts.map +1 -0
  205. package/dist/embeddings/types.js +23 -0
  206. package/dist/embeddings/types.js.map +1 -0
  207. package/dist/index.d.ts +12 -0
  208. package/dist/index.d.ts.map +1 -0
  209. package/dist/index.js +2028 -0
  210. package/dist/index.js.map +1 -0
  211. package/dist/memory/index.d.ts +63 -0
  212. package/dist/memory/index.d.ts.map +1 -0
  213. package/dist/memory/index.js +168 -0
  214. package/dist/memory/index.js.map +1 -0
  215. package/dist/search/ast-chunker.d.ts +29 -0
  216. package/dist/search/ast-chunker.d.ts.map +1 -0
  217. package/dist/search/ast-chunker.js +236 -0
  218. package/dist/search/ast-chunker.js.map +1 -0
  219. package/dist/search/chunk-utils.d.ts +24 -0
  220. package/dist/search/chunk-utils.d.ts.map +1 -0
  221. package/dist/search/chunk-utils.js +41 -0
  222. package/dist/search/chunk-utils.js.map +1 -0
  223. package/dist/search/clustering.d.ts +77 -0
  224. package/dist/search/clustering.d.ts.map +1 -0
  225. package/dist/search/clustering.js +455 -0
  226. package/dist/search/clustering.js.map +1 -0
  227. package/dist/search/indexer.d.ts +377 -0
  228. package/dist/search/indexer.d.ts.map +1 -0
  229. package/dist/search/indexer.js +1557 -0
  230. package/dist/search/indexer.js.map +1 -0
  231. package/dist/search/tree-sitter-chunker.d.ts +64 -0
  232. package/dist/search/tree-sitter-chunker.d.ts.map +1 -0
  233. package/dist/search/tree-sitter-chunker.js +412 -0
  234. package/dist/search/tree-sitter-chunker.js.map +1 -0
  235. package/dist/symbols/index.d.ts +14 -0
  236. package/dist/symbols/index.d.ts.map +1 -0
  237. package/dist/symbols/index.js +19 -0
  238. package/dist/symbols/index.js.map +1 -0
  239. package/dist/symbols/name-path.d.ts +113 -0
  240. package/dist/symbols/name-path.d.ts.map +1 -0
  241. package/dist/symbols/name-path.js +194 -0
  242. package/dist/symbols/name-path.js.map +1 -0
  243. package/dist/symbols/pattern-search.d.ts +14 -0
  244. package/dist/symbols/pattern-search.d.ts.map +1 -0
  245. package/dist/symbols/pattern-search.js +224 -0
  246. package/dist/symbols/pattern-search.js.map +1 -0
  247. package/dist/symbols/reference-finder.d.ts +38 -0
  248. package/dist/symbols/reference-finder.d.ts.map +1 -0
  249. package/dist/symbols/reference-finder.js +376 -0
  250. package/dist/symbols/reference-finder.js.map +1 -0
  251. package/dist/symbols/symbol-editor.d.ts +81 -0
  252. package/dist/symbols/symbol-editor.d.ts.map +1 -0
  253. package/dist/symbols/symbol-editor.js +257 -0
  254. package/dist/symbols/symbol-editor.js.map +1 -0
  255. package/dist/symbols/symbol-extractor.d.ts +49 -0
  256. package/dist/symbols/symbol-extractor.d.ts.map +1 -0
  257. package/dist/symbols/symbol-extractor.js +593 -0
  258. package/dist/symbols/symbol-extractor.js.map +1 -0
  259. package/dist/symbols/symbol-renamer.d.ts +81 -0
  260. package/dist/symbols/symbol-renamer.d.ts.map +1 -0
  261. package/dist/symbols/symbol-renamer.js +204 -0
  262. package/dist/symbols/symbol-renamer.js.map +1 -0
  263. package/dist/symbols/types.d.ts +234 -0
  264. package/dist/symbols/types.d.ts.map +1 -0
  265. package/dist/symbols/types.js +106 -0
  266. package/dist/symbols/types.js.map +1 -0
  267. package/dist/tools/clustering-handlers.d.ts +70 -0
  268. package/dist/tools/clustering-handlers.d.ts.map +1 -0
  269. package/dist/tools/clustering-handlers.js +150 -0
  270. package/dist/tools/clustering-handlers.js.map +1 -0
  271. package/dist/tools/commit-handlers.d.ts +77 -0
  272. package/dist/tools/commit-handlers.d.ts.map +1 -0
  273. package/dist/tools/commit-handlers.js +251 -0
  274. package/dist/tools/commit-handlers.js.map +1 -0
  275. package/dist/tools/index-handlers.d.ts +31 -0
  276. package/dist/tools/index-handlers.d.ts.map +1 -0
  277. package/dist/tools/index-handlers.js +53 -0
  278. package/dist/tools/index-handlers.js.map +1 -0
  279. package/dist/tools/instructions-handlers.d.ts +25 -0
  280. package/dist/tools/instructions-handlers.d.ts.map +1 -0
  281. package/dist/tools/instructions-handlers.js +36 -0
  282. package/dist/tools/instructions-handlers.js.map +1 -0
  283. package/dist/tools/memory-handlers.d.ts +95 -0
  284. package/dist/tools/memory-handlers.d.ts.map +1 -0
  285. package/dist/tools/memory-handlers.js +126 -0
  286. package/dist/tools/memory-handlers.js.map +1 -0
  287. package/dist/tools/search-handlers.d.ts +51 -0
  288. package/dist/tools/search-handlers.d.ts.map +1 -0
  289. package/dist/tools/search-handlers.js +128 -0
  290. package/dist/tools/search-handlers.js.map +1 -0
  291. package/dist/tools/symbol-handlers.d.ts +171 -0
  292. package/dist/tools/symbol-handlers.d.ts.map +1 -0
  293. package/dist/tools/symbol-handlers.js +456 -0
  294. package/dist/tools/symbol-handlers.js.map +1 -0
  295. package/dist/tools/types.d.ts +32 -0
  296. package/dist/tools/types.d.ts.map +1 -0
  297. package/dist/tools/types.js +17 -0
  298. package/dist/tools/types.js.map +1 -0
  299. package/dist/tools/worktree-handlers.d.ts +96 -0
  300. package/dist/tools/worktree-handlers.d.ts.map +1 -0
  301. package/dist/tools/worktree-handlers.js +186 -0
  302. package/dist/tools/worktree-handlers.js.map +1 -0
  303. package/dist/utils/cache.d.ts +77 -0
  304. package/dist/utils/cache.d.ts.map +1 -0
  305. package/dist/utils/cache.js +134 -0
  306. package/dist/utils/cache.js.map +1 -0
  307. package/dist/utils/concurrency.d.ts +32 -0
  308. package/dist/utils/concurrency.d.ts.map +1 -0
  309. package/dist/utils/concurrency.js +57 -0
  310. package/dist/utils/concurrency.js.map +1 -0
  311. package/dist/utils/errors.d.ts +36 -0
  312. package/dist/utils/errors.d.ts.map +1 -0
  313. package/dist/utils/errors.js +91 -0
  314. package/dist/utils/errors.js.map +1 -0
  315. package/dist/utils/logger.d.ts +37 -0
  316. package/dist/utils/logger.d.ts.map +1 -0
  317. package/dist/utils/logger.js +114 -0
  318. package/dist/utils/logger.js.map +1 -0
  319. package/dist/utils/type-guards.d.ts +17 -0
  320. package/dist/utils/type-guards.d.ts.map +1 -0
  321. package/dist/utils/type-guards.js +25 -0
  322. package/dist/utils/type-guards.js.map +1 -0
  323. package/dist/worktree/index.d.ts +6 -0
  324. package/dist/worktree/index.d.ts.map +1 -0
  325. package/dist/worktree/index.js +6 -0
  326. package/dist/worktree/index.js.map +1 -0
  327. package/dist/worktree/types.d.ts +101 -0
  328. package/dist/worktree/types.d.ts.map +1 -0
  329. package/dist/worktree/types.js +6 -0
  330. package/dist/worktree/types.js.map +1 -0
  331. package/dist/worktree/worktree-manager.d.ts +80 -0
  332. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  333. package/dist/worktree/worktree-manager.js +407 -0
  334. package/dist/worktree/worktree-manager.js.map +1 -0
  335. package/package.json +87 -0
  336. package/scripts/postinstall.js +48 -0
@@ -0,0 +1,1305 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { CodeIndexer } from '../../search/indexer.js';
3
+ import { createMockEmbeddingBackend } from '../mocks/embedding-backend.mock.js';
4
+ import { createMockConnection, createMockTable } from '../mocks/lancedb.mock.js';
5
+ // Mock the lancedb module
6
+ vi.mock('@lancedb/lancedb', () => ({
7
+ connect: vi.fn(),
8
+ }));
9
+ // Mock fs/promises for file operations
10
+ vi.mock('fs/promises', () => ({
11
+ readFile: vi.fn(),
12
+ writeFile: vi.fn(),
13
+ stat: vi.fn(),
14
+ mkdir: vi.fn(),
15
+ unlink: vi.fn(),
16
+ }));
17
+ // Mock config
18
+ vi.mock('../../config.js', async (_importOriginal) => {
19
+ return {
20
+ loadConfig: vi
21
+ .fn()
22
+ .mockResolvedValue({ patterns: ['**/*.ts'], excludePatterns: ['**/node_modules/**'] }),
23
+ getDefaultPatterns: vi.fn().mockReturnValue(['**/*.ts', '**/*.js']),
24
+ getDefaultExcludePatterns: vi.fn().mockReturnValue(['**/node_modules/**']),
25
+ getChunkingConfig: vi.fn().mockReturnValue({ maxLines: 100, overlap: 20 }),
26
+ getSearchConfig: vi.fn().mockReturnValue({ semanticWeight: 0.7, keywordWeight: 0.3 }),
27
+ getIndexingConfig: vi.fn().mockReturnValue({ batchSize: 32, batchDelayMs: 0 }),
28
+ };
29
+ });
30
+ describe('CodeIndexer', () => {
31
+ let mockBackend;
32
+ let mockConnection;
33
+ let lancedb;
34
+ let fsPromises;
35
+ let configModule;
36
+ beforeEach(async () => {
37
+ mockBackend = createMockEmbeddingBackend();
38
+ mockConnection = createMockConnection();
39
+ lancedb = await import('@lancedb/lancedb');
40
+ fsPromises = await import('fs/promises');
41
+ configModule = await import('../../config.js');
42
+ vi.mocked(lancedb.connect).mockResolvedValue(mockConnection);
43
+ // Re-establish the config mock after resetAllMocks
44
+ vi.mocked(configModule.loadConfig).mockResolvedValue({
45
+ patterns: ['**/*.ts'],
46
+ excludePatterns: ['**/node_modules/**'],
47
+ chunking: { maxLines: 100, overlap: 20 },
48
+ search: { semanticWeight: 0.7, keywordWeight: 0.3 },
49
+ });
50
+ vi.mocked(configModule.getDefaultPatterns).mockReturnValue(['**/*.ts', '**/*.js']);
51
+ vi.mocked(configModule.getDefaultExcludePatterns).mockReturnValue(['**/node_modules/**']);
52
+ vi.mocked(configModule.getChunkingConfig).mockReturnValue({ maxLines: 100, overlap: 20 });
53
+ vi.mocked(configModule.getSearchConfig).mockReturnValue({
54
+ semanticWeight: 0.7,
55
+ keywordWeight: 0.3,
56
+ autoReindex: false, // Disable for tests to avoid reindexing during search
57
+ });
58
+ vi.mocked(configModule.getIndexingConfig).mockReturnValue({ batchSize: 32, batchDelayMs: 0 });
59
+ });
60
+ afterEach(() => {
61
+ vi.resetAllMocks();
62
+ });
63
+ describe('constructor', () => {
64
+ it('should set correct index path', () => {
65
+ const indexer = new CodeIndexer('/project', mockBackend);
66
+ // Access private property for testing
67
+ expect(indexer.projectPath).toBe('/project');
68
+ });
69
+ });
70
+ describe('initialize', () => {
71
+ it('should connect to LanceDB', async () => {
72
+ const indexer = new CodeIndexer('/project', mockBackend);
73
+ await indexer.initialize();
74
+ expect(lancedb.connect).toHaveBeenCalledWith('/project/.glancey');
75
+ });
76
+ });
77
+ describe('getStatus', () => {
78
+ it('should return indexed:false when no table exists', async () => {
79
+ mockConnection.tableNames.mockResolvedValue([]);
80
+ const indexer = new CodeIndexer('/project', mockBackend);
81
+ await indexer.initialize();
82
+ const status = await indexer.getStatus();
83
+ expect(status.indexed).toBe(false);
84
+ expect(status.fileCount).toBe(0);
85
+ expect(status.chunkCount).toBe(0);
86
+ expect(status.lastUpdated).toBeNull();
87
+ });
88
+ it('should return correct counts when indexed', async () => {
89
+ const mockTable = createMockTable([
90
+ {
91
+ id: '1',
92
+ filepath: 'test.ts',
93
+ content: 'code',
94
+ startLine: 1,
95
+ endLine: 10,
96
+ language: 'typescript',
97
+ },
98
+ {
99
+ id: '2',
100
+ filepath: 'test.ts',
101
+ content: 'more',
102
+ startLine: 11,
103
+ endLine: 20,
104
+ language: 'typescript',
105
+ },
106
+ ]);
107
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
108
+ mockConnection.openTable.mockResolvedValue(mockTable);
109
+ // Mock metadata file
110
+ vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
111
+ lastUpdated: '2024-01-01T00:00:00Z',
112
+ fileCount: 5,
113
+ chunkCount: 10,
114
+ embeddingBackend: 'mock',
115
+ embeddingDimensions: 1536,
116
+ }));
117
+ const indexer = new CodeIndexer('/project', mockBackend);
118
+ await indexer.initialize();
119
+ const status = await indexer.getStatus();
120
+ expect(status.indexed).toBe(true);
121
+ expect(status.fileCount).toBe(5);
122
+ expect(status.chunkCount).toBe(2); // From table.countRows
123
+ expect(status.lastUpdated).toBe('2024-01-01T00:00:00Z');
124
+ expect(status.embeddingBackend).toBe('mock');
125
+ });
126
+ it('should handle missing metadata gracefully', async () => {
127
+ const mockTable = createMockTable([
128
+ {
129
+ id: '1',
130
+ filepath: 'test.ts',
131
+ content: 'code',
132
+ startLine: 1,
133
+ endLine: 10,
134
+ language: 'typescript',
135
+ },
136
+ ]);
137
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
138
+ mockConnection.openTable.mockResolvedValue(mockTable);
139
+ // Metadata file doesn't exist
140
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
141
+ const indexer = new CodeIndexer('/project', mockBackend);
142
+ await indexer.initialize();
143
+ const status = await indexer.getStatus();
144
+ expect(status.indexed).toBe(true);
145
+ expect(status.fileCount).toBe(0);
146
+ expect(status.lastUpdated).toBeNull();
147
+ });
148
+ });
149
+ describe('search', () => {
150
+ it('should throw when not indexed', async () => {
151
+ mockConnection.tableNames.mockResolvedValue([]);
152
+ const indexer = new CodeIndexer('/project', mockBackend);
153
+ await indexer.initialize();
154
+ await expect(indexer.search('query')).rejects.toThrow('Codebase not indexed');
155
+ });
156
+ it('should embed query via backend', async () => {
157
+ const mockTable = createMockTable([
158
+ {
159
+ id: '1',
160
+ filepath: 'test.ts',
161
+ content: 'function test',
162
+ startLine: 1,
163
+ endLine: 10,
164
+ language: 'typescript',
165
+ },
166
+ ]);
167
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
168
+ mockConnection.openTable.mockResolvedValue(mockTable);
169
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
170
+ const indexer = new CodeIndexer('/project', mockBackend);
171
+ await indexer.initialize();
172
+ await indexer.search('find function');
173
+ expect(mockBackend.embed).toHaveBeenCalledWith('find function');
174
+ });
175
+ it('should respect limit parameter', async () => {
176
+ const chunks = Array(10)
177
+ .fill(null)
178
+ .map((_, i) => ({
179
+ id: `${i}`,
180
+ filepath: `test${i}.ts`,
181
+ content: `content ${i}`,
182
+ startLine: 1,
183
+ endLine: 10,
184
+ language: 'typescript',
185
+ }));
186
+ const mockTable = createMockTable(chunks);
187
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
188
+ mockConnection.openTable.mockResolvedValue(mockTable);
189
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
190
+ const indexer = new CodeIndexer('/project', mockBackend);
191
+ await indexer.initialize();
192
+ const results = await indexer.search('query', 3);
193
+ expect(results.length).toBe(3);
194
+ });
195
+ it('should return results with correct structure', async () => {
196
+ const mockTable = createMockTable([
197
+ {
198
+ id: 'test.ts:1-10',
199
+ filepath: 'test.ts',
200
+ content: 'function hello() {}',
201
+ startLine: 1,
202
+ endLine: 10,
203
+ language: 'typescript',
204
+ },
205
+ ]);
206
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
207
+ mockConnection.openTable.mockResolvedValue(mockTable);
208
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
209
+ const indexer = new CodeIndexer('/project', mockBackend);
210
+ await indexer.initialize();
211
+ const results = await indexer.search('hello');
212
+ expect(results[0]).toMatchObject({
213
+ id: 'test.ts:1-10',
214
+ filepath: 'test.ts',
215
+ content: 'function hello() {}',
216
+ startLine: 1,
217
+ endLine: 10,
218
+ language: 'typescript',
219
+ });
220
+ });
221
+ it('should include symbol context when available', async () => {
222
+ const mockTable = createMockTable([
223
+ {
224
+ id: 'test.ts:1-10:hello',
225
+ filepath: 'test.ts',
226
+ content: 'function hello() {}',
227
+ startLine: 1,
228
+ endLine: 10,
229
+ language: 'typescript',
230
+ symbolType: 'function',
231
+ symbolName: 'hello',
232
+ },
233
+ ]);
234
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
235
+ mockConnection.openTable.mockResolvedValue(mockTable);
236
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
237
+ const indexer = new CodeIndexer('/project', mockBackend);
238
+ await indexer.initialize();
239
+ const results = await indexer.search('hello');
240
+ expect(results[0]).toMatchObject({
241
+ id: 'test.ts:1-10:hello',
242
+ filepath: 'test.ts',
243
+ symbolType: 'function',
244
+ symbolName: 'hello',
245
+ });
246
+ });
247
+ });
248
+ describe('query embedding cache', () => {
249
+ it('should cache query embeddings and not recompute for identical queries', async () => {
250
+ const mockTable = createMockTable([
251
+ {
252
+ id: 'test.ts:1-10',
253
+ filepath: 'test.ts',
254
+ content: 'function test() {}',
255
+ startLine: 1,
256
+ endLine: 10,
257
+ language: 'typescript',
258
+ },
259
+ ]);
260
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
261
+ mockConnection.openTable.mockResolvedValue(mockTable);
262
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
263
+ const indexer = new CodeIndexer('/project', mockBackend);
264
+ await indexer.initialize();
265
+ // First search - should call embed
266
+ await indexer.search('test query');
267
+ expect(mockBackend.embed).toHaveBeenCalledTimes(1);
268
+ // Second search with same query - should use cache
269
+ await indexer.search('test query');
270
+ expect(mockBackend.embed).toHaveBeenCalledTimes(1); // Still 1, not 2
271
+ // Different query - should call embed again
272
+ await indexer.search('different query');
273
+ expect(mockBackend.embed).toHaveBeenCalledTimes(2);
274
+ });
275
+ it('should clear cache when index is cleared', async () => {
276
+ const mockTable = createMockTable([
277
+ {
278
+ id: 'test.ts:1-10',
279
+ filepath: 'test.ts',
280
+ content: 'function test() {}',
281
+ startLine: 1,
282
+ endLine: 10,
283
+ language: 'typescript',
284
+ },
285
+ ]);
286
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
287
+ mockConnection.openTable.mockResolvedValue(mockTable);
288
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
289
+ const indexer = new CodeIndexer('/project', mockBackend);
290
+ await indexer.initialize();
291
+ // First search - populates cache
292
+ await indexer.search('cached query');
293
+ expect(mockBackend.embed).toHaveBeenCalledTimes(1);
294
+ // Clear index - should clear cache
295
+ await indexer.clearIndex();
296
+ // Re-setup the mock table for next search
297
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
298
+ mockConnection.openTable.mockResolvedValue(mockTable);
299
+ // Same query after clear - should recompute
300
+ await indexer.search('cached query');
301
+ expect(mockBackend.embed).toHaveBeenCalledTimes(2);
302
+ });
303
+ });
304
+ describe('search filtering', () => {
305
+ it('should filter results by pathPattern glob', async () => {
306
+ const mockTable = createMockTable([
307
+ {
308
+ id: 'src/utils.ts:1-10',
309
+ filepath: 'src/utils.ts',
310
+ content: 'export function utility() {}',
311
+ startLine: 1,
312
+ endLine: 10,
313
+ language: 'typescript',
314
+ },
315
+ {
316
+ id: 'test/utils.test.ts:1-10',
317
+ filepath: 'test/utils.test.ts',
318
+ content: 'describe("utility", () => {})',
319
+ startLine: 1,
320
+ endLine: 10,
321
+ language: 'typescript',
322
+ },
323
+ {
324
+ id: 'src/index.ts:1-10',
325
+ filepath: 'src/index.ts',
326
+ content: 'import { utility } from "./utils"',
327
+ startLine: 1,
328
+ endLine: 10,
329
+ language: 'typescript',
330
+ },
331
+ ]);
332
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
333
+ mockConnection.openTable.mockResolvedValue(mockTable);
334
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
335
+ const indexer = new CodeIndexer('/project', mockBackend);
336
+ await indexer.initialize();
337
+ // Filter to only src directory
338
+ const results = await indexer.search({
339
+ query: 'utility',
340
+ pathPattern: 'src/**',
341
+ });
342
+ expect(results).toHaveLength(2);
343
+ expect(results.map((r) => r.filepath)).toEqual(['src/utils.ts', 'src/index.ts']);
344
+ });
345
+ it('should filter results by negation pathPattern', async () => {
346
+ const mockTable = createMockTable([
347
+ {
348
+ id: 'src/utils.ts:1-10',
349
+ filepath: 'src/utils.ts',
350
+ content: 'export function utility() {}',
351
+ startLine: 1,
352
+ endLine: 10,
353
+ language: 'typescript',
354
+ },
355
+ {
356
+ id: 'test/utils.test.ts:1-10',
357
+ filepath: 'test/utils.test.ts',
358
+ content: 'describe("utility", () => {})',
359
+ startLine: 1,
360
+ endLine: 10,
361
+ language: 'typescript',
362
+ },
363
+ ]);
364
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
365
+ mockConnection.openTable.mockResolvedValue(mockTable);
366
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
367
+ const indexer = new CodeIndexer('/project', mockBackend);
368
+ await indexer.initialize();
369
+ // Exclude test files
370
+ const results = await indexer.search({
371
+ query: 'utility',
372
+ pathPattern: '!test/**',
373
+ });
374
+ expect(results).toHaveLength(1);
375
+ expect(results[0].filepath).toBe('src/utils.ts');
376
+ });
377
+ it('should filter results by languages array', async () => {
378
+ const mockTable = createMockTable([
379
+ {
380
+ id: 'app.ts:1-10',
381
+ filepath: 'app.ts',
382
+ content: 'const app = express()',
383
+ startLine: 1,
384
+ endLine: 10,
385
+ language: 'typescript',
386
+ },
387
+ {
388
+ id: 'app.js:1-10',
389
+ filepath: 'app.js',
390
+ content: 'const app = require("express")()',
391
+ startLine: 1,
392
+ endLine: 10,
393
+ language: 'javascript',
394
+ },
395
+ {
396
+ id: 'styles.css:1-10',
397
+ filepath: 'styles.css',
398
+ content: '.app { display: flex; }',
399
+ startLine: 1,
400
+ endLine: 10,
401
+ language: 'css',
402
+ },
403
+ ]);
404
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
405
+ mockConnection.openTable.mockResolvedValue(mockTable);
406
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
407
+ const indexer = new CodeIndexer('/project', mockBackend);
408
+ await indexer.initialize();
409
+ // Filter to only TypeScript files
410
+ const results = await indexer.search({
411
+ query: 'app',
412
+ languages: ['typescript'],
413
+ });
414
+ expect(results).toHaveLength(1);
415
+ expect(results[0].language).toBe('typescript');
416
+ });
417
+ it('should filter by multiple languages', async () => {
418
+ const mockTable = createMockTable([
419
+ {
420
+ id: 'app.ts:1-10',
421
+ filepath: 'app.ts',
422
+ content: 'const app = express()',
423
+ startLine: 1,
424
+ endLine: 10,
425
+ language: 'typescript',
426
+ },
427
+ {
428
+ id: 'app.js:1-10',
429
+ filepath: 'app.js',
430
+ content: 'const app = require("express")()',
431
+ startLine: 1,
432
+ endLine: 10,
433
+ language: 'javascript',
434
+ },
435
+ {
436
+ id: 'styles.css:1-10',
437
+ filepath: 'styles.css',
438
+ content: '.app { display: flex; }',
439
+ startLine: 1,
440
+ endLine: 10,
441
+ language: 'css',
442
+ },
443
+ ]);
444
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
445
+ mockConnection.openTable.mockResolvedValue(mockTable);
446
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
447
+ const indexer = new CodeIndexer('/project', mockBackend);
448
+ await indexer.initialize();
449
+ // Filter to TypeScript and JavaScript
450
+ const results = await indexer.search({
451
+ query: 'app',
452
+ languages: ['typescript', 'javascript'],
453
+ });
454
+ expect(results).toHaveLength(2);
455
+ expect(results.map((r) => r.language).sort()).toEqual(['javascript', 'typescript']);
456
+ });
457
+ it('should combine pathPattern and languages filters', async () => {
458
+ const mockTable = createMockTable([
459
+ {
460
+ id: 'src/app.ts:1-10',
461
+ filepath: 'src/app.ts',
462
+ content: 'const app = express()',
463
+ startLine: 1,
464
+ endLine: 10,
465
+ language: 'typescript',
466
+ },
467
+ {
468
+ id: 'src/app.js:1-10',
469
+ filepath: 'src/app.js',
470
+ content: 'const app = require("express")()',
471
+ startLine: 1,
472
+ endLine: 10,
473
+ language: 'javascript',
474
+ },
475
+ {
476
+ id: 'test/app.test.ts:1-10',
477
+ filepath: 'test/app.test.ts',
478
+ content: 'describe("app", () => {})',
479
+ startLine: 1,
480
+ endLine: 10,
481
+ language: 'typescript',
482
+ },
483
+ ]);
484
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
485
+ mockConnection.openTable.mockResolvedValue(mockTable);
486
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
487
+ const indexer = new CodeIndexer('/project', mockBackend);
488
+ await indexer.initialize();
489
+ // Filter to src directory AND TypeScript only
490
+ const results = await indexer.search({
491
+ query: 'app',
492
+ pathPattern: 'src/**',
493
+ languages: ['typescript'],
494
+ });
495
+ expect(results).toHaveLength(1);
496
+ expect(results[0].filepath).toBe('src/app.ts');
497
+ });
498
+ it('should handle case-insensitive language filtering', async () => {
499
+ const mockTable = createMockTable([
500
+ {
501
+ id: 'app.ts:1-10',
502
+ filepath: 'app.ts',
503
+ content: 'const app = express()',
504
+ startLine: 1,
505
+ endLine: 10,
506
+ language: 'TypeScript',
507
+ },
508
+ ]);
509
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
510
+ mockConnection.openTable.mockResolvedValue(mockTable);
511
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
512
+ const indexer = new CodeIndexer('/project', mockBackend);
513
+ await indexer.initialize();
514
+ // Use lowercase in filter
515
+ const results = await indexer.search({
516
+ query: 'app',
517
+ languages: ['typescript'],
518
+ });
519
+ expect(results).toHaveLength(1);
520
+ });
521
+ it('should support SearchOptions object syntax', async () => {
522
+ const mockTable = createMockTable([
523
+ {
524
+ id: 'test.ts:1-10',
525
+ filepath: 'test.ts',
526
+ content: 'function test() {}',
527
+ startLine: 1,
528
+ endLine: 10,
529
+ language: 'typescript',
530
+ },
531
+ ]);
532
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
533
+ mockConnection.openTable.mockResolvedValue(mockTable);
534
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
535
+ const indexer = new CodeIndexer('/project', mockBackend);
536
+ await indexer.initialize();
537
+ // Use SearchOptions object
538
+ const results = await indexer.search({
539
+ query: 'test',
540
+ limit: 5,
541
+ });
542
+ expect(results).toHaveLength(1);
543
+ });
544
+ });
545
+ describe('clearIndex', () => {
546
+ it('should drop table when it exists', async () => {
547
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
548
+ const indexer = new CodeIndexer('/project', mockBackend);
549
+ await indexer.initialize();
550
+ await indexer.clearIndex();
551
+ expect(mockConnection.dropTable).toHaveBeenCalledWith('code_chunks');
552
+ });
553
+ it('should handle non-existent table gracefully', async () => {
554
+ mockConnection.tableNames.mockResolvedValue([]);
555
+ const indexer = new CodeIndexer('/project', mockBackend);
556
+ await indexer.initialize();
557
+ // Should not throw
558
+ await expect(indexer.clearIndex()).resolves.toBeUndefined();
559
+ expect(mockConnection.dropTable).not.toHaveBeenCalled();
560
+ });
561
+ });
562
+ describe('searchSimilar', () => {
563
+ it('should find similar code with code snippet input', async () => {
564
+ const chunks = [
565
+ {
566
+ id: 'file1.ts:1-10',
567
+ filepath: 'file1.ts',
568
+ content: 'function authenticate(user) { return true; }',
569
+ startLine: 1,
570
+ endLine: 10,
571
+ language: 'typescript',
572
+ _distance: 0.1,
573
+ },
574
+ {
575
+ id: 'file2.ts:1-10',
576
+ filepath: 'file2.ts',
577
+ content: 'function validate(input) { return false; }',
578
+ startLine: 1,
579
+ endLine: 10,
580
+ language: 'typescript',
581
+ _distance: 0.5,
582
+ },
583
+ ];
584
+ const mockTable = createMockTable(chunks);
585
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
586
+ mockConnection.openTable.mockResolvedValue(mockTable);
587
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
588
+ const indexer = new CodeIndexer('/project', mockBackend);
589
+ await indexer.initialize();
590
+ const results = await indexer.searchSimilar({
591
+ code: 'function login(user) { return true; }',
592
+ limit: 10,
593
+ });
594
+ expect(results.length).toBe(2);
595
+ expect(results[0].filepath).toBe('file1.ts');
596
+ expect(results[0]).toHaveProperty('similarity');
597
+ expect(mockBackend.embed).toHaveBeenCalledWith('function login(user) { return true; }');
598
+ });
599
+ it('should exclude self when excludeSelf is true', async () => {
600
+ const chunks = [
601
+ {
602
+ id: 'file1.ts:1-10',
603
+ filepath: 'file1.ts',
604
+ content: 'function test() {}',
605
+ startLine: 1,
606
+ endLine: 10,
607
+ language: 'typescript',
608
+ _distance: 0,
609
+ },
610
+ {
611
+ id: 'file2.ts:1-10',
612
+ filepath: 'file2.ts',
613
+ content: 'function other() {}',
614
+ startLine: 1,
615
+ endLine: 10,
616
+ language: 'typescript',
617
+ _distance: 0.3,
618
+ },
619
+ ];
620
+ const mockTable = createMockTable(chunks);
621
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
622
+ mockConnection.openTable.mockResolvedValue(mockTable);
623
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
624
+ const indexer = new CodeIndexer('/project', mockBackend);
625
+ await indexer.initialize();
626
+ // Should exclude exact content match
627
+ const results = await indexer.searchSimilar({
628
+ code: 'function test() {}',
629
+ excludeSelf: true,
630
+ });
631
+ expect(results.every((r) => r.content.trim() !== 'function test() {}')).toBe(true);
632
+ });
633
+ it('should throw error when neither code nor filepath is provided', async () => {
634
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
635
+ const indexer = new CodeIndexer('/project', mockBackend);
636
+ await indexer.initialize();
637
+ await expect(indexer.searchSimilar({})).rejects.toThrow('Either code or filepath must be provided');
638
+ });
639
+ it('should respect threshold parameter', async () => {
640
+ const chunks = [
641
+ {
642
+ id: 'file1.ts:1-10',
643
+ filepath: 'file1.ts',
644
+ content: 'high similarity',
645
+ startLine: 1,
646
+ endLine: 10,
647
+ language: 'typescript',
648
+ _distance: 0.1,
649
+ },
650
+ {
651
+ id: 'file2.ts:1-10',
652
+ filepath: 'file2.ts',
653
+ content: 'low similarity',
654
+ startLine: 1,
655
+ endLine: 10,
656
+ language: 'typescript',
657
+ _distance: 0.9,
658
+ },
659
+ ];
660
+ const mockTable = createMockTable(chunks);
661
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
662
+ mockConnection.openTable.mockResolvedValue(mockTable);
663
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
664
+ const indexer = new CodeIndexer('/project', mockBackend);
665
+ await indexer.initialize();
666
+ // With high threshold, should filter out low similarity results
667
+ const results = await indexer.searchSimilar({
668
+ code: 'test code',
669
+ threshold: 0.5,
670
+ });
671
+ // Only high similarity result should pass
672
+ expect(results.length).toBe(1);
673
+ expect(results[0].filepath).toBe('file1.ts');
674
+ });
675
+ });
676
+ describe('indexCodebase', () => {
677
+ beforeEach(() => {
678
+ // Mock glob
679
+ vi.doMock('glob', () => ({
680
+ glob: vi.fn().mockResolvedValue([]),
681
+ }));
682
+ // Mock file stats
683
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
684
+ vi.mocked(fsPromises.writeFile).mockResolvedValue();
685
+ });
686
+ it('should use provided patterns over config', async () => {
687
+ const { glob } = await import('glob');
688
+ vi.mocked(glob).mockResolvedValue([]);
689
+ mockConnection.tableNames.mockResolvedValue([]);
690
+ const indexer = new CodeIndexer('/project', mockBackend);
691
+ await indexer.initialize();
692
+ await indexer.indexCodebase(['**/*.py'], ['**/venv/**']);
693
+ expect(glob).toHaveBeenCalledWith('**/*.py', expect.objectContaining({
694
+ ignore: ['**/venv/**'],
695
+ }));
696
+ });
697
+ it('should detect incremental vs full indexing', async () => {
698
+ const { glob } = await import('glob');
699
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
700
+ vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
701
+ if (path.includes('index-metadata')) {
702
+ return JSON.stringify({
703
+ lastUpdated: '2024-01-01',
704
+ fileCount: 1,
705
+ chunkCount: 1,
706
+ embeddingBackend: 'mock',
707
+ embeddingDimensions: 1536,
708
+ });
709
+ }
710
+ return 'const x = 1;';
711
+ });
712
+ // Has existing index
713
+ mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
714
+ const mockTable = createMockTable([]);
715
+ const mockMetadataTable = createMockTable([]);
716
+ mockMetadataTable.query = vi.fn().mockReturnValue({
717
+ toArray: vi.fn().mockResolvedValue([{ filepath: 'test.ts', mtime: Date.now() }]),
718
+ });
719
+ mockConnection.openTable.mockImplementation(async (name) => {
720
+ if (name === 'file_metadata')
721
+ return mockMetadataTable;
722
+ return mockTable;
723
+ });
724
+ const indexer = new CodeIndexer('/project', mockBackend);
725
+ await indexer.initialize();
726
+ const result = await indexer.indexCodebase();
727
+ expect(result.incremental).toBe(true);
728
+ });
729
+ it('should handle force reindex flag', async () => {
730
+ const { glob } = await import('glob');
731
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
732
+ vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
733
+ // Has existing index
734
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
735
+ const indexer = new CodeIndexer('/project', mockBackend);
736
+ await indexer.initialize();
737
+ const result = await indexer.indexCodebase(undefined, undefined, true);
738
+ expect(result.incremental).toBe(false);
739
+ expect(mockConnection.dropTable).toHaveBeenCalledWith('code_chunks');
740
+ });
741
+ it('should report progress via callback', async () => {
742
+ const { glob } = await import('glob');
743
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
744
+ vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
745
+ mockConnection.tableNames.mockResolvedValue([]);
746
+ const indexer = new CodeIndexer('/project', mockBackend);
747
+ await indexer.initialize();
748
+ const progressUpdates = [];
749
+ await indexer.indexCodebase(undefined, undefined, false, (progress) => {
750
+ progressUpdates.push(progress);
751
+ });
752
+ expect(progressUpdates.length).toBeGreaterThan(0);
753
+ expect(progressUpdates.some((p) => p.phase === 'scanning')).toBe(true);
754
+ expect(progressUpdates.some((p) => p.phase === 'chunking')).toBe(true);
755
+ expect(progressUpdates.some((p) => p.phase === 'embedding')).toBe(true);
756
+ });
757
+ });
758
+ describe('hybrid scoring', () => {
759
+ it('should apply 70/30 semantic/keyword split', async () => {
760
+ // This tests the calculateKeywordScore indirectly through search
761
+ const mockTable = createMockTable([
762
+ {
763
+ id: '1',
764
+ filepath: 'auth.ts',
765
+ content: 'function authenticate() {}',
766
+ startLine: 1,
767
+ endLine: 1,
768
+ language: 'typescript',
769
+ },
770
+ {
771
+ id: '2',
772
+ filepath: 'other.ts',
773
+ content: 'function other() {}',
774
+ startLine: 1,
775
+ endLine: 1,
776
+ language: 'typescript',
777
+ },
778
+ ]);
779
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
780
+ mockConnection.openTable.mockResolvedValue(mockTable);
781
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
782
+ const indexer = new CodeIndexer('/project', mockBackend);
783
+ await indexer.initialize();
784
+ const results = await indexer.search('authenticate auth');
785
+ // auth.ts should rank higher due to keyword match in both content and filepath
786
+ expect(results[0].filepath).toBe('auth.ts');
787
+ });
788
+ });
789
+ describe('corruption detection', () => {
790
+ it('should detect missing metadata as corruption', async () => {
791
+ const mockTable = createMockTable([
792
+ {
793
+ id: '1',
794
+ filepath: 'test.ts',
795
+ content: 'code',
796
+ startLine: 1,
797
+ endLine: 10,
798
+ language: 'typescript',
799
+ },
800
+ ]);
801
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
802
+ mockConnection.openTable.mockResolvedValue(mockTable);
803
+ // Metadata file doesn't exist
804
+ vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
805
+ const indexer = new CodeIndexer('/project', mockBackend);
806
+ await indexer.initialize();
807
+ const status = await indexer.getStatus();
808
+ expect(status.corrupted).toBe(true);
809
+ expect(status.corruptionReason).toContain('Missing index metadata');
810
+ });
811
+ it('should detect chunk count mismatch as corruption', async () => {
812
+ const mockTable = createMockTable([
813
+ {
814
+ id: '1',
815
+ filepath: 'test.ts',
816
+ content: 'code',
817
+ startLine: 1,
818
+ endLine: 10,
819
+ language: 'typescript',
820
+ },
821
+ ]);
822
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
823
+ mockConnection.openTable.mockResolvedValue(mockTable);
824
+ // Metadata says 5 chunks but table only has 1
825
+ vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
826
+ lastUpdated: '2024-01-01T00:00:00Z',
827
+ fileCount: 1,
828
+ chunkCount: 5,
829
+ embeddingBackend: 'mock',
830
+ embeddingDimensions: 1536,
831
+ checksum: 'abc123',
832
+ }));
833
+ const indexer = new CodeIndexer('/project', mockBackend);
834
+ await indexer.initialize();
835
+ const status = await indexer.getStatus();
836
+ expect(status.corrupted).toBe(true);
837
+ expect(status.corruptionReason).toContain('Chunk count mismatch');
838
+ });
839
+ it('should report healthy index when metadata matches', async () => {
840
+ const mockTable = createMockTable([
841
+ {
842
+ id: '1',
843
+ filepath: 'test.ts',
844
+ content: 'code',
845
+ startLine: 1,
846
+ endLine: 10,
847
+ language: 'typescript',
848
+ },
849
+ ]);
850
+ const mockMetadataTable = createMockTable([{ filepath: 'test.ts', mtime: Date.now() }]);
851
+ mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
852
+ mockConnection.openTable.mockImplementation(async (name) => {
853
+ if (name === 'file_metadata')
854
+ return mockMetadataTable;
855
+ return mockTable;
856
+ });
857
+ // Metadata matches actual state
858
+ vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
859
+ lastUpdated: '2024-01-01T00:00:00Z',
860
+ fileCount: 1,
861
+ chunkCount: 1,
862
+ embeddingBackend: 'mock',
863
+ embeddingDimensions: 1536,
864
+ checksum: '4694592dbdda93c1',
865
+ }));
866
+ const indexer = new CodeIndexer('/project', mockBackend);
867
+ await indexer.initialize();
868
+ const status = await indexer.getStatus();
869
+ expect(status.corrupted).toBe(false);
870
+ expect(status.corruptionReason).toBeUndefined();
871
+ });
872
+ it('should report no corruption when index is empty', async () => {
873
+ mockConnection.tableNames.mockResolvedValue([]);
874
+ const indexer = new CodeIndexer('/project', mockBackend);
875
+ await indexer.initialize();
876
+ const status = await indexer.getStatus();
877
+ expect(status.indexed).toBe(false);
878
+ // Empty index has no corruption field (undefined, not false)
879
+ expect(status.corrupted).toBeUndefined();
880
+ });
881
+ });
882
+ describe('checkpoint-based indexing', () => {
883
+ it('should save checkpoint after chunking phase', async () => {
884
+ const { glob } = await import('glob');
885
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
886
+ vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
887
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
888
+ mockConnection.tableNames.mockResolvedValue([]);
889
+ const indexer = new CodeIndexer('/project', mockBackend);
890
+ await indexer.initialize();
891
+ await indexer.indexCodebase();
892
+ // Should have written checkpoint at least once
893
+ const writeFileCalls = vi.mocked(fsPromises.writeFile).mock.calls;
894
+ const checkpointWrites = writeFileCalls.filter((call) => call[0] === '/project/.glancey/checkpoint.json');
895
+ expect(checkpointWrites.length).toBeGreaterThan(0);
896
+ // First checkpoint should be after chunking phase
897
+ const firstCheckpoint = JSON.parse(checkpointWrites[0][1]);
898
+ expect(firstCheckpoint.phase).toBe('chunking');
899
+ expect(firstCheckpoint.embeddingBackend).toBe('mock');
900
+ });
901
+ it('should clear checkpoint after successful indexing', async () => {
902
+ const { glob } = await import('glob');
903
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
904
+ vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
905
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
906
+ mockConnection.tableNames.mockResolvedValue([]);
907
+ const indexer = new CodeIndexer('/project', mockBackend);
908
+ await indexer.initialize();
909
+ await indexer.indexCodebase();
910
+ // Should have tried to unlink the checkpoint file
911
+ expect(fsPromises.unlink).toHaveBeenCalledWith('/project/.glancey/checkpoint.json');
912
+ });
913
+ it('should resume from checkpoint with embedded chunks', async () => {
914
+ // Use a fixed mtime for checkpoint freshness validation
915
+ const checkpointMtime = Date.now() - 1000; // 1 second ago
916
+ // Create a checkpoint file with embedded chunks
917
+ const checkpoint = {
918
+ phase: 'embedding',
919
+ startedAt: '2024-01-01T00:00:00Z',
920
+ files: ['/project/test.ts'],
921
+ processedFiles: [],
922
+ embeddedChunks: [
923
+ {
924
+ id: 'test.ts:1-10',
925
+ filepath: 'test.ts',
926
+ content: 'const x = 1;',
927
+ startLine: 1,
928
+ endLine: 10,
929
+ language: 'typescript',
930
+ embedding: [0.1, 0.2, 0.3],
931
+ },
932
+ ],
933
+ embeddingBackend: 'mock',
934
+ embeddingModel: 'mock-model',
935
+ fileMtimes: { 'test.ts': checkpointMtime },
936
+ };
937
+ let checkpointCleared = false;
938
+ vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
939
+ if (path === '/project/.glancey/checkpoint.json') {
940
+ if (checkpointCleared) {
941
+ throw new Error('ENOENT');
942
+ }
943
+ return JSON.stringify(checkpoint);
944
+ }
945
+ return 'const x = 1;';
946
+ });
947
+ // Return same mtime as checkpoint to indicate file hasn't changed
948
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: checkpointMtime });
949
+ vi.mocked(fsPromises.unlink).mockImplementation(async (path) => {
950
+ if (path === '/project/.glancey/checkpoint.json') {
951
+ checkpointCleared = true;
952
+ }
953
+ });
954
+ mockConnection.tableNames.mockResolvedValue([]);
955
+ const indexer = new CodeIndexer('/project', mockBackend);
956
+ await indexer.initialize();
957
+ const result = await indexer.indexCodebase();
958
+ // Should have resumed and completed without re-embedding
959
+ expect(result.filesIndexed).toBe(1);
960
+ expect(result.chunksCreated).toBe(1);
961
+ // Should not have called embedBatch since chunks were already embedded
962
+ expect(mockBackend.embedBatch).not.toHaveBeenCalled();
963
+ });
964
+ it('should discard checkpoint when embedding backend changes', async () => {
965
+ const { glob } = await import('glob');
966
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
967
+ // Checkpoint was created with different backend
968
+ const checkpoint = {
969
+ phase: 'chunking',
970
+ startedAt: '2024-01-01T00:00:00Z',
971
+ files: ['/project/test.ts'],
972
+ processedFiles: [],
973
+ pendingChunks: [
974
+ {
975
+ id: 'test.ts:1-10',
976
+ filepath: 'test.ts',
977
+ content: 'const x = 1;',
978
+ startLine: 1,
979
+ endLine: 10,
980
+ language: 'typescript',
981
+ },
982
+ ],
983
+ embeddingBackend: 'different-backend',
984
+ embeddingModel: 'different-model',
985
+ };
986
+ let checkpointCleared = false;
987
+ vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
988
+ if (path === '/project/.glancey/checkpoint.json') {
989
+ if (checkpointCleared) {
990
+ throw new Error('ENOENT');
991
+ }
992
+ return JSON.stringify(checkpoint);
993
+ }
994
+ return 'const x = 1;';
995
+ });
996
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
997
+ vi.mocked(fsPromises.unlink).mockImplementation(async (path) => {
998
+ if (path === '/project/.glancey/checkpoint.json') {
999
+ checkpointCleared = true;
1000
+ }
1001
+ });
1002
+ mockConnection.tableNames.mockResolvedValue([]);
1003
+ const indexer = new CodeIndexer('/project', mockBackend);
1004
+ await indexer.initialize();
1005
+ await indexer.indexCodebase();
1006
+ // Should have cleared the incompatible checkpoint
1007
+ expect(fsPromises.unlink).toHaveBeenCalledWith('/project/.glancey/checkpoint.json');
1008
+ // Should have re-embedded since backend changed
1009
+ expect(mockBackend.embedBatch).toHaveBeenCalled();
1010
+ });
1011
+ it('should clear checkpoint when clearIndex is called', async () => {
1012
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
1013
+ const indexer = new CodeIndexer('/project', mockBackend);
1014
+ await indexer.initialize();
1015
+ await indexer.clearIndex();
1016
+ // Should have tried to unlink the checkpoint file
1017
+ expect(fsPromises.unlink).toHaveBeenCalledWith('/project/.glancey/checkpoint.json');
1018
+ });
1019
+ it('should handle invalid checkpoint gracefully', async () => {
1020
+ const { glob } = await import('glob');
1021
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
1022
+ // Invalid checkpoint (missing required fields)
1023
+ let checkpointCleared = false;
1024
+ vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
1025
+ if (path === '/project/.glancey/checkpoint.json') {
1026
+ if (checkpointCleared) {
1027
+ throw new Error('ENOENT');
1028
+ }
1029
+ return JSON.stringify({ phase: 'chunking' }); // Missing files and processedFiles
1030
+ }
1031
+ return 'const x = 1;';
1032
+ });
1033
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
1034
+ vi.mocked(fsPromises.unlink).mockImplementation(async (path) => {
1035
+ if (path === '/project/.glancey/checkpoint.json') {
1036
+ checkpointCleared = true;
1037
+ }
1038
+ });
1039
+ mockConnection.tableNames.mockResolvedValue([]);
1040
+ const indexer = new CodeIndexer('/project', mockBackend);
1041
+ await indexer.initialize();
1042
+ // Should not throw and should do full indexing
1043
+ const result = await indexer.indexCodebase();
1044
+ expect(result.filesIndexed).toBe(1);
1045
+ });
1046
+ });
1047
+ describe('incremental indexing race conditions', () => {
1048
+ it('should handle file deletion detected via metadata', async () => {
1049
+ const { glob } = await import('glob');
1050
+ // Only return test.ts, simulating deleted.ts was already removed before glob
1051
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
1052
+ vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
1053
+ const pathStr = String(filePath);
1054
+ if (pathStr.includes('checkpoint.json')) {
1055
+ throw new Error('ENOENT');
1056
+ }
1057
+ if (pathStr.includes('metadata.json')) {
1058
+ return JSON.stringify({
1059
+ lastUpdated: new Date().toISOString(),
1060
+ fileCount: 2,
1061
+ chunkCount: 2,
1062
+ embeddingBackend: 'mock',
1063
+ embeddingModel: 'mock-model',
1064
+ embeddingDimensions: 1536,
1065
+ version: '1.0.0',
1066
+ });
1067
+ }
1068
+ return 'const x = 1;';
1069
+ });
1070
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
1071
+ // Include file_metadata table to enable incremental detection
1072
+ mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
1073
+ const mockTable = createMockTable();
1074
+ // Mock metadata table with two files - one was deleted
1075
+ const mockMetadataTable = createMockTable();
1076
+ mockMetadataTable.query.mockReturnValue({
1077
+ toArray: vi.fn().mockResolvedValue([
1078
+ { filepath: 'test.ts', mtime: Date.now() - 1000 },
1079
+ { filepath: 'deleted.ts', mtime: Date.now() - 1000 },
1080
+ ]),
1081
+ });
1082
+ mockConnection.openTable.mockImplementation(async (name) => {
1083
+ if (name === 'file_metadata') {
1084
+ return mockMetadataTable;
1085
+ }
1086
+ return mockTable;
1087
+ });
1088
+ const indexer = new CodeIndexer('/project', mockBackend);
1089
+ await indexer.initialize();
1090
+ // Should handle gracefully - detect deleted.ts as deleted since it's in metadata but not in glob
1091
+ const result = await indexer.indexCodebase();
1092
+ expect(result.incremental).toBe(true);
1093
+ // The deleted file should be removed from the index
1094
+ expect(mockTable.delete).toHaveBeenCalled();
1095
+ });
1096
+ it('should handle file modified during change detection', async () => {
1097
+ const { glob } = await import('glob');
1098
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
1099
+ // File mtime changes between calls to stat
1100
+ let statCallCount = 0;
1101
+ const initialMtime = Date.now() - 5000;
1102
+ const modifiedMtime = Date.now();
1103
+ vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
1104
+ const pathStr = String(filePath);
1105
+ if (pathStr.includes('checkpoint.json')) {
1106
+ throw new Error('ENOENT');
1107
+ }
1108
+ if (pathStr.includes('metadata.json')) {
1109
+ return JSON.stringify({
1110
+ lastUpdated: new Date().toISOString(),
1111
+ fileCount: 1,
1112
+ chunkCount: 1,
1113
+ embeddingBackend: 'mock',
1114
+ embeddingModel: 'mock-model',
1115
+ embeddingDimensions: 1536, // Match mock backend dimensions
1116
+ version: '1.0.0',
1117
+ });
1118
+ }
1119
+ if (pathStr.includes('files.json')) {
1120
+ return JSON.stringify({
1121
+ 'test.ts': initialMtime,
1122
+ });
1123
+ }
1124
+ return 'const x = 1;';
1125
+ });
1126
+ vi.mocked(fsPromises.stat).mockImplementation(async () => {
1127
+ statCallCount++;
1128
+ // First call during change detection returns old mtime
1129
+ // Second call (if any) returns new mtime simulating modification
1130
+ return { mtimeMs: statCallCount === 1 ? initialMtime : modifiedMtime };
1131
+ });
1132
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
1133
+ const mockTable = createMockTable();
1134
+ mockConnection.openTable.mockResolvedValue(mockTable);
1135
+ const indexer = new CodeIndexer('/project', mockBackend);
1136
+ await indexer.initialize();
1137
+ // Should complete without error - the file appears unchanged based on first stat
1138
+ const result = await indexer.indexCodebase();
1139
+ expect(result.incremental).toBe(true);
1140
+ });
1141
+ it('should handle concurrent indexCodebase calls gracefully', async () => {
1142
+ const { glob } = await import('glob');
1143
+ vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
1144
+ vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
1145
+ const pathStr = String(filePath);
1146
+ if (pathStr.includes('checkpoint.json')) {
1147
+ throw new Error('ENOENT');
1148
+ }
1149
+ return 'const x = 1;';
1150
+ });
1151
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
1152
+ mockConnection.tableNames.mockResolvedValue([]);
1153
+ const indexer = new CodeIndexer('/project', mockBackend);
1154
+ await indexer.initialize();
1155
+ // Start two concurrent indexing operations
1156
+ const promise1 = indexer.indexCodebase();
1157
+ const promise2 = indexer.indexCodebase();
1158
+ // Both should complete without throwing
1159
+ const [result1, result2] = await Promise.all([promise1, promise2]);
1160
+ // Both should report success (though the actual behavior depends on implementation)
1161
+ expect(result1.filesIndexed).toBeGreaterThanOrEqual(0);
1162
+ expect(result2.filesIndexed).toBeGreaterThanOrEqual(0);
1163
+ });
1164
+ it('should handle empty file list during indexing', async () => {
1165
+ const { glob } = await import('glob');
1166
+ // All files were deleted or none match
1167
+ vi.mocked(glob).mockResolvedValue([]);
1168
+ vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
1169
+ const pathStr = String(filePath);
1170
+ if (pathStr.includes('checkpoint.json')) {
1171
+ throw new Error('ENOENT');
1172
+ }
1173
+ return '';
1174
+ });
1175
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
1176
+ // No existing index
1177
+ mockConnection.tableNames.mockResolvedValue([]);
1178
+ const indexer = new CodeIndexer('/project', mockBackend);
1179
+ await indexer.initialize();
1180
+ // Should handle gracefully - create empty index
1181
+ const result = await indexer.indexCodebase();
1182
+ expect(result.filesIndexed).toBe(0);
1183
+ expect(result.chunksCreated).toBe(0);
1184
+ });
1185
+ });
1186
+ describe('checkIfStale', () => {
1187
+ it('should return stale=true when index does not exist', async () => {
1188
+ mockConnection.tableNames.mockResolvedValue([]);
1189
+ const indexer = new CodeIndexer('/project', mockBackend);
1190
+ await indexer.initialize();
1191
+ const result = await indexer.checkIfStale();
1192
+ expect(result.stale).toBe(true);
1193
+ expect(result.reason).toBe('Index does not exist');
1194
+ });
1195
+ it('should return stale=true when file_metadata table is missing', async () => {
1196
+ mockConnection.tableNames.mockResolvedValue(['code_chunks']);
1197
+ const indexer = new CodeIndexer('/project', mockBackend);
1198
+ await indexer.initialize();
1199
+ const result = await indexer.checkIfStale();
1200
+ expect(result.stale).toBe(true);
1201
+ expect(result.reason).toBe('No file metadata stored');
1202
+ });
1203
+ it('should return stale=false when no files have changed', async () => {
1204
+ const { glob } = await import('glob');
1205
+ const testFilePath = '/project/test.ts';
1206
+ const storedMtime = 1000;
1207
+ vi.mocked(glob).mockResolvedValue([testFilePath]);
1208
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: storedMtime });
1209
+ // Mock metadata table with matching mtime - needs query().toArray() chain
1210
+ const mockMetadataTable = {
1211
+ query: vi.fn().mockReturnValue({
1212
+ toArray: vi.fn().mockResolvedValue([{ filepath: 'test.ts', mtime: storedMtime }]),
1213
+ }),
1214
+ };
1215
+ mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
1216
+ mockConnection.openTable.mockImplementation(async (tableName) => {
1217
+ if (tableName === 'file_metadata') {
1218
+ return mockMetadataTable;
1219
+ }
1220
+ return createMockTable([]);
1221
+ });
1222
+ const indexer = new CodeIndexer('/project', mockBackend);
1223
+ await indexer.initialize();
1224
+ const result = await indexer.checkIfStale();
1225
+ expect(result.stale).toBe(false);
1226
+ expect(result.reason).toBeUndefined();
1227
+ });
1228
+ it('should return stale=true with reason when files are modified', async () => {
1229
+ const { glob } = await import('glob');
1230
+ const testFilePath = '/project/test.ts';
1231
+ const storedMtime = 1000;
1232
+ const newMtime = 2000; // Newer than stored
1233
+ vi.mocked(glob).mockResolvedValue([testFilePath]);
1234
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: newMtime });
1235
+ // Mock metadata table with older mtime - needs query().toArray() chain
1236
+ const mockMetadataTable = {
1237
+ query: vi.fn().mockReturnValue({
1238
+ toArray: vi.fn().mockResolvedValue([{ filepath: 'test.ts', mtime: storedMtime }]),
1239
+ }),
1240
+ };
1241
+ mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
1242
+ mockConnection.openTable.mockImplementation(async (tableName) => {
1243
+ if (tableName === 'file_metadata') {
1244
+ return mockMetadataTable;
1245
+ }
1246
+ return createMockTable([]);
1247
+ });
1248
+ const indexer = new CodeIndexer('/project', mockBackend);
1249
+ await indexer.initialize();
1250
+ const result = await indexer.checkIfStale();
1251
+ expect(result.stale).toBe(true);
1252
+ expect(result.reason).toContain('modified');
1253
+ });
1254
+ it('should return stale=true with reason when new files are added', async () => {
1255
+ const { glob } = await import('glob');
1256
+ vi.mocked(glob).mockResolvedValue(['/project/existing.ts', '/project/new.ts']);
1257
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: 1000 });
1258
+ // Mock metadata table with only one file - needs query().toArray() chain
1259
+ const mockMetadataTable = {
1260
+ query: vi.fn().mockReturnValue({
1261
+ toArray: vi.fn().mockResolvedValue([{ filepath: 'existing.ts', mtime: 1000 }]),
1262
+ }),
1263
+ };
1264
+ mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
1265
+ mockConnection.openTable.mockImplementation(async (tableName) => {
1266
+ if (tableName === 'file_metadata') {
1267
+ return mockMetadataTable;
1268
+ }
1269
+ return createMockTable([]);
1270
+ });
1271
+ const indexer = new CodeIndexer('/project', mockBackend);
1272
+ await indexer.initialize();
1273
+ const result = await indexer.checkIfStale();
1274
+ expect(result.stale).toBe(true);
1275
+ expect(result.reason).toContain('new file');
1276
+ });
1277
+ it('should return stale=true with reason when files are deleted', async () => {
1278
+ const { glob } = await import('glob');
1279
+ vi.mocked(glob).mockResolvedValue(['/project/remaining.ts']);
1280
+ vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: 1000 });
1281
+ // Mock metadata table with two files (one deleted) - needs query().toArray() chain
1282
+ const mockMetadataTable = {
1283
+ query: vi.fn().mockReturnValue({
1284
+ toArray: vi.fn().mockResolvedValue([
1285
+ { filepath: 'remaining.ts', mtime: 1000 },
1286
+ { filepath: 'deleted.ts', mtime: 1000 },
1287
+ ]),
1288
+ }),
1289
+ };
1290
+ mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
1291
+ mockConnection.openTable.mockImplementation(async (tableName) => {
1292
+ if (tableName === 'file_metadata') {
1293
+ return mockMetadataTable;
1294
+ }
1295
+ return createMockTable([]);
1296
+ });
1297
+ const indexer = new CodeIndexer('/project', mockBackend);
1298
+ await indexer.initialize();
1299
+ const result = await indexer.checkIfStale();
1300
+ expect(result.stale).toBe(true);
1301
+ expect(result.reason).toContain('deleted');
1302
+ });
1303
+ });
1304
+ });
1305
+ //# sourceMappingURL=indexer.test.js.map