grepmind-core 0.1.0-alpha

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 (370) hide show
  1. package/LICENSE +190 -0
  2. package/dist/config/types.d.ts +174 -0
  3. package/dist/config/types.d.ts.map +1 -0
  4. package/dist/config/types.js +137 -0
  5. package/dist/config/types.js.map +1 -0
  6. package/dist/git.d.ts +98 -0
  7. package/dist/git.d.ts.map +1 -0
  8. package/dist/git.js +298 -0
  9. package/dist/git.js.map +1 -0
  10. package/dist/git.test.d.ts +7 -0
  11. package/dist/git.test.d.ts.map +1 -0
  12. package/dist/git.test.js +242 -0
  13. package/dist/git.test.js.map +1 -0
  14. package/dist/index.d.ts +44 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +67 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/indexer/branch.d.ts +121 -0
  19. package/dist/indexer/branch.d.ts.map +1 -0
  20. package/dist/indexer/branch.js +451 -0
  21. package/dist/indexer/branch.js.map +1 -0
  22. package/dist/indexer/chunker.d.ts +9 -0
  23. package/dist/indexer/chunker.d.ts.map +1 -0
  24. package/dist/indexer/chunker.js +70 -0
  25. package/dist/indexer/chunker.js.map +1 -0
  26. package/dist/indexer/chunker.test.d.ts +2 -0
  27. package/dist/indexer/chunker.test.d.ts.map +1 -0
  28. package/dist/indexer/chunker.test.js +180 -0
  29. package/dist/indexer/chunker.test.js.map +1 -0
  30. package/dist/indexer/code/branch.d.ts +155 -0
  31. package/dist/indexer/code/branch.d.ts.map +1 -0
  32. package/dist/indexer/code/branch.js +550 -0
  33. package/dist/indexer/code/branch.js.map +1 -0
  34. package/dist/indexer/code/branch.test.d.ts +7 -0
  35. package/dist/indexer/code/branch.test.d.ts.map +1 -0
  36. package/dist/indexer/code/branch.test.js +241 -0
  37. package/dist/indexer/code/branch.test.js.map +1 -0
  38. package/dist/indexer/code/chunker.d.ts +61 -0
  39. package/dist/indexer/code/chunker.d.ts.map +1 -0
  40. package/dist/indexer/code/chunker.js +311 -0
  41. package/dist/indexer/code/chunker.js.map +1 -0
  42. package/dist/indexer/code/chunker.test.d.ts +2 -0
  43. package/dist/indexer/code/chunker.test.d.ts.map +1 -0
  44. package/dist/indexer/code/chunker.test.js +552 -0
  45. package/dist/indexer/code/chunker.test.js.map +1 -0
  46. package/dist/indexer/code/fts.test.d.ts +2 -0
  47. package/dist/indexer/code/fts.test.d.ts.map +1 -0
  48. package/dist/indexer/code/fts.test.js +14 -0
  49. package/dist/indexer/code/fts.test.js.map +1 -0
  50. package/dist/indexer/code/graph/embedded.d.ts +11 -0
  51. package/dist/indexer/code/graph/embedded.d.ts.map +1 -0
  52. package/dist/indexer/code/graph/embedded.js +152 -0
  53. package/dist/indexer/code/graph/embedded.js.map +1 -0
  54. package/dist/indexer/code/graph/embedded.test.d.ts +2 -0
  55. package/dist/indexer/code/graph/embedded.test.d.ts.map +1 -0
  56. package/dist/indexer/code/graph/embedded.test.js +105 -0
  57. package/dist/indexer/code/graph/embedded.test.js.map +1 -0
  58. package/dist/indexer/code/graph/facts.d.ts +11 -0
  59. package/dist/indexer/code/graph/facts.d.ts.map +1 -0
  60. package/dist/indexer/code/graph/facts.js +456 -0
  61. package/dist/indexer/code/graph/facts.js.map +1 -0
  62. package/dist/indexer/code/graph/facts.test.d.ts +2 -0
  63. package/dist/indexer/code/graph/facts.test.d.ts.map +1 -0
  64. package/dist/indexer/code/graph/facts.test.js +181 -0
  65. package/dist/indexer/code/graph/facts.test.js.map +1 -0
  66. package/dist/indexer/code/graph/id.d.ts +14 -0
  67. package/dist/indexer/code/graph/id.d.ts.map +1 -0
  68. package/dist/indexer/code/graph/id.js +40 -0
  69. package/dist/indexer/code/graph/id.js.map +1 -0
  70. package/dist/indexer/code/graph/id.test.d.ts +2 -0
  71. package/dist/indexer/code/graph/id.test.d.ts.map +1 -0
  72. package/dist/indexer/code/graph/id.test.js +86 -0
  73. package/dist/indexer/code/graph/id.test.js.map +1 -0
  74. package/dist/indexer/code/graph/index.d.ts +133 -0
  75. package/dist/indexer/code/graph/index.d.ts.map +1 -0
  76. package/dist/indexer/code/graph/index.js +1876 -0
  77. package/dist/indexer/code/graph/index.js.map +1 -0
  78. package/dist/indexer/code/graph/index.test.d.ts +2 -0
  79. package/dist/indexer/code/graph/index.test.d.ts.map +1 -0
  80. package/dist/indexer/code/graph/index.test.js +210 -0
  81. package/dist/indexer/code/graph/index.test.js.map +1 -0
  82. package/dist/indexer/code/graph/queries.d.ts +22 -0
  83. package/dist/indexer/code/graph/queries.d.ts.map +1 -0
  84. package/dist/indexer/code/graph/queries.js +79 -0
  85. package/dist/indexer/code/graph/queries.js.map +1 -0
  86. package/dist/indexer/code/graph/queries.test.d.ts +2 -0
  87. package/dist/indexer/code/graph/queries.test.d.ts.map +1 -0
  88. package/dist/indexer/code/graph/queries.test.js +108 -0
  89. package/dist/indexer/code/graph/queries.test.js.map +1 -0
  90. package/dist/indexer/code/graph/resolver.d.ts +136 -0
  91. package/dist/indexer/code/graph/resolver.d.ts.map +1 -0
  92. package/dist/indexer/code/graph/resolver.js +839 -0
  93. package/dist/indexer/code/graph/resolver.js.map +1 -0
  94. package/dist/indexer/code/graph/resolver.test.d.ts +2 -0
  95. package/dist/indexer/code/graph/resolver.test.d.ts.map +1 -0
  96. package/dist/indexer/code/graph/resolver.test.js +482 -0
  97. package/dist/indexer/code/graph/resolver.test.js.map +1 -0
  98. package/dist/indexer/code/graph/semantic.d.ts +33 -0
  99. package/dist/indexer/code/graph/semantic.d.ts.map +1 -0
  100. package/dist/indexer/code/graph/semantic.js +279 -0
  101. package/dist/indexer/code/graph/semantic.js.map +1 -0
  102. package/dist/indexer/code/graph/semantic.test.d.ts +2 -0
  103. package/dist/indexer/code/graph/semantic.test.d.ts.map +1 -0
  104. package/dist/indexer/code/graph/semantic.test.js +127 -0
  105. package/dist/indexer/code/graph/semantic.test.js.map +1 -0
  106. package/dist/indexer/code/index.d.ts +404 -0
  107. package/dist/indexer/code/index.d.ts.map +1 -0
  108. package/dist/indexer/code/index.js +2070 -0
  109. package/dist/indexer/code/index.js.map +1 -0
  110. package/dist/indexer/code/languages/bash.d.ts +14 -0
  111. package/dist/indexer/code/languages/bash.d.ts.map +1 -0
  112. package/dist/indexer/code/languages/bash.js +125 -0
  113. package/dist/indexer/code/languages/bash.js.map +1 -0
  114. package/dist/indexer/code/languages/css.d.ts +16 -0
  115. package/dist/indexer/code/languages/css.d.ts.map +1 -0
  116. package/dist/indexer/code/languages/css.js +204 -0
  117. package/dist/indexer/code/languages/css.js.map +1 -0
  118. package/dist/indexer/code/languages/generic.d.ts +61 -0
  119. package/dist/indexer/code/languages/generic.d.ts.map +1 -0
  120. package/dist/indexer/code/languages/generic.js +150 -0
  121. package/dist/indexer/code/languages/generic.js.map +1 -0
  122. package/dist/indexer/code/languages/graphql.d.ts +13 -0
  123. package/dist/indexer/code/languages/graphql.d.ts.map +1 -0
  124. package/dist/indexer/code/languages/graphql.js +180 -0
  125. package/dist/indexer/code/languages/graphql.js.map +1 -0
  126. package/dist/indexer/code/languages/html.d.ts +16 -0
  127. package/dist/indexer/code/languages/html.d.ts.map +1 -0
  128. package/dist/indexer/code/languages/html.js +138 -0
  129. package/dist/indexer/code/languages/html.js.map +1 -0
  130. package/dist/indexer/code/languages/index.d.ts +9 -0
  131. package/dist/indexer/code/languages/index.d.ts.map +1 -0
  132. package/dist/indexer/code/languages/index.js +12 -0
  133. package/dist/indexer/code/languages/index.js.map +1 -0
  134. package/dist/indexer/code/languages/json.d.ts +12 -0
  135. package/dist/indexer/code/languages/json.d.ts.map +1 -0
  136. package/dist/indexer/code/languages/json.js +66 -0
  137. package/dist/indexer/code/languages/json.js.map +1 -0
  138. package/dist/indexer/code/languages/registry.d.ts +78 -0
  139. package/dist/indexer/code/languages/registry.d.ts.map +1 -0
  140. package/dist/indexer/code/languages/registry.js +72 -0
  141. package/dist/indexer/code/languages/registry.js.map +1 -0
  142. package/dist/indexer/code/languages/typescript.d.ts +39 -0
  143. package/dist/indexer/code/languages/typescript.d.ts.map +1 -0
  144. package/dist/indexer/code/languages/typescript.js +300 -0
  145. package/dist/indexer/code/languages/typescript.js.map +1 -0
  146. package/dist/indexer/code/languages/yaml.d.ts +13 -0
  147. package/dist/indexer/code/languages/yaml.d.ts.map +1 -0
  148. package/dist/indexer/code/languages/yaml.js +90 -0
  149. package/dist/indexer/code/languages/yaml.js.map +1 -0
  150. package/dist/indexer/code/parser.d.ts +26 -0
  151. package/dist/indexer/code/parser.d.ts.map +1 -0
  152. package/dist/indexer/code/parser.js +332 -0
  153. package/dist/indexer/code/parser.js.map +1 -0
  154. package/dist/indexer/code/retry.d.ts +58 -0
  155. package/dist/indexer/code/retry.d.ts.map +1 -0
  156. package/dist/indexer/code/retry.js +192 -0
  157. package/dist/indexer/code/retry.js.map +1 -0
  158. package/dist/indexer/code/tree/builder.d.ts +30 -0
  159. package/dist/indexer/code/tree/builder.d.ts.map +1 -0
  160. package/dist/indexer/code/tree/builder.js +132 -0
  161. package/dist/indexer/code/tree/builder.js.map +1 -0
  162. package/dist/indexer/code/tree/builder.test.d.ts +2 -0
  163. package/dist/indexer/code/tree/builder.test.d.ts.map +1 -0
  164. package/dist/indexer/code/tree/builder.test.js +31 -0
  165. package/dist/indexer/code/tree/builder.test.js.map +1 -0
  166. package/dist/indexer/code/tree/cache.d.ts +22 -0
  167. package/dist/indexer/code/tree/cache.d.ts.map +1 -0
  168. package/dist/indexer/code/tree/cache.js +85 -0
  169. package/dist/indexer/code/tree/cache.js.map +1 -0
  170. package/dist/indexer/code/tree/context.d.ts +32 -0
  171. package/dist/indexer/code/tree/context.d.ts.map +1 -0
  172. package/dist/indexer/code/tree/context.js +78 -0
  173. package/dist/indexer/code/tree/context.js.map +1 -0
  174. package/dist/indexer/code/tree/embedding.d.ts +9 -0
  175. package/dist/indexer/code/tree/embedding.d.ts.map +1 -0
  176. package/dist/indexer/code/tree/embedding.js +53 -0
  177. package/dist/indexer/code/tree/embedding.js.map +1 -0
  178. package/dist/indexer/code/tree/embedding.test.d.ts +2 -0
  179. package/dist/indexer/code/tree/embedding.test.d.ts.map +1 -0
  180. package/dist/indexer/code/tree/embedding.test.js +57 -0
  181. package/dist/indexer/code/tree/embedding.test.js.map +1 -0
  182. package/dist/indexer/code/tree/id.d.ts +3 -0
  183. package/dist/indexer/code/tree/id.d.ts.map +1 -0
  184. package/dist/indexer/code/tree/id.js +8 -0
  185. package/dist/indexer/code/tree/id.js.map +1 -0
  186. package/dist/indexer/code/tree/index.d.ts +113 -0
  187. package/dist/indexer/code/tree/index.d.ts.map +1 -0
  188. package/dist/indexer/code/tree/index.js +1146 -0
  189. package/dist/indexer/code/tree/index.js.map +1 -0
  190. package/dist/indexer/code/tree/rename.d.ts +13 -0
  191. package/dist/indexer/code/tree/rename.d.ts.map +1 -0
  192. package/dist/indexer/code/tree/rename.js +46 -0
  193. package/dist/indexer/code/tree/rename.js.map +1 -0
  194. package/dist/indexer/code/tree/repomap.d.ts +29 -0
  195. package/dist/indexer/code/tree/repomap.d.ts.map +1 -0
  196. package/dist/indexer/code/tree/repomap.js +95 -0
  197. package/dist/indexer/code/tree/repomap.js.map +1 -0
  198. package/dist/indexer/code/tree/repomap.test.d.ts +2 -0
  199. package/dist/indexer/code/tree/repomap.test.d.ts.map +1 -0
  200. package/dist/indexer/code/tree/repomap.test.js +93 -0
  201. package/dist/indexer/code/tree/repomap.test.js.map +1 -0
  202. package/dist/indexer/code/tree/stats.d.ts +26 -0
  203. package/dist/indexer/code/tree/stats.d.ts.map +1 -0
  204. package/dist/indexer/code/tree/stats.js +49 -0
  205. package/dist/indexer/code/tree/stats.js.map +1 -0
  206. package/dist/indexer/code/tree/types.d.ts +186 -0
  207. package/dist/indexer/code/tree/types.d.ts.map +1 -0
  208. package/dist/indexer/code/tree/types.js +10 -0
  209. package/dist/indexer/code/tree/types.js.map +1 -0
  210. package/dist/indexer/code/wal.d.ts +144 -0
  211. package/dist/indexer/code/wal.d.ts.map +1 -0
  212. package/dist/indexer/code/wal.js +283 -0
  213. package/dist/indexer/code/wal.js.map +1 -0
  214. package/dist/indexer/embeddings.d.ts +113 -0
  215. package/dist/indexer/embeddings.d.ts.map +1 -0
  216. package/dist/indexer/embeddings.js +477 -0
  217. package/dist/indexer/embeddings.js.map +1 -0
  218. package/dist/indexer/git-sync.d.ts +117 -0
  219. package/dist/indexer/git-sync.d.ts.map +1 -0
  220. package/dist/indexer/git-sync.js +398 -0
  221. package/dist/indexer/git-sync.js.map +1 -0
  222. package/dist/indexer/index.d.ts +175 -0
  223. package/dist/indexer/index.d.ts.map +1 -0
  224. package/dist/indexer/index.js +1096 -0
  225. package/dist/indexer/index.js.map +1 -0
  226. package/dist/indexer/mocks/mock-reranker.d.ts +12 -0
  227. package/dist/indexer/mocks/mock-reranker.d.ts.map +1 -0
  228. package/dist/indexer/mocks/mock-reranker.js +26 -0
  229. package/dist/indexer/mocks/mock-reranker.js.map +1 -0
  230. package/dist/indexer/parser.d.ts +8 -0
  231. package/dist/indexer/parser.d.ts.map +1 -0
  232. package/dist/indexer/parser.js +44 -0
  233. package/dist/indexer/parser.js.map +1 -0
  234. package/dist/indexer/parser.test.d.ts +2 -0
  235. package/dist/indexer/parser.test.d.ts.map +1 -0
  236. package/dist/indexer/parser.test.js +197 -0
  237. package/dist/indexer/parser.test.js.map +1 -0
  238. package/dist/indexer/reranking.d.ts +71 -0
  239. package/dist/indexer/reranking.d.ts.map +1 -0
  240. package/dist/indexer/reranking.integration.test.d.ts +2 -0
  241. package/dist/indexer/reranking.integration.test.d.ts.map +1 -0
  242. package/dist/indexer/reranking.integration.test.js +104 -0
  243. package/dist/indexer/reranking.integration.test.js.map +1 -0
  244. package/dist/indexer/reranking.js +256 -0
  245. package/dist/indexer/reranking.js.map +1 -0
  246. package/dist/indexer/reranking.test.d.ts +2 -0
  247. package/dist/indexer/reranking.test.d.ts.map +1 -0
  248. package/dist/indexer/reranking.test.js +130 -0
  249. package/dist/indexer/reranking.test.js.map +1 -0
  250. package/dist/indexer/wal/file-storage.d.ts +60 -0
  251. package/dist/indexer/wal/file-storage.d.ts.map +1 -0
  252. package/dist/indexer/wal/file-storage.js +277 -0
  253. package/dist/indexer/wal/file-storage.js.map +1 -0
  254. package/dist/indexer/wal/file-storage.test.d.ts +8 -0
  255. package/dist/indexer/wal/file-storage.test.d.ts.map +1 -0
  256. package/dist/indexer/wal/file-storage.test.js +444 -0
  257. package/dist/indexer/wal/file-storage.test.js.map +1 -0
  258. package/dist/indexer/wal/index.d.ts +41 -0
  259. package/dist/indexer/wal/index.d.ts.map +1 -0
  260. package/dist/indexer/wal/index.js +61 -0
  261. package/dist/indexer/wal/index.js.map +1 -0
  262. package/dist/indexer/wal/integration.test.d.ts +11 -0
  263. package/dist/indexer/wal/integration.test.d.ts.map +1 -0
  264. package/dist/indexer/wal/integration.test.js +378 -0
  265. package/dist/indexer/wal/integration.test.js.map +1 -0
  266. package/dist/indexer/wal/lancedb-storage.d.ts +72 -0
  267. package/dist/indexer/wal/lancedb-storage.d.ts.map +1 -0
  268. package/dist/indexer/wal/lancedb-storage.js +462 -0
  269. package/dist/indexer/wal/lancedb-storage.js.map +1 -0
  270. package/dist/indexer/wal/lancedb-storage.test.d.ts +8 -0
  271. package/dist/indexer/wal/lancedb-storage.test.d.ts.map +1 -0
  272. package/dist/indexer/wal/lancedb-storage.test.js +415 -0
  273. package/dist/indexer/wal/lancedb-storage.test.js.map +1 -0
  274. package/dist/indexer/wal/sync-wal.d.ts +144 -0
  275. package/dist/indexer/wal/sync-wal.d.ts.map +1 -0
  276. package/dist/indexer/wal/sync-wal.js +863 -0
  277. package/dist/indexer/wal/sync-wal.js.map +1 -0
  278. package/dist/indexer/wal/sync-wal.test.d.ts +8 -0
  279. package/dist/indexer/wal/sync-wal.test.d.ts.map +1 -0
  280. package/dist/indexer/wal/sync-wal.test.js +752 -0
  281. package/dist/indexer/wal/sync-wal.test.js.map +1 -0
  282. package/dist/indexer/wal/types.d.ts +167 -0
  283. package/dist/indexer/wal/types.d.ts.map +1 -0
  284. package/dist/indexer/wal/types.js +12 -0
  285. package/dist/indexer/wal/types.js.map +1 -0
  286. package/dist/indexer/watcher.d.ts +36 -0
  287. package/dist/indexer/watcher.d.ts.map +1 -0
  288. package/dist/indexer/watcher.js +110 -0
  289. package/dist/indexer/watcher.js.map +1 -0
  290. package/dist/search/explore.d.ts +62 -0
  291. package/dist/search/explore.d.ts.map +1 -0
  292. package/dist/search/explore.js +111 -0
  293. package/dist/search/explore.js.map +1 -0
  294. package/dist/search/fts.d.ts +23 -0
  295. package/dist/search/fts.d.ts.map +1 -0
  296. package/dist/search/fts.js +64 -0
  297. package/dist/search/fts.js.map +1 -0
  298. package/dist/search/fts.test.d.ts +2 -0
  299. package/dist/search/fts.test.d.ts.map +1 -0
  300. package/dist/search/fts.test.js +27 -0
  301. package/dist/search/fts.test.js.map +1 -0
  302. package/dist/search/grep.d.ts +75 -0
  303. package/dist/search/grep.d.ts.map +1 -0
  304. package/dist/search/grep.js +96 -0
  305. package/dist/search/grep.js.map +1 -0
  306. package/dist/search/grep.test.d.ts +2 -0
  307. package/dist/search/grep.test.d.ts.map +1 -0
  308. package/dist/search/grep.test.js +178 -0
  309. package/dist/search/grep.test.js.map +1 -0
  310. package/dist/search/hybrid-grep.d.ts +43 -0
  311. package/dist/search/hybrid-grep.d.ts.map +1 -0
  312. package/dist/search/hybrid-grep.js +130 -0
  313. package/dist/search/hybrid-grep.js.map +1 -0
  314. package/dist/search/hybrid-grep.test.d.ts +2 -0
  315. package/dist/search/hybrid-grep.test.d.ts.map +1 -0
  316. package/dist/search/hybrid-grep.test.js +133 -0
  317. package/dist/search/hybrid-grep.test.js.map +1 -0
  318. package/dist/search/rg-executor.d.ts +63 -0
  319. package/dist/search/rg-executor.d.ts.map +1 -0
  320. package/dist/search/rg-executor.js +146 -0
  321. package/dist/search/rg-executor.js.map +1 -0
  322. package/dist/search/rg-executor.test.d.ts +2 -0
  323. package/dist/search/rg-executor.test.d.ts.map +1 -0
  324. package/dist/search/rg-executor.test.js +104 -0
  325. package/dist/search/rg-executor.test.js.map +1 -0
  326. package/dist/search/rg-parser/extractor.d.ts +14 -0
  327. package/dist/search/rg-parser/extractor.d.ts.map +1 -0
  328. package/dist/search/rg-parser/extractor.js +82 -0
  329. package/dist/search/rg-parser/extractor.js.map +1 -0
  330. package/dist/search/rg-parser/extractor.test.d.ts +2 -0
  331. package/dist/search/rg-parser/extractor.test.d.ts.map +1 -0
  332. package/dist/search/rg-parser/extractor.test.js +35 -0
  333. package/dist/search/rg-parser/extractor.test.js.map +1 -0
  334. package/dist/search/rg-parser/fts-builder.d.ts +7 -0
  335. package/dist/search/rg-parser/fts-builder.d.ts.map +1 -0
  336. package/dist/search/rg-parser/fts-builder.js +18 -0
  337. package/dist/search/rg-parser/fts-builder.js.map +1 -0
  338. package/dist/search/rg-parser/fts-builder.test.d.ts +2 -0
  339. package/dist/search/rg-parser/fts-builder.test.d.ts.map +1 -0
  340. package/dist/search/rg-parser/fts-builder.test.js +26 -0
  341. package/dist/search/rg-parser/fts-builder.test.js.map +1 -0
  342. package/dist/search/rg-parser/index.d.ts +36 -0
  343. package/dist/search/rg-parser/index.d.ts.map +1 -0
  344. package/dist/search/rg-parser/index.js +83 -0
  345. package/dist/search/rg-parser/index.js.map +1 -0
  346. package/dist/search/rg-parser/index.test.d.ts +2 -0
  347. package/dist/search/rg-parser/index.test.d.ts.map +1 -0
  348. package/dist/search/rg-parser/index.test.js +34 -0
  349. package/dist/search/rg-parser/index.test.js.map +1 -0
  350. package/dist/search/rg-parser/strategy.d.ts +14 -0
  351. package/dist/search/rg-parser/strategy.d.ts.map +1 -0
  352. package/dist/search/rg-parser/strategy.js +31 -0
  353. package/dist/search/rg-parser/strategy.js.map +1 -0
  354. package/dist/search/rg-parser/strategy.test.d.ts +2 -0
  355. package/dist/search/rg-parser/strategy.test.d.ts.map +1 -0
  356. package/dist/search/rg-parser/strategy.test.js +29 -0
  357. package/dist/search/rg-parser/strategy.test.js.map +1 -0
  358. package/dist/types.d.ts +345 -0
  359. package/dist/types.d.ts.map +1 -0
  360. package/dist/types.js +7 -0
  361. package/dist/types.js.map +1 -0
  362. package/dist/utils/vault.d.ts +84 -0
  363. package/dist/utils/vault.d.ts.map +1 -0
  364. package/dist/utils/vault.js +138 -0
  365. package/dist/utils/vault.js.map +1 -0
  366. package/dist/utils/vault.test.d.ts +2 -0
  367. package/dist/utils/vault.test.d.ts.map +1 -0
  368. package/dist/utils/vault.test.js +153 -0
  369. package/dist/utils/vault.test.js.map +1 -0
  370. package/package.json +69 -0
@@ -0,0 +1,752 @@
1
+ /**
2
+ * SyncWal Tests
3
+ *
4
+ * Tests for SyncWal business logic.
5
+ * Covers: task lifecycle, checkpoint, recovery, lock management, cancellation.
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
+ import * as fs from 'node:fs/promises';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import { SyncWal } from './sync-wal.js';
12
+ import { FileWalStorage } from './file-storage.js';
13
+ describe('SyncWal', () => {
14
+ let tempDir;
15
+ let storage;
16
+ let syncWal;
17
+ beforeEach(async () => {
18
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sync-wal-test-'));
19
+ storage = new FileWalStorage(tempDir);
20
+ // Use small checkpoint interval for testing
21
+ syncWal = new SyncWal(storage, { checkpointInterval: 5 });
22
+ });
23
+ afterEach(async () => {
24
+ // Cleanup
25
+ syncWal.stopHeartbeat();
26
+ await fs.rm(tempDir, { recursive: true, force: true });
27
+ });
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // INITIALIZATION
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+ describe('Initialization', () => {
32
+ it('should initialize with no recovery plan when no previous task', async () => {
33
+ const plan = await syncWal.initialize();
34
+ expect(plan).toBeNull();
35
+ });
36
+ it('should restore LSN from manifest', async () => {
37
+ // Setup manifest with high LSN
38
+ await storage.writeManifest({
39
+ version: 1,
40
+ current: null,
41
+ lastCompleted: null,
42
+ walLsn: 100,
43
+ lastCheckpointLsn: 50,
44
+ });
45
+ await syncWal.initialize();
46
+ // Start a task and check LSN is restored
47
+ const taskId = await syncWal.startTask('notes', 10, 'hash');
48
+ // The new entry should have LSN >= 100
49
+ const entries = await storage.getAllEntries();
50
+ expect(entries[0].lsn).toBeGreaterThanOrEqual(100);
51
+ await syncWal.completeTask(taskId);
52
+ });
53
+ });
54
+ // ═══════════════════════════════════════════════════════════════════════════
55
+ // TASK LIFECYCLE
56
+ // ═══════════════════════════════════════════════════════════════════════════
57
+ describe('Task Lifecycle', () => {
58
+ it('should start a task and acquire lock', async () => {
59
+ const taskId = await syncWal.startTask('notes', 100, 'manifest-hash');
60
+ expect(taskId).toBeTruthy();
61
+ expect(syncWal.isRunning()).toBe(true);
62
+ expect(syncWal.getCurrentTaskId()).toBe(taskId);
63
+ // Lock should be acquired
64
+ const lock = await storage.getLock();
65
+ expect(lock).not.toBeNull();
66
+ expect(lock?.id).toBe(taskId);
67
+ expect(lock?.type).toBe('notes');
68
+ await syncWal.completeTask(taskId);
69
+ });
70
+ it('should write task:start entry to WAL', async () => {
71
+ const taskId = await syncWal.startTask('notes', 100, 'hash123');
72
+ const entries = await storage.getAllEntries();
73
+ expect(entries).toHaveLength(1);
74
+ expect(entries[0].op).toBe('task:start');
75
+ expect(entries[0].taskId).toBe(taskId);
76
+ expect(entries[0].data?.type).toBe('notes');
77
+ expect(entries[0].data?.totalFiles).toBe(100);
78
+ expect(entries[0].data?.manifestHash).toBe('hash123');
79
+ await syncWal.completeTask(taskId);
80
+ });
81
+ it('should create manifest with current task', async () => {
82
+ const taskId = await syncWal.startTask('notes', 100);
83
+ const manifest = await storage.getManifest();
84
+ expect(manifest?.current).not.toBeNull();
85
+ expect(manifest?.current?.id).toBe(taskId);
86
+ expect(manifest?.current?.type).toBe('notes');
87
+ expect(manifest?.current?.status).toBe('running');
88
+ expect(manifest?.current?.totalFiles).toBe(100);
89
+ await syncWal.completeTask(taskId);
90
+ });
91
+ it('should fail to start second task when one is running', async () => {
92
+ const taskId1 = await syncWal.startTask('notes', 100);
93
+ await expect(syncWal.startTask('code', 50)).rejects.toThrow(/another sync is already running/);
94
+ await syncWal.completeTask(taskId1);
95
+ });
96
+ it('should complete task and release lock', async () => {
97
+ const taskId = await syncWal.startTask('notes', 100);
98
+ await syncWal.completeTask(taskId);
99
+ expect(syncWal.isRunning()).toBe(false);
100
+ expect(await storage.getLock()).toBeNull();
101
+ // Manifest should have lastCompleted
102
+ const manifest = await storage.getManifest();
103
+ expect(manifest?.current).toBeNull();
104
+ expect(manifest?.lastCompleted).not.toBeNull();
105
+ expect(manifest?.lastCompleted?.id).toBe(taskId);
106
+ expect(manifest?.lastCompleted?.status).toBe('completed');
107
+ });
108
+ it('should fail task and release lock', async () => {
109
+ const taskId = await syncWal.startTask('notes', 100);
110
+ await syncWal.failTask(taskId, 'Something went wrong');
111
+ expect(syncWal.isRunning()).toBe(false);
112
+ expect(await storage.getLock()).toBeNull();
113
+ const manifest = await storage.getManifest();
114
+ expect(manifest?.lastCompleted?.status).toBe('failed');
115
+ });
116
+ it('should cancel task and release lock', async () => {
117
+ const taskId = await syncWal.startTask('notes', 100);
118
+ await syncWal.cancelTask(taskId);
119
+ expect(syncWal.isRunning()).toBe(false);
120
+ expect(await storage.getLock()).toBeNull();
121
+ const manifest = await storage.getManifest();
122
+ expect(manifest?.lastCompleted?.status).toBe('cancelled');
123
+ });
124
+ it('should throw when completing non-active task', async () => {
125
+ const taskId = await syncWal.startTask('notes', 100);
126
+ await syncWal.completeTask(taskId);
127
+ await expect(syncWal.completeTask(taskId)).rejects.toThrow(/Task not active/);
128
+ });
129
+ });
130
+ // ═══════════════════════════════════════════════════════════════════════════
131
+ // FILE PROCESSING
132
+ // ═══════════════════════════════════════════════════════════════════════════
133
+ describe('File Processing', () => {
134
+ it('should record file:done entry', async () => {
135
+ const taskId = await syncWal.startTask('notes', 10);
136
+ await syncWal.fileProcessed(taskId, '/test.md', 3);
137
+ const entries = await storage.getAllEntries();
138
+ const fileEntry = entries.find((e) => e.op === 'file:done');
139
+ expect(fileEntry).toBeDefined();
140
+ expect(fileEntry?.data?.path).toBe('/test.md');
141
+ expect(fileEntry?.data?.chunks).toBe(3);
142
+ await syncWal.completeTask(taskId);
143
+ });
144
+ it('should track added vs updated files', async () => {
145
+ const taskId = await syncWal.startTask('notes', 10);
146
+ await syncWal.fileProcessed(taskId, '/new.md', 1, 'added');
147
+ await syncWal.fileProcessed(taskId, '/existing.md', 1, 'updated');
148
+ const progress = syncWal.getProgress();
149
+ expect(progress?.added).toBe(1);
150
+ expect(progress?.updated).toBe(1);
151
+ await syncWal.completeTask(taskId);
152
+ });
153
+ it('should record file:fail entry', async () => {
154
+ const taskId = await syncWal.startTask('notes', 10);
155
+ await syncWal.fileFailed(taskId, '/bad.md', 'Parse error', 3);
156
+ const entries = await storage.getAllEntries();
157
+ const failEntry = entries.find((e) => e.op === 'file:fail');
158
+ expect(failEntry).toBeDefined();
159
+ expect(failEntry?.data?.path).toBe('/bad.md');
160
+ expect(failEntry?.data?.error).toBe('Parse error');
161
+ expect(failEntry?.data?.retries).toBe(3);
162
+ const progress = syncWal.getProgress();
163
+ expect(progress?.failedCount).toBe(1);
164
+ await syncWal.completeTask(taskId);
165
+ });
166
+ it('should record deleted files', async () => {
167
+ const taskId = await syncWal.startTask('notes', 10);
168
+ await syncWal.fileDeleted(taskId, '/removed.md');
169
+ const progress = syncWal.getProgress();
170
+ expect(progress?.deleted).toBe(1);
171
+ await syncWal.completeTask(taskId);
172
+ });
173
+ it('should throw when processing file for non-active task', async () => {
174
+ await expect(syncWal.fileProcessed('non-existent', '/test.md', 1)).rejects.toThrow(/Task not active/);
175
+ });
176
+ });
177
+ // ═══════════════════════════════════════════════════════════════════════════
178
+ // CHECKPOINT
179
+ // ═══════════════════════════════════════════════════════════════════════════
180
+ describe('Checkpoint', () => {
181
+ it('should create checkpoint after configured interval', async () => {
182
+ // Using checkpointInterval: 5 from config
183
+ const taskId = await syncWal.startTask('notes', 20);
184
+ // Process 5 files to trigger checkpoint
185
+ for (let i = 0; i < 5; i++) {
186
+ await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
187
+ }
188
+ const checkpoint = await storage.getCheckpoint();
189
+ expect(checkpoint).not.toBeNull();
190
+ expect(checkpoint?.task.processedCount).toBe(5);
191
+ await syncWal.completeTask(taskId);
192
+ });
193
+ it('should truncate WAL after checkpoint', async () => {
194
+ const taskId = await syncWal.startTask('notes', 20);
195
+ // Process files to trigger checkpoint
196
+ for (let i = 0; i < 6; i++) {
197
+ await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
198
+ }
199
+ // WAL should be truncated - only entries from checkpoint onwards
200
+ const entries = await storage.getAllEntries();
201
+ // Should have: checkpoint entry + 1 file:done entry (6th file after checkpoint)
202
+ // WAL is truncated BEFORE checkpoint LSN, so checkpoint entry and later entries remain
203
+ expect(entries.length).toBeLessThan(10);
204
+ await syncWal.completeTask(taskId);
205
+ });
206
+ it('should accumulate stats across checkpoints', async () => {
207
+ const taskId = await syncWal.startTask('notes', 15);
208
+ // First batch (triggers checkpoint)
209
+ for (let i = 0; i < 5; i++) {
210
+ await syncWal.fileProcessed(taskId, `/batch1-${i}.md`, 1, 'added');
211
+ }
212
+ // Second batch (triggers another checkpoint)
213
+ for (let i = 0; i < 5; i++) {
214
+ await syncWal.fileProcessed(taskId, `/batch2-${i}.md`, 1, 'updated');
215
+ }
216
+ const progress = syncWal.getProgress();
217
+ expect(progress?.processedFiles).toBe(10);
218
+ expect(progress?.added).toBe(5);
219
+ expect(progress?.updated).toBe(5);
220
+ await syncWal.completeTask(taskId);
221
+ });
222
+ it('should update manifest on checkpoint', async () => {
223
+ const taskId = await syncWal.startTask('notes', 20);
224
+ for (let i = 0; i < 5; i++) {
225
+ await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
226
+ }
227
+ const manifest = await storage.getManifest();
228
+ expect(manifest?.current?.processedFiles).toBe(5);
229
+ await syncWal.completeTask(taskId);
230
+ });
231
+ });
232
+ // ═══════════════════════════════════════════════════════════════════════════
233
+ // RECOVERY
234
+ // ═══════════════════════════════════════════════════════════════════════════
235
+ describe('Recovery', () => {
236
+ it('should return recovery plan for incomplete task', async () => {
237
+ // Simulate crashed task by writing checkpoint directly
238
+ const now = Date.now();
239
+ await storage.writeCheckpoint({
240
+ version: 1,
241
+ lsn: 100,
242
+ createdAt: now,
243
+ task: {
244
+ id: 'crashed-task',
245
+ type: 'notes',
246
+ status: 'running',
247
+ startedAt: now - 10000,
248
+ totalFiles: 1000,
249
+ processedCount: 500,
250
+ manifestHash: 'hash123',
251
+ },
252
+ stats: {
253
+ added: 200,
254
+ updated: 300,
255
+ deleted: 0,
256
+ failed: [],
257
+ },
258
+ });
259
+ // Add some WAL entries after checkpoint
260
+ await storage.appendEntry({
261
+ lsn: 101,
262
+ ts: now,
263
+ op: 'file:done',
264
+ taskId: 'crashed-task',
265
+ data: { path: '/file501.md', chunks: 1 },
266
+ });
267
+ await storage.appendEntry({
268
+ lsn: 102,
269
+ ts: now,
270
+ op: 'file:done',
271
+ taskId: 'crashed-task',
272
+ data: { path: '/file502.md', chunks: 1 },
273
+ });
274
+ const plan = await syncWal.recover();
275
+ expect(plan).not.toBeNull();
276
+ expect(plan?.taskId).toBe('crashed-task');
277
+ expect(plan?.type).toBe('notes');
278
+ expect(plan?.totalFiles).toBe(1000);
279
+ expect(plan?.skipCount).toBe(502); // 500 from checkpoint + 2 from WAL
280
+ expect(plan?.manifestHash).toBe('hash123');
281
+ expect(plan?.stats.added).toBe(200);
282
+ expect(plan?.stats.updated).toBe(300);
283
+ });
284
+ it('should return null when no incomplete task', async () => {
285
+ // Complete task checkpoint
286
+ await storage.writeCheckpoint({
287
+ version: 1,
288
+ lsn: 100,
289
+ createdAt: Date.now(),
290
+ task: {
291
+ id: 'completed-task',
292
+ type: 'notes',
293
+ status: 'completed',
294
+ startedAt: Date.now(),
295
+ totalFiles: 100,
296
+ processedCount: 100,
297
+ },
298
+ stats: {
299
+ added: 100,
300
+ updated: 0,
301
+ deleted: 0,
302
+ failed: [],
303
+ },
304
+ });
305
+ const plan = await syncWal.recover();
306
+ expect(plan).toBeNull();
307
+ });
308
+ it('should resume task from recovery plan', async () => {
309
+ // Setup recovery scenario
310
+ const now = Date.now();
311
+ await storage.writeCheckpoint({
312
+ version: 1,
313
+ lsn: 50,
314
+ createdAt: now,
315
+ task: {
316
+ id: 'resume-task',
317
+ type: 'code',
318
+ status: 'running',
319
+ startedAt: now - 10000,
320
+ totalFiles: 100,
321
+ processedCount: 25,
322
+ manifestHash: 'hash456',
323
+ },
324
+ stats: {
325
+ added: 10,
326
+ updated: 15,
327
+ deleted: 0,
328
+ failed: [{ path: '/bad.ts', error: 'Syntax error', retries: 3 }],
329
+ },
330
+ });
331
+ const plan = await syncWal.recover();
332
+ expect(plan).not.toBeNull();
333
+ await syncWal.resumeTask(plan);
334
+ expect(syncWal.isRunning()).toBe(true);
335
+ expect(syncWal.getCurrentTaskId()).toBe('resume-task');
336
+ const progress = syncWal.getProgress();
337
+ expect(progress?.type).toBe('code');
338
+ expect(progress?.processedFiles).toBe(25);
339
+ expect(progress?.added).toBe(10);
340
+ expect(progress?.updated).toBe(15);
341
+ expect(progress?.failedCount).toBe(1);
342
+ await syncWal.completeTask('resume-task');
343
+ });
344
+ });
345
+ // ═══════════════════════════════════════════════════════════════════════════
346
+ // LOCK MANAGEMENT
347
+ // ═══════════════════════════════════════════════════════════════════════════
348
+ describe('Lock Management', () => {
349
+ it('should detect no lock', async () => {
350
+ const status = await syncWal.isLocked();
351
+ expect(status.locked).toBe(false);
352
+ expect(status.stale).toBe(false);
353
+ });
354
+ it('should detect active lock', async () => {
355
+ const taskId = await syncWal.startTask('notes', 10);
356
+ const status = await syncWal.isLocked();
357
+ expect(status.locked).toBe(true);
358
+ expect(status.stale).toBe(false);
359
+ expect(status.lock?.id).toBe(taskId);
360
+ await syncWal.completeTask(taskId);
361
+ });
362
+ it('should detect stale lock by heartbeat', async () => {
363
+ // Create lock with old heartbeat
364
+ await storage.acquireLock({
365
+ id: 'old-task',
366
+ type: 'notes',
367
+ pid: process.pid,
368
+ startedAt: Date.now() - 300000,
369
+ heartbeat: Date.now() - 300000, // 5 minutes ago
370
+ });
371
+ // Create SyncWal with short timeout for testing
372
+ const shortTimeoutWal = new SyncWal(storage, { staleLockTimeout: 1000 });
373
+ const status = await shortTimeoutWal.isLocked();
374
+ expect(status.locked).toBe(true);
375
+ expect(status.stale).toBe(true);
376
+ });
377
+ it('should detect stale lock by dead PID', async () => {
378
+ // Create lock with non-existent PID
379
+ await storage.acquireLock({
380
+ id: 'dead-task',
381
+ type: 'notes',
382
+ pid: 999999999, // Very high, unlikely to exist
383
+ startedAt: Date.now(),
384
+ heartbeat: Date.now(),
385
+ });
386
+ const status = await syncWal.isLocked();
387
+ expect(status.locked).toBe(true);
388
+ expect(status.stale).toBe(true);
389
+ });
390
+ it('should force clear stale lock', async () => {
391
+ // Create stale lock
392
+ await storage.acquireLock({
393
+ id: 'stale-task',
394
+ type: 'notes',
395
+ pid: 999999999,
396
+ startedAt: Date.now(),
397
+ heartbeat: Date.now() - 300000,
398
+ });
399
+ const shortTimeoutWal = new SyncWal(storage, { staleLockTimeout: 1000 });
400
+ const cleared = await shortTimeoutWal.forceClearStaleLock();
401
+ expect(cleared).toBe(true);
402
+ expect(await storage.getLock()).toBeNull();
403
+ });
404
+ it('should not force clear active lock', async () => {
405
+ const taskId = await syncWal.startTask('notes', 10);
406
+ const cleared = await syncWal.forceClearStaleLock();
407
+ expect(cleared).toBe(false);
408
+ expect((await storage.getLock())?.id).toBe(taskId);
409
+ await syncWal.completeTask(taskId);
410
+ });
411
+ it('should acquire lock by clearing stale lock', async () => {
412
+ // Create stale lock
413
+ await storage.acquireLock({
414
+ id: 'stale-task',
415
+ type: 'notes',
416
+ pid: 999999999,
417
+ startedAt: Date.now(),
418
+ heartbeat: Date.now() - 300000,
419
+ });
420
+ const shortTimeoutWal = new SyncWal(storage, { staleLockTimeout: 1000 });
421
+ const taskId = await shortTimeoutWal.startTask('notes', 10);
422
+ expect(taskId).toBeTruthy();
423
+ expect((await storage.getLock())?.id).toBe(taskId);
424
+ await shortTimeoutWal.completeTask(taskId);
425
+ });
426
+ });
427
+ // ═══════════════════════════════════════════════════════════════════════════
428
+ // CANCELLATION
429
+ // ═══════════════════════════════════════════════════════════════════════════
430
+ describe('Cancellation', () => {
431
+ it('should request cancellation', async () => {
432
+ const taskId = await syncWal.startTask('notes', 10);
433
+ await syncWal.requestCancel(taskId);
434
+ expect(syncWal.isCancelled(taskId)).toBe(true);
435
+ await syncWal.cancelTask(taskId);
436
+ });
437
+ it('should not mark non-requested task as cancelled', async () => {
438
+ const taskId = await syncWal.startTask('notes', 10);
439
+ expect(syncWal.isCancelled(taskId)).toBe(false);
440
+ expect(syncWal.isCancelled('other-task')).toBe(false);
441
+ await syncWal.completeTask(taskId);
442
+ });
443
+ });
444
+ // ═══════════════════════════════════════════════════════════════════════════
445
+ // PROGRESS
446
+ // ═══════════════════════════════════════════════════════════════════════════
447
+ describe('Progress', () => {
448
+ it('should return null when no task running', () => {
449
+ const progress = syncWal.getProgress();
450
+ expect(progress).toBeNull();
451
+ });
452
+ it('should return progress for running task', async () => {
453
+ const taskId = await syncWal.startTask('notes', 100, 'hash');
454
+ await syncWal.fileProcessed(taskId, '/a.md', 1, 'added');
455
+ await syncWal.fileProcessed(taskId, '/b.md', 1, 'updated');
456
+ await syncWal.fileFailed(taskId, '/c.md', 'error', 0);
457
+ const progress = syncWal.getProgress();
458
+ expect(progress).not.toBeNull();
459
+ expect(progress?.taskId).toBe(taskId);
460
+ expect(progress?.type).toBe('notes');
461
+ expect(progress?.status).toBe('running');
462
+ expect(progress?.phase).toBe('indexing');
463
+ expect(progress?.totalFiles).toBe(100);
464
+ expect(progress?.processedFiles).toBe(3);
465
+ expect(progress?.added).toBe(1);
466
+ expect(progress?.updated).toBe(1);
467
+ expect(progress?.failedCount).toBe(1);
468
+ expect(progress?.elapsedMs).toBeGreaterThan(0);
469
+ await syncWal.completeTask(taskId);
470
+ });
471
+ it('should estimate remaining time', async () => {
472
+ const taskId = await syncWal.startTask('notes', 100);
473
+ // Process some files with a small delay
474
+ for (let i = 0; i < 10; i++) {
475
+ await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
476
+ await new Promise((r) => setTimeout(r, 5)); // Small delay
477
+ }
478
+ const progress = syncWal.getProgress();
479
+ expect(progress?.estimatedRemainingMs).toBeDefined();
480
+ expect(progress?.estimatedRemainingMs).toBeGreaterThan(0);
481
+ await syncWal.completeTask(taskId);
482
+ });
483
+ it('should emit progress via callback', async () => {
484
+ const progressUpdates = [];
485
+ syncWal.onProgress((p) => progressUpdates.push(p));
486
+ const taskId = await syncWal.startTask('notes', 10);
487
+ // Should have initial progress
488
+ expect(progressUpdates.length).toBeGreaterThan(0);
489
+ await syncWal.completeTask(taskId);
490
+ // Should have final progress with phase 'done'
491
+ const lastProgress = progressUpdates[progressUpdates.length - 1];
492
+ expect(lastProgress.phase).toBe('done');
493
+ });
494
+ it('should update phase', async () => {
495
+ const taskId = await syncWal.startTask('notes', 10);
496
+ syncWal.setPhase('deleting');
497
+ expect(syncWal.getProgress()?.phase).toBe('deleting');
498
+ syncWal.setPhase('analyzing');
499
+ expect(syncWal.getProgress()?.phase).toBe('analyzing');
500
+ await syncWal.completeTask(taskId);
501
+ });
502
+ });
503
+ // ═══════════════════════════════════════════════════════════════════════════
504
+ // STATE QUERIES
505
+ // ═══════════════════════════════════════════════════════════════════════════
506
+ describe('State Queries', () => {
507
+ it('should get manifest', async () => {
508
+ const taskId = await syncWal.startTask('notes', 10);
509
+ const manifest = await syncWal.getManifest();
510
+ expect(manifest).not.toBeNull();
511
+ expect(manifest?.current?.id).toBe(taskId);
512
+ await syncWal.completeTask(taskId);
513
+ });
514
+ it('should get current task', async () => {
515
+ const taskId = await syncWal.startTask('notes', 10);
516
+ const task = await syncWal.getCurrentTask();
517
+ expect(task).not.toBeNull();
518
+ expect(task?.id).toBe(taskId);
519
+ await syncWal.completeTask(taskId);
520
+ expect(await syncWal.getCurrentTask()).toBeNull();
521
+ });
522
+ it('should get last completed task', async () => {
523
+ const taskId = await syncWal.startTask('notes', 10);
524
+ await syncWal.completeTask(taskId);
525
+ const lastCompleted = await syncWal.getLastCompleted();
526
+ expect(lastCompleted).not.toBeNull();
527
+ expect(lastCompleted?.id).toBe(taskId);
528
+ expect(lastCompleted?.status).toBe('completed');
529
+ });
530
+ });
531
+ // ═══════════════════════════════════════════════════════════════════════════
532
+ // HEARTBEAT
533
+ // ═══════════════════════════════════════════════════════════════════════════
534
+ describe('Heartbeat', () => {
535
+ it('should update heartbeat periodically', async () => {
536
+ // Use very short heartbeat for testing
537
+ const fastHeartbeatWal = new SyncWal(storage, {
538
+ heartbeatInterval: 50,
539
+ checkpointInterval: 100,
540
+ });
541
+ const taskId = await fastHeartbeatWal.startTask('notes', 10);
542
+ const initialLock = await storage.getLock();
543
+ const initialHeartbeat = initialLock.heartbeat;
544
+ // Wait for heartbeat update
545
+ await new Promise((r) => setTimeout(r, 100));
546
+ const updatedLock = await storage.getLock();
547
+ expect(updatedLock.heartbeat).toBeGreaterThan(initialHeartbeat);
548
+ fastHeartbeatWal.stopHeartbeat();
549
+ await fastHeartbeatWal.completeTask(taskId);
550
+ });
551
+ it('should stop heartbeat on task complete', async () => {
552
+ const taskId = await syncWal.startTask('notes', 10);
553
+ await syncWal.completeTask(taskId);
554
+ // Heartbeat should be stopped (no errors when lock is released)
555
+ await new Promise((r) => setTimeout(r, 50));
556
+ });
557
+ });
558
+ // ═══════════════════════════════════════════════════════════════════════════
559
+ // GRACEFUL SHUTDOWN
560
+ // ═══════════════════════════════════════════════════════════════════════════
561
+ describe('Graceful Shutdown', () => {
562
+ it('should checkpoint and release lock on shutdown', async () => {
563
+ const taskId = await syncWal.startTask('notes', 20);
564
+ // Process some files
565
+ for (let i = 0; i < 3; i++) {
566
+ await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
567
+ }
568
+ await syncWal.shutdown();
569
+ // Lock should be released
570
+ expect(await storage.getLock()).toBeNull();
571
+ // Checkpoint should have latest state
572
+ const checkpoint = await storage.getCheckpoint();
573
+ expect(checkpoint?.task.processedCount).toBe(3);
574
+ });
575
+ it('should handle shutdown with no active task', async () => {
576
+ await expect(syncWal.shutdown()).resolves.not.toThrow();
577
+ });
578
+ });
579
+ // ═══════════════════════════════════════════════════════════════════════════
580
+ // CLEAR WAL STATE
581
+ // ═══════════════════════════════════════════════════════════════════════════
582
+ describe('Clear WAL State', () => {
583
+ it('should clear all WAL state and reset to initial', async () => {
584
+ // Start a task and process some files
585
+ const taskId = await syncWal.startTask('notes', 10);
586
+ await syncWal.fileProcessed(taskId, '/test1.md', 1);
587
+ await syncWal.fileProcessed(taskId, '/test2.md', 1);
588
+ // Verify state exists before clear
589
+ expect(await storage.getLock()).not.toBeNull();
590
+ expect(syncWal.isRunning()).toBe(true);
591
+ // Clear all state
592
+ await syncWal.clear();
593
+ // Verify everything is cleared
594
+ expect(await storage.getLock()).toBeNull();
595
+ expect(await storage.getCheckpoint()).toBeNull();
596
+ expect(await storage.getManifest()).toBeNull();
597
+ expect(syncWal.isRunning()).toBe(false);
598
+ expect(syncWal.getCurrentTaskId()).toBeNull();
599
+ // Should be able to start fresh
600
+ const newTaskId = await syncWal.startTask('notes', 5);
601
+ expect(newTaskId).toBeDefined();
602
+ await syncWal.completeTask(newTaskId);
603
+ });
604
+ it('should clear recovery state when stuck in recovery loop', async () => {
605
+ // Simulate stuck recovery state: start task, checkpoint, then simulate crash
606
+ const taskId = await syncWal.startTask('notes', 100);
607
+ for (let i = 0; i < 10; i++) {
608
+ await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
609
+ }
610
+ await syncWal.checkpoint();
611
+ // Simulate crash by creating new SyncWal without completing task
612
+ const crashedWal = new SyncWal(storage);
613
+ const recoveryPlan = await crashedWal.recover();
614
+ expect(recoveryPlan).not.toBeNull();
615
+ expect(recoveryPlan?.taskId).toBe(taskId);
616
+ // Clear should remove recovery state
617
+ await crashedWal.clear();
618
+ // No more recovery needed
619
+ const noRecovery = await crashedWal.recover();
620
+ expect(noRecovery).toBeNull();
621
+ });
622
+ it('should handle clear with no active state', async () => {
623
+ // Clear on fresh instance should not throw
624
+ await expect(syncWal.clear()).resolves.not.toThrow();
625
+ });
626
+ });
627
+ });
628
+ // ═══════════════════════════════════════════════════════════════════════════
629
+ // ERROR HANDLING TESTS
630
+ // ═══════════════════════════════════════════════════════════════════════════
631
+ describe('Error Handling', () => {
632
+ let tempDir;
633
+ let storage;
634
+ beforeEach(async () => {
635
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sync-wal-err-'));
636
+ storage = new FileWalStorage(tempDir);
637
+ });
638
+ afterEach(async () => {
639
+ await fs.rm(tempDir, { recursive: true, force: true });
640
+ });
641
+ it('should release lock if startTask fails after acquiring lock', async () => {
642
+ // Create mock storage that fails on appendEntry
643
+ const failingStorage = {
644
+ acquireLock: vi.fn(async () => true),
645
+ releaseLock: vi.fn(async () => { }),
646
+ getLock: vi.fn(async () => null),
647
+ updateHeartbeat: vi.fn(async () => { }),
648
+ appendEntry: vi.fn(async () => {
649
+ throw new Error('WAL write failed');
650
+ }),
651
+ getEntriesFromLsn: vi.fn(async () => []),
652
+ truncateBeforeLsn: vi.fn(async () => { }),
653
+ writeCheckpoint: vi.fn(async () => { }),
654
+ getCheckpoint: vi.fn(async () => null),
655
+ writeManifest: vi.fn(async () => { }),
656
+ getManifest: vi.fn(async () => null),
657
+ clear: vi.fn(async () => { }),
658
+ };
659
+ const wal = new SyncWal(failingStorage);
660
+ // startTask should fail and release the lock
661
+ await expect(wal.startTask('notes', 10)).rejects.toThrow('WAL write failed');
662
+ // Lock should have been released
663
+ expect(failingStorage.releaseLock).toHaveBeenCalled();
664
+ // SyncWal state should be clean
665
+ expect(wal.isRunning()).toBe(false);
666
+ expect(wal.getCurrentTaskId()).toBeNull();
667
+ wal.stopHeartbeat();
668
+ });
669
+ it('should release lock if manifest write fails', async () => {
670
+ // Create mock storage that fails on writeManifest
671
+ const failingStorage = {
672
+ acquireLock: vi.fn(async () => true),
673
+ releaseLock: vi.fn(async () => { }),
674
+ getLock: vi.fn(async () => null),
675
+ updateHeartbeat: vi.fn(async () => { }),
676
+ appendEntry: vi.fn(async () => { }),
677
+ getEntriesFromLsn: vi.fn(async () => []),
678
+ truncateBeforeLsn: vi.fn(async () => { }),
679
+ writeCheckpoint: vi.fn(async () => { }),
680
+ getCheckpoint: vi.fn(async () => null),
681
+ writeManifest: vi.fn(async () => {
682
+ throw new Error('Manifest write failed');
683
+ }),
684
+ getManifest: vi.fn(async () => null),
685
+ clear: vi.fn(async () => { }),
686
+ };
687
+ const wal = new SyncWal(failingStorage);
688
+ await expect(wal.startTask('notes', 10)).rejects.toThrow('Manifest write failed');
689
+ expect(failingStorage.releaseLock).toHaveBeenCalled();
690
+ expect(wal.isRunning()).toBe(false);
691
+ wal.stopHeartbeat();
692
+ });
693
+ });
694
+ // ═══════════════════════════════════════════════════════════════════════════
695
+ // MOCK STORAGE TESTS
696
+ // ═══════════════════════════════════════════════════════════════════════════
697
+ describe('SyncWal with Mock Storage', () => {
698
+ it('should work with any WalStorage implementation', async () => {
699
+ // Simple in-memory mock
700
+ const mockEntries = [];
701
+ let mockLock = null;
702
+ let mockCheckpoint = null;
703
+ let mockManifest = null;
704
+ const mockStorage = {
705
+ acquireLock: vi.fn(async (lock) => {
706
+ if (mockLock)
707
+ return false;
708
+ mockLock = lock;
709
+ return true;
710
+ }),
711
+ releaseLock: vi.fn(async (id) => {
712
+ if (mockLock?.id === id)
713
+ mockLock = null;
714
+ }),
715
+ getLock: vi.fn(async () => mockLock),
716
+ updateHeartbeat: vi.fn(async () => { }),
717
+ appendEntry: vi.fn(async (entry) => {
718
+ mockEntries.push(entry);
719
+ }),
720
+ getEntriesFromLsn: vi.fn(async (lsn) => mockEntries.filter((e) => e.lsn >= lsn)),
721
+ truncateBeforeLsn: vi.fn(async (lsn) => {
722
+ const kept = mockEntries.filter((e) => e.lsn >= lsn);
723
+ mockEntries.length = 0;
724
+ mockEntries.push(...kept);
725
+ }),
726
+ writeCheckpoint: vi.fn(async (data) => {
727
+ mockCheckpoint = data;
728
+ }),
729
+ getCheckpoint: vi.fn(async () => mockCheckpoint),
730
+ writeManifest: vi.fn(async (data) => {
731
+ mockManifest = data;
732
+ }),
733
+ getManifest: vi.fn(async () => mockManifest),
734
+ clear: vi.fn(async () => {
735
+ mockEntries.length = 0;
736
+ mockLock = null;
737
+ mockCheckpoint = null;
738
+ mockManifest = null;
739
+ }),
740
+ };
741
+ const wal = new SyncWal(mockStorage, { checkpointInterval: 100 });
742
+ const taskId = await wal.startTask('notes', 50);
743
+ expect(mockStorage.acquireLock).toHaveBeenCalled();
744
+ expect(mockStorage.appendEntry).toHaveBeenCalled();
745
+ expect(mockStorage.writeManifest).toHaveBeenCalled();
746
+ await wal.fileProcessed(taskId, '/test.md', 1);
747
+ await wal.completeTask(taskId);
748
+ expect(mockStorage.releaseLock).toHaveBeenCalled();
749
+ wal.stopHeartbeat();
750
+ });
751
+ });
752
+ //# sourceMappingURL=sync-wal.test.js.map