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