@camaradesuk/git-worktree-tools 1.4.1 → 1.6.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 (200) hide show
  1. package/README.md +82 -11
  2. package/dist/api/list.d.ts +10 -4
  3. package/dist/api/list.d.ts.map +1 -1
  4. package/dist/api/list.js +5 -1
  5. package/dist/api/list.js.map +1 -1
  6. package/dist/api/list.test.d.ts +5 -0
  7. package/dist/api/list.test.d.ts.map +1 -0
  8. package/dist/api/list.test.js +390 -0
  9. package/dist/api/list.test.js.map +1 -0
  10. package/dist/cli/cleanpr.js +35 -4
  11. package/dist/cli/cleanpr.js.map +1 -1
  12. package/dist/cli/cleanpr.test.js +254 -0
  13. package/dist/cli/cleanpr.test.js.map +1 -1
  14. package/dist/cli/lswt.js +54 -6
  15. package/dist/cli/lswt.js.map +1 -1
  16. package/dist/cli/lswt.test.js +207 -0
  17. package/dist/cli/lswt.test.js.map +1 -1
  18. package/dist/cli/newpr.js +94 -66
  19. package/dist/cli/newpr.js.map +1 -1
  20. package/dist/cli/newpr.test.js +10 -9
  21. package/dist/cli/newpr.test.js.map +1 -1
  22. package/dist/cli/wt/clean.d.ts +16 -0
  23. package/dist/cli/wt/clean.d.ts.map +1 -0
  24. package/dist/cli/wt/clean.js +64 -0
  25. package/dist/cli/wt/clean.js.map +1 -0
  26. package/dist/cli/wt/completion.d.ts +12 -0
  27. package/dist/cli/wt/completion.d.ts.map +1 -0
  28. package/dist/cli/wt/completion.js +246 -0
  29. package/dist/cli/wt/completion.js.map +1 -0
  30. package/dist/cli/wt/completion.test.d.ts +5 -0
  31. package/dist/cli/wt/completion.test.d.ts.map +1 -0
  32. package/dist/cli/wt/completion.test.js +173 -0
  33. package/dist/cli/wt/completion.test.js.map +1 -0
  34. package/dist/cli/wt/config.d.ts +13 -0
  35. package/dist/cli/wt/config.d.ts.map +1 -0
  36. package/dist/cli/wt/config.js +40 -0
  37. package/dist/cli/wt/config.js.map +1 -0
  38. package/dist/cli/wt/entry.test.d.ts +8 -0
  39. package/dist/cli/wt/entry.test.d.ts.map +1 -0
  40. package/dist/cli/wt/entry.test.js +198 -0
  41. package/dist/cli/wt/entry.test.js.map +1 -0
  42. package/dist/cli/wt/link.d.ts +13 -0
  43. package/dist/cli/wt/link.d.ts.map +1 -0
  44. package/dist/cli/wt/link.js +88 -0
  45. package/dist/cli/wt/link.js.map +1 -0
  46. package/dist/cli/wt/list.d.ts +16 -0
  47. package/dist/cli/wt/list.d.ts.map +1 -0
  48. package/dist/cli/wt/list.js +65 -0
  49. package/dist/cli/wt/list.js.map +1 -0
  50. package/dist/cli/wt/new.d.ts +18 -0
  51. package/dist/cli/wt/new.d.ts.map +1 -0
  52. package/dist/cli/wt/new.js +78 -0
  53. package/dist/cli/wt/new.js.map +1 -0
  54. package/dist/cli/wt/run-command.d.ts +31 -0
  55. package/dist/cli/wt/run-command.d.ts.map +1 -0
  56. package/dist/cli/wt/run-command.js +49 -0
  57. package/dist/cli/wt/run-command.js.map +1 -0
  58. package/dist/cli/wt/run-command.test.d.ts +5 -0
  59. package/dist/cli/wt/run-command.test.d.ts.map +1 -0
  60. package/dist/cli/wt/run-command.test.js +88 -0
  61. package/dist/cli/wt/run-command.test.js.map +1 -0
  62. package/dist/cli/wt/state.d.ts +13 -0
  63. package/dist/cli/wt/state.d.ts.map +1 -0
  64. package/dist/cli/wt/state.js +38 -0
  65. package/dist/cli/wt/state.js.map +1 -0
  66. package/dist/cli/wt/wt.test.d.ts +8 -0
  67. package/dist/cli/wt/wt.test.d.ts.map +1 -0
  68. package/dist/cli/wt/wt.test.js +378 -0
  69. package/dist/cli/wt/wt.test.js.map +1 -0
  70. package/dist/cli/wt.d.ts +25 -0
  71. package/dist/cli/wt.d.ts.map +1 -0
  72. package/dist/cli/wt.js +74 -0
  73. package/dist/cli/wt.js.map +1 -0
  74. package/dist/cli/wtconfig.js +4 -4
  75. package/dist/cli/wtconfig.js.map +1 -1
  76. package/dist/cli/wtlink.js +66 -9
  77. package/dist/cli/wtlink.js.map +1 -1
  78. package/dist/cli/wtlink.test.js +101 -0
  79. package/dist/cli/wtlink.test.js.map +1 -1
  80. package/dist/e2e/cli.e2e.test.js +156 -1
  81. package/dist/e2e/cli.e2e.test.js.map +1 -1
  82. package/dist/e2e/lswt/lswt.e2e.test.js +33 -0
  83. package/dist/e2e/lswt/lswt.e2e.test.js.map +1 -1
  84. package/dist/e2e/newpr-full-flow.e2e.test.d.ts +2 -0
  85. package/dist/e2e/newpr-full-flow.e2e.test.d.ts.map +1 -0
  86. package/dist/e2e/newpr-full-flow.e2e.test.js +279 -0
  87. package/dist/e2e/newpr-full-flow.e2e.test.js.map +1 -0
  88. package/dist/e2e/wtlink/wtlink.e2e.test.js +52 -0
  89. package/dist/e2e/wtlink/wtlink.e2e.test.js.map +1 -1
  90. package/dist/integration/lswt-remote-pr.integration.test.d.ts +2 -0
  91. package/dist/integration/lswt-remote-pr.integration.test.d.ts.map +1 -0
  92. package/dist/integration/lswt-remote-pr.integration.test.js +222 -0
  93. package/dist/integration/lswt-remote-pr.integration.test.js.map +1 -0
  94. package/dist/integration/newpr-branchfrom-head.integration.test.d.ts +2 -0
  95. package/dist/integration/newpr-branchfrom-head.integration.test.d.ts.map +1 -0
  96. package/dist/integration/newpr-branchfrom-head.integration.test.js +498 -0
  97. package/dist/integration/newpr-branchfrom-head.integration.test.js.map +1 -0
  98. package/dist/lib/git.d.ts +1 -0
  99. package/dist/lib/git.d.ts.map +1 -1
  100. package/dist/lib/git.js +17 -30
  101. package/dist/lib/git.js.map +1 -1
  102. package/dist/lib/git.test.js +154 -123
  103. package/dist/lib/git.test.js.map +1 -1
  104. package/dist/lib/github.d.ts +45 -0
  105. package/dist/lib/github.d.ts.map +1 -1
  106. package/dist/lib/github.js +172 -0
  107. package/dist/lib/github.js.map +1 -1
  108. package/dist/lib/github.test.js +127 -1
  109. package/dist/lib/github.test.js.map +1 -1
  110. package/dist/lib/json-output.d.ts +11 -1
  111. package/dist/lib/json-output.d.ts.map +1 -1
  112. package/dist/lib/json-output.js +42 -1
  113. package/dist/lib/json-output.js.map +1 -1
  114. package/dist/lib/json-output.test.js +2 -0
  115. package/dist/lib/json-output.test.js.map +1 -1
  116. package/dist/lib/lswt/action-executors.d.ts.map +1 -1
  117. package/dist/lib/lswt/action-executors.js +143 -35
  118. package/dist/lib/lswt/action-executors.js.map +1 -1
  119. package/dist/lib/lswt/action-executors.test.js +362 -0
  120. package/dist/lib/lswt/action-executors.test.js.map +1 -1
  121. package/dist/lib/lswt/actions.d.ts.map +1 -1
  122. package/dist/lib/lswt/actions.js +38 -0
  123. package/dist/lib/lswt/actions.js.map +1 -1
  124. package/dist/lib/lswt/actions.test.js +126 -0
  125. package/dist/lib/lswt/actions.test.js.map +1 -1
  126. package/dist/lib/lswt/environment.d.ts +4 -0
  127. package/dist/lib/lswt/environment.d.ts.map +1 -1
  128. package/dist/lib/lswt/environment.js +23 -0
  129. package/dist/lib/lswt/environment.js.map +1 -1
  130. package/dist/lib/lswt/environment.test.js +129 -2
  131. package/dist/lib/lswt/environment.test.js.map +1 -1
  132. package/dist/lib/lswt/formatters.d.ts +2 -1
  133. package/dist/lib/lswt/formatters.d.ts.map +1 -1
  134. package/dist/lib/lswt/formatters.js +27 -2
  135. package/dist/lib/lswt/formatters.js.map +1 -1
  136. package/dist/lib/lswt/formatters.test.js +66 -2
  137. package/dist/lib/lswt/formatters.test.js.map +1 -1
  138. package/dist/lib/lswt/fuzzy-search.d.ts +27 -0
  139. package/dist/lib/lswt/fuzzy-search.d.ts.map +1 -0
  140. package/dist/lib/lswt/fuzzy-search.js +130 -0
  141. package/dist/lib/lswt/fuzzy-search.js.map +1 -0
  142. package/dist/lib/lswt/fuzzy-search.test.d.ts +5 -0
  143. package/dist/lib/lswt/fuzzy-search.test.d.ts.map +1 -0
  144. package/dist/lib/lswt/fuzzy-search.test.js +207 -0
  145. package/dist/lib/lswt/fuzzy-search.test.js.map +1 -0
  146. package/dist/lib/lswt/index.d.ts +3 -1
  147. package/dist/lib/lswt/index.d.ts.map +1 -1
  148. package/dist/lib/lswt/index.js +3 -2
  149. package/dist/lib/lswt/index.js.map +1 -1
  150. package/dist/lib/lswt/interactive.d.ts +50 -4
  151. package/dist/lib/lswt/interactive.d.ts.map +1 -1
  152. package/dist/lib/lswt/interactive.js +458 -56
  153. package/dist/lib/lswt/interactive.js.map +1 -1
  154. package/dist/lib/lswt/interactive.test.js +454 -66
  155. package/dist/lib/lswt/interactive.test.js.map +1 -1
  156. package/dist/lib/lswt/types.d.ts +8 -2
  157. package/dist/lib/lswt/types.d.ts.map +1 -1
  158. package/dist/lib/lswt/worktree-info.d.ts +11 -0
  159. package/dist/lib/lswt/worktree-info.d.ts.map +1 -1
  160. package/dist/lib/lswt/worktree-info.js +48 -0
  161. package/dist/lib/lswt/worktree-info.js.map +1 -1
  162. package/dist/lib/lswt/worktree-info.test.js +169 -0
  163. package/dist/lib/lswt/worktree-info.test.js.map +1 -1
  164. package/dist/lib/newpr/action-deps.test.d.ts +5 -0
  165. package/dist/lib/newpr/action-deps.test.d.ts.map +1 -0
  166. package/dist/lib/newpr/action-deps.test.js +111 -0
  167. package/dist/lib/newpr/action-deps.test.js.map +1 -0
  168. package/dist/lib/newpr/args.d.ts.map +1 -1
  169. package/dist/lib/newpr/args.js +6 -2
  170. package/dist/lib/newpr/args.js.map +1 -1
  171. package/dist/lib/newpr/args.test.js +209 -1
  172. package/dist/lib/newpr/args.test.js.map +1 -1
  173. package/dist/lib/newpr/scenario-handler.d.ts.map +1 -1
  174. package/dist/lib/newpr/scenario-handler.js +14 -5
  175. package/dist/lib/newpr/scenario-handler.js.map +1 -1
  176. package/dist/lib/newpr/scenario-handler.test.js +6 -0
  177. package/dist/lib/newpr/scenario-handler.test.js.map +1 -1
  178. package/dist/lib/prompts.d.ts +4 -0
  179. package/dist/lib/prompts.d.ts.map +1 -1
  180. package/dist/lib/prompts.js +178 -1
  181. package/dist/lib/prompts.js.map +1 -1
  182. package/dist/lib/prompts.test.js +279 -0
  183. package/dist/lib/prompts.test.js.map +1 -1
  184. package/dist/lib/wtlink/link-configs.test.js +282 -2
  185. package/dist/lib/wtlink/link-configs.test.js.map +1 -1
  186. package/dist/lib/wtlink/main-menu.js +1 -0
  187. package/dist/lib/wtlink/main-menu.js.map +1 -1
  188. package/dist/lib/wtlink/main-menu.test.d.ts +5 -0
  189. package/dist/lib/wtlink/main-menu.test.d.ts.map +1 -0
  190. package/dist/lib/wtlink/main-menu.test.js +124 -0
  191. package/dist/lib/wtlink/main-menu.test.js.map +1 -0
  192. package/dist/lib/wtlink/manage-manifest.d.ts +5 -0
  193. package/dist/lib/wtlink/manage-manifest.d.ts.map +1 -1
  194. package/dist/lib/wtlink/manage-manifest.js +65 -2
  195. package/dist/lib/wtlink/manage-manifest.js.map +1 -1
  196. package/dist/lib/wtlink/manage-manifest.test.js +144 -2
  197. package/dist/lib/wtlink/manage-manifest.test.js.map +1 -1
  198. package/dist/mcp/server.test.js +49 -0
  199. package/dist/mcp/server.test.js.map +1 -1
  200. package/package.json +2 -1
@@ -1,12 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { formatWorktreeChoiceWithColors, formatTypeBadgeWithColors, formatStatusWithColors, runInteractiveMode, } from './interactive.js';
3
- // Mock inquirer
4
- vi.mock('inquirer', () => ({
5
- default: {
6
- prompt: vi.fn(),
7
- Separator: vi.fn().mockImplementation(() => ({ type: 'separator' })),
8
- },
9
- }));
2
+ import { formatWorktreeChoiceWithColors, formatTypeBadgeWithColors, formatStatusWithColors, runInteractiveMode, getActionForShortcut, getBadgeText, computeMaxBadgeWidth, } from './interactive.js';
10
3
  // Mock git
11
4
  vi.mock('../git.js', () => ({
12
5
  getRepoRoot: vi.fn(),
@@ -45,11 +38,17 @@ vi.mock('./worktree-info.js', () => ({
45
38
  gatherWorktreeInfo: vi.fn().mockResolvedValue([]),
46
39
  createDefaultDeps: vi.fn().mockReturnValue({}),
47
40
  }));
48
- import inquirer from 'inquirer';
49
41
  import * as git from '../git.js';
50
42
  import { executeAction } from './action-executors.js';
51
43
  import { gatherWorktreeInfo } from './worktree-info.js';
52
44
  describe('lswt/interactive', () => {
45
+ // Helper to create mock interactive deps
46
+ const createMockDeps = (overrides = {}) => ({
47
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
48
+ selectAction: vi.fn().mockResolvedValue('exit'),
49
+ pressEnterToContinue: vi.fn().mockResolvedValue(undefined),
50
+ ...overrides,
51
+ });
53
52
  const makeWorktree = (overrides = {}) => ({
54
53
  path: '/home/user/repo',
55
54
  name: 'repo',
@@ -65,7 +64,8 @@ describe('lswt/interactive', () => {
65
64
  describe('formatTypeBadgeWithColors', () => {
66
65
  it('formats main worktree badge', () => {
67
66
  const worktree = makeWorktree({ type: 'main' });
68
- const result = formatTypeBadgeWithColors(worktree);
67
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
68
+ const result = formatTypeBadgeWithColors(worktree, badgeWidth);
69
69
  expect(result).toContain('[main]');
70
70
  });
71
71
  it('formats PR worktree badge', () => {
@@ -74,7 +74,8 @@ describe('lswt/interactive', () => {
74
74
  prNumber: 42,
75
75
  prState: 'OPEN',
76
76
  });
77
- const result = formatTypeBadgeWithColors(worktree);
77
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
78
+ const result = formatTypeBadgeWithColors(worktree, badgeWidth);
78
79
  expect(result).toContain('[PR #42]');
79
80
  });
80
81
  it('formats draft PR badge with DRAFT indicator', () => {
@@ -84,7 +85,8 @@ describe('lswt/interactive', () => {
84
85
  prState: 'OPEN',
85
86
  isDraft: true,
86
87
  });
87
- const result = formatTypeBadgeWithColors(worktree);
88
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
89
+ const result = formatTypeBadgeWithColors(worktree, badgeWidth);
88
90
  expect(result).toContain('DRAFT');
89
91
  expect(result).toContain('#42');
90
92
  });
@@ -93,7 +95,8 @@ describe('lswt/interactive', () => {
93
95
  type: 'branch',
94
96
  branch: 'feature-branch',
95
97
  });
96
- const result = formatTypeBadgeWithColors(worktree);
98
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
99
+ const result = formatTypeBadgeWithColors(worktree, badgeWidth);
97
100
  expect(result).toContain('[branch]');
98
101
  });
99
102
  it('formats detached worktree badge', () => {
@@ -101,9 +104,33 @@ describe('lswt/interactive', () => {
101
104
  type: 'detached',
102
105
  branch: null,
103
106
  });
104
- const result = formatTypeBadgeWithColors(worktree);
107
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
108
+ const result = formatTypeBadgeWithColors(worktree, badgeWidth);
105
109
  expect(result).toContain('[detached]');
106
110
  });
111
+ it('formats remote PR worktree badge', () => {
112
+ const worktree = makeWorktree({
113
+ type: 'remote_pr',
114
+ prNumber: 42,
115
+ prState: 'OPEN',
116
+ });
117
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
118
+ const result = formatTypeBadgeWithColors(worktree, badgeWidth);
119
+ expect(result).toContain('[PR #42 REMOTE]');
120
+ });
121
+ it('formats remote PR draft worktree badge', () => {
122
+ const worktree = makeWorktree({
123
+ type: 'remote_pr',
124
+ prNumber: 42,
125
+ prState: 'OPEN',
126
+ isDraft: true,
127
+ });
128
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
129
+ const result = formatTypeBadgeWithColors(worktree, badgeWidth);
130
+ expect(result).toContain('REMOTE');
131
+ expect(result).toContain('DRAFT');
132
+ expect(result).toContain('#42');
133
+ });
107
134
  });
108
135
  describe('formatStatusWithColors', () => {
109
136
  it('returns empty string when no status info', () => {
@@ -170,10 +197,74 @@ describe('lswt/interactive', () => {
170
197
  expect(result).toContain('has changes');
171
198
  });
172
199
  });
200
+ describe('getBadgeText', () => {
201
+ it('returns [main] for main worktree', () => {
202
+ const worktree = makeWorktree({ type: 'main' });
203
+ expect(getBadgeText(worktree)).toBe('[main]');
204
+ });
205
+ it('returns [PR #N] for PR worktree', () => {
206
+ const worktree = makeWorktree({ type: 'pr', prNumber: 42 });
207
+ expect(getBadgeText(worktree)).toBe('[PR #42]');
208
+ });
209
+ it('returns [PR #N DRAFT] for draft PR worktree', () => {
210
+ const worktree = makeWorktree({ type: 'pr', prNumber: 42, isDraft: true });
211
+ expect(getBadgeText(worktree)).toBe('[PR #42 DRAFT]');
212
+ });
213
+ it('returns [PR #N REMOTE] for remote PR worktree', () => {
214
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42 });
215
+ expect(getBadgeText(worktree)).toBe('[PR #42 REMOTE]');
216
+ });
217
+ it('returns [PR #N REMOTE DRAFT] for remote draft PR worktree', () => {
218
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, isDraft: true });
219
+ expect(getBadgeText(worktree)).toBe('[PR #42 REMOTE DRAFT]');
220
+ });
221
+ it('returns [branch] for branch worktree', () => {
222
+ const worktree = makeWorktree({ type: 'branch' });
223
+ expect(getBadgeText(worktree)).toBe('[branch]');
224
+ });
225
+ it('returns [detached] for detached worktree', () => {
226
+ const worktree = makeWorktree({ type: 'detached' });
227
+ expect(getBadgeText(worktree)).toBe('[detached]');
228
+ });
229
+ it('handles large PR numbers correctly', () => {
230
+ const worktree = makeWorktree({ type: 'pr', prNumber: 12345 });
231
+ expect(getBadgeText(worktree)).toBe('[PR #12345]');
232
+ });
233
+ });
234
+ describe('computeMaxBadgeWidth', () => {
235
+ it('returns sensible default for empty array', () => {
236
+ expect(computeMaxBadgeWidth([])).toBe(12);
237
+ });
238
+ it('computes width based on longest badge', () => {
239
+ const worktrees = [
240
+ makeWorktree({ type: 'main' }), // [main] = 6
241
+ makeWorktree({ type: 'pr', prNumber: 42 }), // [PR #42] = 8
242
+ ];
243
+ // Max is 8 + 2 padding = 10
244
+ expect(computeMaxBadgeWidth(worktrees)).toBe(10);
245
+ });
246
+ it('handles large PR numbers in width calculation', () => {
247
+ const worktrees = [
248
+ makeWorktree({ type: 'main' }), // [main] = 6
249
+ makeWorktree({ type: 'pr', prNumber: 99999 }), // [PR #99999] = 11
250
+ ];
251
+ // Max is 11 + 2 padding = 13
252
+ expect(computeMaxBadgeWidth(worktrees)).toBe(13);
253
+ });
254
+ it('handles remote PRs in width calculation', () => {
255
+ const worktrees = [
256
+ makeWorktree({ type: 'main' }), // [main] = 6
257
+ makeWorktree({ type: 'remote_pr', prNumber: 42, isDraft: true }), // [PR #42 REMOTE DRAFT] = 21
258
+ ];
259
+ // Max is 21 + 2 padding = 23
260
+ expect(computeMaxBadgeWidth(worktrees)).toBe(23);
261
+ });
262
+ });
173
263
  describe('formatWorktreeChoiceWithColors', () => {
174
264
  it('includes type badge', () => {
175
265
  const worktree = makeWorktree({ type: 'main' });
176
- const result = formatWorktreeChoiceWithColors(worktree);
266
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
267
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
177
268
  expect(result).toContain('[main]');
178
269
  });
179
270
  it('includes branch name', () => {
@@ -181,7 +272,8 @@ describe('lswt/interactive', () => {
181
272
  type: 'branch',
182
273
  branch: 'feature-xyz',
183
274
  });
184
- const result = formatWorktreeChoiceWithColors(worktree);
275
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
276
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
185
277
  expect(result).toContain('feature-xyz');
186
278
  });
187
279
  it('shows (detached) for detached worktrees', () => {
@@ -189,7 +281,8 @@ describe('lswt/interactive', () => {
189
281
  type: 'detached',
190
282
  branch: null,
191
283
  });
192
- const result = formatWorktreeChoiceWithColors(worktree);
284
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
285
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
193
286
  expect(result).toContain('(detached)');
194
287
  });
195
288
  it('includes status for PR worktrees', () => {
@@ -199,7 +292,8 @@ describe('lswt/interactive', () => {
199
292
  prState: 'OPEN',
200
293
  branch: 'feat/something',
201
294
  });
202
- const result = formatWorktreeChoiceWithColors(worktree);
295
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
296
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
203
297
  expect(result).toContain('[PR #42]');
204
298
  expect(result).toContain('feat/something');
205
299
  expect(result).toContain('OPEN');
@@ -212,7 +306,8 @@ describe('lswt/interactive', () => {
212
306
  isDraft: true,
213
307
  branch: 'feat/something',
214
308
  });
215
- const result = formatWorktreeChoiceWithColors(worktree);
309
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
310
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
216
311
  expect(result).toContain('DRAFT');
217
312
  });
218
313
  it('includes has changes indicator', () => {
@@ -221,9 +316,196 @@ describe('lswt/interactive', () => {
221
316
  branch: 'feature',
222
317
  hasChanges: true,
223
318
  });
224
- const result = formatWorktreeChoiceWithColors(worktree);
319
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
320
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
225
321
  expect(result).toContain('has changes');
226
322
  });
323
+ it('shows PR title for remote PRs instead of branch', () => {
324
+ const worktree = makeWorktree({
325
+ type: 'remote_pr',
326
+ prNumber: 42,
327
+ prState: 'OPEN',
328
+ branch: 'feat/some-long-branch-name',
329
+ prTitle: 'Add amazing new feature',
330
+ });
331
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
332
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
333
+ expect(result).toContain('Add amazing new feature');
334
+ expect(result).toContain('[PR #42 REMOTE]');
335
+ });
336
+ it('truncates long PR titles for remote PRs', () => {
337
+ const worktree = makeWorktree({
338
+ type: 'remote_pr',
339
+ prNumber: 42,
340
+ prState: 'OPEN',
341
+ branch: 'feat/feature',
342
+ prTitle: 'This is a very long pull request title that should be truncated for display',
343
+ });
344
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
345
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
346
+ // Title should be truncated to 30 chars with ...
347
+ expect(result).toContain('...');
348
+ });
349
+ it('shows OPEN status for remote PRs', () => {
350
+ const worktree = makeWorktree({
351
+ type: 'remote_pr',
352
+ prNumber: 42,
353
+ prState: 'OPEN',
354
+ prTitle: 'New feature',
355
+ });
356
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
357
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
358
+ expect(result).toContain('OPEN');
359
+ });
360
+ it('includes draft indicator for remote PR drafts', () => {
361
+ const worktree = makeWorktree({
362
+ type: 'remote_pr',
363
+ prNumber: 42,
364
+ prState: 'OPEN',
365
+ isDraft: true,
366
+ prTitle: 'Draft feature',
367
+ });
368
+ const badgeWidth = computeMaxBadgeWidth([worktree]);
369
+ const result = formatWorktreeChoiceWithColors(worktree, badgeWidth);
370
+ expect(result).toContain('DRAFT');
371
+ });
372
+ });
373
+ describe('getActionForShortcut', () => {
374
+ // Basic shortcut mapping tests
375
+ it('returns open_editor for "e" shortcut on regular worktrees', () => {
376
+ const worktree = makeWorktree({ type: 'main' });
377
+ expect(getActionForShortcut('e', worktree)).toBe('open_editor');
378
+ });
379
+ it('returns open_terminal for "t" shortcut', () => {
380
+ const worktree = makeWorktree({ type: 'branch' });
381
+ expect(getActionForShortcut('t', worktree)).toBe('open_terminal');
382
+ });
383
+ it('returns copy_path for "c" shortcut', () => {
384
+ const worktree = makeWorktree({ type: 'main' });
385
+ expect(getActionForShortcut('c', worktree)).toBe('copy_path');
386
+ });
387
+ it('returns show_details for "d" shortcut', () => {
388
+ const worktree = makeWorktree({ type: 'main' });
389
+ expect(getActionForShortcut('d', worktree)).toBe('show_details');
390
+ });
391
+ it('returns link_configs for "l" shortcut', () => {
392
+ const worktree = makeWorktree({ type: 'main' });
393
+ expect(getActionForShortcut('l', worktree)).toBe('link_configs');
394
+ });
395
+ it('returns exit for "q" shortcut', () => {
396
+ const worktree = makeWorktree({ type: 'main' });
397
+ expect(getActionForShortcut('q', worktree)).toBe('exit');
398
+ });
399
+ it('returns null for unknown shortcut keys', () => {
400
+ const worktree = makeWorktree({ type: 'main' });
401
+ expect(getActionForShortcut('x', worktree)).toBeNull();
402
+ expect(getActionForShortcut('z', worktree)).toBeNull();
403
+ expect(getActionForShortcut('1', worktree)).toBeNull();
404
+ });
405
+ // "p" key behavior tests
406
+ describe('"p" key behavior', () => {
407
+ it('returns open_pr_url for "p" on PR worktree', () => {
408
+ const worktree = makeWorktree({ type: 'pr', prNumber: 42, prState: 'OPEN' });
409
+ expect(getActionForShortcut('p', worktree)).toBe('open_pr_url');
410
+ });
411
+ it('returns create_pr for "p" on branch worktree', () => {
412
+ const worktree = makeWorktree({ type: 'branch', branch: 'feature' });
413
+ expect(getActionForShortcut('p', worktree)).toBe('create_pr');
414
+ });
415
+ it('returns open_pr_url for "p" on remote_pr worktree', () => {
416
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
417
+ expect(getActionForShortcut('p', worktree)).toBe('open_pr_url');
418
+ });
419
+ it('returns null for "p" on main worktree', () => {
420
+ const worktree = makeWorktree({ type: 'main' });
421
+ expect(getActionForShortcut('p', worktree)).toBeNull();
422
+ });
423
+ it('returns null for "p" on detached worktree', () => {
424
+ const worktree = makeWorktree({ type: 'detached' });
425
+ expect(getActionForShortcut('p', worktree)).toBeNull();
426
+ });
427
+ });
428
+ // "w" key behavior tests (checkout_pr - only for remote_pr)
429
+ describe('"w" key behavior (checkout_pr)', () => {
430
+ it('returns checkout_pr for "w" on remote_pr worktree', () => {
431
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
432
+ expect(getActionForShortcut('w', worktree)).toBe('checkout_pr');
433
+ });
434
+ it('returns null for "w" on main worktree', () => {
435
+ const worktree = makeWorktree({ type: 'main' });
436
+ expect(getActionForShortcut('w', worktree)).toBeNull();
437
+ });
438
+ it('returns null for "w" on branch worktree', () => {
439
+ const worktree = makeWorktree({ type: 'branch' });
440
+ expect(getActionForShortcut('w', worktree)).toBeNull();
441
+ });
442
+ it('returns null for "w" on pr worktree', () => {
443
+ const worktree = makeWorktree({ type: 'pr', prNumber: 42, prState: 'OPEN' });
444
+ expect(getActionForShortcut('w', worktree)).toBeNull();
445
+ });
446
+ it('returns null for "w" on detached worktree', () => {
447
+ const worktree = makeWorktree({ type: 'detached' });
448
+ expect(getActionForShortcut('w', worktree)).toBeNull();
449
+ });
450
+ });
451
+ // "r" key behavior tests (remove_worktree)
452
+ describe('"r" key behavior (remove_worktree)', () => {
453
+ it('returns remove_worktree for "r" on branch worktree', () => {
454
+ const worktree = makeWorktree({ type: 'branch' });
455
+ expect(getActionForShortcut('r', worktree)).toBe('remove_worktree');
456
+ });
457
+ it('returns remove_worktree for "r" on pr worktree', () => {
458
+ const worktree = makeWorktree({ type: 'pr', prNumber: 42, prState: 'OPEN' });
459
+ expect(getActionForShortcut('r', worktree)).toBe('remove_worktree');
460
+ });
461
+ it('returns remove_worktree for "r" on detached worktree', () => {
462
+ const worktree = makeWorktree({ type: 'detached' });
463
+ expect(getActionForShortcut('r', worktree)).toBe('remove_worktree');
464
+ });
465
+ it('returns null for "r" on main worktree', () => {
466
+ const worktree = makeWorktree({ type: 'main' });
467
+ expect(getActionForShortcut('r', worktree)).toBeNull();
468
+ });
469
+ });
470
+ // Remote PR limited actions tests
471
+ describe('remote_pr limited actions', () => {
472
+ it('returns null for "e" (open_editor) on remote_pr', () => {
473
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
474
+ expect(getActionForShortcut('e', worktree)).toBeNull();
475
+ });
476
+ it('returns null for "t" (open_terminal) on remote_pr', () => {
477
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
478
+ expect(getActionForShortcut('t', worktree)).toBeNull();
479
+ });
480
+ it('returns null for "c" (copy_path) on remote_pr', () => {
481
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
482
+ expect(getActionForShortcut('c', worktree)).toBeNull();
483
+ });
484
+ it('returns null for "l" (link_configs) on remote_pr', () => {
485
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
486
+ expect(getActionForShortcut('l', worktree)).toBeNull();
487
+ });
488
+ it('returns null for "r" (remove_worktree) on remote_pr', () => {
489
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
490
+ expect(getActionForShortcut('r', worktree)).toBeNull();
491
+ });
492
+ it('allows "w" (checkout_pr) on remote_pr', () => {
493
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
494
+ expect(getActionForShortcut('w', worktree)).toBe('checkout_pr');
495
+ });
496
+ it('allows "p" (open_pr_url) on remote_pr', () => {
497
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
498
+ expect(getActionForShortcut('p', worktree)).toBe('open_pr_url');
499
+ });
500
+ it('allows "d" (show_details) on remote_pr', () => {
501
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
502
+ expect(getActionForShortcut('d', worktree)).toBe('show_details');
503
+ });
504
+ it('allows "q" (exit) on remote_pr', () => {
505
+ const worktree = makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN' });
506
+ expect(getActionForShortcut('q', worktree)).toBe('exit');
507
+ });
508
+ });
227
509
  });
228
510
  describe('runInteractiveMode', () => {
229
511
  const defaultOptions = {
@@ -243,39 +525,48 @@ describe('lswt/interactive', () => {
243
525
  });
244
526
  it('returns early with error when not in git repository', async () => {
245
527
  vi.mocked(git.getRepoRoot).mockReturnValue(null);
246
- await runInteractiveMode([makeWorktree()], defaultOptions);
528
+ const deps = createMockDeps();
529
+ await runInteractiveMode([makeWorktree()], defaultOptions, deps);
247
530
  expect(console.error).toHaveBeenCalled();
248
531
  });
249
532
  it('returns early when no worktrees provided', async () => {
250
533
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
251
- await runInteractiveMode([], defaultOptions);
534
+ const deps = createMockDeps();
535
+ await runInteractiveMode([], defaultOptions, deps);
252
536
  expect(console.log).toHaveBeenCalledWith(expect.stringContaining('No worktrees found'));
253
537
  });
254
538
  it('exits when user selects exit from worktree list', async () => {
255
539
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
256
- vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selected: null }); // Exit selection
257
- await runInteractiveMode([makeWorktree()], defaultOptions);
540
+ const deps = createMockDeps({
541
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
542
+ });
543
+ await runInteractiveMode([makeWorktree()], defaultOptions, deps);
258
544
  expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Goodbye'));
259
545
  });
260
546
  it('exits when user selects exit action', async () => {
261
547
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
262
548
  const worktree = makeWorktree();
263
- vi.mocked(inquirer.prompt)
264
- .mockResolvedValueOnce({ selected: worktree }) // Select worktree
265
- .mockResolvedValueOnce({ action: 'exit' }); // Select exit action
266
- await runInteractiveMode([worktree], defaultOptions);
549
+ const deps = createMockDeps({
550
+ selectWorktree: vi.fn().mockResolvedValue({ worktree, action: null }),
551
+ selectAction: vi.fn().mockResolvedValue('exit'),
552
+ });
553
+ await runInteractiveMode([worktree], defaultOptions, deps);
267
554
  expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Goodbye'));
268
555
  });
269
556
  it('continues loop when user selects back action', async () => {
270
557
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
271
558
  const worktree = makeWorktree();
272
- vi.mocked(inquirer.prompt)
273
- .mockResolvedValueOnce({ selected: worktree }) // First loop - select worktree
274
- .mockResolvedValueOnce({ action: 'back' }) // First loop - go back
275
- .mockResolvedValueOnce({ selected: null }); // Second loop - exit
276
- await runInteractiveMode([worktree], defaultOptions);
277
- // Prompt should be called 3 times
278
- expect(inquirer.prompt).toHaveBeenCalledTimes(3);
559
+ const selectWorktreeMock = vi
560
+ .fn()
561
+ .mockResolvedValueOnce({ worktree, action: null }) // First loop - select worktree
562
+ .mockResolvedValueOnce({ worktree: null, action: null }); // Second loop - exit
563
+ const deps = createMockDeps({
564
+ selectWorktree: selectWorktreeMock,
565
+ selectAction: vi.fn().mockResolvedValue('back'),
566
+ });
567
+ await runInteractiveMode([worktree], defaultOptions, deps);
568
+ // selectWorktree should be called 2 times (first loop, then exit)
569
+ expect(selectWorktreeMock).toHaveBeenCalledTimes(2);
279
570
  });
280
571
  it('executes action and shows success message', async () => {
281
572
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
@@ -284,12 +575,15 @@ describe('lswt/interactive', () => {
284
575
  success: true,
285
576
  message: 'Copied to clipboard',
286
577
  });
287
- vi.mocked(inquirer.prompt)
288
- .mockResolvedValueOnce({ selected: worktree }) // Select worktree
289
- .mockResolvedValueOnce({ action: 'copy_path' }) // Select action
290
- .mockResolvedValueOnce({ continue: '' }) // Press enter to continue
291
- .mockResolvedValueOnce({ selected: null }); // Exit
292
- await runInteractiveMode([worktree], defaultOptions);
578
+ const selectWorktreeMock = vi
579
+ .fn()
580
+ .mockResolvedValueOnce({ worktree, action: null })
581
+ .mockResolvedValueOnce({ worktree: null, action: null });
582
+ const deps = createMockDeps({
583
+ selectWorktree: selectWorktreeMock,
584
+ selectAction: vi.fn().mockResolvedValue('copy_path'),
585
+ });
586
+ await runInteractiveMode([worktree], defaultOptions, deps);
293
587
  expect(executeAction).toHaveBeenCalled();
294
588
  expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Copied to clipboard'));
295
589
  });
@@ -300,12 +594,15 @@ describe('lswt/interactive', () => {
300
594
  success: false,
301
595
  message: 'Failed to copy',
302
596
  });
303
- vi.mocked(inquirer.prompt)
304
- .mockResolvedValueOnce({ selected: worktree }) // Select worktree
305
- .mockResolvedValueOnce({ action: 'copy_path' }) // Select action
306
- .mockResolvedValueOnce({ continue: '' }) // Press enter to continue
307
- .mockResolvedValueOnce({ selected: null }); // Exit
308
- await runInteractiveMode([worktree], defaultOptions);
597
+ const selectWorktreeMock = vi
598
+ .fn()
599
+ .mockResolvedValueOnce({ worktree, action: null })
600
+ .mockResolvedValueOnce({ worktree: null, action: null });
601
+ const deps = createMockDeps({
602
+ selectWorktree: selectWorktreeMock,
603
+ selectAction: vi.fn().mockResolvedValue('copy_path'),
604
+ });
605
+ await runInteractiveMode([worktree], defaultOptions, deps);
309
606
  expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Failed to copy'));
310
607
  });
311
608
  it('refreshes worktree list when action returns shouldRefresh', async () => {
@@ -318,14 +615,34 @@ describe('lswt/interactive', () => {
318
615
  shouldRefresh: true,
319
616
  });
320
617
  vi.mocked(gatherWorktreeInfo).mockResolvedValueOnce([newWorktree]);
321
- vi.mocked(inquirer.prompt)
322
- .mockResolvedValueOnce({ selected: worktree }) // Select worktree
323
- .mockResolvedValueOnce({ action: 'remove_worktree' }) // Select action
324
- .mockResolvedValueOnce({ continue: '' }) // Press enter to continue
325
- .mockResolvedValueOnce({ selected: null }); // Exit
326
- await runInteractiveMode([worktree], defaultOptions);
618
+ const selectWorktreeMock = vi
619
+ .fn()
620
+ .mockResolvedValueOnce({ worktree, action: null })
621
+ .mockResolvedValueOnce({ worktree: null, action: null });
622
+ const deps = createMockDeps({
623
+ selectWorktree: selectWorktreeMock,
624
+ selectAction: vi.fn().mockResolvedValue('remove_worktree'),
625
+ });
626
+ await runInteractiveMode([worktree], defaultOptions, deps);
327
627
  expect(gatherWorktreeInfo).toHaveBeenCalled();
328
628
  });
629
+ it('exits when refresh results in no remaining worktrees', async () => {
630
+ vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
631
+ const worktree = makeWorktree();
632
+ vi.mocked(executeAction).mockResolvedValueOnce({
633
+ success: true,
634
+ message: 'Worktree removed',
635
+ shouldRefresh: true,
636
+ });
637
+ // After refresh, no worktrees remain
638
+ vi.mocked(gatherWorktreeInfo).mockResolvedValueOnce([]);
639
+ const deps = createMockDeps({
640
+ selectWorktree: vi.fn().mockResolvedValue({ worktree, action: null }),
641
+ selectAction: vi.fn().mockResolvedValue('remove_worktree'),
642
+ });
643
+ await runInteractiveMode([worktree], defaultOptions, deps);
644
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('No worktrees remaining'));
645
+ });
329
646
  it('exits immediately when action returns shouldExit', async () => {
330
647
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
331
648
  const worktree = makeWorktree();
@@ -333,12 +650,14 @@ describe('lswt/interactive', () => {
333
650
  success: true,
334
651
  shouldExit: true,
335
652
  });
336
- vi.mocked(inquirer.prompt)
337
- .mockResolvedValueOnce({ selected: worktree }) // Select worktree
338
- .mockResolvedValueOnce({ action: 'open_editor' }); // Select action
339
- await runInteractiveMode([worktree], defaultOptions);
340
- // Should not prompt for continue since shouldExit is true
341
- expect(inquirer.prompt).toHaveBeenCalledTimes(2);
653
+ const selectWorktreeMock = vi.fn().mockResolvedValue({ worktree, action: null });
654
+ const deps = createMockDeps({
655
+ selectWorktree: selectWorktreeMock,
656
+ selectAction: vi.fn().mockResolvedValue('open_editor'),
657
+ });
658
+ await runInteractiveMode([worktree], defaultOptions, deps);
659
+ // selectWorktree should only be called once since shouldExit is true
660
+ expect(selectWorktreeMock).toHaveBeenCalledTimes(1);
342
661
  });
343
662
  it('handles worktree header display correctly', async () => {
344
663
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
@@ -352,8 +671,10 @@ describe('lswt/interactive', () => {
352
671
  hasChanges: true,
353
672
  }),
354
673
  ];
355
- vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selected: null }); // Exit immediately
356
- await runInteractiveMode(worktrees, defaultOptions);
674
+ const deps = createMockDeps({
675
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
676
+ });
677
+ await runInteractiveMode(worktrees, defaultOptions, deps);
357
678
  // Should display header with worktree count
358
679
  expect(console.log).toHaveBeenCalledWith(expect.stringContaining('2 worktrees'));
359
680
  });
@@ -364,9 +685,11 @@ describe('lswt/interactive', () => {
364
685
  makeWorktree({ type: 'pr', prNumber: 1, prState: 'OPEN' }),
365
686
  makeWorktree({ type: 'pr', prNumber: 2, prState: 'MERGED' }),
366
687
  ];
367
- vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selected: null }); // Exit immediately
368
- await runInteractiveMode(worktrees, defaultOptions);
369
- expect(console.log).toHaveBeenCalledWith(expect.stringContaining('2 PRs'));
688
+ const deps = createMockDeps({
689
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
690
+ });
691
+ await runInteractiveMode(worktrees, defaultOptions, deps);
692
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('2 local PRs'));
370
693
  });
371
694
  it('displays changes count in header', async () => {
372
695
  vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
@@ -374,10 +697,75 @@ describe('lswt/interactive', () => {
374
697
  makeWorktree({ type: 'main', hasChanges: true }),
375
698
  makeWorktree({ type: 'branch', hasChanges: true }),
376
699
  ];
377
- vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selected: null }); // Exit immediately
378
- await runInteractiveMode(worktrees, defaultOptions);
700
+ const deps = createMockDeps({
701
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
702
+ });
703
+ await runInteractiveMode(worktrees, defaultOptions, deps);
379
704
  expect(console.log).toHaveBeenCalledWith(expect.stringContaining('2 with changes'));
380
705
  });
706
+ it('displays remote PR count in header', async () => {
707
+ vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
708
+ const worktrees = [
709
+ makeWorktree({ type: 'main' }),
710
+ makeWorktree({ type: 'pr', prNumber: 1, prState: 'OPEN' }),
711
+ makeWorktree({ type: 'remote_pr', prNumber: 10, prState: 'OPEN', prTitle: 'Remote PR 1' }),
712
+ makeWorktree({ type: 'remote_pr', prNumber: 20, prState: 'OPEN', prTitle: 'Remote PR 2' }),
713
+ makeWorktree({ type: 'remote_pr', prNumber: 30, prState: 'OPEN', prTitle: 'Remote PR 3' }),
714
+ ];
715
+ const deps = createMockDeps({
716
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
717
+ });
718
+ await runInteractiveMode(worktrees, defaultOptions, deps);
719
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('3 remote PRs'));
720
+ });
721
+ it('shows worktree shortcut in header when remote PRs are present', async () => {
722
+ vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
723
+ const worktrees = [
724
+ makeWorktree({ type: 'main' }),
725
+ makeWorktree({ type: 'remote_pr', prNumber: 42, prState: 'OPEN', prTitle: 'Remote PR' }),
726
+ ];
727
+ const deps = createMockDeps({
728
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
729
+ });
730
+ await runInteractiveMode(worktrees, defaultOptions, deps);
731
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[w]'));
732
+ });
733
+ it('displays correct local worktree count (excluding remote PRs)', async () => {
734
+ vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
735
+ const worktrees = [
736
+ makeWorktree({ type: 'main' }),
737
+ makeWorktree({ type: 'pr', prNumber: 1, prState: 'OPEN' }),
738
+ makeWorktree({ type: 'remote_pr', prNumber: 10, prState: 'OPEN', prTitle: 'Remote PR' }),
739
+ ];
740
+ const deps = createMockDeps({
741
+ selectWorktree: vi.fn().mockResolvedValue({ worktree: null, action: null }),
742
+ });
743
+ await runInteractiveMode(worktrees, defaultOptions, deps);
744
+ // Should show "2 worktrees" (main + local PR), not 3
745
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('2 worktrees'));
746
+ });
747
+ it('executes shortcut action directly when provided with selection', async () => {
748
+ vi.mocked(git.getRepoRoot).mockReturnValue('/home/user/repo');
749
+ const worktree = makeWorktree();
750
+ vi.mocked(executeAction).mockResolvedValueOnce({
751
+ success: true,
752
+ shouldExit: true,
753
+ });
754
+ // Simulate shortcut key press - returns both worktree and action
755
+ const selectActionMock = vi.fn();
756
+ const deps = createMockDeps({
757
+ selectWorktree: vi.fn().mockResolvedValue({ worktree, action: 'open_editor' }),
758
+ selectAction: selectActionMock,
759
+ });
760
+ await runInteractiveMode([worktree], defaultOptions, deps);
761
+ // executeAction should be called with the shortcut action as first arg
762
+ expect(executeAction).toHaveBeenCalled();
763
+ const callArgs = vi.mocked(executeAction).mock.calls[0];
764
+ expect(callArgs[0]).toBe('open_editor');
765
+ expect(callArgs[1]).toEqual(worktree);
766
+ // selectAction should NOT be called since action was provided via shortcut
767
+ expect(selectActionMock).not.toHaveBeenCalled();
768
+ });
381
769
  });
382
770
  });
383
771
  //# sourceMappingURL=interactive.test.js.map