@camaradesuk/git-worktree-tools 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/README.md +48 -27
  2. package/dist/cli/cleanpr.js +74 -53
  3. package/dist/cli/cleanpr.js.map +1 -1
  4. package/dist/cli/cleanpr.test.js +2 -0
  5. package/dist/cli/cleanpr.test.js.map +1 -1
  6. package/dist/cli/lswt.js +32 -56
  7. package/dist/cli/lswt.js.map +1 -1
  8. package/dist/cli/lswt.test.js +17 -27
  9. package/dist/cli/lswt.test.js.map +1 -1
  10. package/dist/cli/newpr.d.ts +13 -1
  11. package/dist/cli/newpr.d.ts.map +1 -1
  12. package/dist/cli/newpr.js +350 -151
  13. package/dist/cli/newpr.js.map +1 -1
  14. package/dist/cli/newpr.test.js +314 -5
  15. package/dist/cli/newpr.test.js.map +1 -1
  16. package/dist/cli/prs.d.ts +3 -10
  17. package/dist/cli/prs.d.ts.map +1 -1
  18. package/dist/cli/prs.js +6 -168
  19. package/dist/cli/prs.js.map +1 -1
  20. package/dist/cli/prs.test.js +55 -0
  21. package/dist/cli/prs.test.js.map +1 -1
  22. package/dist/cli/wt/clean.d.ts +6 -2
  23. package/dist/cli/wt/clean.d.ts.map +1 -1
  24. package/dist/cli/wt/clean.js +401 -20
  25. package/dist/cli/wt/clean.js.map +1 -1
  26. package/dist/cli/wt/clean.test.d.ts +8 -0
  27. package/dist/cli/wt/clean.test.d.ts.map +1 -0
  28. package/dist/cli/wt/clean.test.js +624 -0
  29. package/dist/cli/wt/clean.test.js.map +1 -0
  30. package/dist/cli/wt/completion.d.ts +3 -0
  31. package/dist/cli/wt/completion.d.ts.map +1 -1
  32. package/dist/cli/wt/completion.js +80 -9
  33. package/dist/cli/wt/completion.js.map +1 -1
  34. package/dist/cli/wt/completion.test.js +102 -0
  35. package/dist/cli/wt/completion.test.js.map +1 -1
  36. package/dist/cli/wt/config.d.ts +3 -1
  37. package/dist/cli/wt/config.d.ts.map +1 -1
  38. package/dist/cli/wt/config.js +323 -32
  39. package/dist/cli/wt/config.js.map +1 -1
  40. package/dist/cli/wt/config.test.d.ts +2 -0
  41. package/dist/cli/wt/config.test.d.ts.map +1 -1
  42. package/dist/cli/wt/config.test.js +206 -26
  43. package/dist/cli/wt/config.test.js.map +1 -1
  44. package/dist/cli/wt/interactive-menu.d.ts +2 -0
  45. package/dist/cli/wt/interactive-menu.d.ts.map +1 -1
  46. package/dist/cli/wt/interactive-menu.js +346 -73
  47. package/dist/cli/wt/interactive-menu.js.map +1 -1
  48. package/dist/cli/wt/interactive-menu.test.d.ts +4 -2
  49. package/dist/cli/wt/interactive-menu.test.d.ts.map +1 -1
  50. package/dist/cli/wt/interactive-menu.test.js +383 -323
  51. package/dist/cli/wt/interactive-menu.test.js.map +1 -1
  52. package/dist/cli/wt/link.d.ts +3 -1
  53. package/dist/cli/wt/link.d.ts.map +1 -1
  54. package/dist/cli/wt/link.js +125 -38
  55. package/dist/cli/wt/link.js.map +1 -1
  56. package/dist/cli/wt/list.d.ts +4 -1
  57. package/dist/cli/wt/list.d.ts.map +1 -1
  58. package/dist/cli/wt/list.js +85 -16
  59. package/dist/cli/wt/list.js.map +1 -1
  60. package/dist/cli/wt/list.test.d.ts +10 -0
  61. package/dist/cli/wt/list.test.d.ts.map +1 -0
  62. package/dist/cli/wt/list.test.js +157 -0
  63. package/dist/cli/wt/list.test.js.map +1 -0
  64. package/dist/cli/wt/new.d.ts +8 -2
  65. package/dist/cli/wt/new.d.ts.map +1 -1
  66. package/dist/cli/wt/new.js +91 -46
  67. package/dist/cli/wt/new.js.map +1 -1
  68. package/dist/cli/wt/prs.d.ts +2 -1
  69. package/dist/cli/wt/prs.d.ts.map +1 -1
  70. package/dist/cli/wt/prs.js +3 -164
  71. package/dist/cli/wt/prs.js.map +1 -1
  72. package/dist/cli/wt/run-command.d.ts +4 -2
  73. package/dist/cli/wt/run-command.d.ts.map +1 -1
  74. package/dist/cli/wt/run-command.js +6 -4
  75. package/dist/cli/wt/run-command.js.map +1 -1
  76. package/dist/cli/wt/state.d.ts +3 -1
  77. package/dist/cli/wt/state.d.ts.map +1 -1
  78. package/dist/cli/wt/state.js +74 -10
  79. package/dist/cli/wt/state.js.map +1 -1
  80. package/dist/cli/wt/state.test.d.ts +9 -0
  81. package/dist/cli/wt/state.test.d.ts.map +1 -0
  82. package/dist/cli/wt/state.test.js +127 -0
  83. package/dist/cli/wt/state.test.js.map +1 -0
  84. package/dist/cli/wt/wt.test.d.ts +2 -2
  85. package/dist/cli/wt/wt.test.js +430 -212
  86. package/dist/cli/wt/wt.test.js.map +1 -1
  87. package/dist/cli/wt.d.ts.map +1 -1
  88. package/dist/cli/wt.js +50 -36
  89. package/dist/cli/wt.js.map +1 -1
  90. package/dist/cli/wt.unit.test.js +16 -38
  91. package/dist/cli/wt.unit.test.js.map +1 -1
  92. package/dist/cli/wtconfig.d.ts +1 -0
  93. package/dist/cli/wtconfig.d.ts.map +1 -1
  94. package/dist/cli/wtconfig.js +213 -21
  95. package/dist/cli/wtconfig.js.map +1 -1
  96. package/dist/cli/wtconfig.test.js +3 -0
  97. package/dist/cli/wtconfig.test.js.map +1 -1
  98. package/dist/cli/wtlink.js +116 -73
  99. package/dist/cli/wtlink.js.map +1 -1
  100. package/dist/cli/wtstate.js +21 -2
  101. package/dist/cli/wtstate.js.map +1 -1
  102. package/dist/e2e/wt/interactive-menu.e2e.test.js +17 -17
  103. package/dist/e2e/wt/interactive-menu.e2e.test.js.map +1 -1
  104. package/dist/lib/ai/types.d.ts +12 -0
  105. package/dist/lib/ai/types.d.ts.map +1 -1
  106. package/dist/lib/ai/types.js.map +1 -1
  107. package/dist/lib/cleanpr/args.d.ts.map +1 -1
  108. package/dist/lib/cleanpr/args.js +20 -0
  109. package/dist/lib/cleanpr/args.js.map +1 -1
  110. package/dist/lib/cleanpr/types.d.ts +6 -0
  111. package/dist/lib/cleanpr/types.d.ts.map +1 -1
  112. package/dist/lib/cleanpr/worktree-info.d.ts.map +1 -1
  113. package/dist/lib/cleanpr/worktree-info.js +1 -6
  114. package/dist/lib/cleanpr/worktree-info.js.map +1 -1
  115. package/dist/lib/cleanpr/worktree-info.test.js +10 -13
  116. package/dist/lib/cleanpr/worktree-info.test.js.map +1 -1
  117. package/dist/lib/colors.d.ts +5 -0
  118. package/dist/lib/colors.d.ts.map +1 -1
  119. package/dist/lib/colors.js +13 -6
  120. package/dist/lib/colors.js.map +1 -1
  121. package/dist/lib/config-editor.d.ts.map +1 -1
  122. package/dist/lib/config-editor.js.map +1 -1
  123. package/dist/lib/config-migration/detector.d.ts +25 -0
  124. package/dist/lib/config-migration/detector.d.ts.map +1 -0
  125. package/dist/lib/config-migration/detector.js +372 -0
  126. package/dist/lib/config-migration/detector.js.map +1 -0
  127. package/dist/lib/config-migration/detector.test.d.ts +5 -0
  128. package/dist/lib/config-migration/detector.test.d.ts.map +1 -0
  129. package/dist/lib/config-migration/detector.test.js +201 -0
  130. package/dist/lib/config-migration/detector.test.js.map +1 -0
  131. package/dist/lib/config-migration/index.d.ts +29 -0
  132. package/dist/lib/config-migration/index.d.ts.map +1 -0
  133. package/dist/lib/config-migration/index.js +33 -0
  134. package/dist/lib/config-migration/index.js.map +1 -0
  135. package/dist/lib/config-migration/reporter.d.ts +53 -0
  136. package/dist/lib/config-migration/reporter.d.ts.map +1 -0
  137. package/dist/lib/config-migration/reporter.js +257 -0
  138. package/dist/lib/config-migration/reporter.js.map +1 -0
  139. package/dist/lib/config-migration/reporter.test.d.ts +5 -0
  140. package/dist/lib/config-migration/reporter.test.d.ts.map +1 -0
  141. package/dist/lib/config-migration/reporter.test.js +305 -0
  142. package/dist/lib/config-migration/reporter.test.js.map +1 -0
  143. package/dist/lib/config-migration/runner.d.ts +46 -0
  144. package/dist/lib/config-migration/runner.d.ts.map +1 -0
  145. package/dist/lib/config-migration/runner.js +364 -0
  146. package/dist/lib/config-migration/runner.js.map +1 -0
  147. package/dist/lib/config-migration/runner.test.d.ts +5 -0
  148. package/dist/lib/config-migration/runner.test.d.ts.map +1 -0
  149. package/dist/lib/config-migration/runner.test.js +235 -0
  150. package/dist/lib/config-migration/runner.test.js.map +1 -0
  151. package/dist/lib/config-migration/types.d.ts +120 -0
  152. package/dist/lib/config-migration/types.d.ts.map +1 -0
  153. package/dist/lib/config-migration/types.js +70 -0
  154. package/dist/lib/config-migration/types.js.map +1 -0
  155. package/dist/lib/config-validation.d.ts.map +1 -1
  156. package/dist/lib/config-validation.js +6 -0
  157. package/dist/lib/config-validation.js.map +1 -1
  158. package/dist/lib/config-validation.test.js +25 -0
  159. package/dist/lib/config-validation.test.js.map +1 -1
  160. package/dist/lib/config.d.ts +31 -7
  161. package/dist/lib/config.d.ts.map +1 -1
  162. package/dist/lib/config.js +2 -0
  163. package/dist/lib/config.js.map +1 -1
  164. package/dist/lib/config.test.js +3 -15
  165. package/dist/lib/config.test.js.map +1 -1
  166. package/dist/lib/constants.d.ts +12 -4
  167. package/dist/lib/constants.d.ts.map +1 -1
  168. package/dist/lib/constants.js +24 -5
  169. package/dist/lib/constants.js.map +1 -1
  170. package/dist/lib/constants.test.js +88 -29
  171. package/dist/lib/constants.test.js.map +1 -1
  172. package/dist/lib/deprecation.d.ts +18 -0
  173. package/dist/lib/deprecation.d.ts.map +1 -0
  174. package/dist/lib/deprecation.js +28 -0
  175. package/dist/lib/deprecation.js.map +1 -0
  176. package/dist/lib/deprecation.test.d.ts +2 -0
  177. package/dist/lib/deprecation.test.d.ts.map +1 -0
  178. package/dist/lib/deprecation.test.js +71 -0
  179. package/dist/lib/deprecation.test.js.map +1 -0
  180. package/dist/lib/hooks/confirmation.d.ts +49 -0
  181. package/dist/lib/hooks/confirmation.d.ts.map +1 -0
  182. package/dist/lib/hooks/confirmation.js +147 -0
  183. package/dist/lib/hooks/confirmation.js.map +1 -0
  184. package/dist/lib/hooks/confirmation.test.d.ts +7 -0
  185. package/dist/lib/hooks/confirmation.test.d.ts.map +1 -0
  186. package/dist/lib/hooks/confirmation.test.js +300 -0
  187. package/dist/lib/hooks/confirmation.test.js.map +1 -0
  188. package/dist/lib/hooks/executor.d.ts +16 -1
  189. package/dist/lib/hooks/executor.d.ts.map +1 -1
  190. package/dist/lib/hooks/executor.js +53 -4
  191. package/dist/lib/hooks/executor.js.map +1 -1
  192. package/dist/lib/hooks/index.d.ts +4 -2
  193. package/dist/lib/hooks/index.d.ts.map +1 -1
  194. package/dist/lib/hooks/index.js +3 -2
  195. package/dist/lib/hooks/index.js.map +1 -1
  196. package/dist/lib/hooks/types.d.ts +16 -0
  197. package/dist/lib/hooks/types.d.ts.map +1 -1
  198. package/dist/lib/hooks/types.js +12 -0
  199. package/dist/lib/hooks/types.js.map +1 -1
  200. package/dist/lib/logger.d.ts +40 -155
  201. package/dist/lib/logger.d.ts.map +1 -1
  202. package/dist/lib/logger.js +349 -420
  203. package/dist/lib/logger.js.map +1 -1
  204. package/dist/lib/logger.test.d.ts +10 -1
  205. package/dist/lib/logger.test.d.ts.map +1 -1
  206. package/dist/lib/logger.test.js +658 -258
  207. package/dist/lib/logger.test.js.map +1 -1
  208. package/dist/lib/lswt/action-executors.d.ts +2 -0
  209. package/dist/lib/lswt/action-executors.d.ts.map +1 -1
  210. package/dist/lib/lswt/action-executors.js +4 -3
  211. package/dist/lib/lswt/action-executors.js.map +1 -1
  212. package/dist/lib/lswt/action-executors.test.js +7 -0
  213. package/dist/lib/lswt/action-executors.test.js.map +1 -1
  214. package/dist/lib/lswt/args.d.ts.map +1 -1
  215. package/dist/lib/lswt/args.js +15 -1
  216. package/dist/lib/lswt/args.js.map +1 -1
  217. package/dist/lib/lswt/environment.d.ts +21 -2
  218. package/dist/lib/lswt/environment.d.ts.map +1 -1
  219. package/dist/lib/lswt/environment.js +73 -32
  220. package/dist/lib/lswt/environment.js.map +1 -1
  221. package/dist/lib/lswt/environment.test.js +79 -1
  222. package/dist/lib/lswt/environment.test.js.map +1 -1
  223. package/dist/lib/lswt/index.d.ts +1 -0
  224. package/dist/lib/lswt/index.d.ts.map +1 -1
  225. package/dist/lib/lswt/index.js +2 -0
  226. package/dist/lib/lswt/index.js.map +1 -1
  227. package/dist/lib/lswt/table.d.ts +15 -0
  228. package/dist/lib/lswt/table.d.ts.map +1 -0
  229. package/dist/lib/lswt/table.js +61 -0
  230. package/dist/lib/lswt/table.js.map +1 -0
  231. package/dist/lib/lswt/table.test.d.ts +5 -0
  232. package/dist/lib/lswt/table.test.d.ts.map +1 -0
  233. package/dist/lib/lswt/table.test.js +262 -0
  234. package/dist/lib/lswt/table.test.js.map +1 -0
  235. package/dist/lib/lswt/types.d.ts +4 -0
  236. package/dist/lib/lswt/types.d.ts.map +1 -1
  237. package/dist/lib/lswt/worktree-info.d.ts.map +1 -1
  238. package/dist/lib/lswt/worktree-info.js +1 -6
  239. package/dist/lib/lswt/worktree-info.js.map +1 -1
  240. package/dist/lib/lswt/worktree-info.test.js +5 -17
  241. package/dist/lib/lswt/worktree-info.test.js.map +1 -1
  242. package/dist/lib/newpr/args.d.ts.map +1 -1
  243. package/dist/lib/newpr/args.js +36 -1
  244. package/dist/lib/newpr/args.js.map +1 -1
  245. package/dist/lib/newpr/hook-runner.d.ts +11 -0
  246. package/dist/lib/newpr/hook-runner.d.ts.map +1 -1
  247. package/dist/lib/newpr/hook-runner.js +49 -1
  248. package/dist/lib/newpr/hook-runner.js.map +1 -1
  249. package/dist/lib/newpr/hook-runner.test.js +121 -0
  250. package/dist/lib/newpr/hook-runner.test.js.map +1 -1
  251. package/dist/lib/newpr/plan-generator.d.ts +121 -0
  252. package/dist/lib/newpr/plan-generator.d.ts.map +1 -0
  253. package/dist/lib/newpr/plan-generator.js +185 -0
  254. package/dist/lib/newpr/plan-generator.js.map +1 -0
  255. package/dist/lib/newpr/plan-generator.test.d.ts +7 -0
  256. package/dist/lib/newpr/plan-generator.test.d.ts.map +1 -0
  257. package/dist/lib/newpr/plan-generator.test.js +387 -0
  258. package/dist/lib/newpr/plan-generator.test.js.map +1 -0
  259. package/dist/lib/newpr/types.d.ts +12 -0
  260. package/dist/lib/newpr/types.d.ts.map +1 -1
  261. package/dist/lib/prs/actions.d.ts +5 -1
  262. package/dist/lib/prs/actions.d.ts.map +1 -1
  263. package/dist/lib/prs/actions.js +12 -10
  264. package/dist/lib/prs/actions.js.map +1 -1
  265. package/dist/lib/prs/actions.test.js +48 -5
  266. package/dist/lib/prs/actions.test.js.map +1 -1
  267. package/dist/lib/prs/command.d.ts +21 -0
  268. package/dist/lib/prs/command.d.ts.map +1 -0
  269. package/dist/lib/prs/command.js +175 -0
  270. package/dist/lib/prs/command.js.map +1 -0
  271. package/dist/lib/prs/command.test.d.ts +11 -0
  272. package/dist/lib/prs/command.test.d.ts.map +1 -0
  273. package/dist/lib/prs/command.test.js +409 -0
  274. package/dist/lib/prs/command.test.js.map +1 -0
  275. package/dist/lib/prs/interactive.d.ts.map +1 -1
  276. package/dist/lib/prs/interactive.js +15 -2
  277. package/dist/lib/prs/interactive.js.map +1 -1
  278. package/dist/lib/prs/interactive.test.js +153 -0
  279. package/dist/lib/prs/interactive.test.js.map +1 -1
  280. package/dist/lib/prs/types.d.ts +15 -0
  281. package/dist/lib/prs/types.d.ts.map +1 -1
  282. package/dist/lib/ui/error.d.ts +31 -0
  283. package/dist/lib/ui/error.d.ts.map +1 -0
  284. package/dist/lib/ui/error.js +47 -0
  285. package/dist/lib/ui/error.js.map +1 -0
  286. package/dist/lib/ui/error.test.d.ts +2 -0
  287. package/dist/lib/ui/error.test.d.ts.map +1 -0
  288. package/dist/lib/ui/error.test.js +143 -0
  289. package/dist/lib/ui/error.test.js.map +1 -0
  290. package/dist/lib/ui/index.d.ts +15 -0
  291. package/dist/lib/ui/index.d.ts.map +1 -0
  292. package/dist/lib/ui/index.js +19 -0
  293. package/dist/lib/ui/index.js.map +1 -0
  294. package/dist/lib/ui/output.d.ts +18 -0
  295. package/dist/lib/ui/output.d.ts.map +1 -0
  296. package/dist/lib/ui/output.js +31 -0
  297. package/dist/lib/ui/output.js.map +1 -0
  298. package/dist/lib/ui/output.test.d.ts +2 -0
  299. package/dist/lib/ui/output.test.d.ts.map +1 -0
  300. package/dist/lib/ui/output.test.js +59 -0
  301. package/dist/lib/ui/output.test.js.map +1 -0
  302. package/dist/lib/ui/spinner.d.ts +10 -0
  303. package/dist/lib/ui/spinner.d.ts.map +1 -0
  304. package/dist/lib/ui/spinner.js +10 -0
  305. package/dist/lib/ui/spinner.js.map +1 -0
  306. package/dist/lib/ui/status.d.ts +65 -0
  307. package/dist/lib/ui/status.d.ts.map +1 -0
  308. package/dist/lib/ui/status.js +100 -0
  309. package/dist/lib/ui/status.js.map +1 -0
  310. package/dist/lib/ui/status.test.d.ts +2 -0
  311. package/dist/lib/ui/status.test.d.ts.map +1 -0
  312. package/dist/lib/ui/status.test.js +158 -0
  313. package/dist/lib/ui/status.test.js.map +1 -0
  314. package/dist/lib/ui/table.d.ts +39 -0
  315. package/dist/lib/ui/table.d.ts.map +1 -0
  316. package/dist/lib/ui/table.js +45 -0
  317. package/dist/lib/ui/table.js.map +1 -0
  318. package/dist/lib/ui/table.test.d.ts +2 -0
  319. package/dist/lib/ui/table.test.d.ts.map +1 -0
  320. package/dist/lib/ui/table.test.js +115 -0
  321. package/dist/lib/ui/table.test.js.map +1 -0
  322. package/dist/lib/ui/theme.d.ts +34 -0
  323. package/dist/lib/ui/theme.d.ts.map +1 -0
  324. package/dist/lib/ui/theme.js +37 -0
  325. package/dist/lib/ui/theme.js.map +1 -0
  326. package/dist/lib/ui/theme.test.d.ts +2 -0
  327. package/dist/lib/ui/theme.test.d.ts.map +1 -0
  328. package/dist/lib/ui/theme.test.js +76 -0
  329. package/dist/lib/ui/theme.test.js.map +1 -0
  330. package/dist/lib/wtconfig/environment.d.ts +18 -1
  331. package/dist/lib/wtconfig/environment.d.ts.map +1 -1
  332. package/dist/lib/wtconfig/environment.js +60 -24
  333. package/dist/lib/wtconfig/environment.js.map +1 -1
  334. package/dist/lib/wtconfig/environment.test.js +45 -1
  335. package/dist/lib/wtconfig/environment.test.js.map +1 -1
  336. package/dist/lib/wtlink/config-manifest.test.js +26 -0
  337. package/dist/lib/wtlink/config-manifest.test.js.map +1 -1
  338. package/dist/lib/wtlink/link-configs.js +7 -7
  339. package/dist/lib/wtlink/link-configs.js.map +1 -1
  340. package/dist/lib/wtlink/validate-manifest.d.ts.map +1 -1
  341. package/dist/lib/wtlink/validate-manifest.js +5 -5
  342. package/dist/lib/wtlink/validate-manifest.js.map +1 -1
  343. package/dist/lib/wtstate/args.d.ts.map +1 -1
  344. package/dist/lib/wtstate/args.js +2 -0
  345. package/dist/lib/wtstate/args.js.map +1 -1
  346. package/dist/mcp/server.d.ts +2 -1
  347. package/dist/mcp/server.d.ts.map +1 -1
  348. package/dist/mcp/server.js +264 -44
  349. package/dist/mcp/server.js.map +1 -1
  350. package/dist/mcp/server.test.js +111 -0
  351. package/dist/mcp/server.test.js.map +1 -1
  352. package/package.json +3 -1
  353. package/schemas/worktreerc.schema.json +23 -0
@@ -0,0 +1,624 @@
1
+ /**
2
+ * Tests for wt clean command handler
3
+ *
4
+ * Exercises the handler through different code paths to cover internal
5
+ * helper functions: outputJsonResult, outputJsonError, resultToCleanedInfo, printWorktree.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ // Mock all dependencies before imports
9
+ vi.mock('child_process', () => ({
10
+ execSync: vi.fn(),
11
+ }));
12
+ vi.mock('../../lib/git.js', () => ({
13
+ getRepoRoot: vi.fn().mockReturnValue('/fake/repo'),
14
+ getMainWorktreeRoot: vi.fn().mockReturnValue('/fake/repo'),
15
+ removeWorktree: vi.fn(),
16
+ pruneWorktrees: vi.fn(),
17
+ }));
18
+ vi.mock('../../lib/github.js', () => ({
19
+ isGhInstalled: vi.fn().mockReturnValue(true),
20
+ }));
21
+ vi.mock('../../lib/prompts.js', () => ({
22
+ withSpinner: vi.fn((_msg, fn) => fn()),
23
+ promptChoice: vi.fn(),
24
+ promptConfirm: vi.fn(),
25
+ }));
26
+ vi.mock('../../lib/config.js', () => ({
27
+ loadConfig: vi.fn().mockReturnValue({
28
+ worktreePattern: '{repo}.pr{number}',
29
+ baseBranch: 'main',
30
+ }),
31
+ }));
32
+ vi.mock('../../lib/logger.js', () => ({
33
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
34
+ }));
35
+ vi.mock('../../lib/colors.js', () => ({
36
+ error: vi.fn((s) => s),
37
+ dim: vi.fn((s) => s),
38
+ success: vi.fn((s) => s),
39
+ info: vi.fn((s) => s),
40
+ cyan: vi.fn((s) => s),
41
+ yellow: vi.fn((s) => s),
42
+ red: vi.fn((s) => s),
43
+ green: vi.fn((s) => s),
44
+ bold: vi.fn((s) => s),
45
+ warning: vi.fn((s) => s),
46
+ }));
47
+ vi.mock('../../lib/cleanpr/index.js', () => ({
48
+ gatherPrWorktreeInfo: vi.fn().mockResolvedValue([]),
49
+ createDefaultDeps: vi.fn().mockReturnValue({}),
50
+ groupWorktreesByState: vi.fn().mockReturnValue({ merged: [], closed: [], open: [], unknown: [] }),
51
+ getCleanableWorktrees: vi.fn().mockReturnValue([]),
52
+ findWorktreeByPrNumber: vi.fn().mockReturnValue(null),
53
+ cleanWorktree: vi.fn().mockReturnValue({
54
+ success: true,
55
+ prNumber: 42,
56
+ message: 'Cleaned PR #42',
57
+ localBranchDeleted: true,
58
+ remoteBranchDeleted: false,
59
+ }),
60
+ summarizeResults: vi.fn().mockReturnValue({ cleaned: 0, total: 0, failed: 0 }),
61
+ }));
62
+ vi.mock('../../lib/ui/index.js', () => ({
63
+ setJsonMode: vi.fn(),
64
+ isJsonMode: vi.fn().mockReturnValue(false),
65
+ printStatus: vi.fn(),
66
+ printDim: vi.fn(),
67
+ printError: vi.fn(),
68
+ printHeader: vi.fn(),
69
+ printNextSteps: vi.fn(),
70
+ changeIndicator: vi.fn().mockReturnValue(''),
71
+ errorToDisplay: vi.fn().mockReturnValue({ title: 'error' }),
72
+ }));
73
+ vi.mock('../../lib/json-output.js', async (importOriginal) => {
74
+ const actual = await importOriginal();
75
+ return {
76
+ ...actual,
77
+ createSuccessResult: vi.fn().mockReturnValue({ success: true }),
78
+ createErrorResult: vi.fn().mockReturnValue({ success: false }),
79
+ formatJsonResult: vi.fn().mockReturnValue('{}'),
80
+ };
81
+ });
82
+ // Import the command under test
83
+ import { cleanCommand } from './clean.js';
84
+ // Import mocked modules for assertions and setup
85
+ import * as git from '../../lib/git.js';
86
+ import * as github from '../../lib/github.js';
87
+ import { gatherPrWorktreeInfo, getCleanableWorktrees, findWorktreeByPrNumber, cleanWorktree, summarizeResults, groupWorktreesByState, } from '../../lib/cleanpr/index.js';
88
+ import { setJsonMode, printError, printStatus, printDim, printNextSteps, changeIndicator, } from '../../lib/ui/index.js';
89
+ import { createSuccessResult, createErrorResult, formatJsonResult, ErrorCode, } from '../../lib/json-output.js';
90
+ // Mock process.exit - throws to halt execution (mimics real exit behavior)
91
+ class ExitError extends Error {
92
+ code;
93
+ constructor(code) {
94
+ super(`process.exit(${code})`);
95
+ this.code = code;
96
+ }
97
+ }
98
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation((code) => {
99
+ throw new ExitError(code);
100
+ });
101
+ // Capture console.log output for JSON assertions
102
+ const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => { });
103
+ function makeWorktree(overrides = {}) {
104
+ return {
105
+ path: '/fake/repo.pr42',
106
+ branch: 'feat/thing',
107
+ commit: 'abc1234',
108
+ prNumber: 42,
109
+ prState: 'MERGED',
110
+ hasChanges: false,
111
+ ...overrides,
112
+ };
113
+ }
114
+ function makeCleanupResult(overrides = {}) {
115
+ return {
116
+ success: true,
117
+ prNumber: 42,
118
+ message: 'Cleaned PR #42',
119
+ localBranchDeleted: true,
120
+ remoteBranchDeleted: false,
121
+ ...overrides,
122
+ };
123
+ }
124
+ describe('wt clean handler', () => {
125
+ beforeEach(() => {
126
+ vi.clearAllMocks();
127
+ // Restore default mock return values
128
+ vi.mocked(git.getRepoRoot).mockReturnValue('/fake/repo');
129
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
130
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([]);
131
+ vi.mocked(getCleanableWorktrees).mockReturnValue([]);
132
+ vi.mocked(findWorktreeByPrNumber).mockReturnValue(undefined);
133
+ vi.mocked(changeIndicator).mockReturnValue('');
134
+ });
135
+ afterEach(() => {
136
+ mockExit.mockClear();
137
+ mockConsoleLog.mockClear();
138
+ });
139
+ // =========================================================================
140
+ // Error paths with JSON output
141
+ // =========================================================================
142
+ describe('prerequisite errors with --json', () => {
143
+ it('outputs JSON error when gh is not installed', async () => {
144
+ vi.mocked(github.isGhInstalled).mockReturnValue(false);
145
+ await expect(cleanCommand.handler({
146
+ all: true,
147
+ 'dry-run': false,
148
+ force: false,
149
+ json: true,
150
+ })).rejects.toThrow(ExitError);
151
+ expect(setJsonMode).toHaveBeenCalledWith(true);
152
+ expect(createErrorResult).toHaveBeenCalledWith('cleanpr', ErrorCode.GH_NOT_INSTALLED, expect.stringContaining('GitHub CLI'));
153
+ expect(formatJsonResult).toHaveBeenCalled();
154
+ expect(mockConsoleLog).toHaveBeenCalledWith('{}');
155
+ expect(mockExit).toHaveBeenCalledWith(1);
156
+ });
157
+ it('outputs JSON error when not in a git repo', async () => {
158
+ vi.mocked(git.getRepoRoot).mockReturnValue(null);
159
+ await expect(cleanCommand.handler({
160
+ all: true,
161
+ 'dry-run': false,
162
+ force: false,
163
+ json: true,
164
+ })).rejects.toThrow(ExitError);
165
+ expect(createErrorResult).toHaveBeenCalledWith('cleanpr', ErrorCode.NOT_GIT_REPO, expect.stringContaining('git repository'));
166
+ expect(formatJsonResult).toHaveBeenCalled();
167
+ expect(mockExit).toHaveBeenCalledWith(1);
168
+ });
169
+ });
170
+ // =========================================================================
171
+ // --all --json: no worktrees found
172
+ // =========================================================================
173
+ describe('--all --json with no worktrees', () => {
174
+ it('outputs JSON success with zero cleaned message', async () => {
175
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([]);
176
+ vi.mocked(getCleanableWorktrees).mockReturnValue([]);
177
+ await cleanCommand.handler({
178
+ all: true,
179
+ 'dry-run': false,
180
+ force: false,
181
+ json: true,
182
+ });
183
+ // cleanAll path: no cleanable worktrees, json mode -> outputs success JSON directly
184
+ expect(createSuccessResult).toHaveBeenCalledWith('cleanpr', expect.objectContaining({
185
+ totalCleaned: 0,
186
+ totalSkipped: 0,
187
+ message: expect.stringContaining('No merged or closed'),
188
+ }));
189
+ expect(formatJsonResult).toHaveBeenCalled();
190
+ expect(mockConsoleLog).toHaveBeenCalledWith('{}');
191
+ });
192
+ });
193
+ // =========================================================================
194
+ // --all --json: cleanable worktrees found (exercises outputJsonResult non-dry-run)
195
+ // =========================================================================
196
+ describe('--all --json with cleanable worktrees', () => {
197
+ it('outputs JSON result with cleaned worktrees', async () => {
198
+ const wt1 = makeWorktree({
199
+ prNumber: 10,
200
+ branch: 'feat/a',
201
+ path: '/fake/repo.pr10',
202
+ prState: 'MERGED',
203
+ });
204
+ const wt2 = makeWorktree({
205
+ prNumber: 20,
206
+ branch: 'feat/b',
207
+ path: '/fake/repo.pr20',
208
+ prState: 'CLOSED',
209
+ });
210
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt1, wt2]);
211
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt1, wt2]);
212
+ vi.mocked(cleanWorktree)
213
+ .mockReturnValueOnce(makeCleanupResult({ prNumber: 10, message: 'Cleaned PR #10' }))
214
+ .mockReturnValueOnce(makeCleanupResult({ prNumber: 20, message: 'Cleaned PR #20' }));
215
+ await cleanCommand.handler({
216
+ all: true,
217
+ 'dry-run': false,
218
+ force: false,
219
+ json: true,
220
+ });
221
+ // outputJsonResult called for non-dry-run path
222
+ expect(cleanWorktree).toHaveBeenCalledTimes(2);
223
+ expect(createSuccessResult).toHaveBeenCalledWith('cleanpr', expect.objectContaining({
224
+ totalCleaned: 2,
225
+ totalSkipped: 0,
226
+ }));
227
+ // printStatus should NOT be called in json mode
228
+ expect(printStatus).not.toHaveBeenCalled();
229
+ });
230
+ it('includes skipped entries for failed cleanups', async () => {
231
+ const wt1 = makeWorktree({ prNumber: 10, path: '/fake/repo.pr10' });
232
+ const wt2 = makeWorktree({ prNumber: 20, path: '/fake/repo.pr20' });
233
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt1, wt2]);
234
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt1, wt2]);
235
+ vi.mocked(cleanWorktree)
236
+ .mockReturnValueOnce(makeCleanupResult({ prNumber: 10, success: true }))
237
+ .mockReturnValueOnce(makeCleanupResult({ prNumber: 20, success: false, message: 'Has uncommitted changes' }));
238
+ await cleanCommand.handler({
239
+ all: true,
240
+ 'dry-run': false,
241
+ force: false,
242
+ json: true,
243
+ });
244
+ expect(createSuccessResult).toHaveBeenCalledWith('cleanpr', expect.objectContaining({
245
+ totalCleaned: 1,
246
+ totalSkipped: 1,
247
+ }));
248
+ });
249
+ });
250
+ // =========================================================================
251
+ // --all --json --dry-run: exercises outputJsonResult dry-run branch
252
+ // =========================================================================
253
+ describe('--all --json --dry-run with cleanable worktrees', () => {
254
+ it('outputs dry-run JSON with wouldClean entries', async () => {
255
+ const wt1 = makeWorktree({
256
+ prNumber: 10,
257
+ branch: 'feat/a',
258
+ path: '/fake/repo.pr10',
259
+ prState: 'MERGED',
260
+ });
261
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt1]);
262
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt1]);
263
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 10, success: true, dryRun: true }));
264
+ await cleanCommand.handler({
265
+ all: true,
266
+ 'dry-run': true,
267
+ force: false,
268
+ json: true,
269
+ });
270
+ expect(createSuccessResult).toHaveBeenCalledWith('cleanpr', expect.objectContaining({
271
+ wouldClean: expect.arrayContaining([
272
+ expect.objectContaining({ prNumber: 10, branch: 'feat/a' }),
273
+ ]),
274
+ totalWouldClean: 1,
275
+ message: expect.stringContaining('Would clean 1 PR worktree'),
276
+ }));
277
+ });
278
+ it('outputs dry-run JSON with zero wouldClean when all fail', async () => {
279
+ const wt1 = makeWorktree({ prNumber: 10, path: '/fake/repo.pr10' });
280
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt1]);
281
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt1]);
282
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 10, success: false, message: 'Failed' }));
283
+ await cleanCommand.handler({
284
+ all: true,
285
+ 'dry-run': true,
286
+ force: false,
287
+ json: true,
288
+ });
289
+ expect(createSuccessResult).toHaveBeenCalledWith('cleanpr', expect.objectContaining({
290
+ wouldClean: [],
291
+ totalWouldClean: 0,
292
+ message: expect.stringContaining('No PR worktrees would be cleaned'),
293
+ }));
294
+ });
295
+ });
296
+ // =========================================================================
297
+ // Specific PR number path (cleanSpecific)
298
+ // =========================================================================
299
+ describe('specific PR number', () => {
300
+ it('cleans specific PR and outputs JSON result', async () => {
301
+ const wt = makeWorktree({ prNumber: 42, branch: 'feat/thing', path: '/fake/repo.pr42' });
302
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt]);
303
+ vi.mocked(findWorktreeByPrNumber).mockReturnValue(wt);
304
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 42 }));
305
+ await cleanCommand.handler({
306
+ prNumber: 42,
307
+ all: false,
308
+ 'dry-run': false,
309
+ force: false,
310
+ json: true,
311
+ });
312
+ expect(findWorktreeByPrNumber).toHaveBeenCalled();
313
+ expect(cleanWorktree).toHaveBeenCalledTimes(1);
314
+ // JSON output via outputJsonResult
315
+ expect(createSuccessResult).toHaveBeenCalledWith('cleanpr', expect.objectContaining({
316
+ totalCleaned: 1,
317
+ }));
318
+ });
319
+ it('outputs JSON error when specific PR not found', async () => {
320
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([]);
321
+ vi.mocked(findWorktreeByPrNumber).mockReturnValue(undefined);
322
+ await expect(cleanCommand.handler({
323
+ prNumber: 999,
324
+ all: false,
325
+ 'dry-run': false,
326
+ force: false,
327
+ json: true,
328
+ })).rejects.toThrow(ExitError);
329
+ // outputJsonError path
330
+ expect(createErrorResult).toHaveBeenCalledWith('cleanpr', ErrorCode.PR_NOT_FOUND, expect.stringContaining('No worktree found for PR #999'));
331
+ expect(mockExit).toHaveBeenCalledWith(1);
332
+ });
333
+ it('prints error display when specific PR not found without --json', async () => {
334
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([]);
335
+ vi.mocked(findWorktreeByPrNumber).mockReturnValue(undefined);
336
+ await expect(cleanCommand.handler({
337
+ prNumber: 999,
338
+ all: false,
339
+ 'dry-run': false,
340
+ force: false,
341
+ json: false,
342
+ })).rejects.toThrow(ExitError);
343
+ expect(printError).toHaveBeenCalledWith(expect.objectContaining({
344
+ title: expect.stringContaining('No worktree found for PR #999'),
345
+ }));
346
+ expect(mockExit).toHaveBeenCalledWith(1);
347
+ });
348
+ it('prints success status for non-json specific PR cleanup', async () => {
349
+ const wt = makeWorktree({ prNumber: 42 });
350
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt]);
351
+ vi.mocked(findWorktreeByPrNumber).mockReturnValue(wt);
352
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 42, success: true }));
353
+ await cleanCommand.handler({
354
+ prNumber: 42,
355
+ all: false,
356
+ 'dry-run': false,
357
+ force: false,
358
+ json: false,
359
+ });
360
+ expect(printStatus).toHaveBeenCalledWith('info', expect.stringContaining('Cleaning PR #42'));
361
+ expect(printDim).toHaveBeenCalled();
362
+ expect(printStatus).toHaveBeenCalledWith('success', expect.stringContaining('PR #42 worktree cleaned up successfully'));
363
+ expect(printNextSteps).toHaveBeenCalled();
364
+ });
365
+ it('prints dry-run info status for specific PR', async () => {
366
+ const wt = makeWorktree({ prNumber: 42 });
367
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt]);
368
+ vi.mocked(findWorktreeByPrNumber).mockReturnValue(wt);
369
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 42, success: true, message: 'Would clean PR #42' }));
370
+ await cleanCommand.handler({
371
+ prNumber: 42,
372
+ all: false,
373
+ 'dry-run': true,
374
+ force: false,
375
+ json: false,
376
+ });
377
+ expect(printStatus).toHaveBeenCalledWith('info', expect.stringContaining('Would clean'));
378
+ });
379
+ it('prints warning and exits 1 on failed specific PR cleanup', async () => {
380
+ const wt = makeWorktree({ prNumber: 42 });
381
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt]);
382
+ vi.mocked(findWorktreeByPrNumber).mockReturnValue(wt);
383
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 42, success: false, message: 'Has uncommitted changes' }));
384
+ await expect(cleanCommand.handler({
385
+ prNumber: 42,
386
+ all: false,
387
+ 'dry-run': false,
388
+ force: false,
389
+ json: false,
390
+ })).rejects.toThrow(ExitError);
391
+ expect(printStatus).toHaveBeenCalledWith('warning', 'Has uncommitted changes');
392
+ expect(mockExit).toHaveBeenCalledWith(1);
393
+ });
394
+ });
395
+ // =========================================================================
396
+ // --all without --json: text output paths
397
+ // =========================================================================
398
+ describe('--all without --json (text output)', () => {
399
+ it('prints info when no cleanable worktrees found', async () => {
400
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([]);
401
+ vi.mocked(getCleanableWorktrees).mockReturnValue([]);
402
+ await cleanCommand.handler({
403
+ all: true,
404
+ 'dry-run': false,
405
+ force: false,
406
+ json: false,
407
+ });
408
+ expect(printStatus).toHaveBeenCalledWith('info', expect.stringContaining('No merged or closed'));
409
+ });
410
+ it('prints success/warning per worktree and summary', async () => {
411
+ const wt1 = makeWorktree({ prNumber: 10 });
412
+ const wt2 = makeWorktree({ prNumber: 20 });
413
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt1, wt2]);
414
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt1, wt2]);
415
+ vi.mocked(cleanWorktree)
416
+ .mockReturnValueOnce(makeCleanupResult({ prNumber: 10, success: true, message: 'Cleaned #10' }))
417
+ .mockReturnValueOnce(makeCleanupResult({ prNumber: 20, success: false, message: 'Failed #20' }));
418
+ vi.mocked(summarizeResults).mockReturnValue({ cleaned: 1, total: 2, failed: 1 });
419
+ await cleanCommand.handler({
420
+ all: true,
421
+ 'dry-run': false,
422
+ force: false,
423
+ json: false,
424
+ });
425
+ expect(printStatus).toHaveBeenCalledWith('success', 'Cleaned #10');
426
+ expect(printStatus).toHaveBeenCalledWith('warning', 'Failed #20');
427
+ expect(printStatus).toHaveBeenCalledWith('success', expect.stringContaining('Cleaned 1 of 2'));
428
+ });
429
+ it('prints dry-run summary for text output', async () => {
430
+ const wt1 = makeWorktree({ prNumber: 10 });
431
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt1]);
432
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt1]);
433
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 10, success: true, message: 'Would clean' }));
434
+ vi.mocked(summarizeResults).mockReturnValue({ cleaned: 1, total: 1, failed: 0 });
435
+ await cleanCommand.handler({
436
+ all: true,
437
+ 'dry-run': true,
438
+ force: false,
439
+ json: false,
440
+ });
441
+ expect(printStatus).toHaveBeenCalledWith('info', expect.stringContaining('Would clean 1 of 1'));
442
+ });
443
+ it('shows next steps when worktrees were cleaned', async () => {
444
+ const wt1 = makeWorktree({ prNumber: 10 });
445
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt1]);
446
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt1]);
447
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult({ prNumber: 10, success: true }));
448
+ vi.mocked(summarizeResults).mockReturnValue({ cleaned: 1, total: 1, failed: 0 });
449
+ await cleanCommand.handler({
450
+ all: true,
451
+ 'dry-run': false,
452
+ force: false,
453
+ json: false,
454
+ });
455
+ expect(printNextSteps).toHaveBeenCalled();
456
+ });
457
+ });
458
+ // =========================================================================
459
+ // Interactive mode with --json (not supported)
460
+ // =========================================================================
461
+ describe('interactive mode with --json', () => {
462
+ it('outputs JSON error for interactive + json combination', async () => {
463
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([]);
464
+ await expect(cleanCommand.handler({
465
+ all: false,
466
+ 'dry-run': false,
467
+ force: false,
468
+ json: true,
469
+ // prNumber is undefined -> interactive mode
470
+ })).rejects.toThrow(ExitError);
471
+ expect(createErrorResult).toHaveBeenCalledWith('cleanpr', ErrorCode.INVALID_ARGUMENT, expect.stringContaining('Interactive mode not supported'));
472
+ expect(mockExit).toHaveBeenCalledWith(1);
473
+ });
474
+ });
475
+ // =========================================================================
476
+ // printWorktree coverage (via interactiveClean path)
477
+ // =========================================================================
478
+ describe('interactive mode (non-json)', () => {
479
+ it('prints grouped worktrees and shows no-cleanable message', async () => {
480
+ const openWt = makeWorktree({ prNumber: 10, prState: 'OPEN', branch: 'feat/open' });
481
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([openWt]);
482
+ vi.mocked(groupWorktreesByState).mockReturnValue({
483
+ merged: [],
484
+ closed: [],
485
+ open: [openWt],
486
+ unknown: [],
487
+ });
488
+ vi.mocked(getCleanableWorktrees).mockReturnValue([]);
489
+ await cleanCommand.handler({
490
+ all: false,
491
+ 'dry-run': false,
492
+ force: false,
493
+ json: false,
494
+ });
495
+ // printWorktree is called for open worktrees
496
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #10: feat/open'));
497
+ expect(printStatus).toHaveBeenCalledWith('info', expect.stringContaining('No merged or closed PRs to clean'));
498
+ });
499
+ it('prints all four groups when all states present', async () => {
500
+ const merged = makeWorktree({ prNumber: 1, prState: 'MERGED', branch: 'feat/merged' });
501
+ const closed = makeWorktree({ prNumber: 2, prState: 'CLOSED', branch: 'feat/closed' });
502
+ const open = makeWorktree({ prNumber: 3, prState: 'OPEN', branch: 'feat/open' });
503
+ const unknown = makeWorktree({ prNumber: 4, prState: 'UNKNOWN', branch: 'feat/unknown' });
504
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([merged, closed, open, unknown]);
505
+ vi.mocked(groupWorktreesByState).mockReturnValue({
506
+ merged: [merged],
507
+ closed: [closed],
508
+ open: [open],
509
+ unknown: [unknown],
510
+ });
511
+ vi.mocked(getCleanableWorktrees).mockReturnValue([]);
512
+ await cleanCommand.handler({
513
+ all: false,
514
+ 'dry-run': false,
515
+ force: false,
516
+ json: false,
517
+ });
518
+ // Each group header is printed
519
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Merged (1)'));
520
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Closed (1)'));
521
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Open (1)'));
522
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Unknown (1)'));
523
+ // printWorktree called for each
524
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #1: feat/merged'));
525
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #2: feat/closed'));
526
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #3: feat/open'));
527
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #4: feat/unknown'));
528
+ });
529
+ it('prints "No PR worktrees found" when list is empty', async () => {
530
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([]);
531
+ await cleanCommand.handler({
532
+ all: false,
533
+ 'dry-run': false,
534
+ force: false,
535
+ json: false,
536
+ });
537
+ expect(printStatus).toHaveBeenCalledWith('info', 'No PR worktrees found.');
538
+ });
539
+ });
540
+ // =========================================================================
541
+ // printWorktree with changes indicator
542
+ // =========================================================================
543
+ describe('printWorktree with changes', () => {
544
+ it('includes change indicator in output', async () => {
545
+ const wt = makeWorktree({ prNumber: 55, branch: 'feat/dirty', hasChanges: true });
546
+ vi.mocked(changeIndicator).mockReturnValue(' [modified]');
547
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt]);
548
+ vi.mocked(groupWorktreesByState).mockReturnValue({
549
+ merged: [wt],
550
+ closed: [],
551
+ open: [],
552
+ unknown: [],
553
+ });
554
+ vi.mocked(getCleanableWorktrees).mockReturnValue([]);
555
+ await cleanCommand.handler({
556
+ all: false,
557
+ 'dry-run': false,
558
+ force: false,
559
+ json: false,
560
+ });
561
+ expect(changeIndicator).toHaveBeenCalledWith(true);
562
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #55: feat/dirty [modified]'));
563
+ });
564
+ });
565
+ // =========================================================================
566
+ // Options mapping
567
+ // =========================================================================
568
+ describe('options mapping', () => {
569
+ it('maps --delete-remote to options.deleteRemote', async () => {
570
+ const wt = makeWorktree({ prNumber: 42 });
571
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt]);
572
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt]);
573
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult());
574
+ await cleanCommand.handler({
575
+ all: true,
576
+ 'dry-run': false,
577
+ force: false,
578
+ 'delete-remote': true,
579
+ json: false,
580
+ });
581
+ expect(cleanWorktree).toHaveBeenCalledWith(wt, expect.objectContaining({ deleteRemote: true }), expect.any(Object));
582
+ });
583
+ it('maps --force to options.force', async () => {
584
+ const wt = makeWorktree({ prNumber: 42 });
585
+ vi.mocked(gatherPrWorktreeInfo).mockResolvedValue([wt]);
586
+ vi.mocked(getCleanableWorktrees).mockReturnValue([wt]);
587
+ vi.mocked(cleanWorktree).mockReturnValue(makeCleanupResult());
588
+ await cleanCommand.handler({
589
+ all: true,
590
+ 'dry-run': false,
591
+ force: true,
592
+ json: false,
593
+ });
594
+ expect(cleanWorktree).toHaveBeenCalledWith(wt, expect.objectContaining({ force: true }), expect.any(Object));
595
+ });
596
+ });
597
+ // =========================================================================
598
+ // JSON mode skips spinner
599
+ // =========================================================================
600
+ describe('spinner behavior', () => {
601
+ it('skips spinner in json mode and calls gatherPrWorktreeInfo directly', async () => {
602
+ const { withSpinner } = await import('../../lib/prompts.js');
603
+ await cleanCommand.handler({
604
+ all: true,
605
+ 'dry-run': false,
606
+ force: false,
607
+ json: true,
608
+ });
609
+ expect(withSpinner).not.toHaveBeenCalled();
610
+ expect(gatherPrWorktreeInfo).toHaveBeenCalled();
611
+ });
612
+ it('uses spinner in non-json mode', async () => {
613
+ const { withSpinner } = await import('../../lib/prompts.js');
614
+ await cleanCommand.handler({
615
+ all: true,
616
+ 'dry-run': false,
617
+ force: false,
618
+ json: false,
619
+ });
620
+ expect(withSpinner).toHaveBeenCalledWith('Scanning worktrees...', expect.any(Function));
621
+ });
622
+ });
623
+ });
624
+ //# sourceMappingURL=clean.test.js.map