@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.
- package/README.md +82 -11
- package/dist/api/list.d.ts +10 -4
- package/dist/api/list.d.ts.map +1 -1
- package/dist/api/list.js +5 -1
- package/dist/api/list.js.map +1 -1
- package/dist/api/list.test.d.ts +5 -0
- package/dist/api/list.test.d.ts.map +1 -0
- package/dist/api/list.test.js +390 -0
- package/dist/api/list.test.js.map +1 -0
- package/dist/cli/cleanpr.js +35 -4
- package/dist/cli/cleanpr.js.map +1 -1
- package/dist/cli/cleanpr.test.js +254 -0
- package/dist/cli/cleanpr.test.js.map +1 -1
- package/dist/cli/lswt.js +54 -6
- package/dist/cli/lswt.js.map +1 -1
- package/dist/cli/lswt.test.js +207 -0
- package/dist/cli/lswt.test.js.map +1 -1
- package/dist/cli/newpr.js +94 -66
- package/dist/cli/newpr.js.map +1 -1
- package/dist/cli/newpr.test.js +10 -9
- package/dist/cli/newpr.test.js.map +1 -1
- package/dist/cli/wt/clean.d.ts +16 -0
- package/dist/cli/wt/clean.d.ts.map +1 -0
- package/dist/cli/wt/clean.js +64 -0
- package/dist/cli/wt/clean.js.map +1 -0
- package/dist/cli/wt/completion.d.ts +12 -0
- package/dist/cli/wt/completion.d.ts.map +1 -0
- package/dist/cli/wt/completion.js +246 -0
- package/dist/cli/wt/completion.js.map +1 -0
- package/dist/cli/wt/completion.test.d.ts +5 -0
- package/dist/cli/wt/completion.test.d.ts.map +1 -0
- package/dist/cli/wt/completion.test.js +173 -0
- package/dist/cli/wt/completion.test.js.map +1 -0
- package/dist/cli/wt/config.d.ts +13 -0
- package/dist/cli/wt/config.d.ts.map +1 -0
- package/dist/cli/wt/config.js +40 -0
- package/dist/cli/wt/config.js.map +1 -0
- package/dist/cli/wt/entry.test.d.ts +8 -0
- package/dist/cli/wt/entry.test.d.ts.map +1 -0
- package/dist/cli/wt/entry.test.js +198 -0
- package/dist/cli/wt/entry.test.js.map +1 -0
- package/dist/cli/wt/link.d.ts +13 -0
- package/dist/cli/wt/link.d.ts.map +1 -0
- package/dist/cli/wt/link.js +88 -0
- package/dist/cli/wt/link.js.map +1 -0
- package/dist/cli/wt/list.d.ts +16 -0
- package/dist/cli/wt/list.d.ts.map +1 -0
- package/dist/cli/wt/list.js +65 -0
- package/dist/cli/wt/list.js.map +1 -0
- package/dist/cli/wt/new.d.ts +18 -0
- package/dist/cli/wt/new.d.ts.map +1 -0
- package/dist/cli/wt/new.js +78 -0
- package/dist/cli/wt/new.js.map +1 -0
- package/dist/cli/wt/run-command.d.ts +31 -0
- package/dist/cli/wt/run-command.d.ts.map +1 -0
- package/dist/cli/wt/run-command.js +49 -0
- package/dist/cli/wt/run-command.js.map +1 -0
- package/dist/cli/wt/run-command.test.d.ts +5 -0
- package/dist/cli/wt/run-command.test.d.ts.map +1 -0
- package/dist/cli/wt/run-command.test.js +88 -0
- package/dist/cli/wt/run-command.test.js.map +1 -0
- package/dist/cli/wt/state.d.ts +13 -0
- package/dist/cli/wt/state.d.ts.map +1 -0
- package/dist/cli/wt/state.js +38 -0
- package/dist/cli/wt/state.js.map +1 -0
- package/dist/cli/wt/wt.test.d.ts +8 -0
- package/dist/cli/wt/wt.test.d.ts.map +1 -0
- package/dist/cli/wt/wt.test.js +378 -0
- package/dist/cli/wt/wt.test.js.map +1 -0
- package/dist/cli/wt.d.ts +25 -0
- package/dist/cli/wt.d.ts.map +1 -0
- package/dist/cli/wt.js +74 -0
- package/dist/cli/wt.js.map +1 -0
- package/dist/cli/wtconfig.js +4 -4
- package/dist/cli/wtconfig.js.map +1 -1
- package/dist/cli/wtlink.js +66 -9
- package/dist/cli/wtlink.js.map +1 -1
- package/dist/cli/wtlink.test.js +101 -0
- package/dist/cli/wtlink.test.js.map +1 -1
- package/dist/e2e/cli.e2e.test.js +156 -1
- package/dist/e2e/cli.e2e.test.js.map +1 -1
- package/dist/e2e/lswt/lswt.e2e.test.js +33 -0
- package/dist/e2e/lswt/lswt.e2e.test.js.map +1 -1
- package/dist/e2e/newpr-full-flow.e2e.test.d.ts +2 -0
- package/dist/e2e/newpr-full-flow.e2e.test.d.ts.map +1 -0
- package/dist/e2e/newpr-full-flow.e2e.test.js +279 -0
- package/dist/e2e/newpr-full-flow.e2e.test.js.map +1 -0
- package/dist/e2e/wtlink/wtlink.e2e.test.js +52 -0
- package/dist/e2e/wtlink/wtlink.e2e.test.js.map +1 -1
- package/dist/integration/lswt-remote-pr.integration.test.d.ts +2 -0
- package/dist/integration/lswt-remote-pr.integration.test.d.ts.map +1 -0
- package/dist/integration/lswt-remote-pr.integration.test.js +222 -0
- package/dist/integration/lswt-remote-pr.integration.test.js.map +1 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.d.ts +2 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.d.ts.map +1 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.js +498 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.js.map +1 -0
- package/dist/lib/git.d.ts +1 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +17 -30
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/git.test.js +154 -123
- package/dist/lib/git.test.js.map +1 -1
- package/dist/lib/github.d.ts +45 -0
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +172 -0
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/github.test.js +127 -1
- package/dist/lib/github.test.js.map +1 -1
- package/dist/lib/json-output.d.ts +11 -1
- package/dist/lib/json-output.d.ts.map +1 -1
- package/dist/lib/json-output.js +42 -1
- package/dist/lib/json-output.js.map +1 -1
- package/dist/lib/json-output.test.js +2 -0
- package/dist/lib/json-output.test.js.map +1 -1
- package/dist/lib/lswt/action-executors.d.ts.map +1 -1
- package/dist/lib/lswt/action-executors.js +143 -35
- package/dist/lib/lswt/action-executors.js.map +1 -1
- package/dist/lib/lswt/action-executors.test.js +362 -0
- package/dist/lib/lswt/action-executors.test.js.map +1 -1
- package/dist/lib/lswt/actions.d.ts.map +1 -1
- package/dist/lib/lswt/actions.js +38 -0
- package/dist/lib/lswt/actions.js.map +1 -1
- package/dist/lib/lswt/actions.test.js +126 -0
- package/dist/lib/lswt/actions.test.js.map +1 -1
- package/dist/lib/lswt/environment.d.ts +4 -0
- package/dist/lib/lswt/environment.d.ts.map +1 -1
- package/dist/lib/lswt/environment.js +23 -0
- package/dist/lib/lswt/environment.js.map +1 -1
- package/dist/lib/lswt/environment.test.js +129 -2
- package/dist/lib/lswt/environment.test.js.map +1 -1
- package/dist/lib/lswt/formatters.d.ts +2 -1
- package/dist/lib/lswt/formatters.d.ts.map +1 -1
- package/dist/lib/lswt/formatters.js +27 -2
- package/dist/lib/lswt/formatters.js.map +1 -1
- package/dist/lib/lswt/formatters.test.js +66 -2
- package/dist/lib/lswt/formatters.test.js.map +1 -1
- package/dist/lib/lswt/fuzzy-search.d.ts +27 -0
- package/dist/lib/lswt/fuzzy-search.d.ts.map +1 -0
- package/dist/lib/lswt/fuzzy-search.js +130 -0
- package/dist/lib/lswt/fuzzy-search.js.map +1 -0
- package/dist/lib/lswt/fuzzy-search.test.d.ts +5 -0
- package/dist/lib/lswt/fuzzy-search.test.d.ts.map +1 -0
- package/dist/lib/lswt/fuzzy-search.test.js +207 -0
- package/dist/lib/lswt/fuzzy-search.test.js.map +1 -0
- package/dist/lib/lswt/index.d.ts +3 -1
- package/dist/lib/lswt/index.d.ts.map +1 -1
- package/dist/lib/lswt/index.js +3 -2
- package/dist/lib/lswt/index.js.map +1 -1
- package/dist/lib/lswt/interactive.d.ts +50 -4
- package/dist/lib/lswt/interactive.d.ts.map +1 -1
- package/dist/lib/lswt/interactive.js +458 -56
- package/dist/lib/lswt/interactive.js.map +1 -1
- package/dist/lib/lswt/interactive.test.js +454 -66
- package/dist/lib/lswt/interactive.test.js.map +1 -1
- package/dist/lib/lswt/types.d.ts +8 -2
- package/dist/lib/lswt/types.d.ts.map +1 -1
- package/dist/lib/lswt/worktree-info.d.ts +11 -0
- package/dist/lib/lswt/worktree-info.d.ts.map +1 -1
- package/dist/lib/lswt/worktree-info.js +48 -0
- package/dist/lib/lswt/worktree-info.js.map +1 -1
- package/dist/lib/lswt/worktree-info.test.js +169 -0
- package/dist/lib/lswt/worktree-info.test.js.map +1 -1
- package/dist/lib/newpr/action-deps.test.d.ts +5 -0
- package/dist/lib/newpr/action-deps.test.d.ts.map +1 -0
- package/dist/lib/newpr/action-deps.test.js +111 -0
- package/dist/lib/newpr/action-deps.test.js.map +1 -0
- package/dist/lib/newpr/args.d.ts.map +1 -1
- package/dist/lib/newpr/args.js +6 -2
- package/dist/lib/newpr/args.js.map +1 -1
- package/dist/lib/newpr/args.test.js +209 -1
- package/dist/lib/newpr/args.test.js.map +1 -1
- package/dist/lib/newpr/scenario-handler.d.ts.map +1 -1
- package/dist/lib/newpr/scenario-handler.js +14 -5
- package/dist/lib/newpr/scenario-handler.js.map +1 -1
- package/dist/lib/newpr/scenario-handler.test.js +6 -0
- package/dist/lib/newpr/scenario-handler.test.js.map +1 -1
- package/dist/lib/prompts.d.ts +4 -0
- package/dist/lib/prompts.d.ts.map +1 -1
- package/dist/lib/prompts.js +178 -1
- package/dist/lib/prompts.js.map +1 -1
- package/dist/lib/prompts.test.js +279 -0
- package/dist/lib/prompts.test.js.map +1 -1
- package/dist/lib/wtlink/link-configs.test.js +282 -2
- package/dist/lib/wtlink/link-configs.test.js.map +1 -1
- package/dist/lib/wtlink/main-menu.js +1 -0
- package/dist/lib/wtlink/main-menu.js.map +1 -1
- package/dist/lib/wtlink/main-menu.test.d.ts +5 -0
- package/dist/lib/wtlink/main-menu.test.d.ts.map +1 -0
- package/dist/lib/wtlink/main-menu.test.js +124 -0
- package/dist/lib/wtlink/main-menu.test.js.map +1 -0
- package/dist/lib/wtlink/manage-manifest.d.ts +5 -0
- package/dist/lib/wtlink/manage-manifest.d.ts.map +1 -1
- package/dist/lib/wtlink/manage-manifest.js +65 -2
- package/dist/lib/wtlink/manage-manifest.js.map +1 -1
- package/dist/lib/wtlink/manage-manifest.test.js +144 -2
- package/dist/lib/wtlink/manage-manifest.test.js.map +1 -1
- package/dist/mcp/server.test.js +49 -0
- package/dist/mcp/server.test.js.map +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
264
|
-
.
|
|
265
|
-
.
|
|
266
|
-
|
|
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
|
|
273
|
-
.
|
|
274
|
-
.mockResolvedValueOnce({ action:
|
|
275
|
-
.mockResolvedValueOnce({
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
288
|
-
.
|
|
289
|
-
.mockResolvedValueOnce({ action:
|
|
290
|
-
.mockResolvedValueOnce({
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
304
|
-
.
|
|
305
|
-
.mockResolvedValueOnce({ action:
|
|
306
|
-
.mockResolvedValueOnce({
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
322
|
-
.
|
|
323
|
-
.mockResolvedValueOnce({ action:
|
|
324
|
-
.mockResolvedValueOnce({
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|