@camaradesuk/git-worktree-tools 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -27
- package/dist/cli/cleanpr.js +74 -53
- package/dist/cli/cleanpr.js.map +1 -1
- package/dist/cli/lswt.js +32 -56
- package/dist/cli/lswt.js.map +1 -1
- package/dist/cli/lswt.test.js +17 -27
- package/dist/cli/lswt.test.js.map +1 -1
- package/dist/cli/newpr.d.ts +13 -1
- package/dist/cli/newpr.d.ts.map +1 -1
- package/dist/cli/newpr.js +159 -153
- package/dist/cli/newpr.js.map +1 -1
- package/dist/cli/newpr.test.js +1 -1
- package/dist/cli/newpr.test.js.map +1 -1
- package/dist/cli/prs.d.ts +3 -10
- package/dist/cli/prs.d.ts.map +1 -1
- package/dist/cli/prs.js +6 -168
- package/dist/cli/prs.js.map +1 -1
- package/dist/cli/prs.test.js +53 -0
- package/dist/cli/prs.test.js.map +1 -1
- package/dist/cli/wt/clean.d.ts +6 -2
- package/dist/cli/wt/clean.d.ts.map +1 -1
- package/dist/cli/wt/clean.js +401 -20
- package/dist/cli/wt/clean.js.map +1 -1
- package/dist/cli/wt/clean.test.d.ts +8 -0
- package/dist/cli/wt/clean.test.d.ts.map +1 -0
- package/dist/cli/wt/clean.test.js +624 -0
- package/dist/cli/wt/clean.test.js.map +1 -0
- package/dist/cli/wt/completion.d.ts +3 -0
- package/dist/cli/wt/completion.d.ts.map +1 -1
- package/dist/cli/wt/completion.js +80 -9
- package/dist/cli/wt/completion.js.map +1 -1
- package/dist/cli/wt/completion.test.js +102 -0
- package/dist/cli/wt/completion.test.js.map +1 -1
- package/dist/cli/wt/config.d.ts +3 -1
- package/dist/cli/wt/config.d.ts.map +1 -1
- package/dist/cli/wt/config.js +323 -32
- package/dist/cli/wt/config.js.map +1 -1
- package/dist/cli/wt/config.test.d.ts +2 -0
- package/dist/cli/wt/config.test.d.ts.map +1 -1
- package/dist/cli/wt/config.test.js +206 -26
- package/dist/cli/wt/config.test.js.map +1 -1
- package/dist/cli/wt/interactive-menu.d.ts +2 -0
- package/dist/cli/wt/interactive-menu.d.ts.map +1 -1
- package/dist/cli/wt/interactive-menu.js +346 -73
- package/dist/cli/wt/interactive-menu.js.map +1 -1
- package/dist/cli/wt/interactive-menu.test.d.ts +4 -2
- package/dist/cli/wt/interactive-menu.test.d.ts.map +1 -1
- package/dist/cli/wt/interactive-menu.test.js +380 -323
- package/dist/cli/wt/interactive-menu.test.js.map +1 -1
- package/dist/cli/wt/link.d.ts +3 -1
- package/dist/cli/wt/link.d.ts.map +1 -1
- package/dist/cli/wt/link.js +125 -38
- package/dist/cli/wt/link.js.map +1 -1
- package/dist/cli/wt/list.d.ts +4 -1
- package/dist/cli/wt/list.d.ts.map +1 -1
- package/dist/cli/wt/list.js +85 -16
- package/dist/cli/wt/list.js.map +1 -1
- package/dist/cli/wt/list.test.d.ts +10 -0
- package/dist/cli/wt/list.test.d.ts.map +1 -0
- package/dist/cli/wt/list.test.js +157 -0
- package/dist/cli/wt/list.test.js.map +1 -0
- package/dist/cli/wt/new.d.ts +8 -2
- package/dist/cli/wt/new.d.ts.map +1 -1
- package/dist/cli/wt/new.js +91 -46
- package/dist/cli/wt/new.js.map +1 -1
- package/dist/cli/wt/prs.d.ts +2 -1
- package/dist/cli/wt/prs.d.ts.map +1 -1
- package/dist/cli/wt/prs.js +3 -164
- package/dist/cli/wt/prs.js.map +1 -1
- package/dist/cli/wt/run-command.d.ts +4 -2
- package/dist/cli/wt/run-command.d.ts.map +1 -1
- package/dist/cli/wt/run-command.js +6 -4
- package/dist/cli/wt/run-command.js.map +1 -1
- package/dist/cli/wt/state.d.ts +3 -1
- package/dist/cli/wt/state.d.ts.map +1 -1
- package/dist/cli/wt/state.js +74 -10
- package/dist/cli/wt/state.js.map +1 -1
- package/dist/cli/wt/state.test.d.ts +9 -0
- package/dist/cli/wt/state.test.d.ts.map +1 -0
- package/dist/cli/wt/state.test.js +127 -0
- package/dist/cli/wt/state.test.js.map +1 -0
- package/dist/cli/wt/wt.test.d.ts +2 -2
- package/dist/cli/wt/wt.test.js +430 -212
- package/dist/cli/wt/wt.test.js.map +1 -1
- package/dist/cli/wt.d.ts.map +1 -1
- package/dist/cli/wt.js +50 -36
- package/dist/cli/wt.js.map +1 -1
- package/dist/cli/wt.unit.test.js +16 -38
- package/dist/cli/wt.unit.test.js.map +1 -1
- package/dist/cli/wtconfig.js +99 -22
- package/dist/cli/wtconfig.js.map +1 -1
- package/dist/cli/wtlink.js +85 -61
- package/dist/cli/wtlink.js.map +1 -1
- package/dist/cli/wtstate.js +21 -2
- package/dist/cli/wtstate.js.map +1 -1
- package/dist/e2e/wt/interactive-menu.e2e.test.js +17 -17
- package/dist/e2e/wt/interactive-menu.e2e.test.js.map +1 -1
- package/dist/lib/cleanpr/args.d.ts.map +1 -1
- package/dist/lib/cleanpr/args.js +20 -0
- package/dist/lib/cleanpr/args.js.map +1 -1
- package/dist/lib/cleanpr/types.d.ts +6 -0
- package/dist/lib/cleanpr/types.d.ts.map +1 -1
- package/dist/lib/colors.d.ts +5 -0
- package/dist/lib/colors.d.ts.map +1 -1
- package/dist/lib/colors.js +13 -6
- package/dist/lib/colors.js.map +1 -1
- package/dist/lib/config.test.js +3 -15
- package/dist/lib/config.test.js.map +1 -1
- package/dist/lib/constants.d.ts +12 -4
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +24 -5
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/constants.test.js +88 -29
- package/dist/lib/constants.test.js.map +1 -1
- package/dist/lib/deprecation.d.ts +18 -0
- package/dist/lib/deprecation.d.ts.map +1 -0
- package/dist/lib/deprecation.js +28 -0
- package/dist/lib/deprecation.js.map +1 -0
- package/dist/lib/deprecation.test.d.ts +2 -0
- package/dist/lib/deprecation.test.d.ts.map +1 -0
- package/dist/lib/deprecation.test.js +71 -0
- package/dist/lib/deprecation.test.js.map +1 -0
- package/dist/lib/logger.d.ts +40 -155
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +349 -420
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/logger.test.d.ts +10 -1
- package/dist/lib/logger.test.d.ts.map +1 -1
- package/dist/lib/logger.test.js +658 -258
- package/dist/lib/logger.test.js.map +1 -1
- package/dist/lib/lswt/args.d.ts.map +1 -1
- package/dist/lib/lswt/args.js +15 -1
- package/dist/lib/lswt/args.js.map +1 -1
- package/dist/lib/lswt/index.d.ts +1 -0
- package/dist/lib/lswt/index.d.ts.map +1 -1
- package/dist/lib/lswt/index.js +2 -0
- package/dist/lib/lswt/index.js.map +1 -1
- package/dist/lib/lswt/table.d.ts +15 -0
- package/dist/lib/lswt/table.d.ts.map +1 -0
- package/dist/lib/lswt/table.js +61 -0
- package/dist/lib/lswt/table.js.map +1 -0
- package/dist/lib/lswt/table.test.d.ts +5 -0
- package/dist/lib/lswt/table.test.d.ts.map +1 -0
- package/dist/lib/lswt/table.test.js +262 -0
- package/dist/lib/lswt/table.test.js.map +1 -0
- package/dist/lib/lswt/types.d.ts +4 -0
- package/dist/lib/lswt/types.d.ts.map +1 -1
- package/dist/lib/newpr/args.d.ts.map +1 -1
- package/dist/lib/newpr/args.js +21 -0
- package/dist/lib/newpr/args.js.map +1 -1
- package/dist/lib/newpr/types.d.ts +6 -0
- package/dist/lib/newpr/types.d.ts.map +1 -1
- package/dist/lib/prs/command.d.ts +21 -0
- package/dist/lib/prs/command.d.ts.map +1 -0
- package/dist/lib/prs/command.js +175 -0
- package/dist/lib/prs/command.js.map +1 -0
- package/dist/lib/prs/command.test.d.ts +11 -0
- package/dist/lib/prs/command.test.d.ts.map +1 -0
- package/dist/lib/prs/command.test.js +409 -0
- package/dist/lib/prs/command.test.js.map +1 -0
- package/dist/lib/prs/interactive.d.ts.map +1 -1
- package/dist/lib/prs/interactive.js +15 -2
- package/dist/lib/prs/interactive.js.map +1 -1
- package/dist/lib/prs/interactive.test.js +153 -0
- package/dist/lib/prs/interactive.test.js.map +1 -1
- package/dist/lib/prs/types.d.ts +15 -0
- package/dist/lib/prs/types.d.ts.map +1 -1
- package/dist/lib/ui/error.d.ts +31 -0
- package/dist/lib/ui/error.d.ts.map +1 -0
- package/dist/lib/ui/error.js +47 -0
- package/dist/lib/ui/error.js.map +1 -0
- package/dist/lib/ui/error.test.d.ts +2 -0
- package/dist/lib/ui/error.test.d.ts.map +1 -0
- package/dist/lib/ui/error.test.js +143 -0
- package/dist/lib/ui/error.test.js.map +1 -0
- package/dist/lib/ui/index.d.ts +15 -0
- package/dist/lib/ui/index.d.ts.map +1 -0
- package/dist/lib/ui/index.js +19 -0
- package/dist/lib/ui/index.js.map +1 -0
- package/dist/lib/ui/output.d.ts +18 -0
- package/dist/lib/ui/output.d.ts.map +1 -0
- package/dist/lib/ui/output.js +31 -0
- package/dist/lib/ui/output.js.map +1 -0
- package/dist/lib/ui/output.test.d.ts +2 -0
- package/dist/lib/ui/output.test.d.ts.map +1 -0
- package/dist/lib/ui/output.test.js +59 -0
- package/dist/lib/ui/output.test.js.map +1 -0
- package/dist/lib/ui/spinner.d.ts +10 -0
- package/dist/lib/ui/spinner.d.ts.map +1 -0
- package/dist/lib/ui/spinner.js +10 -0
- package/dist/lib/ui/spinner.js.map +1 -0
- package/dist/lib/ui/status.d.ts +65 -0
- package/dist/lib/ui/status.d.ts.map +1 -0
- package/dist/lib/ui/status.js +100 -0
- package/dist/lib/ui/status.js.map +1 -0
- package/dist/lib/ui/status.test.d.ts +2 -0
- package/dist/lib/ui/status.test.d.ts.map +1 -0
- package/dist/lib/ui/status.test.js +158 -0
- package/dist/lib/ui/status.test.js.map +1 -0
- package/dist/lib/ui/table.d.ts +39 -0
- package/dist/lib/ui/table.d.ts.map +1 -0
- package/dist/lib/ui/table.js +45 -0
- package/dist/lib/ui/table.js.map +1 -0
- package/dist/lib/ui/table.test.d.ts +2 -0
- package/dist/lib/ui/table.test.d.ts.map +1 -0
- package/dist/lib/ui/table.test.js +115 -0
- package/dist/lib/ui/table.test.js.map +1 -0
- package/dist/lib/ui/theme.d.ts +34 -0
- package/dist/lib/ui/theme.d.ts.map +1 -0
- package/dist/lib/ui/theme.js +37 -0
- package/dist/lib/ui/theme.js.map +1 -0
- package/dist/lib/ui/theme.test.d.ts +2 -0
- package/dist/lib/ui/theme.test.d.ts.map +1 -0
- package/dist/lib/ui/theme.test.js +76 -0
- package/dist/lib/ui/theme.test.js.map +1 -0
- package/dist/lib/wtlink/link-configs.js +7 -7
- package/dist/lib/wtlink/link-configs.js.map +1 -1
- package/dist/lib/wtlink/validate-manifest.d.ts.map +1 -1
- package/dist/lib/wtlink/validate-manifest.js +5 -5
- package/dist/lib/wtlink/validate-manifest.js.map +1 -1
- package/dist/lib/wtstate/args.d.ts.map +1 -1
- package/dist/lib/wtstate/args.js +2 -0
- package/dist/lib/wtstate/args.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +264 -44
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/server.test.js +111 -0
- package/dist/mcp/server.test.js.map +1 -1
- package/package.json +2 -1
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These tests verify that each menu flow:
|
|
5
5
|
* 1. Gathers the correct user inputs
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
6
|
+
* 2. Calls the correct library functions with proper arguments
|
|
7
|
+
* 3. Returns to menu after operation execution (not exit)
|
|
8
|
+
* 4. Handles cancellation and back navigation correctly
|
|
9
|
+
* 5. Uses direct library calls (no subprocess spawning)
|
|
8
10
|
*/
|
|
9
11
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
12
|
// Mock modules before importing the module under test
|
|
@@ -23,12 +25,6 @@ vi.mock('../../lib/prompts.js', () => {
|
|
|
23
25
|
UserNavigatedBack: MockUserNavigatedBack,
|
|
24
26
|
};
|
|
25
27
|
});
|
|
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
28
|
vi.mock('../../lib/config.js', () => ({
|
|
33
29
|
loadConfig: vi.fn(() => ({
|
|
34
30
|
configVersion: 1,
|
|
@@ -63,12 +59,90 @@ vi.mock('../../lib/config.js', () => ({
|
|
|
63
59
|
vi.mock('../../lib/git.js', () => ({
|
|
64
60
|
getRepoRoot: vi.fn(() => '/mock/repo'),
|
|
65
61
|
listLocalBranches: vi.fn(() => ['feat/existing-branch', 'fix/bug-fix', 'main', 'develop']),
|
|
62
|
+
removeWorktree: vi.fn(),
|
|
63
|
+
pruneWorktrees: vi.fn(),
|
|
64
|
+
}));
|
|
65
|
+
vi.mock('../../lib/wtlink/config-manifest.js', () => ({
|
|
66
|
+
loadManifestData: vi.fn(() => ({
|
|
67
|
+
enabled: ['.env', '.env.local'],
|
|
68
|
+
disabled: ['config.json'],
|
|
69
|
+
source: 'config',
|
|
70
|
+
})),
|
|
71
|
+
saveManifestData: vi.fn(),
|
|
72
|
+
}));
|
|
73
|
+
// Mock direct library imports
|
|
74
|
+
vi.mock('../../lib/lswt/index.js', () => ({
|
|
75
|
+
gatherWorktreeInfo: vi.fn(async () => []),
|
|
76
|
+
createDefaultDeps: vi.fn(() => ({})),
|
|
77
|
+
runInteractiveMode: vi.fn(async () => { }),
|
|
78
|
+
}));
|
|
79
|
+
vi.mock('../../lib/prs/command.js', () => ({
|
|
80
|
+
runPrsCommand: vi.fn(async () => { }),
|
|
81
|
+
}));
|
|
82
|
+
vi.mock('../newpr.js', () => ({
|
|
83
|
+
runNewprHandler: vi.fn(async () => { }),
|
|
84
|
+
}));
|
|
85
|
+
vi.mock('../../lib/cleanpr/index.js', () => ({
|
|
86
|
+
gatherPrWorktreeInfo: vi.fn(async () => []),
|
|
87
|
+
createDefaultDeps: vi.fn(() => ({})),
|
|
88
|
+
getCleanableWorktrees: vi.fn(() => []),
|
|
89
|
+
cleanWorktree: vi.fn(() => ({ success: true, message: 'Cleaned', prNumber: 42 })),
|
|
90
|
+
findWorktreeByPrNumber: vi.fn(() => null),
|
|
91
|
+
summarizeResults: vi.fn(() => ({ cleaned: 0, total: 0 })),
|
|
92
|
+
}));
|
|
93
|
+
vi.mock('../../lib/wtstate/index.js', () => ({
|
|
94
|
+
analyzeState: vi.fn(() => ({
|
|
95
|
+
scenario: 'main_clean_same',
|
|
96
|
+
scenarioDescription: 'On main, clean, same as origin',
|
|
97
|
+
currentBranch: 'main',
|
|
98
|
+
baseBranch: 'main',
|
|
99
|
+
worktreeType: 'main_worktree',
|
|
100
|
+
hasChanges: false,
|
|
101
|
+
hasStagedChanges: false,
|
|
102
|
+
hasUnstagedChanges: false,
|
|
103
|
+
localCommits: 0,
|
|
104
|
+
stagedFiles: [],
|
|
105
|
+
unstagedFiles: [],
|
|
106
|
+
availableActions: [],
|
|
107
|
+
recommendedAction: null,
|
|
108
|
+
})),
|
|
109
|
+
formatText: vi.fn(() => 'State: main_clean_same'),
|
|
110
|
+
}));
|
|
111
|
+
vi.mock('../../lib/wtlink/link-configs.js', () => ({
|
|
112
|
+
run: vi.fn(async () => { }),
|
|
113
|
+
}));
|
|
114
|
+
vi.mock('../../lib/wtlink/validate-manifest.js', () => ({
|
|
115
|
+
run: vi.fn(() => { }),
|
|
116
|
+
}));
|
|
117
|
+
vi.mock('../../lib/wtconfig/index.js', () => ({
|
|
118
|
+
formatConfigDisplay: vi.fn(() => '{ baseBranch: "main" }'),
|
|
119
|
+
setConfigValue: vi.fn((config, _key, _value) => config),
|
|
120
|
+
loadRepoConfig: vi.fn(() => ({})),
|
|
121
|
+
saveRepoConfig: vi.fn(),
|
|
122
|
+
validateConfig: vi.fn(() => ({ valid: true, errors: [], warnings: [] })),
|
|
123
|
+
}));
|
|
124
|
+
vi.mock('../../lib/constants.js', () => ({
|
|
125
|
+
DEFAULT_MANIFEST_FILE: '.wtlinkrc',
|
|
126
|
+
}));
|
|
127
|
+
vi.mock('../../lib/ui/index.js', () => ({
|
|
128
|
+
printStatus: vi.fn(),
|
|
129
|
+
}));
|
|
130
|
+
vi.mock('child_process', () => ({
|
|
131
|
+
execSync: vi.fn(),
|
|
66
132
|
}));
|
|
67
133
|
// Import mocked modules
|
|
68
134
|
import { promptChoice, promptInput, promptConfirm } from '../../lib/prompts.js';
|
|
69
|
-
import { runSubcommand } from './run-command.js';
|
|
70
135
|
import { loadConfig } from '../../lib/config.js';
|
|
71
136
|
import * as git from '../../lib/git.js';
|
|
137
|
+
import { loadManifestData, saveManifestData } from '../../lib/wtlink/config-manifest.js';
|
|
138
|
+
import { gatherWorktreeInfo, runInteractiveMode } from '../../lib/lswt/index.js';
|
|
139
|
+
import { runPrsCommand } from '../../lib/prs/command.js';
|
|
140
|
+
import { runNewprHandler } from '../newpr.js';
|
|
141
|
+
import { gatherPrWorktreeInfo, getCleanableWorktrees, } from '../../lib/cleanpr/index.js';
|
|
142
|
+
import { analyzeState, formatText } from '../../lib/wtstate/index.js';
|
|
143
|
+
import { run as runWtlinkLink } from '../../lib/wtlink/link-configs.js';
|
|
144
|
+
import { run as runWtlinkValidate } from '../../lib/wtlink/validate-manifest.js';
|
|
145
|
+
import { formatConfigDisplay, setConfigValue, loadRepoConfig, saveRepoConfig, } from '../../lib/wtconfig/index.js';
|
|
72
146
|
// Import flows after mocks are set up
|
|
73
147
|
import { flows, showMainMenu } from './interactive-menu.js';
|
|
74
148
|
// Mock console.log to keep test output clean
|
|
@@ -81,36 +155,41 @@ describe('Interactive Menu Flows', () => {
|
|
|
81
155
|
consoleSpy.mockClear();
|
|
82
156
|
});
|
|
83
157
|
describe('handleListWorktrees', () => {
|
|
84
|
-
it('calls
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
158
|
+
it('calls gatherWorktreeInfo and runInteractiveMode and returns to menu', async () => {
|
|
159
|
+
const result = await flows.handleListWorktrees();
|
|
160
|
+
expect(gatherWorktreeInfo).toHaveBeenCalledWith('/mock/repo', { verbose: false, json: false, showStatus: false }, expect.anything());
|
|
161
|
+
expect(runInteractiveMode).toHaveBeenCalled();
|
|
162
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
163
|
+
});
|
|
164
|
+
it('returns to menu with error message when library call fails', async () => {
|
|
165
|
+
vi.mocked(gatherWorktreeInfo).mockRejectedValueOnce(new Error('git error'));
|
|
166
|
+
const result = await flows.handleListWorktrees();
|
|
167
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
168
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('git error'));
|
|
92
169
|
});
|
|
93
170
|
});
|
|
94
171
|
describe('handleBrowsePRs', () => {
|
|
95
|
-
it('calls
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
172
|
+
it('calls runPrsCommand and returns to menu', async () => {
|
|
173
|
+
const result = await flows.handleBrowsePRs();
|
|
174
|
+
expect(runPrsCommand).toHaveBeenCalledWith({
|
|
175
|
+
state: 'open',
|
|
176
|
+
limit: 50,
|
|
177
|
+
json: false,
|
|
178
|
+
noInteractive: false,
|
|
179
|
+
});
|
|
180
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
103
181
|
});
|
|
104
182
|
});
|
|
105
183
|
describe('handleShowState', () => {
|
|
106
|
-
it('calls
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
expect(
|
|
184
|
+
it('calls analyzeState and formatText and returns to menu', async () => {
|
|
185
|
+
const result = await flows.handleShowState();
|
|
186
|
+
expect(analyzeState).toHaveBeenCalledWith({
|
|
187
|
+
verbose: false,
|
|
188
|
+
json: false,
|
|
189
|
+
baseBranch: 'main',
|
|
190
|
+
});
|
|
191
|
+
expect(formatText).toHaveBeenCalled();
|
|
192
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
114
193
|
});
|
|
115
194
|
});
|
|
116
195
|
describe('handleNewPR', () => {
|
|
@@ -118,7 +197,7 @@ describe('Interactive Menu Flows', () => {
|
|
|
118
197
|
vi.mocked(promptChoice).mockResolvedValueOnce('back');
|
|
119
198
|
const result = await flows.handleNewPR();
|
|
120
199
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
121
|
-
expect(
|
|
200
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
122
201
|
});
|
|
123
202
|
it('handles user cancellation (Ctrl+C)', async () => {
|
|
124
203
|
vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
|
|
@@ -126,7 +205,7 @@ describe('Interactive Menu Flows', () => {
|
|
|
126
205
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
127
206
|
});
|
|
128
207
|
describe('from-description flow', () => {
|
|
129
|
-
it('gathers all inputs and calls
|
|
208
|
+
it('gathers all inputs and calls runNewprHandler with correct Options', async () => {
|
|
130
209
|
vi.mocked(promptChoice)
|
|
131
210
|
.mockResolvedValueOnce('from-description') // New PR sub-menu
|
|
132
211
|
.mockResolvedValueOnce(true); // Draft PR selection
|
|
@@ -136,15 +215,18 @@ describe('Interactive Menu Flows', () => {
|
|
|
136
215
|
vi.mocked(promptConfirm)
|
|
137
216
|
.mockResolvedValueOnce(false) // Install deps
|
|
138
217
|
.mockResolvedValueOnce(false); // Open VS Code
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
218
|
+
const result = await flows.handleNewPR();
|
|
219
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
220
|
+
mode: 'new',
|
|
221
|
+
description: 'Add dark mode support',
|
|
222
|
+
baseBranch: 'main',
|
|
223
|
+
draft: true,
|
|
224
|
+
installDeps: false,
|
|
225
|
+
openEditor: false,
|
|
226
|
+
}));
|
|
227
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
228
|
+
});
|
|
229
|
+
it('passes ready flag when not draft', async () => {
|
|
148
230
|
vi.mocked(promptChoice)
|
|
149
231
|
.mockResolvedValueOnce('from-description')
|
|
150
232
|
.mockResolvedValueOnce(false); // Ready for review (not draft)
|
|
@@ -152,15 +234,15 @@ describe('Interactive Menu Flows', () => {
|
|
|
152
234
|
.mockResolvedValueOnce('Fix critical bug')
|
|
153
235
|
.mockResolvedValueOnce('main');
|
|
154
236
|
vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
expect(
|
|
162
|
-
});
|
|
163
|
-
it('passes
|
|
237
|
+
const result = await flows.handleNewPR();
|
|
238
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
239
|
+
mode: 'new',
|
|
240
|
+
description: 'Fix critical bug',
|
|
241
|
+
draft: false,
|
|
242
|
+
}));
|
|
243
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
244
|
+
});
|
|
245
|
+
it('passes non-main base branch', async () => {
|
|
164
246
|
vi.mocked(promptChoice)
|
|
165
247
|
.mockResolvedValueOnce('from-description')
|
|
166
248
|
.mockResolvedValueOnce(true);
|
|
@@ -168,15 +250,13 @@ describe('Interactive Menu Flows', () => {
|
|
|
168
250
|
.mockResolvedValueOnce('Feature work')
|
|
169
251
|
.mockResolvedValueOnce('develop'); // Non-main base branch
|
|
170
252
|
vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
});
|
|
179
|
-
it('passes --install flag when requested', async () => {
|
|
253
|
+
const result = await flows.handleNewPR();
|
|
254
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
255
|
+
baseBranch: 'develop',
|
|
256
|
+
}));
|
|
257
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
258
|
+
});
|
|
259
|
+
it('passes install flag when requested', async () => {
|
|
180
260
|
vi.mocked(promptChoice)
|
|
181
261
|
.mockResolvedValueOnce('from-description')
|
|
182
262
|
.mockResolvedValueOnce(true);
|
|
@@ -184,27 +264,23 @@ describe('Interactive Menu Flows', () => {
|
|
|
184
264
|
vi.mocked(promptConfirm)
|
|
185
265
|
.mockResolvedValueOnce(true) // Install deps
|
|
186
266
|
.mockResolvedValueOnce(false);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
195
|
-
it('passes --code flag when requested', async () => {
|
|
267
|
+
const result = await flows.handleNewPR();
|
|
268
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
269
|
+
installDeps: true,
|
|
270
|
+
}));
|
|
271
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
272
|
+
});
|
|
273
|
+
it('passes code flag when requested', async () => {
|
|
196
274
|
vi.mocked(promptChoice)
|
|
197
275
|
.mockResolvedValueOnce('from-description')
|
|
198
276
|
.mockResolvedValueOnce(true);
|
|
199
277
|
vi.mocked(promptInput).mockResolvedValueOnce('Add feature').mockResolvedValueOnce('main');
|
|
200
278
|
vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(true); // Open VS Code
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Add feature', '--code']);
|
|
279
|
+
const result = await flows.handleNewPR();
|
|
280
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
281
|
+
openEditor: true,
|
|
282
|
+
}));
|
|
283
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
208
284
|
});
|
|
209
285
|
it('passes all optional flags together', async () => {
|
|
210
286
|
vi.mocked(promptChoice)
|
|
@@ -216,27 +292,23 @@ describe('Interactive Menu Flows', () => {
|
|
|
216
292
|
vi.mocked(promptConfirm)
|
|
217
293
|
.mockResolvedValueOnce(true) // Install
|
|
218
294
|
.mockResolvedValueOnce(true); // VS Code
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'--ready',
|
|
230
|
-
'--install',
|
|
231
|
-
'--code',
|
|
232
|
-
]);
|
|
295
|
+
const result = await flows.handleNewPR();
|
|
296
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
297
|
+
mode: 'new',
|
|
298
|
+
description: 'Full feature',
|
|
299
|
+
baseBranch: 'develop',
|
|
300
|
+
draft: false,
|
|
301
|
+
installDeps: true,
|
|
302
|
+
openEditor: true,
|
|
303
|
+
}));
|
|
304
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
233
305
|
});
|
|
234
306
|
it('returns CANCELLED when description is empty', async () => {
|
|
235
307
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-description');
|
|
236
308
|
vi.mocked(promptInput).mockResolvedValueOnce(''); // Empty description
|
|
237
309
|
const result = await flows.handleNewPR();
|
|
238
310
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
239
|
-
expect(
|
|
311
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
240
312
|
});
|
|
241
313
|
it('handles user cancellation during input', async () => {
|
|
242
314
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-description');
|
|
@@ -246,59 +318,59 @@ describe('Interactive Menu Flows', () => {
|
|
|
246
318
|
});
|
|
247
319
|
});
|
|
248
320
|
describe('from-pr flow', () => {
|
|
249
|
-
it('gathers PR number and calls
|
|
321
|
+
it('gathers PR number and calls runNewprHandler with mode pr', async () => {
|
|
250
322
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
|
|
251
323
|
vi.mocked(promptInput).mockResolvedValueOnce('42');
|
|
252
324
|
vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
it('passes --install and --code flags', async () => {
|
|
325
|
+
const result = await flows.handleNewPR();
|
|
326
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
327
|
+
mode: 'pr',
|
|
328
|
+
prNumber: 42,
|
|
329
|
+
}));
|
|
330
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
331
|
+
});
|
|
332
|
+
it('passes install and code flags', async () => {
|
|
262
333
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
|
|
263
334
|
vi.mocked(promptInput).mockResolvedValueOnce('123');
|
|
264
335
|
vi.mocked(promptConfirm)
|
|
265
336
|
.mockResolvedValueOnce(true) // Install
|
|
266
337
|
.mockResolvedValueOnce(true); // VS Code
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
338
|
+
const result = await flows.handleNewPR();
|
|
339
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
340
|
+
mode: 'pr',
|
|
341
|
+
prNumber: 123,
|
|
342
|
+
installDeps: true,
|
|
343
|
+
openEditor: true,
|
|
344
|
+
}));
|
|
345
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
274
346
|
});
|
|
275
347
|
it('returns CANCELLED when PR number is empty', async () => {
|
|
276
348
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
|
|
277
349
|
vi.mocked(promptInput).mockResolvedValueOnce('');
|
|
278
350
|
const result = await flows.handleNewPR();
|
|
279
351
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
280
|
-
expect(
|
|
352
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
281
353
|
});
|
|
282
354
|
it('returns CANCELLED when PR number is invalid', async () => {
|
|
283
355
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
|
|
284
356
|
vi.mocked(promptInput).mockResolvedValueOnce('not-a-number');
|
|
285
357
|
const result = await flows.handleNewPR();
|
|
286
358
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
287
|
-
expect(
|
|
359
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
288
360
|
});
|
|
289
361
|
it('returns CANCELLED when PR number is zero', async () => {
|
|
290
362
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
|
|
291
363
|
vi.mocked(promptInput).mockResolvedValueOnce('0');
|
|
292
364
|
const result = await flows.handleNewPR();
|
|
293
365
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
294
|
-
expect(
|
|
366
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
295
367
|
});
|
|
296
368
|
it('returns CANCELLED when PR number is negative', async () => {
|
|
297
369
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
|
|
298
370
|
vi.mocked(promptInput).mockResolvedValueOnce('-5');
|
|
299
371
|
const result = await flows.handleNewPR();
|
|
300
372
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
301
|
-
expect(
|
|
373
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
302
374
|
});
|
|
303
375
|
});
|
|
304
376
|
describe('from-branch flow', () => {
|
|
@@ -308,13 +380,12 @@ describe('Interactive Menu Flows', () => {
|
|
|
308
380
|
.mockResolvedValueOnce('feat/existing-branch') // Select branch
|
|
309
381
|
.mockResolvedValueOnce(true); // Draft PR
|
|
310
382
|
vi.mocked(promptInput).mockResolvedValueOnce('main');
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--branch', 'feat/existing-branch']);
|
|
383
|
+
const result = await flows.handleNewPR();
|
|
384
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
385
|
+
mode: 'branch',
|
|
386
|
+
branchName: 'feat/existing-branch',
|
|
387
|
+
}));
|
|
388
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
318
389
|
});
|
|
319
390
|
it('allows typing custom branch name', async () => {
|
|
320
391
|
vi.mocked(promptChoice)
|
|
@@ -324,33 +395,27 @@ describe('Interactive Menu Flows', () => {
|
|
|
324
395
|
vi.mocked(promptInput)
|
|
325
396
|
.mockResolvedValueOnce('feat/my-new-branch') // Custom branch name
|
|
326
397
|
.mockResolvedValueOnce('main');
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
it('passes --base and --ready flags', async () => {
|
|
398
|
+
const result = await flows.handleNewPR();
|
|
399
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
400
|
+
mode: 'branch',
|
|
401
|
+
branchName: 'feat/my-new-branch',
|
|
402
|
+
}));
|
|
403
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
404
|
+
});
|
|
405
|
+
it('passes non-main base branch and ready flag', async () => {
|
|
336
406
|
vi.mocked(promptChoice)
|
|
337
407
|
.mockResolvedValueOnce('from-branch')
|
|
338
408
|
.mockResolvedValueOnce('fix/bug-fix')
|
|
339
409
|
.mockResolvedValueOnce(false); // Ready for review
|
|
340
410
|
vi.mocked(promptInput).mockResolvedValueOnce('develop');
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
'fix/bug-fix',
|
|
350
|
-
'--base',
|
|
351
|
-
'develop',
|
|
352
|
-
'--ready',
|
|
353
|
-
]);
|
|
411
|
+
const result = await flows.handleNewPR();
|
|
412
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
413
|
+
mode: 'branch',
|
|
414
|
+
branchName: 'fix/bug-fix',
|
|
415
|
+
baseBranch: 'develop',
|
|
416
|
+
draft: false,
|
|
417
|
+
}));
|
|
418
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
354
419
|
});
|
|
355
420
|
it('returns CANCELLED when branch name is empty', async () => {
|
|
356
421
|
vi.mocked(promptChoice)
|
|
@@ -359,7 +424,7 @@ describe('Interactive Menu Flows', () => {
|
|
|
359
424
|
vi.mocked(promptInput).mockResolvedValueOnce(''); // Empty branch name
|
|
360
425
|
const result = await flows.handleNewPR();
|
|
361
426
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
362
|
-
expect(
|
|
427
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
363
428
|
});
|
|
364
429
|
it('handles empty branch list gracefully', async () => {
|
|
365
430
|
// Mock empty branch list
|
|
@@ -369,14 +434,10 @@ describe('Interactive Menu Flows', () => {
|
|
|
369
434
|
.mockResolvedValueOnce('feat/new-branch') // Manual branch input
|
|
370
435
|
.mockResolvedValueOnce('main');
|
|
371
436
|
vi.mocked(promptChoice).mockResolvedValueOnce(true); // Draft
|
|
372
|
-
|
|
373
|
-
await flows.handleNewPR();
|
|
374
|
-
}
|
|
375
|
-
catch {
|
|
376
|
-
// Expected
|
|
377
|
-
}
|
|
437
|
+
const result = await flows.handleNewPR();
|
|
378
438
|
// Should have prompted for branch name directly
|
|
379
439
|
expect(promptInput).toHaveBeenCalledWith('Branch name');
|
|
440
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
380
441
|
});
|
|
381
442
|
});
|
|
382
443
|
});
|
|
@@ -385,39 +446,31 @@ describe('Interactive Menu Flows', () => {
|
|
|
385
446
|
vi.mocked(promptChoice).mockResolvedValueOnce('back');
|
|
386
447
|
const result = await flows.handleCleanPRs();
|
|
387
448
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
388
|
-
expect(
|
|
449
|
+
expect(gatherPrWorktreeInfo).not.toHaveBeenCalled();
|
|
389
450
|
});
|
|
390
451
|
describe('clean-all', () => {
|
|
391
|
-
it('calls cleanpr
|
|
452
|
+
it('calls cleanpr library after confirmation and returns to menu', async () => {
|
|
392
453
|
vi.mocked(promptChoice).mockResolvedValueOnce('clean-all');
|
|
393
454
|
vi.mocked(promptConfirm).mockResolvedValueOnce(true);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
// Expected
|
|
399
|
-
}
|
|
400
|
-
expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['--all']);
|
|
455
|
+
const result = await flows.handleCleanPRs();
|
|
456
|
+
expect(gatherPrWorktreeInfo).toHaveBeenCalled();
|
|
457
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
401
458
|
});
|
|
402
459
|
it('returns CANCELLED when not confirmed', async () => {
|
|
403
460
|
vi.mocked(promptChoice).mockResolvedValueOnce('clean-all');
|
|
404
461
|
vi.mocked(promptConfirm).mockResolvedValueOnce(false);
|
|
405
462
|
const result = await flows.handleCleanPRs();
|
|
406
463
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
407
|
-
expect(
|
|
464
|
+
expect(gatherPrWorktreeInfo).not.toHaveBeenCalled();
|
|
408
465
|
});
|
|
409
466
|
});
|
|
410
467
|
describe('clean-specific', () => {
|
|
411
|
-
it('calls cleanpr with PR number', async () => {
|
|
468
|
+
it('calls cleanpr with PR number and returns to menu', async () => {
|
|
412
469
|
vi.mocked(promptChoice).mockResolvedValueOnce('clean-specific');
|
|
413
470
|
vi.mocked(promptInput).mockResolvedValueOnce('42');
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
// Expected
|
|
419
|
-
}
|
|
420
|
-
expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['42']);
|
|
471
|
+
const result = await flows.handleCleanPRs();
|
|
472
|
+
expect(gatherPrWorktreeInfo).toHaveBeenCalled();
|
|
473
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
421
474
|
});
|
|
422
475
|
it('returns CANCELLED when PR number is empty', async () => {
|
|
423
476
|
vi.mocked(promptChoice).mockResolvedValueOnce('clean-specific');
|
|
@@ -433,15 +486,12 @@ describe('Interactive Menu Flows', () => {
|
|
|
433
486
|
});
|
|
434
487
|
});
|
|
435
488
|
describe('dry-run', () => {
|
|
436
|
-
it('calls cleanpr with
|
|
489
|
+
it('calls cleanpr with dry-run and returns to menu', async () => {
|
|
437
490
|
vi.mocked(promptChoice).mockResolvedValueOnce('dry-run');
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
// Expected
|
|
443
|
-
}
|
|
444
|
-
expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['--dry-run']);
|
|
491
|
+
const result = await flows.handleCleanPRs();
|
|
492
|
+
expect(gatherPrWorktreeInfo).toHaveBeenCalled();
|
|
493
|
+
expect(getCleanableWorktrees).toHaveBeenCalled();
|
|
494
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
445
495
|
});
|
|
446
496
|
});
|
|
447
497
|
it('handles user cancellation', async () => {
|
|
@@ -455,72 +505,110 @@ describe('Interactive Menu Flows', () => {
|
|
|
455
505
|
vi.mocked(promptChoice).mockResolvedValueOnce('back');
|
|
456
506
|
const result = await flows.handleLinkConfig();
|
|
457
507
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
458
|
-
expect(runSubcommand).not.toHaveBeenCalled();
|
|
459
508
|
});
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
await flows.handleLinkConfig();
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['add', '.env']);
|
|
490
|
-
});
|
|
491
|
-
it('add returns CANCELLED when file path is empty', async () => {
|
|
492
|
-
vi.mocked(promptChoice).mockResolvedValueOnce('add');
|
|
493
|
-
vi.mocked(promptInput).mockResolvedValueOnce('');
|
|
494
|
-
const result = await flows.handleLinkConfig();
|
|
495
|
-
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
496
|
-
expect(runSubcommand).not.toHaveBeenCalled();
|
|
497
|
-
});
|
|
498
|
-
it('remove calls wtlink remove with file path', async () => {
|
|
499
|
-
vi.mocked(promptChoice).mockResolvedValueOnce('remove');
|
|
500
|
-
vi.mocked(promptInput).mockResolvedValueOnce('.env.local');
|
|
501
|
-
try {
|
|
502
|
-
await flows.handleLinkConfig();
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
// Expected
|
|
506
|
-
}
|
|
507
|
-
expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['remove', '.env.local']);
|
|
508
|
-
});
|
|
509
|
-
it('remove returns CANCELLED when file path is empty', async () => {
|
|
510
|
-
vi.mocked(promptChoice).mockResolvedValueOnce('remove');
|
|
511
|
-
vi.mocked(promptInput).mockResolvedValueOnce('');
|
|
512
|
-
const result = await flows.handleLinkConfig();
|
|
513
|
-
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
509
|
+
describe('view via library', () => {
|
|
510
|
+
it('displays manifest contents from loadManifestData', async () => {
|
|
511
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('view');
|
|
512
|
+
const result = await flows.handleLinkConfig();
|
|
513
|
+
expect(loadManifestData).toHaveBeenCalledWith('/mock/repo');
|
|
514
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
515
|
+
});
|
|
516
|
+
it('shows empty message when manifest has no files', async () => {
|
|
517
|
+
vi.mocked(loadManifestData).mockReturnValueOnce({
|
|
518
|
+
enabled: [],
|
|
519
|
+
disabled: [],
|
|
520
|
+
source: 'empty',
|
|
521
|
+
});
|
|
522
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('view');
|
|
523
|
+
const result = await flows.handleLinkConfig();
|
|
524
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
525
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No files'));
|
|
526
|
+
});
|
|
527
|
+
it('displays enabled and disabled files', async () => {
|
|
528
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('view');
|
|
529
|
+
const result = await flows.handleLinkConfig();
|
|
530
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
531
|
+
// Check enabled files are shown
|
|
532
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Enabled'));
|
|
533
|
+
expect(consoleSpy).toHaveBeenCalledWith(' .env');
|
|
534
|
+
expect(consoleSpy).toHaveBeenCalledWith(' .env.local');
|
|
535
|
+
// Check disabled files are shown
|
|
536
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Disabled'));
|
|
537
|
+
});
|
|
514
538
|
});
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
await flows.handleLinkConfig();
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
539
|
+
describe('sync via wtlink link', () => {
|
|
540
|
+
it('calls wtlink link library function', async () => {
|
|
541
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('sync');
|
|
542
|
+
const result = await flows.handleLinkConfig();
|
|
543
|
+
expect(runWtlinkLink).toHaveBeenCalledWith(expect.objectContaining({
|
|
544
|
+
manifestFile: '.wtlinkrc',
|
|
545
|
+
dryRun: false,
|
|
546
|
+
type: 'hard',
|
|
547
|
+
}));
|
|
548
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
549
|
+
});
|
|
550
|
+
it('shows error when sync fails', async () => {
|
|
551
|
+
vi.mocked(runWtlinkLink).mockRejectedValueOnce(new Error('Link failed'));
|
|
552
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('sync');
|
|
553
|
+
const result = await flows.handleLinkConfig();
|
|
554
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
555
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Link failed'));
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
describe('add via library', () => {
|
|
559
|
+
it('adds file to manifest via saveManifestData', async () => {
|
|
560
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('add');
|
|
561
|
+
vi.mocked(promptInput).mockResolvedValueOnce('.npmrc');
|
|
562
|
+
const result = await flows.handleLinkConfig();
|
|
563
|
+
expect(saveManifestData).toHaveBeenCalledWith('/mock/repo', ['.env', '.env.local', '.npmrc'], ['config.json']);
|
|
564
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
565
|
+
});
|
|
566
|
+
it('skips duplicate files', async () => {
|
|
567
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('add');
|
|
568
|
+
vi.mocked(promptInput).mockResolvedValueOnce('.env');
|
|
569
|
+
const result = await flows.handleLinkConfig();
|
|
570
|
+
expect(saveManifestData).not.toHaveBeenCalled();
|
|
571
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
572
|
+
});
|
|
573
|
+
it('returns CANCELLED when file path is empty', async () => {
|
|
574
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('add');
|
|
575
|
+
vi.mocked(promptInput).mockResolvedValueOnce('');
|
|
576
|
+
const result = await flows.handleLinkConfig();
|
|
577
|
+
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
578
|
+
expect(saveManifestData).not.toHaveBeenCalled();
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
describe('remove via library', () => {
|
|
582
|
+
it('removes file from manifest via saveManifestData', async () => {
|
|
583
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('remove');
|
|
584
|
+
vi.mocked(promptInput).mockResolvedValueOnce('.env');
|
|
585
|
+
const result = await flows.handleLinkConfig();
|
|
586
|
+
expect(saveManifestData).toHaveBeenCalledWith('/mock/repo', ['.env.local'], ['config.json']);
|
|
587
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
588
|
+
});
|
|
589
|
+
it('handles file not in manifest', async () => {
|
|
590
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('remove');
|
|
591
|
+
vi.mocked(promptInput).mockResolvedValueOnce('nonexistent.txt');
|
|
592
|
+
const result = await flows.handleLinkConfig();
|
|
593
|
+
expect(saveManifestData).not.toHaveBeenCalled();
|
|
594
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
595
|
+
});
|
|
596
|
+
it('returns CANCELLED when file path is empty', async () => {
|
|
597
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('remove');
|
|
598
|
+
vi.mocked(promptInput).mockResolvedValueOnce('');
|
|
599
|
+
const result = await flows.handleLinkConfig();
|
|
600
|
+
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
describe('validate', () => {
|
|
604
|
+
it('calls wtlink validate and returns to menu', async () => {
|
|
605
|
+
vi.mocked(promptChoice).mockResolvedValueOnce('validate');
|
|
606
|
+
const result = await flows.handleLinkConfig();
|
|
607
|
+
expect(runWtlinkValidate).toHaveBeenCalledWith(expect.objectContaining({
|
|
608
|
+
manifestFile: '.wtlinkrc',
|
|
609
|
+
}));
|
|
610
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
611
|
+
});
|
|
524
612
|
});
|
|
525
613
|
it('handles user cancellation', async () => {
|
|
526
614
|
vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
|
|
@@ -533,53 +621,41 @@ describe('Interactive Menu Flows', () => {
|
|
|
533
621
|
vi.mocked(promptChoice).mockResolvedValueOnce('back');
|
|
534
622
|
const result = await flows.handleConfigure();
|
|
535
623
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
536
|
-
expect(runSubcommand).not.toHaveBeenCalled();
|
|
537
624
|
});
|
|
538
|
-
it('view calls
|
|
625
|
+
it('view calls formatConfigDisplay and returns to menu', async () => {
|
|
539
626
|
vi.mocked(promptChoice).mockResolvedValueOnce('view');
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['show']);
|
|
547
|
-
});
|
|
548
|
-
it('init calls wtconfig init after confirmation', async () => {
|
|
627
|
+
const result = await flows.handleConfigure();
|
|
628
|
+
expect(loadRepoConfig).toHaveBeenCalledWith('/mock/repo');
|
|
629
|
+
expect(formatConfigDisplay).toHaveBeenCalled();
|
|
630
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
631
|
+
});
|
|
632
|
+
it('init shows redirect message after confirmation and returns to menu', async () => {
|
|
549
633
|
vi.mocked(promptChoice).mockResolvedValueOnce('init');
|
|
550
634
|
vi.mocked(promptConfirm).mockResolvedValueOnce(true);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
catch {
|
|
555
|
-
// Expected
|
|
556
|
-
}
|
|
557
|
-
expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['init']);
|
|
635
|
+
const result = await flows.handleConfigure();
|
|
636
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('wt init'));
|
|
637
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
558
638
|
});
|
|
559
639
|
it('init returns CANCELLED when not confirmed', async () => {
|
|
560
640
|
vi.mocked(promptChoice).mockResolvedValueOnce('init');
|
|
561
641
|
vi.mocked(promptConfirm).mockResolvedValueOnce(false);
|
|
562
642
|
const result = await flows.handleConfigure();
|
|
563
643
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
564
|
-
expect(runSubcommand).not.toHaveBeenCalled();
|
|
565
644
|
});
|
|
566
|
-
it('edit calls
|
|
645
|
+
it('edit calls setConfigValue and saveRepoConfig with setting and value', async () => {
|
|
567
646
|
vi.mocked(promptChoice).mockResolvedValueOnce('edit').mockResolvedValueOnce('baseBranch');
|
|
568
647
|
vi.mocked(promptInput).mockResolvedValueOnce('develop');
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// Expected
|
|
574
|
-
}
|
|
575
|
-
expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['set', 'baseBranch', 'develop']);
|
|
648
|
+
const result = await flows.handleConfigure();
|
|
649
|
+
expect(setConfigValue).toHaveBeenCalledWith({}, 'baseBranch', 'develop');
|
|
650
|
+
expect(saveRepoConfig).toHaveBeenCalled();
|
|
651
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
576
652
|
});
|
|
577
653
|
it('edit returns CANCELLED when value is empty', async () => {
|
|
578
654
|
vi.mocked(promptChoice).mockResolvedValueOnce('edit').mockResolvedValueOnce('branchPrefix');
|
|
579
655
|
vi.mocked(promptInput).mockResolvedValueOnce('');
|
|
580
656
|
const result = await flows.handleConfigure();
|
|
581
657
|
expect(result).toEqual({ completed: false, returnToMenu: true });
|
|
582
|
-
expect(
|
|
658
|
+
expect(saveRepoConfig).not.toHaveBeenCalled();
|
|
583
659
|
});
|
|
584
660
|
it('handles user cancellation', async () => {
|
|
585
661
|
vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
|
|
@@ -591,12 +667,12 @@ describe('Interactive Menu Flows', () => {
|
|
|
591
667
|
it('exits on exit selection', async () => {
|
|
592
668
|
vi.mocked(promptChoice).mockResolvedValueOnce('exit');
|
|
593
669
|
await showMainMenu();
|
|
594
|
-
expect(
|
|
670
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
595
671
|
});
|
|
596
672
|
it('exits on user cancellation', async () => {
|
|
597
673
|
vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
|
|
598
674
|
await showMainMenu();
|
|
599
|
-
expect(
|
|
675
|
+
expect(runNewprHandler).not.toHaveBeenCalled();
|
|
600
676
|
});
|
|
601
677
|
it('re-throws non-cancellation errors', async () => {
|
|
602
678
|
vi.mocked(promptChoice).mockRejectedValueOnce(new Error('Some other error'));
|
|
@@ -611,35 +687,29 @@ describe('Interactive Menu Flows', () => {
|
|
|
611
687
|
// Should have called promptChoice 3 times (menu -> sub-menu -> back to menu -> exit)
|
|
612
688
|
expect(promptChoice).toHaveBeenCalledTimes(3);
|
|
613
689
|
});
|
|
614
|
-
it('handles list worktrees
|
|
615
|
-
vi.mocked(promptChoice)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
await showMainMenu();
|
|
638
|
-
}
|
|
639
|
-
catch {
|
|
640
|
-
// Expected
|
|
641
|
-
}
|
|
642
|
-
expect(runSubcommand).toHaveBeenCalledWith('wtstate', []);
|
|
690
|
+
it('handles list worktrees and returns to menu', async () => {
|
|
691
|
+
vi.mocked(promptChoice)
|
|
692
|
+
.mockResolvedValueOnce('list') // Select list
|
|
693
|
+
.mockResolvedValueOnce('exit'); // Then exit
|
|
694
|
+
await showMainMenu();
|
|
695
|
+
expect(gatherWorktreeInfo).toHaveBeenCalled();
|
|
696
|
+
expect(promptChoice).toHaveBeenCalledTimes(2);
|
|
697
|
+
});
|
|
698
|
+
it('handles browse PRs and returns to menu', async () => {
|
|
699
|
+
vi.mocked(promptChoice)
|
|
700
|
+
.mockResolvedValueOnce('browse-prs') // Select browse-prs
|
|
701
|
+
.mockResolvedValueOnce('exit'); // Then exit
|
|
702
|
+
await showMainMenu();
|
|
703
|
+
expect(runPrsCommand).toHaveBeenCalled();
|
|
704
|
+
expect(promptChoice).toHaveBeenCalledTimes(2);
|
|
705
|
+
});
|
|
706
|
+
it('handles show state and returns to menu', async () => {
|
|
707
|
+
vi.mocked(promptChoice)
|
|
708
|
+
.mockResolvedValueOnce('state') // Select state
|
|
709
|
+
.mockResolvedValueOnce('exit'); // Then exit
|
|
710
|
+
await showMainMenu();
|
|
711
|
+
expect(analyzeState).toHaveBeenCalled();
|
|
712
|
+
expect(promptChoice).toHaveBeenCalledTimes(2);
|
|
643
713
|
});
|
|
644
714
|
});
|
|
645
715
|
describe('FlowResult types', () => {
|
|
@@ -649,17 +719,10 @@ describe('Interactive Menu Flows', () => {
|
|
|
649
719
|
expect(result.completed).toBe(false);
|
|
650
720
|
expect(result.returnToMenu).toBe(true);
|
|
651
721
|
});
|
|
652
|
-
it('flows that run
|
|
722
|
+
it('flows that run operations return completed with returnToMenu=true', async () => {
|
|
653
723
|
vi.mocked(promptChoice).mockResolvedValueOnce('dry-run');
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
await flows.handleCleanPRs();
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
// Expected
|
|
661
|
-
}
|
|
662
|
-
expect(runSubcommand).toHaveBeenCalled();
|
|
724
|
+
const result = await flows.handleCleanPRs();
|
|
725
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
663
726
|
});
|
|
664
727
|
});
|
|
665
728
|
});
|
|
@@ -702,16 +765,14 @@ describe('Config loading in flows', () => {
|
|
|
702
765
|
vi.mocked(promptChoice).mockResolvedValueOnce('from-description').mockResolvedValueOnce(true);
|
|
703
766
|
vi.mocked(promptInput).mockResolvedValueOnce('Test feature').mockResolvedValueOnce('develop'); // User accepts default
|
|
704
767
|
vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
705
|
-
|
|
706
|
-
await flows.handleNewPR();
|
|
707
|
-
}
|
|
708
|
-
catch {
|
|
709
|
-
// Expected
|
|
710
|
-
}
|
|
768
|
+
const result = await flows.handleNewPR();
|
|
711
769
|
// Verify loadConfig was called
|
|
712
770
|
expect(loadConfig).toHaveBeenCalled();
|
|
713
|
-
//
|
|
714
|
-
expect(
|
|
771
|
+
// Verify runNewprHandler was called with develop base branch
|
|
772
|
+
expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
773
|
+
baseBranch: 'develop',
|
|
774
|
+
}));
|
|
775
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
715
776
|
});
|
|
716
777
|
});
|
|
717
778
|
describe('Git branch listing in flows', () => {
|
|
@@ -726,14 +787,10 @@ describe('Git branch listing in flows', () => {
|
|
|
726
787
|
.mockResolvedValueOnce('feat/existing-branch')
|
|
727
788
|
.mockResolvedValueOnce(true);
|
|
728
789
|
vi.mocked(promptInput).mockResolvedValueOnce('main');
|
|
729
|
-
|
|
730
|
-
await flows.handleNewPR();
|
|
731
|
-
}
|
|
732
|
-
catch {
|
|
733
|
-
// Expected
|
|
734
|
-
}
|
|
790
|
+
const result = await flows.handleNewPR();
|
|
735
791
|
// Check that listLocalBranches was called
|
|
736
792
|
expect(git.listLocalBranches).toHaveBeenCalled();
|
|
793
|
+
expect(result).toEqual({ completed: true, returnToMenu: true });
|
|
737
794
|
});
|
|
738
795
|
});
|
|
739
796
|
//# sourceMappingURL=interactive-menu.test.js.map
|