codevault 1.8.4 → 1.8.5

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 (351) hide show
  1. package/README.md +3 -2
  2. package/dist/chunking/token-counter.d.ts.map +1 -1
  3. package/dist/chunking/token-counter.js +16 -10
  4. package/dist/chunking/token-counter.js.map +1 -1
  5. package/dist/cli/commands/index-cmd.d.ts.map +1 -1
  6. package/dist/cli/commands/index-cmd.js +108 -97
  7. package/dist/cli/commands/index-cmd.js.map +1 -1
  8. package/dist/cli/commands/interactive-config.d.ts.map +1 -1
  9. package/dist/cli/commands/interactive-config.js +40 -3
  10. package/dist/cli/commands/interactive-config.js.map +1 -1
  11. package/dist/cli/commands/search-cmd.d.ts.map +1 -1
  12. package/dist/cli/commands/search-cmd.js +11 -7
  13. package/dist/cli/commands/search-cmd.js.map +1 -1
  14. package/dist/cli/commands/search-with-code-cmd.d.ts.map +1 -1
  15. package/dist/cli/commands/search-with-code-cmd.js +3 -1
  16. package/dist/cli/commands/search-with-code-cmd.js.map +1 -1
  17. package/dist/cli/utils.d.ts +56 -0
  18. package/dist/cli/utils.d.ts.map +1 -0
  19. package/dist/cli/utils.js +98 -0
  20. package/dist/cli/utils.js.map +1 -0
  21. package/dist/config/constants.d.ts +4 -0
  22. package/dist/config/constants.d.ts.map +1 -1
  23. package/dist/config/constants.js +2 -0
  24. package/dist/config/constants.js.map +1 -1
  25. package/dist/context/packs.d.ts.map +1 -1
  26. package/dist/context/packs.js +3 -1
  27. package/dist/context/packs.js.map +1 -1
  28. package/dist/core/IndexerEngine.d.ts +2 -0
  29. package/dist/core/IndexerEngine.d.ts.map +1 -1
  30. package/dist/core/IndexerEngine.js +34 -26
  31. package/dist/core/IndexerEngine.js.map +1 -1
  32. package/dist/core/SearchService.d.ts +1 -0
  33. package/dist/core/SearchService.d.ts.map +1 -1
  34. package/dist/core/SearchService.js +18 -12
  35. package/dist/core/SearchService.js.map +1 -1
  36. package/dist/core/batch-indexer.d.ts.map +1 -1
  37. package/dist/core/batch-indexer.js +5 -13
  38. package/dist/core/batch-indexer.js.map +1 -1
  39. package/dist/core/indexing/IndexFinalizationStage.d.ts.map +1 -1
  40. package/dist/core/indexing/IndexFinalizationStage.js +21 -2
  41. package/dist/core/indexing/IndexFinalizationStage.js.map +1 -1
  42. package/dist/core/indexing/IndexState.d.ts +3 -8
  43. package/dist/core/indexing/IndexState.d.ts.map +1 -1
  44. package/dist/core/indexing/IndexState.js.map +1 -1
  45. package/dist/core/indexing/PersistManager.d.ts +1 -1
  46. package/dist/core/indexing/PersistManager.d.ts.map +1 -1
  47. package/dist/core/indexing/PersistManager.js +17 -17
  48. package/dist/core/indexing/PersistManager.js.map +1 -1
  49. package/dist/core/search/HybridFusion.d.ts +0 -1
  50. package/dist/core/search/HybridFusion.d.ts.map +1 -1
  51. package/dist/core/search/HybridFusion.js +2 -14
  52. package/dist/core/search/HybridFusion.js.map +1 -1
  53. package/dist/core/search/ResultMapper.d.ts +0 -1
  54. package/dist/core/search/ResultMapper.d.ts.map +1 -1
  55. package/dist/core/search/ResultMapper.js +2 -13
  56. package/dist/core/search/ResultMapper.js.map +1 -1
  57. package/dist/core/search/SearchContextManager.d.ts +3 -0
  58. package/dist/core/search/SearchContextManager.d.ts.map +1 -1
  59. package/dist/core/search/SearchContextManager.js +15 -2
  60. package/dist/core/search/SearchContextManager.js.map +1 -1
  61. package/dist/core/search.d.ts.map +1 -1
  62. package/dist/core/search.js +9 -4
  63. package/dist/core/search.js.map +1 -1
  64. package/dist/languages/rules.d.ts.map +1 -1
  65. package/dist/languages/rules.js +14 -5
  66. package/dist/languages/rules.js.map +1 -1
  67. package/dist/mcp/schemas.d.ts +2 -2
  68. package/dist/mcp-server.d.ts +1 -0
  69. package/dist/mcp-server.d.ts.map +1 -1
  70. package/dist/mcp-server.js +32 -21
  71. package/dist/mcp-server.js.map +1 -1
  72. package/dist/providers/base.d.ts +3 -2
  73. package/dist/providers/base.d.ts.map +1 -1
  74. package/dist/providers/base.js +3 -1
  75. package/dist/providers/base.js.map +1 -1
  76. package/dist/providers/chat-llm.d.ts +2 -2
  77. package/dist/providers/chat-llm.d.ts.map +1 -1
  78. package/dist/providers/chat-llm.js +4 -1
  79. package/dist/providers/chat-llm.js.map +1 -1
  80. package/dist/providers/openai.d.ts.map +1 -1
  81. package/dist/providers/openai.js +4 -1
  82. package/dist/providers/openai.js.map +1 -1
  83. package/dist/ranking/api-reranker.d.ts.map +1 -1
  84. package/dist/ranking/api-reranker.js +27 -8
  85. package/dist/ranking/api-reranker.js.map +1 -1
  86. package/dist/ranking/symbol-boost.d.ts.map +1 -1
  87. package/dist/ranking/symbol-boost.js +4 -11
  88. package/dist/ranking/symbol-boost.js.map +1 -1
  89. package/dist/search/bm25.d.ts +10 -0
  90. package/dist/search/bm25.d.ts.map +1 -1
  91. package/dist/search/bm25.js +16 -0
  92. package/dist/search/bm25.js.map +1 -1
  93. package/dist/storage/encrypted-chunks.d.ts +3 -0
  94. package/dist/storage/encrypted-chunks.d.ts.map +1 -1
  95. package/dist/storage/encrypted-chunks.js +108 -16
  96. package/dist/storage/encrypted-chunks.js.map +1 -1
  97. package/dist/synthesis/conversational-synthesizer.d.ts +2 -1
  98. package/dist/synthesis/conversational-synthesizer.d.ts.map +1 -1
  99. package/dist/synthesis/conversational-synthesizer.js +6 -1
  100. package/dist/synthesis/conversational-synthesizer.js.map +1 -1
  101. package/dist/synthesis/prompt-builder.d.ts.map +1 -1
  102. package/dist/synthesis/prompt-builder.js +40 -13
  103. package/dist/synthesis/prompt-builder.js.map +1 -1
  104. package/dist/synthesis/synthesizer.d.ts.map +1 -1
  105. package/dist/synthesis/synthesizer.js +14 -2
  106. package/dist/synthesis/synthesizer.js.map +1 -1
  107. package/dist/tests/api-reranker.test.d.ts +2 -0
  108. package/dist/tests/api-reranker.test.d.ts.map +1 -0
  109. package/dist/tests/api-reranker.test.js +575 -0
  110. package/dist/tests/api-reranker.test.js.map +1 -0
  111. package/dist/tests/bm25.test.d.ts +2 -0
  112. package/dist/tests/bm25.test.d.ts.map +1 -0
  113. package/dist/tests/bm25.test.js +340 -0
  114. package/dist/tests/bm25.test.js.map +1 -0
  115. package/dist/tests/chunking/file-grouper.test.d.ts +2 -0
  116. package/dist/tests/chunking/file-grouper.test.d.ts.map +1 -0
  117. package/dist/tests/chunking/file-grouper.test.js +495 -0
  118. package/dist/tests/chunking/file-grouper.test.js.map +1 -0
  119. package/dist/tests/chunking/semantic-chunker.test.d.ts +2 -0
  120. package/dist/tests/chunking/semantic-chunker.test.d.ts.map +1 -0
  121. package/dist/tests/chunking/semantic-chunker.test.js +509 -0
  122. package/dist/tests/chunking/semantic-chunker.test.js.map +1 -0
  123. package/dist/tests/chunking/token-counter.test.d.ts +2 -0
  124. package/dist/tests/chunking/token-counter.test.d.ts.map +1 -0
  125. package/dist/tests/chunking/token-counter.test.js +441 -0
  126. package/dist/tests/chunking/token-counter.test.js.map +1 -0
  127. package/dist/tests/cli/ask-cmd.test.d.ts +2 -0
  128. package/dist/tests/cli/ask-cmd.test.d.ts.map +1 -0
  129. package/dist/tests/cli/ask-cmd.test.js +152 -0
  130. package/dist/tests/cli/ask-cmd.test.js.map +1 -0
  131. package/dist/tests/cli/chat-cmd.test.d.ts +2 -0
  132. package/dist/tests/cli/chat-cmd.test.d.ts.map +1 -0
  133. package/dist/tests/cli/chat-cmd.test.js +118 -0
  134. package/dist/tests/cli/chat-cmd.test.js.map +1 -0
  135. package/dist/tests/cli/config-cmd.test.d.ts +2 -0
  136. package/dist/tests/cli/config-cmd.test.d.ts.map +1 -0
  137. package/dist/tests/cli/config-cmd.test.js +226 -0
  138. package/dist/tests/cli/config-cmd.test.js.map +1 -0
  139. package/dist/tests/cli/context.test.d.ts +2 -0
  140. package/dist/tests/cli/context.test.d.ts.map +1 -0
  141. package/dist/tests/cli/context.test.js +158 -0
  142. package/dist/tests/cli/context.test.js.map +1 -0
  143. package/dist/tests/cli/index-cmd.test.d.ts +2 -0
  144. package/dist/tests/cli/index-cmd.test.d.ts.map +1 -0
  145. package/dist/tests/cli/index-cmd.test.js +89 -0
  146. package/dist/tests/cli/index-cmd.test.js.map +1 -0
  147. package/dist/tests/cli/index.test.d.ts +2 -0
  148. package/dist/tests/cli/index.test.d.ts.map +1 -0
  149. package/dist/tests/cli/index.test.js +167 -0
  150. package/dist/tests/cli/index.test.js.map +1 -0
  151. package/dist/tests/cli/info-cmd.test.d.ts +2 -0
  152. package/dist/tests/cli/info-cmd.test.d.ts.map +1 -0
  153. package/dist/tests/cli/info-cmd.test.js +47 -0
  154. package/dist/tests/cli/info-cmd.test.js.map +1 -0
  155. package/dist/tests/cli/interactive-config.test.d.ts +2 -0
  156. package/dist/tests/cli/interactive-config.test.d.ts.map +1 -0
  157. package/dist/tests/cli/interactive-config.test.js +30 -0
  158. package/dist/tests/cli/interactive-config.test.js.map +1 -0
  159. package/dist/tests/cli/mcp-cmd.test.d.ts +2 -0
  160. package/dist/tests/cli/mcp-cmd.test.d.ts.map +1 -0
  161. package/dist/tests/cli/mcp-cmd.test.js +47 -0
  162. package/dist/tests/cli/mcp-cmd.test.js.map +1 -0
  163. package/dist/tests/cli/search-cmd.test.d.ts +2 -0
  164. package/dist/tests/cli/search-cmd.test.d.ts.map +1 -0
  165. package/dist/tests/cli/search-cmd.test.js +120 -0
  166. package/dist/tests/cli/search-cmd.test.js.map +1 -0
  167. package/dist/tests/cli/search-with-code-cmd.test.d.ts +2 -0
  168. package/dist/tests/cli/search-with-code-cmd.test.d.ts.map +1 -0
  169. package/dist/tests/cli/search-with-code-cmd.test.js +140 -0
  170. package/dist/tests/cli/search-with-code-cmd.test.js.map +1 -0
  171. package/dist/tests/cli/update-cmd.test.d.ts +2 -0
  172. package/dist/tests/cli/update-cmd.test.d.ts.map +1 -0
  173. package/dist/tests/cli/update-cmd.test.js +75 -0
  174. package/dist/tests/cli/update-cmd.test.js.map +1 -0
  175. package/dist/tests/cli/utils.test.d.ts +2 -0
  176. package/dist/tests/cli/utils.test.d.ts.map +1 -0
  177. package/dist/tests/cli/utils.test.js +119 -0
  178. package/dist/tests/cli/utils.test.js.map +1 -0
  179. package/dist/tests/cli/watch-cmd.test.d.ts +2 -0
  180. package/dist/tests/cli/watch-cmd.test.d.ts.map +1 -0
  181. package/dist/tests/cli/watch-cmd.test.js +84 -0
  182. package/dist/tests/cli/watch-cmd.test.js.map +1 -0
  183. package/dist/tests/cli-ui.test.d.ts +2 -0
  184. package/dist/tests/cli-ui.test.d.ts.map +1 -0
  185. package/dist/tests/cli-ui.test.js +608 -0
  186. package/dist/tests/cli-ui.test.js.map +1 -0
  187. package/dist/tests/codemap-io.test.d.ts +2 -0
  188. package/dist/tests/codemap-io.test.d.ts.map +1 -0
  189. package/dist/tests/codemap-io.test.js +992 -0
  190. package/dist/tests/codemap-io.test.js.map +1 -0
  191. package/dist/tests/config/apply-env.test.d.ts +2 -0
  192. package/dist/tests/config/apply-env.test.d.ts.map +1 -0
  193. package/dist/tests/config/apply-env.test.js +717 -0
  194. package/dist/tests/config/apply-env.test.js.map +1 -0
  195. package/dist/tests/config/constants.test.d.ts +2 -0
  196. package/dist/tests/config/constants.test.d.ts.map +1 -0
  197. package/dist/tests/config/constants.test.js +406 -0
  198. package/dist/tests/config/constants.test.js.map +1 -0
  199. package/dist/tests/config/loader.test.d.ts +2 -0
  200. package/dist/tests/config/loader.test.d.ts.map +1 -0
  201. package/dist/tests/config/loader.test.js +716 -0
  202. package/dist/tests/config/loader.test.js.map +1 -0
  203. package/dist/tests/config/resolver.test.d.ts +2 -0
  204. package/dist/tests/config/resolver.test.d.ts.map +1 -0
  205. package/dist/tests/config/resolver.test.js +402 -0
  206. package/dist/tests/config/resolver.test.js.map +1 -0
  207. package/dist/tests/config/types.test.d.ts +2 -0
  208. package/dist/tests/config/types.test.d.ts.map +1 -0
  209. package/dist/tests/config/types.test.js +460 -0
  210. package/dist/tests/config/types.test.js.map +1 -0
  211. package/dist/tests/context-packs.test.d.ts +2 -0
  212. package/dist/tests/context-packs.test.d.ts.map +1 -0
  213. package/dist/tests/context-packs.test.js +826 -0
  214. package/dist/tests/context-packs.test.js.map +1 -0
  215. package/dist/tests/conversational-synthesizer.test.d.ts +2 -0
  216. package/dist/tests/conversational-synthesizer.test.d.ts.map +1 -0
  217. package/dist/tests/conversational-synthesizer.test.js +595 -0
  218. package/dist/tests/conversational-synthesizer.test.js.map +1 -0
  219. package/dist/tests/database.test.d.ts +2 -0
  220. package/dist/tests/database.test.d.ts.map +1 -0
  221. package/dist/tests/database.test.js +965 -0
  222. package/dist/tests/database.test.js.map +1 -0
  223. package/dist/tests/encrypted-chunks.test.d.ts +2 -0
  224. package/dist/tests/encrypted-chunks.test.d.ts.map +1 -0
  225. package/dist/tests/encrypted-chunks.test.js +1470 -0
  226. package/dist/tests/encrypted-chunks.test.js.map +1 -0
  227. package/dist/tests/hybrid.test.d.ts +2 -0
  228. package/dist/tests/hybrid.test.d.ts.map +1 -0
  229. package/dist/tests/hybrid.test.js +456 -0
  230. package/dist/tests/hybrid.test.js.map +1 -0
  231. package/dist/tests/indexer/ChangeQueue.test.d.ts +12 -0
  232. package/dist/tests/indexer/ChangeQueue.test.d.ts.map +1 -0
  233. package/dist/tests/indexer/ChangeQueue.test.js +441 -0
  234. package/dist/tests/indexer/ChangeQueue.test.js.map +1 -0
  235. package/dist/tests/indexer/ProviderManager.test.d.ts +12 -0
  236. package/dist/tests/indexer/ProviderManager.test.d.ts.map +1 -0
  237. package/dist/tests/indexer/ProviderManager.test.js +290 -0
  238. package/dist/tests/indexer/ProviderManager.test.js.map +1 -0
  239. package/dist/tests/indexer/WatchService.test.d.ts +14 -0
  240. package/dist/tests/indexer/WatchService.test.d.ts.map +1 -0
  241. package/dist/tests/indexer/WatchService.test.js +667 -0
  242. package/dist/tests/indexer/WatchService.test.js.map +1 -0
  243. package/dist/tests/indexer/merkle.test.d.ts +11 -0
  244. package/dist/tests/indexer/merkle.test.d.ts.map +1 -0
  245. package/dist/tests/indexer/merkle.test.js +497 -0
  246. package/dist/tests/indexer/merkle.test.js.map +1 -0
  247. package/dist/tests/indexer/update.test.d.ts +10 -0
  248. package/dist/tests/indexer/update.test.d.ts.map +1 -0
  249. package/dist/tests/indexer/update.test.js +317 -0
  250. package/dist/tests/indexer/update.test.js.map +1 -0
  251. package/dist/tests/indexer/watch.test.d.ts +8 -0
  252. package/dist/tests/indexer/watch.test.d.ts.map +1 -0
  253. package/dist/tests/indexer/watch.test.js +95 -0
  254. package/dist/tests/indexer/watch.test.js.map +1 -0
  255. package/dist/tests/integration/index-search.integration.test.js +1 -0
  256. package/dist/tests/integration/index-search.integration.test.js.map +1 -1
  257. package/dist/tests/languages.test.d.ts +2 -0
  258. package/dist/tests/languages.test.d.ts.map +1 -0
  259. package/dist/tests/languages.test.js +575 -0
  260. package/dist/tests/languages.test.js.map +1 -0
  261. package/dist/tests/logger-redaction.test.d.ts +2 -0
  262. package/dist/tests/logger-redaction.test.d.ts.map +1 -0
  263. package/dist/tests/logger-redaction.test.js +48 -0
  264. package/dist/tests/logger-redaction.test.js.map +1 -0
  265. package/dist/tests/logger.test.d.ts +2 -0
  266. package/dist/tests/logger.test.d.ts.map +1 -0
  267. package/dist/tests/logger.test.js +468 -0
  268. package/dist/tests/logger.test.js.map +1 -0
  269. package/dist/tests/markdown-formatter.test.d.ts +2 -0
  270. package/dist/tests/markdown-formatter.test.d.ts.map +1 -0
  271. package/dist/tests/markdown-formatter.test.js +453 -0
  272. package/dist/tests/markdown-formatter.test.js.map +1 -0
  273. package/dist/tests/mcp/tools/use-context-pack.test.d.ts +7 -0
  274. package/dist/tests/mcp/tools/use-context-pack.test.d.ts.map +1 -0
  275. package/dist/tests/mcp/tools/use-context-pack.test.js +505 -0
  276. package/dist/tests/mcp/tools/use-context-pack.test.js.map +1 -0
  277. package/dist/tests/mutex.test.d.ts +2 -0
  278. package/dist/tests/mutex.test.d.ts.map +1 -0
  279. package/dist/tests/mutex.test.js +489 -0
  280. package/dist/tests/mutex.test.js.map +1 -0
  281. package/dist/tests/path-helpers.test.d.ts +2 -0
  282. package/dist/tests/path-helpers.test.d.ts.map +1 -0
  283. package/dist/tests/path-helpers.test.js +332 -0
  284. package/dist/tests/path-helpers.test.js.map +1 -0
  285. package/dist/tests/prompt-builder.test.d.ts +2 -0
  286. package/dist/tests/prompt-builder.test.d.ts.map +1 -0
  287. package/dist/tests/prompt-builder.test.js +417 -0
  288. package/dist/tests/prompt-builder.test.js.map +1 -0
  289. package/dist/tests/providers/base.test.d.ts +2 -0
  290. package/dist/tests/providers/base.test.d.ts.map +1 -0
  291. package/dist/tests/providers/base.test.js +299 -0
  292. package/dist/tests/providers/base.test.js.map +1 -0
  293. package/dist/tests/providers/chat-llm.test.d.ts +2 -0
  294. package/dist/tests/providers/chat-llm.test.d.ts.map +1 -0
  295. package/dist/tests/providers/chat-llm.test.js +435 -0
  296. package/dist/tests/providers/chat-llm.test.js.map +1 -0
  297. package/dist/tests/providers/index.test.d.ts +2 -0
  298. package/dist/tests/providers/index.test.d.ts.map +1 -0
  299. package/dist/tests/providers/index.test.js +204 -0
  300. package/dist/tests/providers/index.test.js.map +1 -0
  301. package/dist/tests/providers/mock.test.d.ts +2 -0
  302. package/dist/tests/providers/mock.test.d.ts.map +1 -0
  303. package/dist/tests/providers/mock.test.js +225 -0
  304. package/dist/tests/providers/mock.test.js.map +1 -0
  305. package/dist/tests/providers/openai.test.d.ts +2 -0
  306. package/dist/tests/providers/openai.test.d.ts.map +1 -0
  307. package/dist/tests/providers/openai.test.js +408 -0
  308. package/dist/tests/providers/openai.test.js.map +1 -0
  309. package/dist/tests/providers/token-counter.test.d.ts +2 -0
  310. package/dist/tests/providers/token-counter.test.d.ts.map +1 -0
  311. package/dist/tests/providers/token-counter.test.js +247 -0
  312. package/dist/tests/providers/token-counter.test.js.map +1 -0
  313. package/dist/tests/rate-limiter.test.js +392 -1
  314. package/dist/tests/rate-limiter.test.js.map +1 -1
  315. package/dist/tests/scope.test.d.ts +2 -0
  316. package/dist/tests/scope.test.d.ts.map +1 -0
  317. package/dist/tests/scope.test.js +529 -0
  318. package/dist/tests/scope.test.js.map +1 -0
  319. package/dist/tests/simple-lru.test.js +377 -0
  320. package/dist/tests/simple-lru.test.js.map +1 -1
  321. package/dist/tests/symbol-boost.test.js +730 -10
  322. package/dist/tests/symbol-boost.test.js.map +1 -1
  323. package/dist/tests/symbols-extract.test.d.ts +2 -0
  324. package/dist/tests/symbols-extract.test.d.ts.map +1 -0
  325. package/dist/tests/symbols-extract.test.js +536 -0
  326. package/dist/tests/symbols-extract.test.js.map +1 -0
  327. package/dist/tests/symbols-graph.test.d.ts +2 -0
  328. package/dist/tests/symbols-graph.test.d.ts.map +1 -0
  329. package/dist/tests/symbols-graph.test.js +656 -0
  330. package/dist/tests/symbols-graph.test.js.map +1 -0
  331. package/dist/tests/synthesizer.test.d.ts +2 -0
  332. package/dist/tests/synthesizer.test.d.ts.map +1 -0
  333. package/dist/tests/synthesizer.test.js +381 -0
  334. package/dist/tests/synthesizer.test.js.map +1 -0
  335. package/dist/types/context-pack.d.ts +3 -3
  336. package/dist/utils/logger.d.ts +5 -1
  337. package/dist/utils/logger.d.ts.map +1 -1
  338. package/dist/utils/logger.js +149 -4
  339. package/dist/utils/logger.js.map +1 -1
  340. package/dist/utils/mutex.d.ts +7 -2
  341. package/dist/utils/mutex.d.ts.map +1 -1
  342. package/dist/utils/mutex.js +31 -7
  343. package/dist/utils/mutex.js.map +1 -1
  344. package/dist/utils/path-helpers.d.ts.map +1 -1
  345. package/dist/utils/path-helpers.js +5 -2
  346. package/dist/utils/path-helpers.js.map +1 -1
  347. package/dist/utils/simple-lru.d.ts +6 -0
  348. package/dist/utils/simple-lru.d.ts.map +1 -1
  349. package/dist/utils/simple-lru.js +26 -0
  350. package/dist/utils/simple-lru.js.map +1 -1
  351. package/package.json +1 -1
@@ -0,0 +1,826 @@
1
+ import test, { describe, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { getContextPackDirectory, loadContextPack, listContextPacks, getActiveContextPack, setActiveContextPack, resolveScopeWithPack, clearContextPackCache } from '../context/packs.js';
7
+ // Test fixture directory for isolated tests
8
+ let testDir;
9
+ let packDir;
10
+ /**
11
+ * Creates a temporary test directory with optional context packs
12
+ */
13
+ function setupTestDir() {
14
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codevault-packs-test-'));
15
+ packDir = path.join(testDir, '.codevault', 'contextpacks');
16
+ }
17
+ /**
18
+ * Cleans up the temporary test directory
19
+ */
20
+ function cleanupTestDir() {
21
+ if (testDir && fs.existsSync(testDir)) {
22
+ fs.rmSync(testDir, { recursive: true, force: true });
23
+ }
24
+ }
25
+ /**
26
+ * Creates a context pack file with the given content
27
+ */
28
+ function createPackFile(name, content) {
29
+ if (!fs.existsSync(packDir)) {
30
+ fs.mkdirSync(packDir, { recursive: true });
31
+ }
32
+ const filePath = path.join(packDir, `${name}.json`);
33
+ fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
34
+ }
35
+ /**
36
+ * Creates an invalid JSON file
37
+ */
38
+ function createInvalidJsonFile(name, content) {
39
+ if (!fs.existsSync(packDir)) {
40
+ fs.mkdirSync(packDir, { recursive: true });
41
+ }
42
+ const filePath = path.join(packDir, `${name}.json`);
43
+ fs.writeFileSync(filePath, content);
44
+ }
45
+ // ============================================================================
46
+ // getContextPackDirectory Tests
47
+ // ============================================================================
48
+ describe('getContextPackDirectory', () => {
49
+ beforeEach(() => {
50
+ setupTestDir();
51
+ clearContextPackCache();
52
+ });
53
+ afterEach(() => {
54
+ cleanupTestDir();
55
+ });
56
+ test('returns correct directory path with default basePath', () => {
57
+ const result = getContextPackDirectory();
58
+ assert.ok(result.endsWith('.codevault/contextpacks') || result.endsWith('.codevault\\contextpacks'));
59
+ });
60
+ test('returns correct directory path with explicit basePath', () => {
61
+ const result = getContextPackDirectory(testDir);
62
+ const expected = path.join(testDir, '.codevault', 'contextpacks');
63
+ assert.equal(result, expected);
64
+ });
65
+ test('resolves relative path to absolute path', () => {
66
+ const result = getContextPackDirectory('.');
67
+ assert.ok(path.isAbsolute(result));
68
+ });
69
+ });
70
+ // ============================================================================
71
+ // loadContextPack Tests
72
+ // ============================================================================
73
+ describe('loadContextPack', () => {
74
+ beforeEach(() => {
75
+ setupTestDir();
76
+ clearContextPackCache();
77
+ });
78
+ afterEach(() => {
79
+ cleanupTestDir();
80
+ });
81
+ test('throws error for empty pack name', () => {
82
+ assert.throws(() => loadContextPack('', testDir), /Context pack name must be a non-empty string/);
83
+ });
84
+ test('throws error for non-string pack name', () => {
85
+ assert.throws(
86
+ // @ts-expect-error Testing invalid input
87
+ () => loadContextPack(null, testDir), /Context pack name must be a non-empty string/);
88
+ });
89
+ test('throws error for undefined pack name', () => {
90
+ assert.throws(
91
+ // @ts-expect-error Testing invalid input
92
+ () => loadContextPack(undefined, testDir), /Context pack name must be a non-empty string/);
93
+ });
94
+ test('throws error when pack file does not exist', () => {
95
+ assert.throws(() => loadContextPack('nonexistent', testDir), /Context pack "nonexistent" not found/);
96
+ });
97
+ test('loads valid context pack with name and description', () => {
98
+ createPackFile('test-pack', {
99
+ name: 'Test Pack',
100
+ description: 'A test context pack',
101
+ path_glob: ['src/**/*.ts'],
102
+ tags: ['typescript'],
103
+ lang: ['typescript']
104
+ });
105
+ const pack = loadContextPack('test-pack', testDir);
106
+ assert.equal(pack.key, 'test-pack');
107
+ assert.equal(pack.name, 'Test Pack');
108
+ assert.equal(pack.description, 'A test context pack');
109
+ assert.deepEqual(pack.scope.path_glob, ['src/**/*.ts']);
110
+ assert.deepEqual(pack.scope.tags, ['typescript']);
111
+ assert.deepEqual(pack.scope.lang, ['typescript']);
112
+ assert.ok(pack.path.endsWith('test-pack.json'));
113
+ });
114
+ test('loads pack with scope nested in scope property', () => {
115
+ createPackFile('nested-scope', {
116
+ name: 'Nested Scope Pack',
117
+ scope: {
118
+ path_glob: ['lib/**/*.js'],
119
+ lang: ['javascript']
120
+ }
121
+ });
122
+ const pack = loadContextPack('nested-scope', testDir);
123
+ assert.equal(pack.name, 'Nested Scope Pack');
124
+ assert.deepEqual(pack.scope.path_glob, ['lib/**/*.js']);
125
+ assert.deepEqual(pack.scope.lang, ['javascript']);
126
+ });
127
+ test('uses pack key as name when name is empty', () => {
128
+ createPackFile('unnamed-pack', {
129
+ name: '',
130
+ path_glob: 'src/**'
131
+ });
132
+ const pack = loadContextPack('unnamed-pack', testDir);
133
+ assert.equal(pack.name, 'unnamed-pack');
134
+ });
135
+ test('uses pack key as name when name is whitespace only', () => {
136
+ createPackFile('whitespace-pack', {
137
+ name: ' ',
138
+ path_glob: 'src/**'
139
+ });
140
+ const pack = loadContextPack('whitespace-pack', testDir);
141
+ assert.equal(pack.name, 'whitespace-pack');
142
+ });
143
+ test('uses pack key as name when name is missing', () => {
144
+ createPackFile('no-name-pack', {
145
+ path_glob: 'src/**'
146
+ });
147
+ const pack = loadContextPack('no-name-pack', testDir);
148
+ assert.equal(pack.name, 'no-name-pack');
149
+ });
150
+ test('sets description to null when empty', () => {
151
+ createPackFile('no-desc-pack', {
152
+ name: 'No Description Pack',
153
+ description: ''
154
+ });
155
+ const pack = loadContextPack('no-desc-pack', testDir);
156
+ assert.equal(pack.description, null);
157
+ });
158
+ test('sets description to null when whitespace only', () => {
159
+ createPackFile('whitespace-desc-pack', {
160
+ name: 'Whitespace Description Pack',
161
+ description: ' '
162
+ });
163
+ const pack = loadContextPack('whitespace-desc-pack', testDir);
164
+ assert.equal(pack.description, null);
165
+ });
166
+ test('throws error for invalid JSON in pack file', () => {
167
+ createInvalidJsonFile('invalid-json', '{ invalid json }');
168
+ assert.throws(() => loadContextPack('invalid-json', testDir), /Invalid JSON in context pack/);
169
+ });
170
+ test('throws error for pack with invalid schema', () => {
171
+ createPackFile('invalid-schema', {
172
+ name: 'Invalid Pack',
173
+ unknown_field: 'not allowed'
174
+ });
175
+ assert.throws(() => loadContextPack('invalid-schema', testDir), /unrecognized_keys/ // Zod validation error for strict schema
176
+ );
177
+ });
178
+ test('caches loaded pack and returns from cache on second call', () => {
179
+ createPackFile('cached-pack', {
180
+ name: 'Cached Pack',
181
+ path_glob: 'src/**'
182
+ });
183
+ const pack1 = loadContextPack('cached-pack', testDir);
184
+ const pack2 = loadContextPack('cached-pack', testDir);
185
+ assert.deepEqual(pack1, pack2);
186
+ });
187
+ test('reloads pack when file mtime changes', async () => {
188
+ createPackFile('mtime-pack', {
189
+ name: 'Original Name',
190
+ path_glob: 'src/**'
191
+ });
192
+ const pack1 = loadContextPack('mtime-pack', testDir);
193
+ assert.equal(pack1.name, 'Original Name');
194
+ // Wait a bit to ensure mtime changes
195
+ await new Promise(resolve => setTimeout(resolve, 50));
196
+ // Update the file
197
+ createPackFile('mtime-pack', {
198
+ name: 'Updated Name',
199
+ path_glob: 'lib/**'
200
+ });
201
+ const pack2 = loadContextPack('mtime-pack', testDir);
202
+ assert.equal(pack2.name, 'Updated Name');
203
+ assert.deepEqual(pack2.scope.path_glob, 'lib/**');
204
+ });
205
+ test('handles pack with boolean scope values', () => {
206
+ createPackFile('boolean-pack', {
207
+ name: 'Boolean Pack',
208
+ hybrid: true,
209
+ bm25: false,
210
+ symbol_boost: true
211
+ });
212
+ const pack = loadContextPack('boolean-pack', testDir);
213
+ assert.equal(pack.scope.hybrid, true);
214
+ assert.equal(pack.scope.bm25, false);
215
+ assert.equal(pack.scope.symbol_boost, true);
216
+ });
217
+ test('handles pack with string boolean values', () => {
218
+ createPackFile('string-bool-pack', {
219
+ name: 'String Boolean Pack',
220
+ hybrid: 'true',
221
+ bm25: 'off',
222
+ symbol_boost: 'on'
223
+ });
224
+ const pack = loadContextPack('string-bool-pack', testDir);
225
+ assert.equal(pack.scope.hybrid, 'true');
226
+ assert.equal(pack.scope.bm25, 'off');
227
+ assert.equal(pack.scope.symbol_boost, 'on');
228
+ });
229
+ test('handles pack with provider and reranker options', () => {
230
+ createPackFile('provider-pack', {
231
+ name: 'Provider Pack',
232
+ provider: 'openai',
233
+ reranker: 'api'
234
+ });
235
+ const pack = loadContextPack('provider-pack', testDir);
236
+ assert.equal(pack.scope.provider, 'openai');
237
+ assert.equal(pack.scope.reranker, 'api');
238
+ });
239
+ test('handles pack with array scope values', () => {
240
+ createPackFile('array-pack', {
241
+ name: 'Array Pack',
242
+ path_glob: ['src/**', 'lib/**'],
243
+ tags: ['core', 'utils'],
244
+ lang: ['typescript', 'javascript']
245
+ });
246
+ const pack = loadContextPack('array-pack', testDir);
247
+ assert.deepEqual(pack.scope.path_glob, ['src/**', 'lib/**']);
248
+ assert.deepEqual(pack.scope.tags, ['core', 'utils']);
249
+ assert.deepEqual(pack.scope.lang, ['typescript', 'javascript']);
250
+ });
251
+ test('handles minimal valid pack (empty object)', () => {
252
+ createPackFile('minimal-pack', {});
253
+ const pack = loadContextPack('minimal-pack', testDir);
254
+ assert.equal(pack.key, 'minimal-pack');
255
+ assert.equal(pack.name, 'minimal-pack');
256
+ assert.equal(pack.description, null);
257
+ assert.deepEqual(pack.scope, {});
258
+ });
259
+ });
260
+ // ============================================================================
261
+ // listContextPacks Tests
262
+ // ============================================================================
263
+ describe('listContextPacks', () => {
264
+ beforeEach(() => {
265
+ setupTestDir();
266
+ clearContextPackCache();
267
+ });
268
+ afterEach(() => {
269
+ cleanupTestDir();
270
+ });
271
+ test('returns empty array when pack directory does not exist', () => {
272
+ const packs = listContextPacks(testDir);
273
+ assert.deepEqual(packs, []);
274
+ });
275
+ test('returns empty array when pack directory is empty', () => {
276
+ fs.mkdirSync(packDir, { recursive: true });
277
+ const packs = listContextPacks(testDir);
278
+ assert.deepEqual(packs, []);
279
+ });
280
+ test('ignores non-json files in pack directory', () => {
281
+ fs.mkdirSync(packDir, { recursive: true });
282
+ fs.writeFileSync(path.join(packDir, 'readme.txt'), 'Not a pack');
283
+ fs.writeFileSync(path.join(packDir, 'config.yaml'), 'name: not-pack');
284
+ const packs = listContextPacks(testDir);
285
+ assert.deepEqual(packs, []);
286
+ });
287
+ test('lists all valid context packs', () => {
288
+ createPackFile('pack-a', { name: 'Pack A', path_glob: 'a/**' });
289
+ createPackFile('pack-b', { name: 'Pack B', path_glob: 'b/**' });
290
+ createPackFile('pack-c', { name: 'Pack C', path_glob: 'c/**' });
291
+ const packs = listContextPacks(testDir);
292
+ assert.equal(packs.length, 3);
293
+ const names = packs.map(p => p.name).sort();
294
+ assert.deepEqual(names, ['Pack A', 'Pack B', 'Pack C']);
295
+ });
296
+ test('includes invalid packs with error description', () => {
297
+ createPackFile('valid-pack', { name: 'Valid Pack', path_glob: 'src/**' });
298
+ createInvalidJsonFile('invalid-pack', '{ broken json }');
299
+ const packs = listContextPacks(testDir);
300
+ assert.equal(packs.length, 2);
301
+ const validPack = packs.find(p => p.key === 'valid-pack');
302
+ assert.ok(validPack);
303
+ assert.equal(validPack.invalid, undefined);
304
+ const invalidPack = packs.find(p => p.key === 'invalid-pack');
305
+ assert.ok(invalidPack);
306
+ assert.equal(invalidPack.invalid, true);
307
+ assert.ok(invalidPack.description?.startsWith('Invalid pack:'));
308
+ });
309
+ test('handles schema validation errors gracefully', () => {
310
+ createPackFile('schema-error', {
311
+ name: 'Invalid',
312
+ unknown_property: 'bad value'
313
+ });
314
+ const packs = listContextPacks(testDir);
315
+ assert.equal(packs.length, 1);
316
+ const pack = packs[0];
317
+ assert.equal(pack.key, 'schema-error');
318
+ assert.equal(pack.invalid, true);
319
+ assert.ok(pack.description?.startsWith('Invalid pack:'));
320
+ });
321
+ test('handles active-pack.json file properly (should be filtered out if not a pack)', () => {
322
+ createPackFile('active-pack', {
323
+ key: 'some-pack',
324
+ appliedAt: new Date().toISOString()
325
+ });
326
+ // This should fail schema validation since it has unknown 'key' at root level
327
+ // Actually the schema allows extra properties? Let me check...
328
+ // The schema uses .strict() so this should be filtered as invalid
329
+ const packs = listContextPacks(testDir);
330
+ // Should be marked as invalid due to strict schema
331
+ const activePack = packs.find(p => p.key === 'active-pack');
332
+ if (activePack) {
333
+ assert.equal(activePack.invalid, true);
334
+ }
335
+ });
336
+ });
337
+ // ============================================================================
338
+ // getActiveContextPack Tests
339
+ // ============================================================================
340
+ describe('getActiveContextPack', () => {
341
+ beforeEach(() => {
342
+ setupTestDir();
343
+ clearContextPackCache();
344
+ });
345
+ afterEach(() => {
346
+ cleanupTestDir();
347
+ });
348
+ test('returns null when no active pack state file exists', () => {
349
+ const result = getActiveContextPack(testDir);
350
+ assert.equal(result, null);
351
+ });
352
+ test('returns null when state file contains invalid JSON', () => {
353
+ fs.mkdirSync(packDir, { recursive: true });
354
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), '{ invalid }');
355
+ const result = getActiveContextPack(testDir);
356
+ assert.equal(result, null);
357
+ });
358
+ test('returns null when state file is not an object', () => {
359
+ fs.mkdirSync(packDir, { recursive: true });
360
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), '"just a string"');
361
+ const result = getActiveContextPack(testDir);
362
+ assert.equal(result, null);
363
+ });
364
+ test('returns null when state file has null value', () => {
365
+ fs.mkdirSync(packDir, { recursive: true });
366
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), 'null');
367
+ const result = getActiveContextPack(testDir);
368
+ assert.equal(result, null);
369
+ });
370
+ test('returns null when state file has no key property', () => {
371
+ fs.mkdirSync(packDir, { recursive: true });
372
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), JSON.stringify({ appliedAt: new Date().toISOString() }));
373
+ const result = getActiveContextPack(testDir);
374
+ assert.equal(result, null);
375
+ });
376
+ test('returns null when key is not a string', () => {
377
+ fs.mkdirSync(packDir, { recursive: true });
378
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), JSON.stringify({ key: 123, appliedAt: new Date().toISOString() }));
379
+ const result = getActiveContextPack(testDir);
380
+ assert.equal(result, null);
381
+ });
382
+ test('returns null when referenced pack does not exist', () => {
383
+ fs.mkdirSync(packDir, { recursive: true });
384
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), JSON.stringify({ key: 'nonexistent-pack', appliedAt: new Date().toISOString() }));
385
+ const result = getActiveContextPack(testDir);
386
+ assert.equal(result, null);
387
+ });
388
+ test('returns active pack with appliedAt timestamp', () => {
389
+ const appliedAt = new Date().toISOString();
390
+ createPackFile('my-pack', { name: 'My Pack', path_glob: 'src/**' });
391
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), JSON.stringify({ key: 'my-pack', appliedAt }));
392
+ const result = getActiveContextPack(testDir);
393
+ assert.ok(result);
394
+ assert.equal(result.key, 'my-pack');
395
+ assert.equal(result.name, 'My Pack');
396
+ assert.equal(result.appliedAt, appliedAt);
397
+ });
398
+ test('returns null appliedAt when not present in state', () => {
399
+ createPackFile('timestamp-pack', { name: 'Timestamp Pack' });
400
+ fs.writeFileSync(path.join(packDir, 'active-pack.json'), JSON.stringify({ key: 'timestamp-pack' }));
401
+ const result = getActiveContextPack(testDir);
402
+ assert.ok(result);
403
+ assert.equal(result.key, 'timestamp-pack');
404
+ assert.equal(result.appliedAt, null);
405
+ });
406
+ });
407
+ // ============================================================================
408
+ // setActiveContextPack Tests
409
+ // ============================================================================
410
+ describe('setActiveContextPack', () => {
411
+ beforeEach(() => {
412
+ setupTestDir();
413
+ clearContextPackCache();
414
+ });
415
+ afterEach(() => {
416
+ cleanupTestDir();
417
+ });
418
+ test('throws error when pack does not exist', () => {
419
+ assert.throws(() => setActiveContextPack('nonexistent', testDir), /Context pack "nonexistent" not found/);
420
+ });
421
+ test('creates pack directory if it does not exist', () => {
422
+ // Create pack file first, then remove directory to simulate edge case
423
+ createPackFile('auto-dir-pack', { name: 'Auto Dir Pack' });
424
+ // Should succeed and create the active-pack.json
425
+ const result = setActiveContextPack('auto-dir-pack', testDir);
426
+ assert.ok(result);
427
+ assert.ok(fs.existsSync(path.join(packDir, 'active-pack.json')));
428
+ });
429
+ test('sets active pack and returns pack info', () => {
430
+ createPackFile('target-pack', {
431
+ name: 'Target Pack',
432
+ description: 'Pack to activate',
433
+ path_glob: 'src/**'
434
+ });
435
+ const result = setActiveContextPack('target-pack', testDir);
436
+ assert.equal(result.key, 'target-pack');
437
+ assert.equal(result.name, 'Target Pack');
438
+ assert.equal(result.description, 'Pack to activate');
439
+ });
440
+ test('writes state file with key and timestamp', () => {
441
+ createPackFile('state-pack', { name: 'State Pack' });
442
+ const beforeTime = new Date().toISOString();
443
+ setActiveContextPack('state-pack', testDir);
444
+ const afterTime = new Date().toISOString();
445
+ const stateFile = path.join(packDir, 'active-pack.json');
446
+ const stateContent = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
447
+ assert.equal(stateContent.key, 'state-pack');
448
+ assert.ok(stateContent.appliedAt >= beforeTime);
449
+ assert.ok(stateContent.appliedAt <= afterTime);
450
+ });
451
+ test('overwrites previous active pack', () => {
452
+ createPackFile('pack-one', { name: 'Pack One' });
453
+ createPackFile('pack-two', { name: 'Pack Two' });
454
+ setActiveContextPack('pack-one', testDir);
455
+ let active = getActiveContextPack(testDir);
456
+ assert.equal(active?.key, 'pack-one');
457
+ setActiveContextPack('pack-two', testDir);
458
+ active = getActiveContextPack(testDir);
459
+ assert.equal(active?.key, 'pack-two');
460
+ });
461
+ });
462
+ // ============================================================================
463
+ // resolveScopeWithPack Tests
464
+ // ============================================================================
465
+ describe('resolveScopeWithPack', () => {
466
+ beforeEach(() => {
467
+ setupTestDir();
468
+ clearContextPackCache();
469
+ });
470
+ afterEach(() => {
471
+ cleanupTestDir();
472
+ });
473
+ test('returns default scope when no pack is active and no overrides', () => {
474
+ const result = resolveScopeWithPack({}, { basePath: testDir });
475
+ assert.equal(result.pack, null);
476
+ // Default scope values from normalizeScopeFilters
477
+ assert.equal(result.scope.reranker, 'off');
478
+ assert.equal(result.scope.hybrid, true);
479
+ assert.equal(result.scope.bm25, true);
480
+ assert.equal(result.scope.symbol_boost, true);
481
+ });
482
+ test('applies active pack scope', () => {
483
+ createPackFile('scope-pack', {
484
+ name: 'Scope Pack',
485
+ path_glob: ['src/**'],
486
+ tags: ['api'],
487
+ lang: ['typescript']
488
+ });
489
+ setActiveContextPack('scope-pack', testDir);
490
+ const result = resolveScopeWithPack({}, { basePath: testDir });
491
+ assert.ok(result.pack);
492
+ assert.equal(result.pack.key, 'scope-pack');
493
+ assert.deepEqual(result.scope.path_glob, ['src/**']);
494
+ assert.deepEqual(result.scope.tags, ['api']);
495
+ assert.deepEqual(result.scope.lang, ['typescript']);
496
+ });
497
+ test('overrides can replace pack scope values', () => {
498
+ createPackFile('base-pack', {
499
+ name: 'Base Pack',
500
+ path_glob: ['src/**'],
501
+ lang: ['typescript']
502
+ });
503
+ setActiveContextPack('base-pack', testDir);
504
+ const result = resolveScopeWithPack({ path_glob: ['lib/**'], lang: ['javascript'] }, { basePath: testDir });
505
+ assert.deepEqual(result.scope.path_glob, ['lib/**']);
506
+ assert.deepEqual(result.scope.lang, ['javascript']);
507
+ });
508
+ test('overrides can add to pack scope', () => {
509
+ createPackFile('partial-pack', {
510
+ name: 'Partial Pack',
511
+ path_glob: ['src/**']
512
+ });
513
+ setActiveContextPack('partial-pack', testDir);
514
+ const result = resolveScopeWithPack({ tags: ['important'] }, { basePath: testDir });
515
+ assert.deepEqual(result.scope.path_glob, ['src/**']);
516
+ assert.deepEqual(result.scope.tags, ['important']);
517
+ });
518
+ test('session pack takes precedence over active pack', () => {
519
+ createPackFile('active-pack', {
520
+ name: 'Active Pack',
521
+ path_glob: ['active/**']
522
+ });
523
+ setActiveContextPack('active-pack', testDir);
524
+ const sessionPack = {
525
+ key: 'session-pack',
526
+ name: 'Session Pack',
527
+ description: 'Temporary session scope',
528
+ scope: { path_glob: ['session/**'] },
529
+ path: '/tmp/session-pack.json',
530
+ basePath: testDir
531
+ };
532
+ const result = resolveScopeWithPack({}, { basePath: testDir, sessionPack });
533
+ assert.ok(result.pack);
534
+ assert.equal(result.pack.key, 'session-pack');
535
+ assert.deepEqual(result.scope.path_glob, ['session/**']);
536
+ });
537
+ test('session pack is ignored when basePath differs', () => {
538
+ // Create the pack file
539
+ createPackFile('active-pack', {
540
+ name: 'Active Pack',
541
+ path_glob: ['active/**']
542
+ });
543
+ // Verify pack file exists
544
+ const packFile = path.join(packDir, 'active-pack.json');
545
+ assert.ok(fs.existsSync(packFile), `Pack file should exist at ${packFile}`);
546
+ // Load the pack directly to verify it works
547
+ const loadedPack = loadContextPack('active-pack', testDir);
548
+ assert.equal(loadedPack.name, 'Active Pack', 'Pack should load correctly');
549
+ // Set it as active
550
+ setActiveContextPack('active-pack', testDir);
551
+ // Verify the state file was created
552
+ const stateFile = path.join(packDir, 'active-pack.json');
553
+ assert.ok(fs.existsSync(stateFile), 'State file should exist');
554
+ // Verify the active pack can be retrieved
555
+ const activePack = getActiveContextPack(testDir);
556
+ assert.ok(activePack, 'Active pack should be retrievable');
557
+ assert.equal(activePack.key, 'active-pack');
558
+ const sessionPack = {
559
+ key: 'session-pack',
560
+ name: 'Session Pack',
561
+ description: null,
562
+ scope: { path_glob: ['session/**'] },
563
+ path: '/other/session-pack.json',
564
+ basePath: '/different/path'
565
+ };
566
+ const result = resolveScopeWithPack({}, { basePath: testDir, sessionPack });
567
+ // Session pack is ignored because its basePath differs from testDir
568
+ // So the active pack should be used instead
569
+ assert.ok(result.pack, 'Active pack should be found when session pack basePath differs');
570
+ assert.equal(result.pack.key, 'active-pack');
571
+ assert.deepEqual(result.scope.path_glob, ['active/**']);
572
+ });
573
+ test('overrides take precedence over session pack', () => {
574
+ const sessionPack = {
575
+ key: 'session-pack',
576
+ name: 'Session Pack',
577
+ description: null,
578
+ scope: { path_glob: ['session/**'], lang: ['python'] },
579
+ path: '/tmp/session-pack.json',
580
+ basePath: testDir
581
+ };
582
+ const result = resolveScopeWithPack({ lang: ['ruby'] }, { basePath: testDir, sessionPack });
583
+ assert.deepEqual(result.scope.path_glob, ['session/**']);
584
+ assert.deepEqual(result.scope.lang, ['ruby']);
585
+ });
586
+ test('handles undefined overrides', () => {
587
+ createPackFile('undef-test', {
588
+ name: 'Undef Test',
589
+ path_glob: ['src/**']
590
+ });
591
+ setActiveContextPack('undef-test', testDir);
592
+ const result = resolveScopeWithPack({ path_glob: undefined, tags: undefined }, { basePath: testDir });
593
+ assert.deepEqual(result.scope.path_glob, ['src/**']);
594
+ assert.equal(result.scope.tags, undefined);
595
+ });
596
+ test('normalizes scope values', () => {
597
+ createPackFile('normalize-pack', {
598
+ name: 'Normalize Pack',
599
+ tags: 'single-tag',
600
+ lang: 'TypeScript',
601
+ hybrid: 'on',
602
+ bm25: 'off'
603
+ });
604
+ setActiveContextPack('normalize-pack', testDir);
605
+ const result = resolveScopeWithPack({}, { basePath: testDir });
606
+ // Tags and lang should be normalized to lowercase arrays
607
+ assert.deepEqual(result.scope.tags, ['single-tag']);
608
+ assert.deepEqual(result.scope.lang, ['typescript']);
609
+ assert.equal(result.scope.hybrid, true);
610
+ assert.equal(result.scope.bm25, false);
611
+ });
612
+ test('returns pack info with description', () => {
613
+ createPackFile('info-pack', {
614
+ name: 'Info Pack',
615
+ description: 'Pack with description'
616
+ });
617
+ setActiveContextPack('info-pack', testDir);
618
+ const result = resolveScopeWithPack({}, { basePath: testDir });
619
+ assert.ok(result.pack);
620
+ assert.equal(result.pack.key, 'info-pack');
621
+ assert.equal(result.pack.name, 'Info Pack');
622
+ assert.equal(result.pack.description, 'Pack with description');
623
+ });
624
+ test('works with default options', () => {
625
+ // Uses current working directory
626
+ const result = resolveScopeWithPack();
627
+ // Should not throw and return default scope
628
+ assert.ok(result);
629
+ assert.equal(result.scope.hybrid, true);
630
+ });
631
+ test('handles empty session pack scope', () => {
632
+ const sessionPack = {
633
+ key: 'empty-session',
634
+ name: 'Empty Session',
635
+ description: null,
636
+ scope: {},
637
+ path: '/tmp/empty.json',
638
+ basePath: testDir
639
+ };
640
+ const result = resolveScopeWithPack({ path_glob: ['override/**'] }, { basePath: testDir, sessionPack });
641
+ assert.ok(result.pack);
642
+ assert.equal(result.pack.key, 'empty-session');
643
+ assert.deepEqual(result.scope.path_glob, ['override/**']);
644
+ });
645
+ test('handles session pack with null scope', () => {
646
+ const sessionPack = {
647
+ key: 'null-scope-session',
648
+ name: 'Null Scope Session',
649
+ description: null,
650
+ scope: {},
651
+ path: '/tmp/null-scope.json',
652
+ basePath: testDir
653
+ };
654
+ const result = resolveScopeWithPack({}, { basePath: testDir, sessionPack });
655
+ assert.ok(result.pack);
656
+ assert.equal(result.pack.key, 'null-scope-session');
657
+ });
658
+ test('handles provider override', () => {
659
+ createPackFile('provider-base', {
660
+ name: 'Provider Base',
661
+ provider: 'openai'
662
+ });
663
+ setActiveContextPack('provider-base', testDir);
664
+ const result = resolveScopeWithPack({ provider: 'anthropic' }, { basePath: testDir });
665
+ assert.equal(result.scope.provider, 'anthropic');
666
+ });
667
+ test('handles reranker override', () => {
668
+ createPackFile('reranker-base', {
669
+ name: 'Reranker Base',
670
+ reranker: 'off'
671
+ });
672
+ setActiveContextPack('reranker-base', testDir);
673
+ const result = resolveScopeWithPack({ reranker: 'api' }, { basePath: testDir });
674
+ assert.equal(result.scope.reranker, 'api');
675
+ });
676
+ });
677
+ // ============================================================================
678
+ // clearContextPackCache Tests
679
+ // ============================================================================
680
+ describe('clearContextPackCache', () => {
681
+ beforeEach(() => {
682
+ setupTestDir();
683
+ clearContextPackCache();
684
+ });
685
+ afterEach(() => {
686
+ cleanupTestDir();
687
+ });
688
+ test('clears cached packs', () => {
689
+ createPackFile('cache-test', {
690
+ name: 'Cache Test',
691
+ path_glob: 'original/**'
692
+ });
693
+ // Load to cache
694
+ const pack1 = loadContextPack('cache-test', testDir);
695
+ assert.equal(pack1.name, 'Cache Test');
696
+ // Modify file without changing mtime (simulating edge case)
697
+ // Actually we need to clear cache to force reload
698
+ clearContextPackCache();
699
+ // Update file content
700
+ createPackFile('cache-test', {
701
+ name: 'Updated Cache Test',
702
+ path_glob: 'updated/**'
703
+ });
704
+ // Should get updated version
705
+ const pack2 = loadContextPack('cache-test', testDir);
706
+ assert.equal(pack2.name, 'Updated Cache Test');
707
+ });
708
+ test('does not throw on empty cache', () => {
709
+ // Should not throw
710
+ clearContextPackCache();
711
+ clearContextPackCache();
712
+ assert.ok(true);
713
+ });
714
+ });
715
+ // ============================================================================
716
+ // Edge Cases and Error Handling
717
+ // ============================================================================
718
+ describe('Edge Cases', () => {
719
+ beforeEach(() => {
720
+ setupTestDir();
721
+ clearContextPackCache();
722
+ });
723
+ afterEach(() => {
724
+ cleanupTestDir();
725
+ });
726
+ test('handles pack with all scope keys', () => {
727
+ createPackFile('full-pack', {
728
+ name: 'Full Pack',
729
+ description: 'Pack with all scope keys',
730
+ path_glob: ['**/*.ts'],
731
+ tags: ['core'],
732
+ lang: ['typescript'],
733
+ provider: 'openai',
734
+ reranker: 'api',
735
+ hybrid: true,
736
+ bm25: true,
737
+ symbol_boost: false
738
+ });
739
+ const pack = loadContextPack('full-pack', testDir);
740
+ // Arrays are stored as-is in the pack scope
741
+ assert.deepEqual(pack.scope.path_glob, ['**/*.ts']);
742
+ assert.deepEqual(pack.scope.tags, ['core']);
743
+ assert.deepEqual(pack.scope.lang, ['typescript']);
744
+ assert.equal(pack.scope.provider, 'openai');
745
+ assert.equal(pack.scope.reranker, 'api');
746
+ assert.equal(pack.scope.hybrid, true);
747
+ assert.equal(pack.scope.bm25, true);
748
+ assert.equal(pack.scope.symbol_boost, false);
749
+ });
750
+ test('handles special characters in pack name', () => {
751
+ createPackFile('pack-with-dashes', { name: 'Dashed Pack' });
752
+ createPackFile('pack_with_underscores', { name: 'Underscored Pack' });
753
+ const dashed = loadContextPack('pack-with-dashes', testDir);
754
+ const underscored = loadContextPack('pack_with_underscores', testDir);
755
+ assert.equal(dashed.name, 'Dashed Pack');
756
+ assert.equal(underscored.name, 'Underscored Pack');
757
+ });
758
+ test('handles deeply nested scope in pack definition', () => {
759
+ createPackFile('nested-deep', {
760
+ name: 'Nested Deep',
761
+ scope: {
762
+ path_glob: 'scope-path/**',
763
+ tags: ['scope-tag']
764
+ },
765
+ // Top-level values should merge with scope
766
+ lang: ['toplevel-lang']
767
+ });
768
+ const pack = loadContextPack('nested-deep', testDir);
769
+ // Both scope-level and top-level values should be captured
770
+ // Values are stored as-is (string stays string, array stays array)
771
+ assert.equal(pack.scope.path_glob, 'scope-path/**');
772
+ assert.deepEqual(pack.scope.tags, ['scope-tag']);
773
+ assert.deepEqual(pack.scope.lang, ['toplevel-lang']);
774
+ });
775
+ test('handles pack with metadata field', () => {
776
+ createPackFile('metadata-pack', {
777
+ name: 'Metadata Pack',
778
+ description: 'Has metadata',
779
+ metadata: {
780
+ author: 'test',
781
+ version: '1.0.0'
782
+ },
783
+ path_glob: 'src/**'
784
+ });
785
+ const pack = loadContextPack('metadata-pack', testDir);
786
+ assert.equal(pack.name, 'Metadata Pack');
787
+ // Metadata is allowed but not included in scope
788
+ assert.equal(pack.scope.path_glob, 'src/**');
789
+ });
790
+ test('resolveScopeWithPack handles missing basePath in options', () => {
791
+ const result = resolveScopeWithPack({}, {});
792
+ // Should use default basePath '.'
793
+ assert.ok(result);
794
+ assert.equal(result.scope.hybrid, true);
795
+ });
796
+ test('pack path is absolute', () => {
797
+ createPackFile('abs-path-pack', { name: 'Abs Path Pack' });
798
+ const pack = loadContextPack('abs-path-pack', testDir);
799
+ assert.ok(path.isAbsolute(pack.path));
800
+ assert.ok(pack.path.includes('abs-path-pack.json'));
801
+ });
802
+ test('concurrent loads return same cached value', async () => {
803
+ createPackFile('concurrent-pack', { name: 'Concurrent Pack' });
804
+ // Load same pack concurrently
805
+ const [pack1, pack2, pack3] = await Promise.all([
806
+ Promise.resolve(loadContextPack('concurrent-pack', testDir)),
807
+ Promise.resolve(loadContextPack('concurrent-pack', testDir)),
808
+ Promise.resolve(loadContextPack('concurrent-pack', testDir))
809
+ ]);
810
+ assert.deepEqual(pack1, pack2);
811
+ assert.deepEqual(pack2, pack3);
812
+ });
813
+ test('listContextPacks with mixed valid and invalid packs', () => {
814
+ createPackFile('valid-1', { name: 'Valid 1' });
815
+ createInvalidJsonFile('invalid-1', 'not json');
816
+ createPackFile('valid-2', { name: 'Valid 2' });
817
+ createPackFile('schema-invalid', { name: 'Schema Invalid', bad_key: true });
818
+ createInvalidJsonFile('invalid-2', '{ "unclosed": }');
819
+ const packs = listContextPacks(testDir);
820
+ const valid = packs.filter(p => !p.invalid);
821
+ const invalid = packs.filter(p => p.invalid);
822
+ assert.equal(valid.length, 2);
823
+ assert.equal(invalid.length, 3);
824
+ });
825
+ });
826
+ //# sourceMappingURL=context-packs.test.js.map