@camaradesuk/git-worktree-tools 1.5.0 → 1.7.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 (274) hide show
  1. package/README.md +330 -370
  2. package/dist/cli/cleanpr.js +35 -4
  3. package/dist/cli/cleanpr.js.map +1 -1
  4. package/dist/cli/cleanpr.test.js +256 -0
  5. package/dist/cli/cleanpr.test.js.map +1 -1
  6. package/dist/cli/lswt.js +54 -6
  7. package/dist/cli/lswt.js.map +1 -1
  8. package/dist/cli/lswt.test.js +207 -0
  9. package/dist/cli/lswt.test.js.map +1 -1
  10. package/dist/cli/newpr.js +135 -76
  11. package/dist/cli/newpr.js.map +1 -1
  12. package/dist/cli/newpr.test.js +35 -16
  13. package/dist/cli/newpr.test.js.map +1 -1
  14. package/dist/cli/wt/clean.d.ts +17 -0
  15. package/dist/cli/wt/clean.d.ts.map +1 -0
  16. package/dist/cli/wt/clean.js +74 -0
  17. package/dist/cli/wt/clean.js.map +1 -0
  18. package/dist/cli/wt/completion.d.ts +12 -0
  19. package/dist/cli/wt/completion.d.ts.map +1 -0
  20. package/dist/cli/wt/completion.js +246 -0
  21. package/dist/cli/wt/completion.js.map +1 -0
  22. package/dist/cli/wt/completion.test.d.ts +5 -0
  23. package/dist/cli/wt/completion.test.d.ts.map +1 -0
  24. package/dist/cli/wt/completion.test.js +173 -0
  25. package/dist/cli/wt/completion.test.js.map +1 -0
  26. package/dist/cli/wt/config.d.ts +13 -0
  27. package/dist/cli/wt/config.d.ts.map +1 -0
  28. package/dist/cli/wt/config.js +175 -0
  29. package/dist/cli/wt/config.js.map +1 -0
  30. package/dist/cli/wt/config.test.d.ts +5 -0
  31. package/dist/cli/wt/config.test.d.ts.map +1 -0
  32. package/dist/cli/wt/config.test.js +260 -0
  33. package/dist/cli/wt/config.test.js.map +1 -0
  34. package/dist/cli/wt/entry.test.d.ts +8 -0
  35. package/dist/cli/wt/entry.test.d.ts.map +1 -0
  36. package/dist/cli/wt/entry.test.js +201 -0
  37. package/dist/cli/wt/entry.test.js.map +1 -0
  38. package/dist/cli/wt/init.d.ts +14 -0
  39. package/dist/cli/wt/init.d.ts.map +1 -0
  40. package/dist/cli/wt/init.js +209 -0
  41. package/dist/cli/wt/init.js.map +1 -0
  42. package/dist/cli/wt/init.test.d.ts +5 -0
  43. package/dist/cli/wt/init.test.d.ts.map +1 -0
  44. package/dist/cli/wt/init.test.js +165 -0
  45. package/dist/cli/wt/init.test.js.map +1 -0
  46. package/dist/cli/wt/init.unit.test.d.ts +5 -0
  47. package/dist/cli/wt/init.unit.test.d.ts.map +1 -0
  48. package/dist/cli/wt/init.unit.test.js +432 -0
  49. package/dist/cli/wt/init.unit.test.js.map +1 -0
  50. package/dist/cli/wt/interactive-menu.d.ts +41 -0
  51. package/dist/cli/wt/interactive-menu.d.ts.map +1 -0
  52. package/dist/cli/wt/interactive-menu.js +639 -0
  53. package/dist/cli/wt/interactive-menu.js.map +1 -0
  54. package/dist/cli/wt/interactive-menu.test.d.ts +10 -0
  55. package/dist/cli/wt/interactive-menu.test.d.ts.map +1 -0
  56. package/dist/cli/wt/interactive-menu.test.js +711 -0
  57. package/dist/cli/wt/interactive-menu.test.js.map +1 -0
  58. package/dist/cli/wt/link.d.ts +22 -0
  59. package/dist/cli/wt/link.d.ts.map +1 -0
  60. package/dist/cli/wt/link.js +115 -0
  61. package/dist/cli/wt/link.js.map +1 -0
  62. package/dist/cli/wt/list.d.ts +16 -0
  63. package/dist/cli/wt/list.d.ts.map +1 -0
  64. package/dist/cli/wt/list.js +65 -0
  65. package/dist/cli/wt/list.js.map +1 -0
  66. package/dist/cli/wt/new.d.ts +24 -0
  67. package/dist/cli/wt/new.d.ts.map +1 -0
  68. package/dist/cli/wt/new.js +130 -0
  69. package/dist/cli/wt/new.js.map +1 -0
  70. package/dist/cli/wt/run-command.d.ts +31 -0
  71. package/dist/cli/wt/run-command.d.ts.map +1 -0
  72. package/dist/cli/wt/run-command.js +49 -0
  73. package/dist/cli/wt/run-command.js.map +1 -0
  74. package/dist/cli/wt/run-command.test.d.ts +5 -0
  75. package/dist/cli/wt/run-command.test.d.ts.map +1 -0
  76. package/dist/cli/wt/run-command.test.js +88 -0
  77. package/dist/cli/wt/run-command.test.js.map +1 -0
  78. package/dist/cli/wt/state.d.ts +13 -0
  79. package/dist/cli/wt/state.d.ts.map +1 -0
  80. package/dist/cli/wt/state.js +38 -0
  81. package/dist/cli/wt/state.js.map +1 -0
  82. package/dist/cli/wt/wt.test.d.ts +8 -0
  83. package/dist/cli/wt/wt.test.d.ts.map +1 -0
  84. package/dist/cli/wt/wt.test.js +521 -0
  85. package/dist/cli/wt/wt.test.js.map +1 -0
  86. package/dist/cli/wt.d.ts +26 -0
  87. package/dist/cli/wt.d.ts.map +1 -0
  88. package/dist/cli/wt.js +169 -0
  89. package/dist/cli/wt.js.map +1 -0
  90. package/dist/cli/wt.unit.test.d.ts +7 -0
  91. package/dist/cli/wt.unit.test.d.ts.map +1 -0
  92. package/dist/cli/wt.unit.test.js +182 -0
  93. package/dist/cli/wt.unit.test.js.map +1 -0
  94. package/dist/cli/wtconfig.js +22 -8
  95. package/dist/cli/wtconfig.js.map +1 -1
  96. package/dist/cli/wtconfig.test.js +18 -16
  97. package/dist/cli/wtconfig.test.js.map +1 -1
  98. package/dist/cli/wtlink.js +66 -9
  99. package/dist/cli/wtlink.js.map +1 -1
  100. package/dist/cli/wtlink.test.js +101 -0
  101. package/dist/cli/wtlink.test.js.map +1 -1
  102. package/dist/e2e/cli.e2e.test.js +97 -1
  103. package/dist/e2e/cli.e2e.test.js.map +1 -1
  104. package/dist/e2e/lswt/lswt.e2e.test.js +33 -0
  105. package/dist/e2e/lswt/lswt.e2e.test.js.map +1 -1
  106. package/dist/e2e/newpr/scenarios.e2e.test.js +7 -7
  107. package/dist/e2e/newpr/scenarios.e2e.test.js.map +1 -1
  108. package/dist/e2e/wt/wt.e2e.test.d.ts +9 -0
  109. package/dist/e2e/wt/wt.e2e.test.d.ts.map +1 -0
  110. package/dist/e2e/wt/wt.e2e.test.js +384 -0
  111. package/dist/e2e/wt/wt.e2e.test.js.map +1 -0
  112. package/dist/e2e/wtlink/wtlink.e2e.test.js +52 -0
  113. package/dist/e2e/wtlink/wtlink.e2e.test.js.map +1 -1
  114. package/dist/lib/ai/base-provider.d.ts +22 -0
  115. package/dist/lib/ai/base-provider.d.ts.map +1 -1
  116. package/dist/lib/ai/base-provider.js +180 -99
  117. package/dist/lib/ai/base-provider.js.map +1 -1
  118. package/dist/lib/ai/base-provider.test.js +13 -14
  119. package/dist/lib/ai/base-provider.test.js.map +1 -1
  120. package/dist/lib/ai/cli-provider.d.ts +11 -7
  121. package/dist/lib/ai/cli-provider.d.ts.map +1 -1
  122. package/dist/lib/ai/cli-provider.js +19 -49
  123. package/dist/lib/ai/cli-provider.js.map +1 -1
  124. package/dist/lib/ai/cli-provider.test.js +47 -49
  125. package/dist/lib/ai/cli-provider.test.js.map +1 -1
  126. package/dist/lib/ai/index.d.ts +2 -1
  127. package/dist/lib/ai/index.d.ts.map +1 -1
  128. package/dist/lib/ai/index.js +2 -0
  129. package/dist/lib/ai/index.js.map +1 -1
  130. package/dist/lib/ai/provider-manager.js +2 -2
  131. package/dist/lib/ai/provider-manager.js.map +1 -1
  132. package/dist/lib/ai/repo-docs.d.ts +43 -0
  133. package/dist/lib/ai/repo-docs.d.ts.map +1 -0
  134. package/dist/lib/ai/repo-docs.js +274 -0
  135. package/dist/lib/ai/repo-docs.js.map +1 -0
  136. package/dist/lib/ai/repo-docs.test.d.ts +5 -0
  137. package/dist/lib/ai/repo-docs.test.d.ts.map +1 -0
  138. package/dist/lib/ai/repo-docs.test.js +357 -0
  139. package/dist/lib/ai/repo-docs.test.js.map +1 -0
  140. package/dist/lib/ai/types.d.ts +18 -2
  141. package/dist/lib/ai/types.d.ts.map +1 -1
  142. package/dist/lib/ai/types.js.map +1 -1
  143. package/dist/lib/config-editor.d.ts +21 -0
  144. package/dist/lib/config-editor.d.ts.map +1 -0
  145. package/dist/lib/config-editor.js +729 -0
  146. package/dist/lib/config-editor.js.map +1 -0
  147. package/dist/lib/config-editor.test.d.ts +11 -0
  148. package/dist/lib/config-editor.test.d.ts.map +1 -0
  149. package/dist/lib/config-editor.test.js +526 -0
  150. package/dist/lib/config-editor.test.js.map +1 -0
  151. package/dist/lib/config-validation.d.ts +28 -0
  152. package/dist/lib/config-validation.d.ts.map +1 -0
  153. package/dist/lib/config-validation.js +534 -0
  154. package/dist/lib/config-validation.js.map +1 -0
  155. package/dist/lib/config-validation.test.d.ts +5 -0
  156. package/dist/lib/config-validation.test.d.ts.map +1 -0
  157. package/dist/lib/config-validation.test.js +398 -0
  158. package/dist/lib/config-validation.test.js.map +1 -0
  159. package/dist/lib/config.d.ts +115 -6
  160. package/dist/lib/config.d.ts.map +1 -1
  161. package/dist/lib/config.js +251 -55
  162. package/dist/lib/config.js.map +1 -1
  163. package/dist/lib/config.test.js +2 -1
  164. package/dist/lib/config.test.js.map +1 -1
  165. package/dist/lib/constants.d.ts +50 -1
  166. package/dist/lib/constants.d.ts.map +1 -1
  167. package/dist/lib/constants.js +67 -1
  168. package/dist/lib/constants.js.map +1 -1
  169. package/dist/lib/constants.test.d.ts +5 -0
  170. package/dist/lib/constants.test.d.ts.map +1 -0
  171. package/dist/lib/constants.test.js +121 -0
  172. package/dist/lib/constants.test.js.map +1 -0
  173. package/dist/lib/git.d.ts +12 -0
  174. package/dist/lib/git.d.ts.map +1 -1
  175. package/dist/lib/git.js +26 -0
  176. package/dist/lib/git.js.map +1 -1
  177. package/dist/lib/global-check.d.ts +38 -0
  178. package/dist/lib/global-check.d.ts.map +1 -0
  179. package/dist/lib/global-check.js +135 -0
  180. package/dist/lib/global-check.js.map +1 -0
  181. package/dist/lib/global-check.test.d.ts +5 -0
  182. package/dist/lib/global-check.test.d.ts.map +1 -0
  183. package/dist/lib/global-check.test.js +153 -0
  184. package/dist/lib/global-check.test.js.map +1 -0
  185. package/dist/lib/global-config.d.ts +102 -0
  186. package/dist/lib/global-config.d.ts.map +1 -0
  187. package/dist/lib/global-config.js +234 -0
  188. package/dist/lib/global-config.js.map +1 -0
  189. package/dist/lib/global-config.test.d.ts +5 -0
  190. package/dist/lib/global-config.test.d.ts.map +1 -0
  191. package/dist/lib/global-config.test.js +282 -0
  192. package/dist/lib/global-config.test.js.map +1 -0
  193. package/dist/lib/json-output.d.ts +11 -1
  194. package/dist/lib/json-output.d.ts.map +1 -1
  195. package/dist/lib/json-output.js +42 -1
  196. package/dist/lib/json-output.js.map +1 -1
  197. package/dist/lib/json-output.test.js +2 -0
  198. package/dist/lib/json-output.test.js.map +1 -1
  199. package/dist/lib/logger.d.ts +175 -0
  200. package/dist/lib/logger.d.ts.map +1 -0
  201. package/dist/lib/logger.js +475 -0
  202. package/dist/lib/logger.js.map +1 -0
  203. package/dist/lib/logger.test.d.ts +5 -0
  204. package/dist/lib/logger.test.d.ts.map +1 -0
  205. package/dist/lib/logger.test.js +292 -0
  206. package/dist/lib/logger.test.js.map +1 -0
  207. package/dist/lib/lswt/action-executors.test.js +2 -0
  208. package/dist/lib/lswt/action-executors.test.js.map +1 -1
  209. package/dist/lib/lswt/formatters.d.ts +1 -0
  210. package/dist/lib/lswt/formatters.d.ts.map +1 -1
  211. package/dist/lib/lswt/formatters.js +6 -1
  212. package/dist/lib/lswt/formatters.js.map +1 -1
  213. package/dist/lib/lswt/formatters.test.js +2 -2
  214. package/dist/lib/lswt/formatters.test.js.map +1 -1
  215. package/dist/lib/lswt/fuzzy-search.d.ts +27 -0
  216. package/dist/lib/lswt/fuzzy-search.d.ts.map +1 -0
  217. package/dist/lib/lswt/fuzzy-search.js +130 -0
  218. package/dist/lib/lswt/fuzzy-search.js.map +1 -0
  219. package/dist/lib/lswt/fuzzy-search.test.d.ts +5 -0
  220. package/dist/lib/lswt/fuzzy-search.test.d.ts.map +1 -0
  221. package/dist/lib/lswt/fuzzy-search.test.js +207 -0
  222. package/dist/lib/lswt/fuzzy-search.test.js.map +1 -0
  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/interactive.d.ts +8 -0
  228. package/dist/lib/lswt/interactive.d.ts.map +1 -1
  229. package/dist/lib/lswt/interactive.js +169 -20
  230. package/dist/lib/lswt/interactive.js.map +1 -1
  231. package/dist/lib/newpr/action-deps.test.d.ts +5 -0
  232. package/dist/lib/newpr/action-deps.test.d.ts.map +1 -0
  233. package/dist/lib/newpr/action-deps.test.js +111 -0
  234. package/dist/lib/newpr/action-deps.test.js.map +1 -0
  235. package/dist/lib/newpr/args.d.ts.map +1 -1
  236. package/dist/lib/newpr/args.js +15 -4
  237. package/dist/lib/newpr/args.js.map +1 -1
  238. package/dist/lib/newpr/args.test.js +210 -2
  239. package/dist/lib/newpr/args.test.js.map +1 -1
  240. package/dist/lib/newpr/types.d.ts +2 -0
  241. package/dist/lib/newpr/types.d.ts.map +1 -1
  242. package/dist/lib/prompts.d.ts +10 -0
  243. package/dist/lib/prompts.d.ts.map +1 -1
  244. package/dist/lib/prompts.js +200 -1
  245. package/dist/lib/prompts.js.map +1 -1
  246. package/dist/lib/prompts.test.js +351 -1
  247. package/dist/lib/prompts.test.js.map +1 -1
  248. package/dist/lib/schema.test.d.ts +10 -0
  249. package/dist/lib/schema.test.d.ts.map +1 -0
  250. package/dist/lib/schema.test.js +309 -0
  251. package/dist/lib/schema.test.js.map +1 -0
  252. package/dist/lib/wtconfig/environment.d.ts.map +1 -1
  253. package/dist/lib/wtconfig/environment.js +6 -4
  254. package/dist/lib/wtconfig/environment.js.map +1 -1
  255. package/dist/lib/wtconfig/environment.test.js +2 -7
  256. package/dist/lib/wtconfig/environment.test.js.map +1 -1
  257. package/dist/lib/wtconfig/types.d.ts +3 -1
  258. package/dist/lib/wtconfig/types.d.ts.map +1 -1
  259. package/dist/lib/wtlink/link-configs.test.js +282 -2
  260. package/dist/lib/wtlink/link-configs.test.js.map +1 -1
  261. package/dist/lib/wtlink/main-menu.js +1 -0
  262. package/dist/lib/wtlink/main-menu.js.map +1 -1
  263. package/dist/lib/wtlink/main-menu.test.d.ts +5 -0
  264. package/dist/lib/wtlink/main-menu.test.d.ts.map +1 -0
  265. package/dist/lib/wtlink/main-menu.test.js +124 -0
  266. package/dist/lib/wtlink/main-menu.test.js.map +1 -0
  267. package/dist/lib/wtlink/manage-manifest.d.ts +5 -0
  268. package/dist/lib/wtlink/manage-manifest.d.ts.map +1 -1
  269. package/dist/lib/wtlink/manage-manifest.js +65 -2
  270. package/dist/lib/wtlink/manage-manifest.js.map +1 -1
  271. package/dist/lib/wtlink/manage-manifest.test.js +282 -2
  272. package/dist/lib/wtlink/manage-manifest.test.js.map +1 -1
  273. package/package.json +3 -1
  274. package/schemas/worktreerc.schema.json +416 -0
@@ -0,0 +1,711 @@
1
+ /**
2
+ * Integration tests for interactive menu flows
3
+ *
4
+ * These tests verify that each menu flow:
5
+ * 1. Gathers the correct user inputs
6
+ * 2. Passes the correct arguments to subcommands
7
+ * 3. Handles cancellation and back navigation correctly
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ // Mock modules before importing the module under test
11
+ vi.mock('../../lib/prompts.js', () => {
12
+ // Define UserNavigatedBack inside the factory to avoid hoisting issues
13
+ class MockUserNavigatedBack extends Error {
14
+ constructor() {
15
+ super('User navigated back');
16
+ this.name = 'UserNavigatedBack';
17
+ }
18
+ }
19
+ return {
20
+ promptChoice: vi.fn(),
21
+ promptInput: vi.fn(),
22
+ promptConfirm: vi.fn(),
23
+ UserNavigatedBack: MockUserNavigatedBack,
24
+ };
25
+ });
26
+ vi.mock('./run-command.js', () => ({
27
+ runSubcommand: vi.fn(() => {
28
+ // Mock never returns - simulate process.exit
29
+ throw new Error('process.exit called');
30
+ }),
31
+ }));
32
+ vi.mock('../../lib/config.js', () => ({
33
+ loadConfig: vi.fn(() => ({
34
+ sharedRepos: [],
35
+ baseBranch: 'main',
36
+ draftPr: true,
37
+ worktreePattern: '{repo}.pr{number}',
38
+ worktreeParent: '..',
39
+ syncPatterns: [],
40
+ branchPrefix: 'feat',
41
+ preferredEditor: 'vscode',
42
+ ai: {
43
+ provider: 'auto',
44
+ fallback: 'none',
45
+ branchName: false,
46
+ prTitle: false,
47
+ prDescription: false,
48
+ commitMessage: false,
49
+ planDocument: false,
50
+ },
51
+ hooks: {},
52
+ hookDefaults: { timeout: 30000, maxTimeout: 60000 },
53
+ plugins: [],
54
+ generators: {},
55
+ integrations: {},
56
+ logging: { level: 'info', timestamps: true },
57
+ global: { warnNotGlobal: true },
58
+ })),
59
+ }));
60
+ vi.mock('../../lib/git.js', () => ({
61
+ getRepoRoot: vi.fn(() => '/mock/repo'),
62
+ listLocalBranches: vi.fn(() => ['feat/existing-branch', 'fix/bug-fix', 'main', 'develop']),
63
+ }));
64
+ // Import mocked modules
65
+ import { promptChoice, promptInput, promptConfirm } from '../../lib/prompts.js';
66
+ import { runSubcommand } from './run-command.js';
67
+ import { loadConfig } from '../../lib/config.js';
68
+ import * as git from '../../lib/git.js';
69
+ // Import flows after mocks are set up
70
+ import { flows, showMainMenu } from './interactive-menu.js';
71
+ // Mock console.log to keep test output clean
72
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
73
+ describe('Interactive Menu Flows', () => {
74
+ beforeEach(() => {
75
+ vi.clearAllMocks();
76
+ });
77
+ afterEach(() => {
78
+ consoleSpy.mockClear();
79
+ });
80
+ describe('handleListWorktrees', () => {
81
+ it('calls lswt subcommand with no args', async () => {
82
+ try {
83
+ await flows.handleListWorktrees();
84
+ }
85
+ catch {
86
+ // Expected - runSubcommand throws
87
+ }
88
+ expect(runSubcommand).toHaveBeenCalledWith('lswt', []);
89
+ });
90
+ });
91
+ describe('handleShowState', () => {
92
+ it('calls wtstate subcommand with no args', async () => {
93
+ try {
94
+ await flows.handleShowState();
95
+ }
96
+ catch {
97
+ // Expected - runSubcommand throws
98
+ }
99
+ expect(runSubcommand).toHaveBeenCalledWith('wtstate', []);
100
+ });
101
+ });
102
+ describe('handleNewPR', () => {
103
+ it('returns CANCELLED when user selects back', async () => {
104
+ vi.mocked(promptChoice).mockResolvedValueOnce('back');
105
+ const result = await flows.handleNewPR();
106
+ expect(result).toEqual({ completed: false, returnToMenu: true });
107
+ expect(runSubcommand).not.toHaveBeenCalled();
108
+ });
109
+ it('handles user cancellation (Ctrl+C)', async () => {
110
+ vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
111
+ const result = await flows.handleNewPR();
112
+ expect(result).toEqual({ completed: false, returnToMenu: true });
113
+ });
114
+ describe('from-description flow', () => {
115
+ it('gathers all inputs and calls newpr with correct args', async () => {
116
+ vi.mocked(promptChoice)
117
+ .mockResolvedValueOnce('from-description') // New PR sub-menu
118
+ .mockResolvedValueOnce(true); // Draft PR selection
119
+ vi.mocked(promptInput)
120
+ .mockResolvedValueOnce('Add dark mode support') // Description
121
+ .mockResolvedValueOnce('main'); // Base branch
122
+ vi.mocked(promptConfirm)
123
+ .mockResolvedValueOnce(false) // Install deps
124
+ .mockResolvedValueOnce(false); // Open VS Code
125
+ try {
126
+ await flows.handleNewPR();
127
+ }
128
+ catch {
129
+ // Expected
130
+ }
131
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Add dark mode support']);
132
+ });
133
+ it('passes --ready flag when not draft', async () => {
134
+ vi.mocked(promptChoice)
135
+ .mockResolvedValueOnce('from-description')
136
+ .mockResolvedValueOnce(false); // Ready for review (not draft)
137
+ vi.mocked(promptInput)
138
+ .mockResolvedValueOnce('Fix critical bug')
139
+ .mockResolvedValueOnce('main');
140
+ vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
141
+ try {
142
+ await flows.handleNewPR();
143
+ }
144
+ catch {
145
+ // Expected
146
+ }
147
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Fix critical bug', '--ready']);
148
+ });
149
+ it('passes --base flag when not main', async () => {
150
+ vi.mocked(promptChoice)
151
+ .mockResolvedValueOnce('from-description')
152
+ .mockResolvedValueOnce(true);
153
+ vi.mocked(promptInput)
154
+ .mockResolvedValueOnce('Feature work')
155
+ .mockResolvedValueOnce('develop'); // Non-main base branch
156
+ vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
157
+ try {
158
+ await flows.handleNewPR();
159
+ }
160
+ catch {
161
+ // Expected
162
+ }
163
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Feature work', '--base', 'develop']);
164
+ });
165
+ it('passes --install flag when requested', async () => {
166
+ vi.mocked(promptChoice)
167
+ .mockResolvedValueOnce('from-description')
168
+ .mockResolvedValueOnce(true);
169
+ vi.mocked(promptInput).mockResolvedValueOnce('Add feature').mockResolvedValueOnce('main');
170
+ vi.mocked(promptConfirm)
171
+ .mockResolvedValueOnce(true) // Install deps
172
+ .mockResolvedValueOnce(false);
173
+ try {
174
+ await flows.handleNewPR();
175
+ }
176
+ catch {
177
+ // Expected
178
+ }
179
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Add feature', '--install']);
180
+ });
181
+ it('passes --code flag when requested', async () => {
182
+ vi.mocked(promptChoice)
183
+ .mockResolvedValueOnce('from-description')
184
+ .mockResolvedValueOnce(true);
185
+ vi.mocked(promptInput).mockResolvedValueOnce('Add feature').mockResolvedValueOnce('main');
186
+ vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(true); // Open VS Code
187
+ try {
188
+ await flows.handleNewPR();
189
+ }
190
+ catch {
191
+ // Expected
192
+ }
193
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Add feature', '--code']);
194
+ });
195
+ it('passes all optional flags together', async () => {
196
+ vi.mocked(promptChoice)
197
+ .mockResolvedValueOnce('from-description')
198
+ .mockResolvedValueOnce(false); // Ready
199
+ vi.mocked(promptInput)
200
+ .mockResolvedValueOnce('Full feature')
201
+ .mockResolvedValueOnce('develop');
202
+ vi.mocked(promptConfirm)
203
+ .mockResolvedValueOnce(true) // Install
204
+ .mockResolvedValueOnce(true); // VS Code
205
+ try {
206
+ await flows.handleNewPR();
207
+ }
208
+ catch {
209
+ // Expected
210
+ }
211
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', [
212
+ 'Full feature',
213
+ '--base',
214
+ 'develop',
215
+ '--ready',
216
+ '--install',
217
+ '--code',
218
+ ]);
219
+ });
220
+ it('returns CANCELLED when description is empty', async () => {
221
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-description');
222
+ vi.mocked(promptInput).mockResolvedValueOnce(''); // Empty description
223
+ const result = await flows.handleNewPR();
224
+ expect(result).toEqual({ completed: false, returnToMenu: true });
225
+ expect(runSubcommand).not.toHaveBeenCalled();
226
+ });
227
+ it('handles user cancellation during input', async () => {
228
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-description');
229
+ vi.mocked(promptInput).mockRejectedValueOnce(new Error('User cancelled'));
230
+ const result = await flows.handleNewPR();
231
+ expect(result).toEqual({ completed: false, returnToMenu: true });
232
+ });
233
+ });
234
+ describe('from-pr flow', () => {
235
+ it('gathers PR number and calls newpr with --pr flag', async () => {
236
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
237
+ vi.mocked(promptInput).mockResolvedValueOnce('42');
238
+ vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
239
+ try {
240
+ await flows.handleNewPR();
241
+ }
242
+ catch {
243
+ // Expected
244
+ }
245
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--pr', '42']);
246
+ });
247
+ it('passes --install and --code flags', async () => {
248
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
249
+ vi.mocked(promptInput).mockResolvedValueOnce('123');
250
+ vi.mocked(promptConfirm)
251
+ .mockResolvedValueOnce(true) // Install
252
+ .mockResolvedValueOnce(true); // VS Code
253
+ try {
254
+ await flows.handleNewPR();
255
+ }
256
+ catch {
257
+ // Expected
258
+ }
259
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--pr', '123', '--install', '--code']);
260
+ });
261
+ it('returns CANCELLED when PR number is empty', async () => {
262
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
263
+ vi.mocked(promptInput).mockResolvedValueOnce('');
264
+ const result = await flows.handleNewPR();
265
+ expect(result).toEqual({ completed: false, returnToMenu: true });
266
+ expect(runSubcommand).not.toHaveBeenCalled();
267
+ });
268
+ it('returns CANCELLED when PR number is invalid', async () => {
269
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
270
+ vi.mocked(promptInput).mockResolvedValueOnce('not-a-number');
271
+ const result = await flows.handleNewPR();
272
+ expect(result).toEqual({ completed: false, returnToMenu: true });
273
+ expect(runSubcommand).not.toHaveBeenCalled();
274
+ });
275
+ it('returns CANCELLED when PR number is zero', async () => {
276
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
277
+ vi.mocked(promptInput).mockResolvedValueOnce('0');
278
+ const result = await flows.handleNewPR();
279
+ expect(result).toEqual({ completed: false, returnToMenu: true });
280
+ expect(runSubcommand).not.toHaveBeenCalled();
281
+ });
282
+ it('returns CANCELLED when PR number is negative', async () => {
283
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
284
+ vi.mocked(promptInput).mockResolvedValueOnce('-5');
285
+ const result = await flows.handleNewPR();
286
+ expect(result).toEqual({ completed: false, returnToMenu: true });
287
+ expect(runSubcommand).not.toHaveBeenCalled();
288
+ });
289
+ });
290
+ describe('from-branch flow', () => {
291
+ it('allows selecting from existing branches', async () => {
292
+ vi.mocked(promptChoice)
293
+ .mockResolvedValueOnce('from-branch') // New PR sub-menu
294
+ .mockResolvedValueOnce('feat/existing-branch') // Select branch
295
+ .mockResolvedValueOnce(true); // Draft PR
296
+ vi.mocked(promptInput).mockResolvedValueOnce('main');
297
+ try {
298
+ await flows.handleNewPR();
299
+ }
300
+ catch {
301
+ // Expected
302
+ }
303
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--branch', 'feat/existing-branch']);
304
+ });
305
+ it('allows typing custom branch name', async () => {
306
+ vi.mocked(promptChoice)
307
+ .mockResolvedValueOnce('from-branch')
308
+ .mockResolvedValueOnce('__custom__') // Select custom option
309
+ .mockResolvedValueOnce(true);
310
+ vi.mocked(promptInput)
311
+ .mockResolvedValueOnce('feat/my-new-branch') // Custom branch name
312
+ .mockResolvedValueOnce('main');
313
+ try {
314
+ await flows.handleNewPR();
315
+ }
316
+ catch {
317
+ // Expected
318
+ }
319
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--branch', 'feat/my-new-branch']);
320
+ });
321
+ it('passes --base and --ready flags', async () => {
322
+ vi.mocked(promptChoice)
323
+ .mockResolvedValueOnce('from-branch')
324
+ .mockResolvedValueOnce('fix/bug-fix')
325
+ .mockResolvedValueOnce(false); // Ready for review
326
+ vi.mocked(promptInput).mockResolvedValueOnce('develop');
327
+ try {
328
+ await flows.handleNewPR();
329
+ }
330
+ catch {
331
+ // Expected
332
+ }
333
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', [
334
+ '--branch',
335
+ 'fix/bug-fix',
336
+ '--base',
337
+ 'develop',
338
+ '--ready',
339
+ ]);
340
+ });
341
+ it('returns CANCELLED when branch name is empty', async () => {
342
+ vi.mocked(promptChoice)
343
+ .mockResolvedValueOnce('from-branch')
344
+ .mockResolvedValueOnce('__custom__');
345
+ vi.mocked(promptInput).mockResolvedValueOnce(''); // Empty branch name
346
+ const result = await flows.handleNewPR();
347
+ expect(result).toEqual({ completed: false, returnToMenu: true });
348
+ expect(runSubcommand).not.toHaveBeenCalled();
349
+ });
350
+ it('handles empty branch list gracefully', async () => {
351
+ // Mock empty branch list
352
+ vi.mocked(git.listLocalBranches).mockReturnValueOnce([]);
353
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-branch');
354
+ vi.mocked(promptInput)
355
+ .mockResolvedValueOnce('feat/new-branch') // Manual branch input
356
+ .mockResolvedValueOnce('main');
357
+ vi.mocked(promptChoice).mockResolvedValueOnce(true); // Draft
358
+ try {
359
+ await flows.handleNewPR();
360
+ }
361
+ catch {
362
+ // Expected
363
+ }
364
+ // Should have prompted for branch name directly
365
+ expect(promptInput).toHaveBeenCalledWith('Branch name');
366
+ });
367
+ });
368
+ });
369
+ describe('handleCleanPRs', () => {
370
+ it('returns CANCELLED when user selects back', async () => {
371
+ vi.mocked(promptChoice).mockResolvedValueOnce('back');
372
+ const result = await flows.handleCleanPRs();
373
+ expect(result).toEqual({ completed: false, returnToMenu: true });
374
+ expect(runSubcommand).not.toHaveBeenCalled();
375
+ });
376
+ describe('clean-all', () => {
377
+ it('calls cleanpr with --all after confirmation', async () => {
378
+ vi.mocked(promptChoice).mockResolvedValueOnce('clean-all');
379
+ vi.mocked(promptConfirm).mockResolvedValueOnce(true);
380
+ try {
381
+ await flows.handleCleanPRs();
382
+ }
383
+ catch {
384
+ // Expected
385
+ }
386
+ expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['--all']);
387
+ });
388
+ it('returns CANCELLED when not confirmed', async () => {
389
+ vi.mocked(promptChoice).mockResolvedValueOnce('clean-all');
390
+ vi.mocked(promptConfirm).mockResolvedValueOnce(false);
391
+ const result = await flows.handleCleanPRs();
392
+ expect(result).toEqual({ completed: false, returnToMenu: true });
393
+ expect(runSubcommand).not.toHaveBeenCalled();
394
+ });
395
+ });
396
+ describe('clean-specific', () => {
397
+ it('calls cleanpr with PR number', async () => {
398
+ vi.mocked(promptChoice).mockResolvedValueOnce('clean-specific');
399
+ vi.mocked(promptInput).mockResolvedValueOnce('42');
400
+ try {
401
+ await flows.handleCleanPRs();
402
+ }
403
+ catch {
404
+ // Expected
405
+ }
406
+ expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['42']);
407
+ });
408
+ it('returns CANCELLED when PR number is empty', async () => {
409
+ vi.mocked(promptChoice).mockResolvedValueOnce('clean-specific');
410
+ vi.mocked(promptInput).mockResolvedValueOnce('');
411
+ const result = await flows.handleCleanPRs();
412
+ expect(result).toEqual({ completed: false, returnToMenu: true });
413
+ });
414
+ it('returns CANCELLED when PR number is invalid', async () => {
415
+ vi.mocked(promptChoice).mockResolvedValueOnce('clean-specific');
416
+ vi.mocked(promptInput).mockResolvedValueOnce('invalid');
417
+ const result = await flows.handleCleanPRs();
418
+ expect(result).toEqual({ completed: false, returnToMenu: true });
419
+ });
420
+ });
421
+ describe('dry-run', () => {
422
+ it('calls cleanpr with --dry-run', async () => {
423
+ vi.mocked(promptChoice).mockResolvedValueOnce('dry-run');
424
+ try {
425
+ await flows.handleCleanPRs();
426
+ }
427
+ catch {
428
+ // Expected
429
+ }
430
+ expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['--dry-run']);
431
+ });
432
+ });
433
+ it('handles user cancellation', async () => {
434
+ vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
435
+ const result = await flows.handleCleanPRs();
436
+ expect(result).toEqual({ completed: false, returnToMenu: true });
437
+ });
438
+ });
439
+ describe('handleLinkConfig', () => {
440
+ it('returns CANCELLED when user selects back', async () => {
441
+ vi.mocked(promptChoice).mockResolvedValueOnce('back');
442
+ const result = await flows.handleLinkConfig();
443
+ expect(result).toEqual({ completed: false, returnToMenu: true });
444
+ expect(runSubcommand).not.toHaveBeenCalled();
445
+ });
446
+ it('view calls wtlink list', async () => {
447
+ vi.mocked(promptChoice).mockResolvedValueOnce('view');
448
+ try {
449
+ await flows.handleLinkConfig();
450
+ }
451
+ catch {
452
+ // Expected
453
+ }
454
+ expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['list']);
455
+ });
456
+ it('sync calls wtlink sync', async () => {
457
+ vi.mocked(promptChoice).mockResolvedValueOnce('sync');
458
+ try {
459
+ await flows.handleLinkConfig();
460
+ }
461
+ catch {
462
+ // Expected
463
+ }
464
+ expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['sync']);
465
+ });
466
+ it('add calls wtlink add with file path', async () => {
467
+ vi.mocked(promptChoice).mockResolvedValueOnce('add');
468
+ vi.mocked(promptInput).mockResolvedValueOnce('.env');
469
+ try {
470
+ await flows.handleLinkConfig();
471
+ }
472
+ catch {
473
+ // Expected
474
+ }
475
+ expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['add', '.env']);
476
+ });
477
+ it('add returns CANCELLED when file path is empty', async () => {
478
+ vi.mocked(promptChoice).mockResolvedValueOnce('add');
479
+ vi.mocked(promptInput).mockResolvedValueOnce('');
480
+ const result = await flows.handleLinkConfig();
481
+ expect(result).toEqual({ completed: false, returnToMenu: true });
482
+ expect(runSubcommand).not.toHaveBeenCalled();
483
+ });
484
+ it('remove calls wtlink remove with file path', async () => {
485
+ vi.mocked(promptChoice).mockResolvedValueOnce('remove');
486
+ vi.mocked(promptInput).mockResolvedValueOnce('.env.local');
487
+ try {
488
+ await flows.handleLinkConfig();
489
+ }
490
+ catch {
491
+ // Expected
492
+ }
493
+ expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['remove', '.env.local']);
494
+ });
495
+ it('remove returns CANCELLED when file path is empty', async () => {
496
+ vi.mocked(promptChoice).mockResolvedValueOnce('remove');
497
+ vi.mocked(promptInput).mockResolvedValueOnce('');
498
+ const result = await flows.handleLinkConfig();
499
+ expect(result).toEqual({ completed: false, returnToMenu: true });
500
+ });
501
+ it('validate calls wtlink validate', async () => {
502
+ vi.mocked(promptChoice).mockResolvedValueOnce('validate');
503
+ try {
504
+ await flows.handleLinkConfig();
505
+ }
506
+ catch {
507
+ // Expected
508
+ }
509
+ expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['validate']);
510
+ });
511
+ it('handles user cancellation', async () => {
512
+ vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
513
+ const result = await flows.handleLinkConfig();
514
+ expect(result).toEqual({ completed: false, returnToMenu: true });
515
+ });
516
+ });
517
+ describe('handleConfigure', () => {
518
+ it('returns CANCELLED when user selects back', async () => {
519
+ vi.mocked(promptChoice).mockResolvedValueOnce('back');
520
+ const result = await flows.handleConfigure();
521
+ expect(result).toEqual({ completed: false, returnToMenu: true });
522
+ expect(runSubcommand).not.toHaveBeenCalled();
523
+ });
524
+ it('view calls wtconfig show', async () => {
525
+ vi.mocked(promptChoice).mockResolvedValueOnce('view');
526
+ try {
527
+ await flows.handleConfigure();
528
+ }
529
+ catch {
530
+ // Expected
531
+ }
532
+ expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['show']);
533
+ });
534
+ it('init calls wtconfig init after confirmation', async () => {
535
+ vi.mocked(promptChoice).mockResolvedValueOnce('init');
536
+ vi.mocked(promptConfirm).mockResolvedValueOnce(true);
537
+ try {
538
+ await flows.handleConfigure();
539
+ }
540
+ catch {
541
+ // Expected
542
+ }
543
+ expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['init']);
544
+ });
545
+ it('init returns CANCELLED when not confirmed', async () => {
546
+ vi.mocked(promptChoice).mockResolvedValueOnce('init');
547
+ vi.mocked(promptConfirm).mockResolvedValueOnce(false);
548
+ const result = await flows.handleConfigure();
549
+ expect(result).toEqual({ completed: false, returnToMenu: true });
550
+ expect(runSubcommand).not.toHaveBeenCalled();
551
+ });
552
+ it('edit calls wtconfig set with setting and value', async () => {
553
+ vi.mocked(promptChoice).mockResolvedValueOnce('edit').mockResolvedValueOnce('baseBranch');
554
+ vi.mocked(promptInput).mockResolvedValueOnce('develop');
555
+ try {
556
+ await flows.handleConfigure();
557
+ }
558
+ catch {
559
+ // Expected
560
+ }
561
+ expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['set', 'baseBranch', 'develop']);
562
+ });
563
+ it('edit returns CANCELLED when value is empty', async () => {
564
+ vi.mocked(promptChoice).mockResolvedValueOnce('edit').mockResolvedValueOnce('branchPrefix');
565
+ vi.mocked(promptInput).mockResolvedValueOnce('');
566
+ const result = await flows.handleConfigure();
567
+ expect(result).toEqual({ completed: false, returnToMenu: true });
568
+ expect(runSubcommand).not.toHaveBeenCalled();
569
+ });
570
+ it('handles user cancellation', async () => {
571
+ vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
572
+ const result = await flows.handleConfigure();
573
+ expect(result).toEqual({ completed: false, returnToMenu: true });
574
+ });
575
+ });
576
+ describe('showMainMenu', () => {
577
+ it('exits on exit selection', async () => {
578
+ vi.mocked(promptChoice).mockResolvedValueOnce('exit');
579
+ await showMainMenu();
580
+ expect(runSubcommand).not.toHaveBeenCalled();
581
+ });
582
+ it('exits on user cancellation', async () => {
583
+ vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
584
+ await showMainMenu();
585
+ expect(runSubcommand).not.toHaveBeenCalled();
586
+ });
587
+ it('re-throws non-cancellation errors', async () => {
588
+ vi.mocked(promptChoice).mockRejectedValueOnce(new Error('Some other error'));
589
+ await expect(showMainMenu()).rejects.toThrow('Some other error');
590
+ });
591
+ it('returns to menu when flow returns returnToMenu=true', async () => {
592
+ vi.mocked(promptChoice)
593
+ .mockResolvedValueOnce('new-pr') // First: select new-pr
594
+ .mockResolvedValueOnce('back') // Then: go back from new-pr sub-menu
595
+ .mockResolvedValueOnce('exit'); // Finally: exit
596
+ await showMainMenu();
597
+ // Should have called promptChoice 3 times (menu -> sub-menu -> back to menu -> exit)
598
+ expect(promptChoice).toHaveBeenCalledTimes(3);
599
+ });
600
+ it('handles list worktrees selection', async () => {
601
+ vi.mocked(promptChoice).mockResolvedValueOnce('list');
602
+ try {
603
+ await showMainMenu();
604
+ }
605
+ catch {
606
+ // Expected - runSubcommand throws
607
+ }
608
+ expect(runSubcommand).toHaveBeenCalledWith('lswt', []);
609
+ });
610
+ it('handles show state selection', async () => {
611
+ vi.mocked(promptChoice).mockResolvedValueOnce('state');
612
+ try {
613
+ await showMainMenu();
614
+ }
615
+ catch {
616
+ // Expected
617
+ }
618
+ expect(runSubcommand).toHaveBeenCalledWith('wtstate', []);
619
+ });
620
+ });
621
+ describe('FlowResult types', () => {
622
+ it('CANCELLED has correct structure', async () => {
623
+ vi.mocked(promptChoice).mockResolvedValueOnce('back');
624
+ const result = await flows.handleNewPR();
625
+ expect(result.completed).toBe(false);
626
+ expect(result.returnToMenu).toBe(true);
627
+ });
628
+ it('flows that run subcommands return COMPLETED_EXIT', async () => {
629
+ vi.mocked(promptChoice).mockResolvedValueOnce('dry-run');
630
+ // We can't test the actual return value since runSubcommand throws,
631
+ // but we can verify the flow attempted to call the subcommand
632
+ try {
633
+ await flows.handleCleanPRs();
634
+ }
635
+ catch {
636
+ // Expected
637
+ }
638
+ expect(runSubcommand).toHaveBeenCalled();
639
+ });
640
+ });
641
+ });
642
+ describe('Config loading in flows', () => {
643
+ beforeEach(() => {
644
+ vi.clearAllMocks();
645
+ });
646
+ it('uses config default for base branch', async () => {
647
+ // Set up config mock to return custom baseBranch
648
+ vi.mocked(loadConfig).mockReturnValueOnce({
649
+ sharedRepos: [],
650
+ baseBranch: 'develop',
651
+ draftPr: true,
652
+ worktreePattern: '{repo}.pr{number}',
653
+ worktreeParent: '..',
654
+ syncPatterns: [],
655
+ branchPrefix: 'feat',
656
+ preferredEditor: 'vscode',
657
+ ai: {
658
+ provider: 'auto',
659
+ fallback: 'none',
660
+ branchName: false,
661
+ prTitle: false,
662
+ prDescription: false,
663
+ commitMessage: false,
664
+ planDocument: false,
665
+ },
666
+ hooks: {},
667
+ hookDefaults: { timeout: 30000, maxTimeout: 60000 },
668
+ plugins: [],
669
+ generators: {},
670
+ integrations: {},
671
+ logging: { level: 'info', timestamps: true },
672
+ global: { warnNotGlobal: true },
673
+ });
674
+ vi.mocked(promptChoice).mockResolvedValueOnce('from-description').mockResolvedValueOnce(true);
675
+ vi.mocked(promptInput).mockResolvedValueOnce('Test feature').mockResolvedValueOnce('develop'); // User accepts default
676
+ vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
677
+ try {
678
+ await flows.handleNewPR();
679
+ }
680
+ catch {
681
+ // Expected
682
+ }
683
+ // Verify loadConfig was called
684
+ expect(loadConfig).toHaveBeenCalled();
685
+ // Since user entered 'develop' (matching config default), no --base flag
686
+ expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Test feature', '--base', 'develop']);
687
+ });
688
+ });
689
+ describe('Git branch listing in flows', () => {
690
+ beforeEach(() => {
691
+ vi.clearAllMocks();
692
+ });
693
+ it('filters out main/master/develop from branch selection', async () => {
694
+ // The mock already returns ['feat/existing-branch', 'fix/bug-fix', 'main', 'develop']
695
+ // The flow should filter out main and develop
696
+ vi.mocked(promptChoice)
697
+ .mockResolvedValueOnce('from-branch')
698
+ .mockResolvedValueOnce('feat/existing-branch')
699
+ .mockResolvedValueOnce(true);
700
+ vi.mocked(promptInput).mockResolvedValueOnce('main');
701
+ try {
702
+ await flows.handleNewPR();
703
+ }
704
+ catch {
705
+ // Expected
706
+ }
707
+ // Check that listLocalBranches was called
708
+ expect(git.listLocalBranches).toHaveBeenCalled();
709
+ });
710
+ });
711
+ //# sourceMappingURL=interactive-menu.test.js.map