@camaradesuk/git-worktree-tools 1.10.0 → 1.12.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 (431) hide show
  1. package/README.md +4 -4
  2. package/dist/lib/config.d.ts +2 -1
  3. package/dist/lib/config.d.ts.map +1 -1
  4. package/dist/lib/config.js +11 -0
  5. package/dist/lib/config.js.map +1 -1
  6. package/package.json +12 -3
  7. package/schemas/worktreerc.schema.json +1 -1
  8. package/dist/api/list.test.d.ts +0 -5
  9. package/dist/api/list.test.d.ts.map +0 -1
  10. package/dist/api/list.test.js +0 -390
  11. package/dist/api/list.test.js.map +0 -1
  12. package/dist/cli/cleanpr.test.d.ts +0 -2
  13. package/dist/cli/cleanpr.test.d.ts.map +0 -1
  14. package/dist/cli/cleanpr.test.js +0 -954
  15. package/dist/cli/cleanpr.test.js.map +0 -1
  16. package/dist/cli/lswt.test.d.ts +0 -2
  17. package/dist/cli/lswt.test.d.ts.map +0 -1
  18. package/dist/cli/lswt.test.js +0 -376
  19. package/dist/cli/lswt.test.js.map +0 -1
  20. package/dist/cli/newpr.test.d.ts +0 -2
  21. package/dist/cli/newpr.test.d.ts.map +0 -1
  22. package/dist/cli/newpr.test.js +0 -1182
  23. package/dist/cli/newpr.test.js.map +0 -1
  24. package/dist/cli/prs.test.d.ts +0 -8
  25. package/dist/cli/prs.test.d.ts.map +0 -1
  26. package/dist/cli/prs.test.js +0 -463
  27. package/dist/cli/prs.test.js.map +0 -1
  28. package/dist/cli/wt/clean.test.d.ts +0 -8
  29. package/dist/cli/wt/clean.test.d.ts.map +0 -1
  30. package/dist/cli/wt/clean.test.js +0 -624
  31. package/dist/cli/wt/clean.test.js.map +0 -1
  32. package/dist/cli/wt/completion.test.d.ts +0 -5
  33. package/dist/cli/wt/completion.test.d.ts.map +0 -1
  34. package/dist/cli/wt/completion.test.js +0 -275
  35. package/dist/cli/wt/completion.test.js.map +0 -1
  36. package/dist/cli/wt/config.test.d.ts +0 -7
  37. package/dist/cli/wt/config.test.d.ts.map +0 -1
  38. package/dist/cli/wt/config.test.js +0 -440
  39. package/dist/cli/wt/config.test.js.map +0 -1
  40. package/dist/cli/wt/entry.test.d.ts +0 -8
  41. package/dist/cli/wt/entry.test.d.ts.map +0 -1
  42. package/dist/cli/wt/entry.test.js +0 -201
  43. package/dist/cli/wt/entry.test.js.map +0 -1
  44. package/dist/cli/wt/init.test.d.ts +0 -5
  45. package/dist/cli/wt/init.test.d.ts.map +0 -1
  46. package/dist/cli/wt/init.test.js +0 -165
  47. package/dist/cli/wt/init.test.js.map +0 -1
  48. package/dist/cli/wt/init.unit.test.d.ts +0 -5
  49. package/dist/cli/wt/init.unit.test.d.ts.map +0 -1
  50. package/dist/cli/wt/init.unit.test.js +0 -432
  51. package/dist/cli/wt/init.unit.test.js.map +0 -1
  52. package/dist/cli/wt/interactive-menu.test.d.ts +0 -12
  53. package/dist/cli/wt/interactive-menu.test.d.ts.map +0 -1
  54. package/dist/cli/wt/interactive-menu.test.js +0 -796
  55. package/dist/cli/wt/interactive-menu.test.js.map +0 -1
  56. package/dist/cli/wt/list.test.d.ts +0 -10
  57. package/dist/cli/wt/list.test.d.ts.map +0 -1
  58. package/dist/cli/wt/list.test.js +0 -157
  59. package/dist/cli/wt/list.test.js.map +0 -1
  60. package/dist/cli/wt/prs.test.d.ts +0 -5
  61. package/dist/cli/wt/prs.test.d.ts.map +0 -1
  62. package/dist/cli/wt/prs.test.js +0 -410
  63. package/dist/cli/wt/prs.test.js.map +0 -1
  64. package/dist/cli/wt/run-command.test.d.ts +0 -5
  65. package/dist/cli/wt/run-command.test.d.ts.map +0 -1
  66. package/dist/cli/wt/run-command.test.js +0 -88
  67. package/dist/cli/wt/run-command.test.js.map +0 -1
  68. package/dist/cli/wt/state.test.d.ts +0 -9
  69. package/dist/cli/wt/state.test.d.ts.map +0 -1
  70. package/dist/cli/wt/state.test.js +0 -127
  71. package/dist/cli/wt/state.test.js.map +0 -1
  72. package/dist/cli/wt/wt.test.d.ts +0 -8
  73. package/dist/cli/wt/wt.test.d.ts.map +0 -1
  74. package/dist/cli/wt/wt.test.js +0 -739
  75. package/dist/cli/wt/wt.test.js.map +0 -1
  76. package/dist/cli/wt.unit.test.d.ts +0 -7
  77. package/dist/cli/wt.unit.test.d.ts.map +0 -1
  78. package/dist/cli/wt.unit.test.js +0 -160
  79. package/dist/cli/wt.unit.test.js.map +0 -1
  80. package/dist/cli/wtconfig.test.d.ts +0 -5
  81. package/dist/cli/wtconfig.test.d.ts.map +0 -1
  82. package/dist/cli/wtconfig.test.js +0 -1289
  83. package/dist/cli/wtconfig.test.js.map +0 -1
  84. package/dist/cli/wtlink.test.d.ts +0 -2
  85. package/dist/cli/wtlink.test.d.ts.map +0 -1
  86. package/dist/cli/wtlink.test.js +0 -249
  87. package/dist/cli/wtlink.test.js.map +0 -1
  88. package/dist/cli/wtstate.test.d.ts +0 -5
  89. package/dist/cli/wtstate.test.d.ts.map +0 -1
  90. package/dist/cli/wtstate.test.js +0 -193
  91. package/dist/cli/wtstate.test.js.map +0 -1
  92. package/dist/e2e/cleanpr/cleanpr.e2e.test.d.ts +0 -2
  93. package/dist/e2e/cleanpr/cleanpr.e2e.test.d.ts.map +0 -1
  94. package/dist/e2e/cleanpr/cleanpr.e2e.test.js +0 -326
  95. package/dist/e2e/cleanpr/cleanpr.e2e.test.js.map +0 -1
  96. package/dist/e2e/cli.e2e.test.d.ts +0 -2
  97. package/dist/e2e/cli.e2e.test.d.ts.map +0 -1
  98. package/dist/e2e/cli.e2e.test.js +0 -417
  99. package/dist/e2e/cli.e2e.test.js.map +0 -1
  100. package/dist/e2e/lswt/lswt.e2e.test.d.ts +0 -2
  101. package/dist/e2e/lswt/lswt.e2e.test.d.ts.map +0 -1
  102. package/dist/e2e/lswt/lswt.e2e.test.js +0 -361
  103. package/dist/e2e/lswt/lswt.e2e.test.js.map +0 -1
  104. package/dist/e2e/newpr/newpr.e2e.test.d.ts +0 -2
  105. package/dist/e2e/newpr/newpr.e2e.test.d.ts.map +0 -1
  106. package/dist/e2e/newpr/newpr.e2e.test.js +0 -286
  107. package/dist/e2e/newpr/newpr.e2e.test.js.map +0 -1
  108. package/dist/e2e/newpr/scenarios.e2e.test.d.ts +0 -2
  109. package/dist/e2e/newpr/scenarios.e2e.test.d.ts.map +0 -1
  110. package/dist/e2e/newpr/scenarios.e2e.test.js +0 -426
  111. package/dist/e2e/newpr/scenarios.e2e.test.js.map +0 -1
  112. package/dist/e2e/newpr-full-flow.e2e.test.d.ts +0 -2
  113. package/dist/e2e/newpr-full-flow.e2e.test.d.ts.map +0 -1
  114. package/dist/e2e/newpr-full-flow.e2e.test.js +0 -280
  115. package/dist/e2e/newpr-full-flow.e2e.test.js.map +0 -1
  116. package/dist/e2e/prs/prs.e2e.test.d.ts +0 -7
  117. package/dist/e2e/prs/prs.e2e.test.d.ts.map +0 -1
  118. package/dist/e2e/prs/prs.e2e.test.js +0 -606
  119. package/dist/e2e/prs/prs.e2e.test.js.map +0 -1
  120. package/dist/e2e/workflows/pr-lifecycle.e2e.test.d.ts +0 -2
  121. package/dist/e2e/workflows/pr-lifecycle.e2e.test.d.ts.map +0 -1
  122. package/dist/e2e/workflows/pr-lifecycle.e2e.test.js +0 -298
  123. package/dist/e2e/workflows/pr-lifecycle.e2e.test.js.map +0 -1
  124. package/dist/e2e/wt/interactive-menu.e2e.test.d.ts +0 -8
  125. package/dist/e2e/wt/interactive-menu.e2e.test.d.ts.map +0 -1
  126. package/dist/e2e/wt/interactive-menu.e2e.test.js +0 -583
  127. package/dist/e2e/wt/interactive-menu.e2e.test.js.map +0 -1
  128. package/dist/e2e/wt/wt.e2e.test.d.ts +0 -9
  129. package/dist/e2e/wt/wt.e2e.test.d.ts.map +0 -1
  130. package/dist/e2e/wt/wt.e2e.test.js +0 -597
  131. package/dist/e2e/wt/wt.e2e.test.js.map +0 -1
  132. package/dist/e2e/wtlink/wtlink.e2e.test.d.ts +0 -2
  133. package/dist/e2e/wtlink/wtlink.e2e.test.d.ts.map +0 -1
  134. package/dist/e2e/wtlink/wtlink.e2e.test.js +0 -416
  135. package/dist/e2e/wtlink/wtlink.e2e.test.js.map +0 -1
  136. package/dist/integration/git.integration.test.d.ts +0 -2
  137. package/dist/integration/git.integration.test.d.ts.map +0 -1
  138. package/dist/integration/git.integration.test.js +0 -336
  139. package/dist/integration/git.integration.test.js.map +0 -1
  140. package/dist/integration/lswt-remote-pr.integration.test.d.ts +0 -2
  141. package/dist/integration/lswt-remote-pr.integration.test.d.ts.map +0 -1
  142. package/dist/integration/lswt-remote-pr.integration.test.js +0 -222
  143. package/dist/integration/lswt-remote-pr.integration.test.js.map +0 -1
  144. package/dist/integration/newpr-branchfrom-head.integration.test.d.ts +0 -2
  145. package/dist/integration/newpr-branchfrom-head.integration.test.d.ts.map +0 -1
  146. package/dist/integration/newpr-branchfrom-head.integration.test.js +0 -498
  147. package/dist/integration/newpr-branchfrom-head.integration.test.js.map +0 -1
  148. package/dist/integration/newpr.integration.test.d.ts +0 -2
  149. package/dist/integration/newpr.integration.test.d.ts.map +0 -1
  150. package/dist/integration/newpr.integration.test.js +0 -460
  151. package/dist/integration/newpr.integration.test.js.map +0 -1
  152. package/dist/integration/prs.integration.test.d.ts +0 -8
  153. package/dist/integration/prs.integration.test.d.ts.map +0 -1
  154. package/dist/integration/prs.integration.test.js +0 -478
  155. package/dist/integration/prs.integration.test.js.map +0 -1
  156. package/dist/lib/ai/base-provider.test.d.ts +0 -7
  157. package/dist/lib/ai/base-provider.test.d.ts.map +0 -1
  158. package/dist/lib/ai/base-provider.test.js +0 -319
  159. package/dist/lib/ai/base-provider.test.js.map +0 -1
  160. package/dist/lib/ai/cli-provider.test.d.ts +0 -5
  161. package/dist/lib/ai/cli-provider.test.d.ts.map +0 -1
  162. package/dist/lib/ai/cli-provider.test.js +0 -460
  163. package/dist/lib/ai/cli-provider.test.js.map +0 -1
  164. package/dist/lib/ai/fallback-provider.test.d.ts +0 -7
  165. package/dist/lib/ai/fallback-provider.test.d.ts.map +0 -1
  166. package/dist/lib/ai/fallback-provider.test.js +0 -165
  167. package/dist/lib/ai/fallback-provider.test.js.map +0 -1
  168. package/dist/lib/ai/generation-service.test.d.ts +0 -7
  169. package/dist/lib/ai/generation-service.test.d.ts.map +0 -1
  170. package/dist/lib/ai/generation-service.test.js +0 -213
  171. package/dist/lib/ai/generation-service.test.js.map +0 -1
  172. package/dist/lib/ai/provider-manager.test.d.ts +0 -5
  173. package/dist/lib/ai/provider-manager.test.d.ts.map +0 -1
  174. package/dist/lib/ai/provider-manager.test.js +0 -312
  175. package/dist/lib/ai/provider-manager.test.js.map +0 -1
  176. package/dist/lib/ai/repo-docs.test.d.ts +0 -5
  177. package/dist/lib/ai/repo-docs.test.d.ts.map +0 -1
  178. package/dist/lib/ai/repo-docs.test.js +0 -357
  179. package/dist/lib/ai/repo-docs.test.js.map +0 -1
  180. package/dist/lib/cleanpr/args.test.d.ts +0 -2
  181. package/dist/lib/cleanpr/args.test.d.ts.map +0 -1
  182. package/dist/lib/cleanpr/args.test.js +0 -269
  183. package/dist/lib/cleanpr/args.test.js.map +0 -1
  184. package/dist/lib/cleanpr/cleanup.test.d.ts +0 -2
  185. package/dist/lib/cleanpr/cleanup.test.d.ts.map +0 -1
  186. package/dist/lib/cleanpr/cleanup.test.js +0 -296
  187. package/dist/lib/cleanpr/cleanup.test.js.map +0 -1
  188. package/dist/lib/cleanpr/worktree-info.test.d.ts +0 -2
  189. package/dist/lib/cleanpr/worktree-info.test.d.ts.map +0 -1
  190. package/dist/lib/cleanpr/worktree-info.test.js +0 -228
  191. package/dist/lib/cleanpr/worktree-info.test.js.map +0 -1
  192. package/dist/lib/colors.test.d.ts +0 -2
  193. package/dist/lib/colors.test.d.ts.map +0 -1
  194. package/dist/lib/colors.test.js +0 -142
  195. package/dist/lib/colors.test.js.map +0 -1
  196. package/dist/lib/config-editor.test.d.ts +0 -11
  197. package/dist/lib/config-editor.test.d.ts.map +0 -1
  198. package/dist/lib/config-editor.test.js +0 -526
  199. package/dist/lib/config-editor.test.js.map +0 -1
  200. package/dist/lib/config-migration/detector.test.d.ts +0 -5
  201. package/dist/lib/config-migration/detector.test.d.ts.map +0 -1
  202. package/dist/lib/config-migration/detector.test.js +0 -201
  203. package/dist/lib/config-migration/detector.test.js.map +0 -1
  204. package/dist/lib/config-migration/reporter.test.d.ts +0 -5
  205. package/dist/lib/config-migration/reporter.test.d.ts.map +0 -1
  206. package/dist/lib/config-migration/reporter.test.js +0 -305
  207. package/dist/lib/config-migration/reporter.test.js.map +0 -1
  208. package/dist/lib/config-migration/runner.test.d.ts +0 -5
  209. package/dist/lib/config-migration/runner.test.d.ts.map +0 -1
  210. package/dist/lib/config-migration/runner.test.js +0 -235
  211. package/dist/lib/config-migration/runner.test.js.map +0 -1
  212. package/dist/lib/config-validation.test.d.ts +0 -5
  213. package/dist/lib/config-validation.test.d.ts.map +0 -1
  214. package/dist/lib/config-validation.test.js +0 -423
  215. package/dist/lib/config-validation.test.js.map +0 -1
  216. package/dist/lib/config.test.d.ts +0 -2
  217. package/dist/lib/config.test.d.ts.map +0 -1
  218. package/dist/lib/config.test.js +0 -554
  219. package/dist/lib/config.test.js.map +0 -1
  220. package/dist/lib/constants.test.d.ts +0 -5
  221. package/dist/lib/constants.test.d.ts.map +0 -1
  222. package/dist/lib/constants.test.js +0 -180
  223. package/dist/lib/constants.test.js.map +0 -1
  224. package/dist/lib/deprecation.test.d.ts +0 -2
  225. package/dist/lib/deprecation.test.d.ts.map +0 -1
  226. package/dist/lib/deprecation.test.js +0 -71
  227. package/dist/lib/deprecation.test.js.map +0 -1
  228. package/dist/lib/errors.test.d.ts +0 -2
  229. package/dist/lib/errors.test.d.ts.map +0 -1
  230. package/dist/lib/errors.test.js +0 -117
  231. package/dist/lib/errors.test.js.map +0 -1
  232. package/dist/lib/git.test.d.ts +0 -2
  233. package/dist/lib/git.test.d.ts.map +0 -1
  234. package/dist/lib/git.test.js +0 -608
  235. package/dist/lib/git.test.js.map +0 -1
  236. package/dist/lib/github.test.d.ts +0 -2
  237. package/dist/lib/github.test.d.ts.map +0 -1
  238. package/dist/lib/github.test.js +0 -441
  239. package/dist/lib/github.test.js.map +0 -1
  240. package/dist/lib/global-check.test.d.ts +0 -5
  241. package/dist/lib/global-check.test.d.ts.map +0 -1
  242. package/dist/lib/global-check.test.js +0 -150
  243. package/dist/lib/global-check.test.js.map +0 -1
  244. package/dist/lib/global-config.test.d.ts +0 -5
  245. package/dist/lib/global-config.test.d.ts.map +0 -1
  246. package/dist/lib/global-config.test.js +0 -282
  247. package/dist/lib/global-config.test.js.map +0 -1
  248. package/dist/lib/hooks/confirmation.test.d.ts +0 -7
  249. package/dist/lib/hooks/confirmation.test.d.ts.map +0 -1
  250. package/dist/lib/hooks/confirmation.test.js +0 -300
  251. package/dist/lib/hooks/confirmation.test.js.map +0 -1
  252. package/dist/lib/hooks/executor.test.d.ts +0 -5
  253. package/dist/lib/hooks/executor.test.d.ts.map +0 -1
  254. package/dist/lib/hooks/executor.test.js +0 -648
  255. package/dist/lib/hooks/executor.test.js.map +0 -1
  256. package/dist/lib/hooks/templates.test.d.ts +0 -5
  257. package/dist/lib/hooks/templates.test.d.ts.map +0 -1
  258. package/dist/lib/hooks/templates.test.js +0 -163
  259. package/dist/lib/hooks/templates.test.js.map +0 -1
  260. package/dist/lib/hooks/types.test.d.ts +0 -5
  261. package/dist/lib/hooks/types.test.d.ts.map +0 -1
  262. package/dist/lib/hooks/types.test.js +0 -132
  263. package/dist/lib/hooks/types.test.js.map +0 -1
  264. package/dist/lib/json-output.test.d.ts +0 -5
  265. package/dist/lib/json-output.test.d.ts.map +0 -1
  266. package/dist/lib/json-output.test.js +0 -261
  267. package/dist/lib/json-output.test.js.map +0 -1
  268. package/dist/lib/logger.test.d.ts +0 -14
  269. package/dist/lib/logger.test.d.ts.map +0 -1
  270. package/dist/lib/logger.test.js +0 -692
  271. package/dist/lib/logger.test.js.map +0 -1
  272. package/dist/lib/lswt/action-executors.test.d.ts +0 -2
  273. package/dist/lib/lswt/action-executors.test.d.ts.map +0 -1
  274. package/dist/lib/lswt/action-executors.test.js +0 -1127
  275. package/dist/lib/lswt/action-executors.test.js.map +0 -1
  276. package/dist/lib/lswt/actions.test.d.ts +0 -2
  277. package/dist/lib/lswt/actions.test.d.ts.map +0 -1
  278. package/dist/lib/lswt/actions.test.js +0 -497
  279. package/dist/lib/lswt/actions.test.js.map +0 -1
  280. package/dist/lib/lswt/args.test.d.ts +0 -2
  281. package/dist/lib/lswt/args.test.d.ts.map +0 -1
  282. package/dist/lib/lswt/args.test.js +0 -195
  283. package/dist/lib/lswt/args.test.js.map +0 -1
  284. package/dist/lib/lswt/environment.test.d.ts +0 -2
  285. package/dist/lib/lswt/environment.test.d.ts.map +0 -1
  286. package/dist/lib/lswt/environment.test.js +0 -544
  287. package/dist/lib/lswt/environment.test.js.map +0 -1
  288. package/dist/lib/lswt/formatters.test.d.ts +0 -2
  289. package/dist/lib/lswt/formatters.test.d.ts.map +0 -1
  290. package/dist/lib/lswt/formatters.test.js +0 -323
  291. package/dist/lib/lswt/formatters.test.js.map +0 -1
  292. package/dist/lib/lswt/fuzzy-search.test.d.ts +0 -5
  293. package/dist/lib/lswt/fuzzy-search.test.d.ts.map +0 -1
  294. package/dist/lib/lswt/fuzzy-search.test.js +0 -207
  295. package/dist/lib/lswt/fuzzy-search.test.js.map +0 -1
  296. package/dist/lib/lswt/interactive.test.d.ts +0 -2
  297. package/dist/lib/lswt/interactive.test.d.ts.map +0 -1
  298. package/dist/lib/lswt/interactive.test.js +0 -771
  299. package/dist/lib/lswt/interactive.test.js.map +0 -1
  300. package/dist/lib/lswt/table.test.d.ts +0 -5
  301. package/dist/lib/lswt/table.test.d.ts.map +0 -1
  302. package/dist/lib/lswt/table.test.js +0 -262
  303. package/dist/lib/lswt/table.test.js.map +0 -1
  304. package/dist/lib/lswt/worktree-info.test.d.ts +0 -2
  305. package/dist/lib/lswt/worktree-info.test.d.ts.map +0 -1
  306. package/dist/lib/lswt/worktree-info.test.js +0 -484
  307. package/dist/lib/lswt/worktree-info.test.js.map +0 -1
  308. package/dist/lib/newpr/action-deps.test.d.ts +0 -5
  309. package/dist/lib/newpr/action-deps.test.d.ts.map +0 -1
  310. package/dist/lib/newpr/action-deps.test.js +0 -111
  311. package/dist/lib/newpr/action-deps.test.js.map +0 -1
  312. package/dist/lib/newpr/actions.test.d.ts +0 -2
  313. package/dist/lib/newpr/actions.test.d.ts.map +0 -1
  314. package/dist/lib/newpr/actions.test.js +0 -254
  315. package/dist/lib/newpr/actions.test.js.map +0 -1
  316. package/dist/lib/newpr/args.test.d.ts +0 -2
  317. package/dist/lib/newpr/args.test.d.ts.map +0 -1
  318. package/dist/lib/newpr/args.test.js +0 -479
  319. package/dist/lib/newpr/args.test.js.map +0 -1
  320. package/dist/lib/newpr/hook-runner.test.d.ts +0 -7
  321. package/dist/lib/newpr/hook-runner.test.d.ts.map +0 -1
  322. package/dist/lib/newpr/hook-runner.test.js +0 -422
  323. package/dist/lib/newpr/hook-runner.test.js.map +0 -1
  324. package/dist/lib/newpr/plan-generator.test.d.ts +0 -7
  325. package/dist/lib/newpr/plan-generator.test.d.ts.map +0 -1
  326. package/dist/lib/newpr/plan-generator.test.js +0 -387
  327. package/dist/lib/newpr/plan-generator.test.js.map +0 -1
  328. package/dist/lib/newpr/scenario-handler.test.d.ts +0 -2
  329. package/dist/lib/newpr/scenario-handler.test.d.ts.map +0 -1
  330. package/dist/lib/newpr/scenario-handler.test.js +0 -256
  331. package/dist/lib/newpr/scenario-handler.test.js.map +0 -1
  332. package/dist/lib/prompts.test.d.ts +0 -2
  333. package/dist/lib/prompts.test.d.ts.map +0 -1
  334. package/dist/lib/prompts.test.js +0 -807
  335. package/dist/lib/prompts.test.js.map +0 -1
  336. package/dist/lib/prs/actions.test.d.ts +0 -5
  337. package/dist/lib/prs/actions.test.d.ts.map +0 -1
  338. package/dist/lib/prs/actions.test.js +0 -356
  339. package/dist/lib/prs/actions.test.js.map +0 -1
  340. package/dist/lib/prs/command.test.d.ts +0 -11
  341. package/dist/lib/prs/command.test.d.ts.map +0 -1
  342. package/dist/lib/prs/command.test.js +0 -409
  343. package/dist/lib/prs/command.test.js.map +0 -1
  344. package/dist/lib/prs/data.test.d.ts +0 -5
  345. package/dist/lib/prs/data.test.d.ts.map +0 -1
  346. package/dist/lib/prs/data.test.js +0 -417
  347. package/dist/lib/prs/data.test.js.map +0 -1
  348. package/dist/lib/prs/details.test.d.ts +0 -5
  349. package/dist/lib/prs/details.test.d.ts.map +0 -1
  350. package/dist/lib/prs/details.test.js +0 -325
  351. package/dist/lib/prs/details.test.js.map +0 -1
  352. package/dist/lib/prs/filters.test.d.ts +0 -5
  353. package/dist/lib/prs/filters.test.d.ts.map +0 -1
  354. package/dist/lib/prs/filters.test.js +0 -312
  355. package/dist/lib/prs/filters.test.js.map +0 -1
  356. package/dist/lib/prs/formatters.test.d.ts +0 -2
  357. package/dist/lib/prs/formatters.test.d.ts.map +0 -1
  358. package/dist/lib/prs/formatters.test.js +0 -387
  359. package/dist/lib/prs/formatters.test.js.map +0 -1
  360. package/dist/lib/prs/interactive.test.d.ts +0 -5
  361. package/dist/lib/prs/interactive.test.d.ts.map +0 -1
  362. package/dist/lib/prs/interactive.test.js +0 -517
  363. package/dist/lib/prs/interactive.test.js.map +0 -1
  364. package/dist/lib/schema.test.d.ts +0 -10
  365. package/dist/lib/schema.test.d.ts.map +0 -1
  366. package/dist/lib/schema.test.js +0 -309
  367. package/dist/lib/schema.test.js.map +0 -1
  368. package/dist/lib/state-detection.test.d.ts +0 -2
  369. package/dist/lib/state-detection.test.d.ts.map +0 -1
  370. package/dist/lib/state-detection.test.js +0 -451
  371. package/dist/lib/state-detection.test.js.map +0 -1
  372. package/dist/lib/ui/error.test.d.ts +0 -2
  373. package/dist/lib/ui/error.test.d.ts.map +0 -1
  374. package/dist/lib/ui/error.test.js +0 -143
  375. package/dist/lib/ui/error.test.js.map +0 -1
  376. package/dist/lib/ui/output.test.d.ts +0 -2
  377. package/dist/lib/ui/output.test.d.ts.map +0 -1
  378. package/dist/lib/ui/output.test.js +0 -59
  379. package/dist/lib/ui/output.test.js.map +0 -1
  380. package/dist/lib/ui/status.test.d.ts +0 -2
  381. package/dist/lib/ui/status.test.d.ts.map +0 -1
  382. package/dist/lib/ui/status.test.js +0 -158
  383. package/dist/lib/ui/status.test.js.map +0 -1
  384. package/dist/lib/ui/table.test.d.ts +0 -2
  385. package/dist/lib/ui/table.test.d.ts.map +0 -1
  386. package/dist/lib/ui/table.test.js +0 -115
  387. package/dist/lib/ui/table.test.js.map +0 -1
  388. package/dist/lib/ui/theme.test.d.ts +0 -2
  389. package/dist/lib/ui/theme.test.d.ts.map +0 -1
  390. package/dist/lib/ui/theme.test.js +0 -76
  391. package/dist/lib/ui/theme.test.js.map +0 -1
  392. package/dist/lib/wtconfig/config-manager.test.d.ts +0 -5
  393. package/dist/lib/wtconfig/config-manager.test.d.ts.map +0 -1
  394. package/dist/lib/wtconfig/config-manager.test.js +0 -501
  395. package/dist/lib/wtconfig/config-manager.test.js.map +0 -1
  396. package/dist/lib/wtconfig/environment.test.d.ts +0 -5
  397. package/dist/lib/wtconfig/environment.test.d.ts.map +0 -1
  398. package/dist/lib/wtconfig/environment.test.js +0 -285
  399. package/dist/lib/wtconfig/environment.test.js.map +0 -1
  400. package/dist/lib/wtlink/config-manifest.test.d.ts +0 -2
  401. package/dist/lib/wtlink/config-manifest.test.d.ts.map +0 -1
  402. package/dist/lib/wtlink/config-manifest.test.js +0 -486
  403. package/dist/lib/wtlink/config-manifest.test.js.map +0 -1
  404. package/dist/lib/wtlink/link-configs.test.d.ts +0 -2
  405. package/dist/lib/wtlink/link-configs.test.d.ts.map +0 -1
  406. package/dist/lib/wtlink/link-configs.test.js +0 -612
  407. package/dist/lib/wtlink/link-configs.test.js.map +0 -1
  408. package/dist/lib/wtlink/main-menu.test.d.ts +0 -5
  409. package/dist/lib/wtlink/main-menu.test.d.ts.map +0 -1
  410. package/dist/lib/wtlink/main-menu.test.js +0 -126
  411. package/dist/lib/wtlink/main-menu.test.js.map +0 -1
  412. package/dist/lib/wtlink/manage-manifest.test.d.ts +0 -2
  413. package/dist/lib/wtlink/manage-manifest.test.d.ts.map +0 -1
  414. package/dist/lib/wtlink/manage-manifest.test.js +0 -714
  415. package/dist/lib/wtlink/manage-manifest.test.js.map +0 -1
  416. package/dist/lib/wtlink/validate-manifest.test.d.ts +0 -2
  417. package/dist/lib/wtlink/validate-manifest.test.d.ts.map +0 -1
  418. package/dist/lib/wtlink/validate-manifest.test.js +0 -220
  419. package/dist/lib/wtlink/validate-manifest.test.js.map +0 -1
  420. package/dist/lib/wtstate/analyze.test.d.ts +0 -5
  421. package/dist/lib/wtstate/analyze.test.d.ts.map +0 -1
  422. package/dist/lib/wtstate/analyze.test.js +0 -282
  423. package/dist/lib/wtstate/analyze.test.js.map +0 -1
  424. package/dist/lib/wtstate/args.test.d.ts +0 -5
  425. package/dist/lib/wtstate/args.test.d.ts.map +0 -1
  426. package/dist/lib/wtstate/args.test.js +0 -120
  427. package/dist/lib/wtstate/args.test.js.map +0 -1
  428. package/dist/mcp/server.test.d.ts +0 -9
  429. package/dist/mcp/server.test.d.ts.map +0 -1
  430. package/dist/mcp/server.test.js +0 -550
  431. package/dist/mcp/server.test.js.map +0 -1
@@ -1,1182 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- // Mock modules before importing the CLI
3
- vi.mock('../lib/git.js', () => ({
4
- getRepoRoot: vi.fn(),
5
- getRepoName: vi.fn(),
6
- getMainWorktreeRoot: vi.fn(),
7
- fetch: vi.fn(),
8
- fetchAsync: vi.fn().mockResolvedValue(undefined),
9
- getCurrentBranch: vi.fn(),
10
- checkout: vi.fn(),
11
- exec: vi.fn(),
12
- push: vi.fn(),
13
- pushAsync: vi.fn().mockResolvedValue(undefined),
14
- commit: vi.fn(),
15
- add: vi.fn(),
16
- stash: vi.fn(),
17
- stashApply: vi.fn(),
18
- stashDrop: vi.fn(),
19
- stashPop: vi.fn(),
20
- addWorktree: vi.fn(),
21
- addWorktreeAsync: vi.fn().mockResolvedValue(undefined),
22
- getStagedFiles: vi.fn(),
23
- getUnstagedFiles: vi.fn(),
24
- getStatusOutput: vi.fn(),
25
- getCommitsAhead: vi.fn(),
26
- remoteBranchExists: vi.fn(),
27
- branchExists: vi.fn(),
28
- getChangedFiles: vi.fn(),
29
- getCommitMessages: vi.fn(),
30
- }));
31
- vi.mock('../lib/github.js', () => ({
32
- isGhInstalled: vi.fn(),
33
- isAuthenticated: vi.fn(),
34
- getPr: vi.fn(),
35
- getPrByBranch: vi.fn(),
36
- createPr: vi.fn(),
37
- }));
38
- vi.mock('../lib/prompts.js', () => ({
39
- promptChoiceIndex: vi.fn(),
40
- promptConfirm: vi.fn(),
41
- withSpinner: vi.fn(async (message, fn) => {
42
- return await fn();
43
- }),
44
- }));
45
- vi.mock('../lib/config.js', () => ({
46
- loadConfig: vi.fn(),
47
- generateBranchNameAsync: vi.fn(),
48
- generateWorktreePath: vi.fn(),
49
- generatePRContentAsync: vi.fn(),
50
- }));
51
- vi.mock('../lib/state-detection.js', () => ({
52
- analyzeGitState: vi.fn(),
53
- detectScenario: vi.fn(),
54
- }));
55
- // Create a mock hook runner that always allows hooks to pass
56
- const mockHookRunner = {
57
- runHook: vi.fn().mockResolvedValue(true),
58
- runCleanup: vi.fn().mockResolvedValue(undefined),
59
- updateContext: vi.fn(),
60
- hasConfiguredHooks: vi.fn().mockReturnValue(false),
61
- getConfiguredHooks: vi.fn().mockReturnValue([]),
62
- getContext: vi.fn().mockReturnValue({}),
63
- };
64
- vi.mock('../lib/newpr/index.js', () => ({
65
- parseArgs: vi.fn(),
66
- getHelpText: vi.fn(),
67
- getScenarioContext: vi.fn(),
68
- isPrWorktreeScenario: vi.fn(),
69
- isExistingBranchAction: vi.fn(),
70
- executeStateAction: vi.fn(),
71
- getBranchPoint: vi.fn(),
72
- getScenarioMessageLevel: vi.fn(),
73
- createHookRunner: vi.fn(() => mockHookRunner),
74
- createActionDeps: vi.fn(() => ({
75
- gitAdd: vi.fn(),
76
- gitStash: vi.fn(),
77
- gitPush: vi.fn(),
78
- gitCommit: vi.fn(),
79
- })),
80
- HookRunner: vi.fn(),
81
- runLifecycleHook: vi.fn().mockResolvedValue(true),
82
- }));
83
- vi.mock('fs', () => ({
84
- default: {
85
- existsSync: vi.fn(),
86
- symlinkSync: vi.fn(),
87
- },
88
- existsSync: vi.fn(),
89
- symlinkSync: vi.fn(),
90
- }));
91
- vi.mock('../lib/wtlink/config-manifest.js', () => ({
92
- getEnabledFiles: vi.fn(),
93
- }));
94
- vi.mock('../lib/wtlink/link-configs.js', () => ({
95
- run: vi.fn(),
96
- }));
97
- // Import after mocking
98
- import * as git from '../lib/git.js';
99
- import * as github from '../lib/github.js';
100
- import * as prompts from '../lib/prompts.js';
101
- import { loadConfig, generateBranchNameAsync, generateWorktreePath, generatePRContentAsync, } from '../lib/config.js';
102
- import { analyzeGitState, detectScenario } from '../lib/state-detection.js';
103
- import * as newpr from '../lib/newpr/index.js';
104
- import fs from 'fs';
105
- import { getEnabledFiles } from '../lib/wtlink/config-manifest.js';
106
- import { run as runWtlink } from '../lib/wtlink/link-configs.js';
107
- describe('cli/newpr', () => {
108
- let mockConsoleLog;
109
- let mockConsoleError;
110
- let mockProcessExit;
111
- let originalArgv;
112
- const defaultConfig = {
113
- configVersion: 1,
114
- baseBranch: 'main',
115
- worktreePattern: '{repo}.pr{number}',
116
- worktreeParent: '..',
117
- draftPr: false,
118
- sharedRepos: [],
119
- branchPrefix: 'feature',
120
- previewLabel: 'preview',
121
- syncPatterns: [],
122
- preferredEditor: 'auto',
123
- ai: { provider: 'none' },
124
- hooks: {},
125
- hookDefaults: { timeout: 30000, maxTimeout: 60000 },
126
- plugins: [],
127
- generators: {},
128
- integrations: {},
129
- logging: { level: 'info', timestamps: true },
130
- global: { warnNotGlobal: true },
131
- wtlink: { enabled: [], disabled: [] },
132
- linkConfigFiles: undefined,
133
- };
134
- const defaultOptions = {
135
- baseBranch: 'main',
136
- draft: false,
137
- installDeps: false,
138
- openEditor: false,
139
- runWtlink: false,
140
- json: false,
141
- nonInteractive: false,
142
- noHooks: false,
143
- };
144
- const makePrInfo = (overrides = {}) => ({
145
- number: 123,
146
- title: 'Test PR',
147
- state: 'OPEN',
148
- headBranch: 'feature-123',
149
- baseBranch: 'main',
150
- url: 'https://github.com/org/repo/pull/123',
151
- isDraft: false,
152
- ...overrides,
153
- });
154
- const makeGitState = (overrides = {}) => ({
155
- worktreeType: 'main_worktree',
156
- branchType: 'main',
157
- currentBranch: 'main',
158
- commitRelationship: 'same',
159
- workingTreeStatus: 'clean',
160
- localCommits: [],
161
- stagedFiles: [],
162
- unstagedFiles: [],
163
- repoRoot: '/repo',
164
- repoName: 'repo',
165
- ...overrides,
166
- });
167
- beforeEach(() => {
168
- vi.resetAllMocks();
169
- // Reset async git mocks (resetAllMocks clears implementations)
170
- vi.mocked(git.fetchAsync).mockResolvedValue(undefined);
171
- vi.mocked(git.pushAsync).mockResolvedValue(undefined);
172
- vi.mocked(git.addWorktreeAsync).mockResolvedValue(undefined);
173
- // Reset withSpinner mock (resetAllMocks clears the implementation)
174
- vi.mocked(prompts.withSpinner).mockImplementation(async (message, fn) => {
175
- return await fn();
176
- });
177
- // Reset the hook runner mock to allow all hooks to pass
178
- mockHookRunner.runHook.mockResolvedValue(true);
179
- mockHookRunner.runCleanup.mockResolvedValue(undefined);
180
- mockHookRunner.updateContext.mockReturnValue(undefined);
181
- mockHookRunner.hasConfiguredHooks.mockReturnValue(false);
182
- mockHookRunner.getConfiguredHooks.mockReturnValue([]);
183
- mockHookRunner.getContext.mockReturnValue({});
184
- // Reset createHookRunner to return the mock
185
- vi.mocked(newpr.createHookRunner).mockReturnValue(mockHookRunner);
186
- // Reset createActionDeps to return a mock deps object
187
- vi.mocked(newpr.createActionDeps).mockReturnValue({
188
- gitAdd: vi.fn(),
189
- gitStash: vi.fn(),
190
- gitPush: vi.fn(),
191
- gitCommit: vi.fn(),
192
- });
193
- // Default mocks for AI generation functions
194
- vi.mocked(generatePRContentAsync).mockResolvedValue({
195
- title: 'Test PR',
196
- description: '',
197
- aiGenerated: false,
198
- });
199
- vi.mocked(git.getChangedFiles).mockReturnValue([]);
200
- vi.mocked(git.getCommitMessages).mockReturnValue([]);
201
- // Default mocks for auto-link feature (throw by default to skip auto-link)
202
- vi.mocked(git.getMainWorktreeRoot).mockImplementation(() => {
203
- throw new Error('Mock: getMainWorktreeRoot not configured');
204
- });
205
- vi.mocked(getEnabledFiles).mockReturnValue([]);
206
- vi.mocked(runWtlink).mockResolvedValue(undefined);
207
- mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => { });
208
- mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
209
- // @ts-expect-error - process.exit mock type is complex
210
- mockProcessExit = vi.spyOn(process, 'exit').mockImplementation((() => { }));
211
- originalArgv = process.argv;
212
- });
213
- afterEach(() => {
214
- mockConsoleLog.mockRestore();
215
- mockConsoleError.mockRestore();
216
- mockProcessExit.mockRestore();
217
- process.argv = originalArgv;
218
- vi.resetModules();
219
- });
220
- async function runCli(args = []) {
221
- process.argv = ['node', '/path/to/newpr.js', ...args];
222
- await import('./newpr.js');
223
- // Allow time for all async operations to complete
224
- await new Promise((resolve) => setTimeout(resolve, 100));
225
- }
226
- describe('help option', () => {
227
- it('prints help and exits 0 on --help', async () => {
228
- vi.mocked(newpr.parseArgs).mockReturnValue({ kind: 'help' });
229
- vi.mocked(newpr.getHelpText).mockReturnValue('Usage: newpr [options]');
230
- await runCli(['--help']);
231
- expect(newpr.getHelpText).toHaveBeenCalled();
232
- expect(mockConsoleLog).toHaveBeenCalledWith('Usage: newpr [options]');
233
- expect(mockProcessExit).toHaveBeenCalledWith(0);
234
- });
235
- });
236
- describe('error handling', () => {
237
- it('prints error and exits 1 on parse error', async () => {
238
- vi.mocked(newpr.parseArgs).mockReturnValue({
239
- kind: 'error',
240
- message: 'Invalid option: --invalid',
241
- });
242
- await runCli(['--invalid']);
243
- expect(mockConsoleError).toHaveBeenCalled();
244
- expect(mockProcessExit).toHaveBeenCalledWith(1);
245
- });
246
- it('exits 1 when gh not installed', async () => {
247
- vi.mocked(newpr.parseArgs).mockReturnValue({
248
- kind: 'success',
249
- options: { mode: 'new', description: 'test', ...defaultOptions },
250
- });
251
- vi.mocked(github.isGhInstalled).mockReturnValue(false);
252
- await runCli(['test']);
253
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('GitHub CLI'));
254
- expect(mockProcessExit).toHaveBeenCalledWith(1);
255
- });
256
- it('exits 1 when gh not authenticated', async () => {
257
- vi.mocked(newpr.parseArgs).mockReturnValue({
258
- kind: 'success',
259
- options: { mode: 'new', description: 'test', ...defaultOptions },
260
- });
261
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
262
- vi.mocked(github.isAuthenticated).mockReturnValue(false);
263
- await runCli(['test']);
264
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('not authenticated'));
265
- expect(mockProcessExit).toHaveBeenCalledWith(1);
266
- });
267
- });
268
- describe('--pr mode', () => {
269
- it('sets up worktree for existing PR', async () => {
270
- vi.mocked(newpr.parseArgs).mockReturnValue({
271
- kind: 'success',
272
- options: { mode: 'pr', prNumber: 123, ...defaultOptions },
273
- });
274
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
275
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
276
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
277
- vi.mocked(git.getRepoName).mockReturnValue('repo');
278
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
279
- vi.mocked(github.getPr).mockReturnValue(makePrInfo());
280
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
281
- vi.mocked(fs.existsSync).mockReturnValue(false);
282
- await runCli(['--pr', '123']);
283
- // Verify wiring: worktree path, branch, and options are correctly passed through
284
- // Non-JSON mode uses addWorktreeAsync
285
- expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr123', // path from generateWorktreePath
286
- 'feature-123', // branch from PR info
287
- expect.objectContaining({
288
- createBranch: true,
289
- startPoint: 'origin/feature-123',
290
- }));
291
- expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #123'));
292
- });
293
- it('exits 1 when PR not found', async () => {
294
- vi.mocked(newpr.parseArgs).mockReturnValue({
295
- kind: 'success',
296
- options: { mode: 'pr', prNumber: 999, ...defaultOptions },
297
- });
298
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
299
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
300
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
301
- vi.mocked(git.getRepoName).mockReturnValue('repo');
302
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
303
- vi.mocked(github.getPr).mockReturnValue(null);
304
- await runCli(['--pr', '999']);
305
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Could not find PR'));
306
- expect(mockProcessExit).toHaveBeenCalledWith(1);
307
- });
308
- });
309
- describe('--branch mode', () => {
310
- it('creates PR for existing branch', async () => {
311
- vi.mocked(newpr.parseArgs).mockReturnValue({
312
- kind: 'success',
313
- options: { mode: 'branch', branchName: 'my-feature', ...defaultOptions },
314
- });
315
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
316
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
317
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
318
- vi.mocked(git.getRepoName).mockReturnValue('repo');
319
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
320
- vi.mocked(git.remoteBranchExists).mockReturnValue(true);
321
- vi.mocked(github.getPrByBranch).mockReturnValue(null);
322
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 456 }));
323
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr456');
324
- vi.mocked(fs.existsSync).mockReturnValue(false);
325
- await runCli(['--branch', 'my-feature']);
326
- // Verify wiring: createPr receives correct branch and description
327
- expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
328
- head: 'my-feature',
329
- base: 'main', // from config
330
- }));
331
- // Verify wiring: addWorktreeAsync receives correct path, branch, and options (non-JSON mode)
332
- expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr456', // path from generateWorktreePath
333
- 'my-feature', // the branch name
334
- expect.objectContaining({
335
- createBranch: true,
336
- startPoint: 'origin/my-feature',
337
- }));
338
- });
339
- it('uses existing PR if branch already has one', async () => {
340
- const existingPr = makePrInfo({ number: 789 });
341
- vi.mocked(newpr.parseArgs).mockReturnValue({
342
- kind: 'success',
343
- options: { mode: 'branch', branchName: 'my-feature', ...defaultOptions },
344
- });
345
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
346
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
347
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
348
- vi.mocked(git.getRepoName).mockReturnValue('repo');
349
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
350
- vi.mocked(git.remoteBranchExists).mockReturnValue(true);
351
- vi.mocked(github.getPrByBranch).mockReturnValue(existingPr);
352
- vi.mocked(github.getPr).mockReturnValue(existingPr);
353
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr789');
354
- vi.mocked(fs.existsSync).mockReturnValue(false);
355
- await runCli(['--branch', 'my-feature']);
356
- expect(github.createPr).not.toHaveBeenCalled();
357
- expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('PR #789 already exists'));
358
- });
359
- });
360
- describe('new feature mode', () => {
361
- it('creates new branch, PR, and worktree', async () => {
362
- vi.mocked(newpr.parseArgs).mockReturnValue({
363
- kind: 'success',
364
- options: { mode: 'new', description: 'Add new feature', ...defaultOptions },
365
- });
366
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
367
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
368
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
369
- vi.mocked(git.getRepoName).mockReturnValue('repo');
370
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
371
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
372
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
373
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
374
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
375
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
376
- message: 'No changes detected',
377
- choices: [
378
- {
379
- label: 'Create empty commit',
380
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
381
- },
382
- { label: 'Cancel', action: null },
383
- ],
384
- });
385
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
386
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1); // 1-based index
387
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
388
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
389
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
390
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
391
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
392
- vi.mocked(git.getStagedFiles).mockReturnValue([]);
393
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
394
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
395
- vi.mocked(generatePRContentAsync).mockResolvedValue({
396
- title: 'Add new feature',
397
- description: '',
398
- aiGenerated: false,
399
- });
400
- await runCli(['Add new feature']);
401
- // Verify wiring: checkout uses generated branch name and branch point with repoRoot
402
- expect(git.exec).toHaveBeenCalledWith([
403
- 'checkout',
404
- '-b',
405
- 'feature/add-new-feature', // from generateBranchNameAsync
406
- 'origin/main', // from getBranchPoint
407
- ], { cwd: '/repo' } // repoRoot from getRepoRoot()
408
- );
409
- // Verify wiring: createPr receives correct branch and title (from generatePRContentAsync)
410
- expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
411
- head: 'feature/add-new-feature',
412
- base: 'main',
413
- title: 'Add new feature',
414
- }));
415
- // Verify wiring: addWorktreeAsync receives correct path, branch, and cwd option
416
- expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr100', // path from generateWorktreePath
417
- 'feature/add-new-feature', // the branch name
418
- { cwd: '/repo' } // repoRoot from getRepoRoot()
419
- );
420
- });
421
- it('exits 1 when user cancels', async () => {
422
- vi.mocked(newpr.parseArgs).mockReturnValue({
423
- kind: 'success',
424
- options: { mode: 'new', description: 'Add new feature', ...defaultOptions },
425
- });
426
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
427
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
428
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
429
- vi.mocked(git.getRepoName).mockReturnValue('repo');
430
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
431
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
432
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
433
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
434
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
435
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
436
- message: 'No changes detected',
437
- choices: [
438
- {
439
- label: 'Create empty commit',
440
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
441
- },
442
- { label: 'Cancel', action: null },
443
- ],
444
- });
445
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
446
- // User selects Cancel (option 2 in 1-based, array index 1)
447
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(2); // 1-based index
448
- await runCli(['Add new feature']);
449
- expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Aborted'));
450
- expect(mockProcessExit).toHaveBeenCalledWith(1);
451
- });
452
- it('shows helpful error when checkout fails due to conflicting changes', async () => {
453
- vi.mocked(newpr.parseArgs).mockReturnValue({
454
- kind: 'success',
455
- options: { mode: 'new', description: 'Add new feature', ...defaultOptions },
456
- });
457
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
458
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
459
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
460
- vi.mocked(git.getRepoName).mockReturnValue('repo');
461
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
462
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
463
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
464
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
465
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
466
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
467
- message: 'No changes detected',
468
- choices: [
469
- {
470
- label: 'Create empty commit',
471
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
472
- },
473
- { label: 'Cancel', action: null },
474
- ],
475
- });
476
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
477
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1); // 1-based index
478
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
479
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
480
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
481
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
482
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
483
- vi.mocked(git.getStagedFiles).mockReturnValue(['README.md']);
484
- vi.mocked(git.getUnstagedFiles).mockReturnValue([]);
485
- // Mock checkout to fail with a conflict error
486
- vi.mocked(git.exec).mockImplementation(() => {
487
- throw new Error("error: Your local changes to 'README.md' would be overwritten by checkout");
488
- });
489
- await runCli(['Add new feature']);
490
- // Verify helpful error messages are shown
491
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Checkout failed due to conflicting changes'));
492
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Your staged changes are preserved'));
493
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Commit your changes first'));
494
- expect(mockProcessExit).toHaveBeenCalledWith(1);
495
- });
496
- });
497
- describe('JSON output mode', () => {
498
- it('outputs JSON error when gh not installed with --json flag', async () => {
499
- vi.mocked(newpr.parseArgs).mockReturnValue({
500
- kind: 'success',
501
- options: { mode: 'new', description: 'test', ...defaultOptions, json: true },
502
- });
503
- vi.mocked(github.isGhInstalled).mockReturnValue(false);
504
- await runCli(['test', '--json']);
505
- // Should output JSON error, not plain text
506
- const jsonOutput = mockConsoleLog.mock.calls.find((call) => String(call[0]).includes('"success": false'));
507
- expect(jsonOutput).toBeDefined();
508
- expect(jsonOutput[0]).toContain('"code": "GH_NOT_INSTALLED"');
509
- expect(mockProcessExit).toHaveBeenCalledWith(1);
510
- });
511
- it('outputs JSON error when gh not authenticated with --json flag', async () => {
512
- vi.mocked(newpr.parseArgs).mockReturnValue({
513
- kind: 'success',
514
- options: { mode: 'new', description: 'test', ...defaultOptions, json: true },
515
- });
516
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
517
- vi.mocked(github.isAuthenticated).mockReturnValue(false);
518
- await runCli(['test', '--json']);
519
- const jsonOutput = mockConsoleLog.mock.calls.find((call) => String(call[0]).includes('"success": false'));
520
- expect(jsonOutput).toBeDefined();
521
- expect(jsonOutput[0]).toContain('"code": "GH_NOT_AUTHENTICATED"');
522
- expect(mockProcessExit).toHaveBeenCalledWith(1);
523
- });
524
- it('outputs JSON success for --pr mode', async () => {
525
- vi.mocked(newpr.parseArgs).mockReturnValue({
526
- kind: 'success',
527
- options: { mode: 'pr', prNumber: 123, ...defaultOptions, json: true },
528
- });
529
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
530
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
531
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
532
- vi.mocked(git.getRepoName).mockReturnValue('repo');
533
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
534
- vi.mocked(github.getPr).mockReturnValue(makePrInfo());
535
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
536
- vi.mocked(fs.existsSync).mockReturnValue(false);
537
- await runCli(['--pr', '123', '--json']);
538
- const jsonOutput = mockConsoleLog.mock.calls.find((call) => String(call[0]).includes('"success": true'));
539
- expect(jsonOutput).toBeDefined();
540
- expect(jsonOutput[0]).toContain('"prNumber": 123');
541
- });
542
- it('outputs JSON error when parse fails with --json flag', async () => {
543
- vi.mocked(newpr.parseArgs).mockReturnValue({
544
- kind: 'error',
545
- message: 'Missing required argument',
546
- });
547
- await runCli(['--json']);
548
- const jsonOutput = mockConsoleLog.mock.calls.find((call) => String(call[0]).includes('"success": false'));
549
- expect(jsonOutput).toBeDefined();
550
- expect(jsonOutput[0]).toContain('"code": "INVALID_ARGUMENT"');
551
- expect(mockProcessExit).toHaveBeenCalledWith(1);
552
- });
553
- });
554
- describe('non-interactive mode', () => {
555
- it('uses first available action when no --action specified', async () => {
556
- vi.mocked(newpr.parseArgs).mockReturnValue({
557
- kind: 'success',
558
- options: {
559
- mode: 'new',
560
- description: 'Add new feature',
561
- ...defaultOptions,
562
- nonInteractive: true,
563
- },
564
- });
565
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
566
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
567
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
568
- vi.mocked(git.getRepoName).mockReturnValue('repo');
569
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
570
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
571
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
572
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
573
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
574
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
575
- message: 'No changes detected',
576
- choices: [
577
- {
578
- label: 'Create empty commit',
579
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
580
- },
581
- { label: 'Cancel', action: null },
582
- ],
583
- });
584
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
585
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
586
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
587
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
588
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
589
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
590
- vi.mocked(git.getStagedFiles).mockReturnValue([]);
591
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
592
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
593
- await runCli(['Add new feature', '--non-interactive']);
594
- // Should not prompt - should use first available action
595
- expect(prompts.promptChoiceIndex).not.toHaveBeenCalled();
596
- expect(newpr.executeStateAction).toHaveBeenCalledWith(expect.objectContaining({ action: 'empty_commit' }), expect.any(String), expect.any(String), expect.any(Object), expect.any(String));
597
- });
598
- it('uses specified action when --action is provided', async () => {
599
- vi.mocked(newpr.parseArgs).mockReturnValue({
600
- kind: 'success',
601
- options: {
602
- mode: 'new',
603
- description: 'Add new feature',
604
- ...defaultOptions,
605
- nonInteractive: true,
606
- action: 'commit_staged',
607
- },
608
- });
609
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
610
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
611
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
612
- vi.mocked(git.getRepoName).mockReturnValue('repo');
613
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
614
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
615
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState({
616
- workingTreeStatus: 'has_staged',
617
- stagedFiles: ['file.ts'],
618
- }));
619
- vi.mocked(detectScenario).mockReturnValue('main_staged_same');
620
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
621
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
622
- message: 'You have staged changes',
623
- choices: [
624
- {
625
- label: 'Commit staged changes',
626
- action: { action: 'commit_staged', branchFrom: 'origin_main', stashUnstaged: false },
627
- },
628
- {
629
- label: 'Create empty commit',
630
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
631
- },
632
- { label: 'Cancel', action: null },
633
- ],
634
- });
635
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('info');
636
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
637
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
638
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
639
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
640
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
641
- vi.mocked(git.getStagedFiles).mockReturnValue(['file.ts']);
642
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
643
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
644
- await runCli(['Add new feature', '--non-interactive', '--action', 'commit_staged']);
645
- expect(prompts.promptChoiceIndex).not.toHaveBeenCalled();
646
- expect(newpr.executeStateAction).toHaveBeenCalledWith(expect.objectContaining({ action: 'commit_staged' }), expect.any(String), expect.any(String), expect.any(Object), expect.any(String));
647
- });
648
- it('exits with error when --action specifies invalid action', async () => {
649
- vi.mocked(newpr.parseArgs).mockReturnValue({
650
- kind: 'success',
651
- options: {
652
- mode: 'new',
653
- description: 'Add new feature',
654
- ...defaultOptions,
655
- nonInteractive: true,
656
- action: 'invalid_action', // intentionally invalid for test
657
- },
658
- });
659
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
660
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
661
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
662
- vi.mocked(git.getRepoName).mockReturnValue('repo');
663
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
664
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
665
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
666
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
667
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
668
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
669
- message: 'No changes detected',
670
- choices: [
671
- {
672
- label: 'Create empty commit',
673
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
674
- },
675
- { label: 'Cancel', action: null },
676
- ],
677
- });
678
- await runCli(['Add new feature', '--non-interactive', '--action', 'invalid_action']);
679
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining("Action 'invalid_action' is not available"));
680
- expect(mockProcessExit).toHaveBeenCalledWith(1);
681
- });
682
- it('exits with error in non-interactive mode from PR worktree', async () => {
683
- vi.mocked(newpr.parseArgs).mockReturnValue({
684
- kind: 'success',
685
- options: {
686
- mode: 'new',
687
- description: 'Add new feature',
688
- ...defaultOptions,
689
- nonInteractive: true,
690
- },
691
- });
692
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
693
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
694
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo.pr123');
695
- vi.mocked(git.getRepoName).mockReturnValue('repo');
696
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
697
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
698
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState({
699
- worktreeType: 'pr_worktree',
700
- }));
701
- vi.mocked(detectScenario).mockReturnValue('pr_worktree');
702
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(true);
703
- await runCli(['Add new feature', '--non-interactive']);
704
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Cannot create PR from a PR worktree in non-interactive mode'));
705
- expect(mockProcessExit).toHaveBeenCalledWith(1);
706
- });
707
- it('outputs JSON error when --action is invalid with --json flag', async () => {
708
- vi.mocked(newpr.parseArgs).mockReturnValue({
709
- kind: 'success',
710
- options: {
711
- mode: 'new',
712
- description: 'Add new feature',
713
- ...defaultOptions,
714
- nonInteractive: true,
715
- action: 'invalid_action', // intentionally invalid for test
716
- json: true,
717
- },
718
- });
719
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
720
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
721
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
722
- vi.mocked(git.getRepoName).mockReturnValue('repo');
723
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
724
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
725
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
726
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
727
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
728
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
729
- message: 'No changes detected',
730
- choices: [
731
- {
732
- label: 'Create empty commit',
733
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
734
- },
735
- { label: 'Cancel', action: null },
736
- ],
737
- });
738
- await runCli([
739
- 'Add new feature',
740
- '--non-interactive',
741
- '--action',
742
- 'invalid_action',
743
- '--json',
744
- ]);
745
- const jsonOutput = mockConsoleLog.mock.calls.find((call) => String(call[0]).includes('"success": false'));
746
- expect(jsonOutput).toBeDefined();
747
- expect(jsonOutput[0]).toContain('"code": "INVALID_ACTION"');
748
- expect(mockProcessExit).toHaveBeenCalledWith(1);
749
- });
750
- });
751
- describe('PR not found handling', () => {
752
- it('outputs JSON error when PR not found with --json flag', async () => {
753
- vi.mocked(newpr.parseArgs).mockReturnValue({
754
- kind: 'success',
755
- options: { mode: 'pr', prNumber: 999, ...defaultOptions, json: true },
756
- });
757
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
758
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
759
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
760
- vi.mocked(git.getRepoName).mockReturnValue('repo');
761
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
762
- vi.mocked(github.getPr).mockReturnValue(null);
763
- await runCli(['--pr', '999', '--json']);
764
- // JSON mode outputs to console.log, not console.error
765
- expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Could not find PR'));
766
- expect(mockProcessExit).toHaveBeenCalledWith(1);
767
- });
768
- });
769
- describe('user cancellation handling', () => {
770
- it('outputs JSON error when user cancels with --json flag', async () => {
771
- vi.mocked(newpr.parseArgs).mockReturnValue({
772
- kind: 'success',
773
- options: { mode: 'new', description: 'test', ...defaultOptions, json: true },
774
- });
775
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
776
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
777
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
778
- vi.mocked(git.getRepoName).mockReturnValue('repo');
779
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
780
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/test');
781
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
782
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
783
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
784
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
785
- message: 'No changes detected',
786
- choices: [
787
- {
788
- label: 'Create empty commit',
789
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
790
- },
791
- { label: 'Cancel', action: null },
792
- ],
793
- });
794
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
795
- // User selects Cancel (index 1 in array, so 2 in 1-based indexing)
796
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(2);
797
- await runCli(['test', '--json']);
798
- const jsonOutput = mockConsoleLog.mock.calls.find((call) => String(call[0]).includes('"success": false'));
799
- expect(jsonOutput).toBeDefined();
800
- expect(jsonOutput[0]).toContain('"code": "USER_CANCELLED"');
801
- expect(mockProcessExit).toHaveBeenCalledWith(1);
802
- });
803
- });
804
- describe('executeStateAction parameter verification', () => {
805
- it('passes repoRoot to executeStateAction for existing branch actions', async () => {
806
- const repoRoot = '/repo';
807
- vi.mocked(newpr.parseArgs).mockReturnValue({
808
- kind: 'success',
809
- options: { mode: 'new', description: 'Add new feature', ...defaultOptions },
810
- });
811
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
812
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
813
- vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
814
- vi.mocked(git.getRepoName).mockReturnValue('repo');
815
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
816
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
817
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState({
818
- currentBranch: 'feature/existing-branch',
819
- branchType: 'feature',
820
- workingTreeStatus: 'has_staged',
821
- stagedFiles: ['file.ts'],
822
- }));
823
- vi.mocked(detectScenario).mockReturnValue('branch_with_changes');
824
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
825
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
826
- message: 'You have uncommitted changes',
827
- choices: [
828
- {
829
- label: 'Commit all and create PR',
830
- action: { action: 'commit_all', branchFrom: 'head', stashUnstaged: false },
831
- },
832
- { label: 'Cancel', action: null },
833
- ],
834
- });
835
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('info');
836
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1); // 1-based index
837
- // KEY: This is the existing branch action path
838
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(true);
839
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
840
- vi.mocked(git.remoteBranchExists).mockReturnValue(true);
841
- vi.mocked(git.getCurrentBranch).mockReturnValue('feature/existing-branch');
842
- vi.mocked(github.getPrByBranch).mockReturnValue(null);
843
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
844
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
845
- await runCli(['Add new feature']);
846
- // Verify executeStateAction was called with repoRoot as the 5th parameter
847
- expect(newpr.executeStateAction).toHaveBeenCalledWith(expect.objectContaining({ action: 'commit_all' }), expect.any(String), 'feature/existing-branch', expect.any(Object), repoRoot // This is the critical parameter that was missing
848
- );
849
- });
850
- it('passes repoRoot to executeStateAction for new branch actions', async () => {
851
- const repoRoot = '/repo';
852
- vi.mocked(newpr.parseArgs).mockReturnValue({
853
- kind: 'success',
854
- options: { mode: 'new', description: 'Add new feature', ...defaultOptions },
855
- });
856
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
857
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
858
- vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
859
- vi.mocked(git.getRepoName).mockReturnValue('repo');
860
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
861
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feature/add-new-feature');
862
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
863
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
864
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
865
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
866
- message: 'No changes detected',
867
- choices: [
868
- {
869
- label: 'Create empty commit',
870
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
871
- },
872
- { label: 'Cancel', action: null },
873
- ],
874
- });
875
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
876
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1); // 1-based index
877
- // This is the new branch action path
878
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
879
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
880
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
881
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
882
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
883
- vi.mocked(git.getStagedFiles).mockReturnValue([]);
884
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
885
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
886
- await runCli(['Add new feature']);
887
- // Verify executeStateAction was called with repoRoot as the 5th parameter
888
- expect(newpr.executeStateAction).toHaveBeenCalledWith(expect.objectContaining({ action: 'empty_commit' }), expect.any(String), 'feature/add-new-feature', expect.any(Object), repoRoot // This ensures git operations run from repo root
889
- );
890
- });
891
- });
892
- describe('Bug fix: git operations must use repoRoot (empty commit worktree bug)', () => {
893
- // These tests verify the fix for the bug where creating a PR worktree
894
- // with an empty commit failed because git.checkout() didn't switch back
895
- // to main (missing repoRoot parameter), causing git.addWorktree() to fail
896
- // with "branch already checked out" error.
897
- it('git.checkout after push must use repoRoot parameter', async () => {
898
- const repoRoot = '/repo';
899
- vi.mocked(newpr.parseArgs).mockReturnValue({
900
- kind: 'success',
901
- options: { mode: 'new', description: 'Test feature', ...defaultOptions },
902
- });
903
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
904
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
905
- vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
906
- vi.mocked(git.getRepoName).mockReturnValue('repo');
907
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
908
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
909
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
910
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
911
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
912
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
913
- message: 'No changes detected',
914
- choices: [
915
- {
916
- label: 'Continue with empty initial commit',
917
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
918
- },
919
- { label: 'Cancel', action: null },
920
- ],
921
- });
922
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
923
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
924
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
925
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
926
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
927
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
928
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
929
- vi.mocked(git.getStagedFiles).mockReturnValue([]);
930
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
931
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
932
- await runCli(['Test feature']);
933
- // CRITICAL: git.checkout must be called with repoRoot to switch back to main
934
- // Without this, the branch remains checked out and worktree creation fails
935
- expect(git.checkout).toHaveBeenCalledWith('main', repoRoot);
936
- });
937
- it('git.addWorktreeAsync must use { cwd: repoRoot } option', async () => {
938
- const repoRoot = '/repo';
939
- vi.mocked(newpr.parseArgs).mockReturnValue({
940
- kind: 'success',
941
- options: { mode: 'new', description: 'Test feature', ...defaultOptions },
942
- });
943
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
944
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
945
- vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
946
- vi.mocked(git.getRepoName).mockReturnValue('repo');
947
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
948
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
949
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
950
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
951
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
952
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
953
- message: 'No changes detected',
954
- choices: [
955
- {
956
- label: 'Continue with empty initial commit',
957
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
958
- },
959
- { label: 'Cancel', action: null },
960
- ],
961
- });
962
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
963
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
964
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
965
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
966
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
967
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
968
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
969
- vi.mocked(git.getStagedFiles).mockReturnValue([]);
970
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
971
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
972
- await runCli(['Test feature']);
973
- // CRITICAL: git.addWorktreeAsync must be called with { cwd: repoRoot }
974
- // to ensure worktree is created from the correct directory
975
- expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr100', 'feat/test-feature', {
976
- cwd: repoRoot,
977
- });
978
- });
979
- it('git.pushAsync must use repoRoot parameter', async () => {
980
- const repoRoot = '/repo';
981
- vi.mocked(newpr.parseArgs).mockReturnValue({
982
- kind: 'success',
983
- options: { mode: 'new', description: 'Test feature', ...defaultOptions },
984
- });
985
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
986
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
987
- vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
988
- vi.mocked(git.getRepoName).mockReturnValue('repo');
989
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
990
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
991
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
992
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
993
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
994
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
995
- message: 'No changes detected',
996
- choices: [
997
- {
998
- label: 'Continue with empty initial commit',
999
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
1000
- },
1001
- { label: 'Cancel', action: null },
1002
- ],
1003
- });
1004
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
1005
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
1006
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
1007
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
1008
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
1009
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
1010
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
1011
- vi.mocked(git.getStagedFiles).mockReturnValue([]);
1012
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
1013
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
1014
- await runCli(['Test feature']);
1015
- // git.pushAsync must be called with repoRoot to push from correct directory
1016
- expect(git.pushAsync).toHaveBeenCalledWith({ setUpstream: true, remote: 'origin', branch: 'feat/test-feature' }, repoRoot);
1017
- });
1018
- it('git.exec for branch checkout must use { cwd: repoRoot } option', async () => {
1019
- const repoRoot = '/repo';
1020
- vi.mocked(newpr.parseArgs).mockReturnValue({
1021
- kind: 'success',
1022
- options: { mode: 'new', description: 'Test feature', ...defaultOptions },
1023
- });
1024
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
1025
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
1026
- vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
1027
- vi.mocked(git.getRepoName).mockReturnValue('repo');
1028
- vi.mocked(loadConfig).mockReturnValue(defaultConfig);
1029
- vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
1030
- vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
1031
- vi.mocked(detectScenario).mockReturnValue('main_clean_same');
1032
- vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
1033
- vi.mocked(newpr.getScenarioContext).mockReturnValue({
1034
- message: 'No changes detected',
1035
- choices: [
1036
- {
1037
- label: 'Continue with empty initial commit',
1038
- action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
1039
- },
1040
- { label: 'Cancel', action: null },
1041
- ],
1042
- });
1043
- vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
1044
- vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
1045
- vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
1046
- vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
1047
- vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
1048
- vi.mocked(git.remoteBranchExists).mockReturnValue(false);
1049
- vi.mocked(git.getCurrentBranch).mockReturnValue('main');
1050
- vi.mocked(git.getStagedFiles).mockReturnValue([]);
1051
- vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
1052
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
1053
- await runCli(['Test feature']);
1054
- // git.exec for branch creation must use cwd option to ensure
1055
- // branch is created in the correct worktree
1056
- expect(git.exec).toHaveBeenCalledWith(['checkout', '-b', 'feat/test-feature', 'origin/main'], { cwd: repoRoot });
1057
- });
1058
- });
1059
- describe('auto-link config files (linkConfigFiles feature)', () => {
1060
- // Helper to set up all the mocks for a successful PR creation
1061
- const setupPrCreationMocks = (configOverrides = {}) => {
1062
- vi.mocked(newpr.parseArgs).mockReturnValue({
1063
- kind: 'success',
1064
- options: { mode: 'pr', prNumber: 123, ...defaultOptions },
1065
- });
1066
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
1067
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
1068
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
1069
- vi.mocked(git.getRepoName).mockReturnValue('repo');
1070
- vi.mocked(git.getMainWorktreeRoot).mockReturnValue('/main-repo');
1071
- vi.mocked(loadConfig).mockReturnValue({ ...defaultConfig, ...configOverrides });
1072
- vi.mocked(github.getPr).mockReturnValue(makePrInfo());
1073
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
1074
- vi.mocked(fs.existsSync).mockReturnValue(false);
1075
- };
1076
- it('auto-links config files when linkConfigFiles is true', async () => {
1077
- setupPrCreationMocks({ linkConfigFiles: true });
1078
- vi.mocked(getEnabledFiles).mockReturnValue(['.env.local', '.vscode/settings.json']);
1079
- vi.mocked(runWtlink).mockResolvedValue(undefined);
1080
- await runCli(['--pr', '123']);
1081
- expect(getEnabledFiles).toHaveBeenCalledWith('/main-repo');
1082
- expect(runWtlink).toHaveBeenCalledWith(expect.objectContaining({
1083
- source: '/main-repo',
1084
- destination: '/repo.pr123',
1085
- dryRun: false,
1086
- yes: true,
1087
- }));
1088
- expect(prompts.promptConfirm).not.toHaveBeenCalled();
1089
- });
1090
- it('skips linking when linkConfigFiles is false', async () => {
1091
- setupPrCreationMocks({ linkConfigFiles: false });
1092
- vi.mocked(getEnabledFiles).mockReturnValue(['.env.local', '.vscode/settings.json']);
1093
- await runCli(['--pr', '123']);
1094
- expect(getEnabledFiles).toHaveBeenCalledWith('/main-repo');
1095
- expect(runWtlink).not.toHaveBeenCalled();
1096
- expect(prompts.promptConfirm).not.toHaveBeenCalled();
1097
- });
1098
- it('prompts user when linkConfigFiles is undefined in interactive mode', async () => {
1099
- setupPrCreationMocks({ linkConfigFiles: undefined });
1100
- vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1101
- vi.mocked(prompts.promptConfirm).mockResolvedValue(true);
1102
- vi.mocked(runWtlink).mockResolvedValue(undefined);
1103
- await runCli(['--pr', '123']);
1104
- expect(getEnabledFiles).toHaveBeenCalledWith('/main-repo');
1105
- expect(prompts.promptConfirm).toHaveBeenCalledWith(expect.stringContaining('Link these config files'), true);
1106
- expect(runWtlink).toHaveBeenCalled();
1107
- });
1108
- it('skips linking when user declines the prompt', async () => {
1109
- setupPrCreationMocks({ linkConfigFiles: undefined });
1110
- vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1111
- vi.mocked(prompts.promptConfirm).mockResolvedValue(false);
1112
- await runCli(['--pr', '123']);
1113
- expect(prompts.promptConfirm).toHaveBeenCalled();
1114
- expect(runWtlink).not.toHaveBeenCalled();
1115
- });
1116
- it('defaults to linking in non-interactive mode when linkConfigFiles is undefined', async () => {
1117
- vi.mocked(newpr.parseArgs).mockReturnValue({
1118
- kind: 'success',
1119
- options: { mode: 'pr', prNumber: 123, ...defaultOptions, nonInteractive: true },
1120
- });
1121
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
1122
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
1123
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
1124
- vi.mocked(git.getRepoName).mockReturnValue('repo');
1125
- vi.mocked(git.getMainWorktreeRoot).mockReturnValue('/main-repo');
1126
- vi.mocked(loadConfig).mockReturnValue({ ...defaultConfig, linkConfigFiles: undefined });
1127
- vi.mocked(github.getPr).mockReturnValue(makePrInfo());
1128
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
1129
- vi.mocked(fs.existsSync).mockReturnValue(false);
1130
- vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1131
- vi.mocked(runWtlink).mockResolvedValue(undefined);
1132
- await runCli(['--pr', '123', '--non-interactive']);
1133
- expect(prompts.promptConfirm).not.toHaveBeenCalled();
1134
- expect(runWtlink).toHaveBeenCalled();
1135
- });
1136
- it('defaults to linking in JSON mode when linkConfigFiles is undefined', async () => {
1137
- vi.mocked(newpr.parseArgs).mockReturnValue({
1138
- kind: 'success',
1139
- options: { mode: 'pr', prNumber: 123, ...defaultOptions, json: true },
1140
- });
1141
- vi.mocked(github.isGhInstalled).mockReturnValue(true);
1142
- vi.mocked(github.isAuthenticated).mockReturnValue(true);
1143
- vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
1144
- vi.mocked(git.getRepoName).mockReturnValue('repo');
1145
- vi.mocked(git.getMainWorktreeRoot).mockReturnValue('/main-repo');
1146
- vi.mocked(loadConfig).mockReturnValue({ ...defaultConfig, linkConfigFiles: undefined });
1147
- vi.mocked(github.getPr).mockReturnValue(makePrInfo());
1148
- vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
1149
- vi.mocked(fs.existsSync).mockReturnValue(false);
1150
- vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1151
- vi.mocked(runWtlink).mockResolvedValue(undefined);
1152
- await runCli(['--pr', '123', '--json']);
1153
- expect(prompts.promptConfirm).not.toHaveBeenCalled();
1154
- expect(runWtlink).toHaveBeenCalled();
1155
- });
1156
- it('does not link when there are no enabled files', async () => {
1157
- setupPrCreationMocks({ linkConfigFiles: true });
1158
- vi.mocked(getEnabledFiles).mockReturnValue([]);
1159
- await runCli(['--pr', '123']);
1160
- expect(runWtlink).not.toHaveBeenCalled();
1161
- expect(prompts.promptConfirm).not.toHaveBeenCalled();
1162
- });
1163
- it('handles linking errors gracefully', async () => {
1164
- setupPrCreationMocks({ linkConfigFiles: true });
1165
- vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1166
- vi.mocked(runWtlink).mockRejectedValue(new Error('Permission denied'));
1167
- await runCli(['--pr', '123']);
1168
- // Should not crash, just warn about the failure
1169
- expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Failed to link config files'));
1170
- });
1171
- it('skips config linking when main worktree cannot be determined', async () => {
1172
- setupPrCreationMocks({ linkConfigFiles: true });
1173
- vi.mocked(git.getMainWorktreeRoot).mockImplementation(() => {
1174
- throw new Error('Not in a git worktree');
1175
- });
1176
- await runCli(['--pr', '123']);
1177
- expect(getEnabledFiles).not.toHaveBeenCalled();
1178
- expect(runWtlink).not.toHaveBeenCalled();
1179
- });
1180
- });
1181
- });
1182
- //# sourceMappingURL=newpr.test.js.map