@akiojin/gwt 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.ja.md +323 -0
  2. package/README.md +347 -0
  3. package/bin/gwt.js +5 -0
  4. package/package.json +125 -0
  5. package/src/claude-history.ts +717 -0
  6. package/src/claude.ts +292 -0
  7. package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
  8. package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
  9. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
  10. package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
  11. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
  12. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
  13. package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
  14. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
  15. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
  16. package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
  17. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
  18. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
  19. package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
  20. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
  21. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
  22. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
  23. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
  24. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
  25. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
  26. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
  27. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
  28. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
  29. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
  30. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
  31. package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
  32. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
  33. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
  34. package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
  35. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
  36. package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
  37. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
  38. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
  39. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
  40. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
  41. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
  42. package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
  43. package/src/cli/ui/components/App.tsx +793 -0
  44. package/src/cli/ui/components/common/Confirm.tsx +40 -0
  45. package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
  46. package/src/cli/ui/components/common/Input.tsx +36 -0
  47. package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
  48. package/src/cli/ui/components/common/Select.tsx +216 -0
  49. package/src/cli/ui/components/parts/Footer.tsx +41 -0
  50. package/src/cli/ui/components/parts/Header.test.tsx +85 -0
  51. package/src/cli/ui/components/parts/Header.tsx +63 -0
  52. package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
  53. package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
  54. package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
  55. package/src/cli/ui/components/parts/Stats.tsx +67 -0
  56. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
  57. package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
  58. package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
  59. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
  60. package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
  61. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
  62. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
  63. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
  64. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
  65. package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
  66. package/src/cli/ui/hooks/useGitData.ts +157 -0
  67. package/src/cli/ui/hooks/useScreenState.ts +44 -0
  68. package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
  69. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
  70. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
  71. package/src/cli/ui/types.ts +295 -0
  72. package/src/cli/ui/utils/baseBranch.ts +34 -0
  73. package/src/cli/ui/utils/branchFormatter.ts +222 -0
  74. package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
  75. package/src/codex.ts +139 -0
  76. package/src/config/builtin-tools.ts +44 -0
  77. package/src/config/constants.ts +100 -0
  78. package/src/config/env-history.ts +45 -0
  79. package/src/config/index.ts +204 -0
  80. package/src/config/tools.ts +293 -0
  81. package/src/git.ts +1102 -0
  82. package/src/github.ts +158 -0
  83. package/src/index.test.ts +87 -0
  84. package/src/index.ts +684 -0
  85. package/src/index.ts.backup +1543 -0
  86. package/src/launcher.ts +142 -0
  87. package/src/repositories/git.repository.ts +129 -0
  88. package/src/repositories/github.repository.ts +83 -0
  89. package/src/repositories/worktree.repository.ts +69 -0
  90. package/src/services/BatchMergeService.ts +251 -0
  91. package/src/services/WorktreeOrchestrator.ts +115 -0
  92. package/src/services/__tests__/BatchMergeService.test.ts +518 -0
  93. package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
  94. package/src/services/dependency-installer.ts +199 -0
  95. package/src/services/git.service.ts +113 -0
  96. package/src/services/github.service.ts +61 -0
  97. package/src/services/worktree.service.ts +66 -0
  98. package/src/types/api.ts +241 -0
  99. package/src/types/tools.ts +235 -0
  100. package/src/utils/spinner.ts +54 -0
  101. package/src/utils/terminal.ts +272 -0
  102. package/src/utils.test.ts +43 -0
  103. package/src/utils.ts +60 -0
  104. package/src/web/client/index.html +12 -0
  105. package/src/web/client/src/components/BranchGraph.tsx +231 -0
  106. package/src/web/client/src/components/EnvEditor.tsx +145 -0
  107. package/src/web/client/src/components/Terminal.tsx +137 -0
  108. package/src/web/client/src/hooks/useBranches.ts +41 -0
  109. package/src/web/client/src/hooks/useConfig.ts +31 -0
  110. package/src/web/client/src/hooks/useSessions.ts +59 -0
  111. package/src/web/client/src/hooks/useWorktrees.ts +47 -0
  112. package/src/web/client/src/index.css +834 -0
  113. package/src/web/client/src/lib/api.ts +184 -0
  114. package/src/web/client/src/lib/websocket.ts +174 -0
  115. package/src/web/client/src/main.tsx +29 -0
  116. package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
  117. package/src/web/client/src/pages/BranchListPage.tsx +264 -0
  118. package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
  119. package/src/web/client/src/router.tsx +27 -0
  120. package/src/web/client/vite.config.ts +21 -0
  121. package/src/web/server/env/importer.ts +54 -0
  122. package/src/web/server/index.ts +74 -0
  123. package/src/web/server/pty/manager.ts +189 -0
  124. package/src/web/server/routes/branches.ts +126 -0
  125. package/src/web/server/routes/config.ts +220 -0
  126. package/src/web/server/routes/index.ts +37 -0
  127. package/src/web/server/routes/sessions.ts +130 -0
  128. package/src/web/server/routes/worktrees.ts +108 -0
  129. package/src/web/server/services/branches.ts +368 -0
  130. package/src/web/server/services/worktrees.ts +85 -0
  131. package/src/web/server/websocket/handler.ts +180 -0
  132. package/src/worktree.ts +703 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
5
+ import type { Mock } from 'vitest';
6
+ import { render, waitFor } from '@testing-library/react';
7
+ import { act } from 'react-dom/test-utils';
8
+ import React from 'react';
9
+ import { App } from '../../components/App.js';
10
+ import { Window } from 'happy-dom';
11
+ import type { BranchInfo, BranchItem } from '../../types.js';
12
+ import * as BranchListScreenModule from '../../components/screens/BranchListScreen.js';
13
+ import * as BranchActionSelectorScreenModule from '../../screens/BranchActionSelectorScreen.js';
14
+
15
+ vi.mock('../../../../git.ts', () => ({
16
+ __esModule: true,
17
+ getAllBranches: vi.fn(),
18
+ getRepositoryRoot: vi.fn(async () => '/repo'),
19
+ deleteBranch: vi.fn(async () => undefined),
20
+ }));
21
+
22
+ const { mockIsProtectedBranchName, mockSwitchToProtectedBranch } = vi.hoisted(() => ({
23
+ mockIsProtectedBranchName: vi.fn(() => false),
24
+ mockSwitchToProtectedBranch: vi.fn(async () => 'none' as const),
25
+ }));
26
+
27
+ vi.mock('../../../../worktree.ts', () => ({
28
+ __esModule: true,
29
+ listAdditionalWorktrees: vi.fn(),
30
+ createWorktree: vi.fn(async () => undefined),
31
+ generateWorktreePath: vi.fn(async () => '/repo/.git/worktree/test'),
32
+ getMergedPRWorktrees: vi.fn(async () => []),
33
+ removeWorktree: vi.fn(async () => undefined),
34
+ isProtectedBranchName: mockIsProtectedBranchName,
35
+ switchToProtectedBranch: mockSwitchToProtectedBranch,
36
+ }));
37
+
38
+ const aiToolScreenProps: unknown[] = [];
39
+
40
+ vi.mock('../../components/screens/AIToolSelectorScreen.js', () => {
41
+ return {
42
+ AIToolSelectorScreen: (props: unknown) => {
43
+ aiToolScreenProps.push(props);
44
+ return React.createElement('div');
45
+ },
46
+ };
47
+ });
48
+
49
+ import { getAllBranches, getRepositoryRoot, deleteBranch } from '../../../../git.ts';
50
+ import {
51
+ listAdditionalWorktrees,
52
+ createWorktree,
53
+ generateWorktreePath,
54
+ getMergedPRWorktrees,
55
+ removeWorktree,
56
+ } from '../../../../worktree.ts';
57
+
58
+ const mockedGetAllBranches = getAllBranches as Mock;
59
+ const mockedGetRepositoryRoot = getRepositoryRoot as Mock;
60
+ const mockedDeleteBranch = deleteBranch as Mock;
61
+ const mockedListAdditionalWorktrees = listAdditionalWorktrees as Mock;
62
+ const mockedCreateWorktree = createWorktree as Mock;
63
+ const mockedGenerateWorktreePath = generateWorktreePath as Mock;
64
+ const mockedGetMergedPRWorktrees = getMergedPRWorktrees as Mock;
65
+ const mockedRemoveWorktree = removeWorktree as Mock;
66
+ const mockedIsProtectedBranchName = mockIsProtectedBranchName as Mock;
67
+ const mockedSwitchToProtectedBranch = mockSwitchToProtectedBranch as Mock;
68
+ const originalBranchListScreen = BranchListScreenModule.BranchListScreen;
69
+ const originalBranchActionSelectorScreen =
70
+ BranchActionSelectorScreenModule.BranchActionSelectorScreen;
71
+
72
+ describe('Navigation Integration Tests', () => {
73
+ beforeEach(() => {
74
+ // Setup happy-dom
75
+ const window = new Window();
76
+ globalThis.window = window as any;
77
+ globalThis.document = window.document as any;
78
+
79
+ // Reset mocks
80
+ mockedGetAllBranches.mockReset();
81
+ mockedListAdditionalWorktrees.mockReset();
82
+ mockedGetRepositoryRoot.mockReset();
83
+ mockedDeleteBranch.mockReset();
84
+ mockedCreateWorktree.mockReset();
85
+ mockedGenerateWorktreePath.mockReset();
86
+ mockedGetMergedPRWorktrees.mockReset();
87
+ mockedRemoveWorktree.mockReset();
88
+ mockedIsProtectedBranchName.mockReset();
89
+ mockedSwitchToProtectedBranch.mockReset();
90
+ mockedGetRepositoryRoot.mockResolvedValue('/repo');
91
+ mockedSwitchToProtectedBranch.mockResolvedValue('local');
92
+ });
93
+
94
+ const mockBranches: BranchInfo[] = [
95
+ {
96
+ name: 'main',
97
+ type: 'local',
98
+ branchType: 'main',
99
+ isCurrent: true,
100
+ },
101
+ {
102
+ name: 'feature/test',
103
+ type: 'local',
104
+ branchType: 'feature',
105
+ isCurrent: false,
106
+ },
107
+ ];
108
+
109
+ it('should start with branch-list screen', async () => {
110
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
111
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
112
+
113
+ const onExit = vi.fn();
114
+ const { getByText } = render(<App onExit={onExit} />);
115
+
116
+ await waitFor(() => {
117
+ expect(getByText(/gwt - Branch Selection/i)).toBeDefined();
118
+ expect(getByText(/main/)).toBeDefined();
119
+ });
120
+ });
121
+
122
+ it('should support navigation between screens', async () => {
123
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
124
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
125
+
126
+ const onExit = vi.fn();
127
+ const { container } = render(<App onExit={onExit} />);
128
+
129
+ await waitFor(() => {
130
+ expect(container).toBeDefined();
131
+ });
132
+
133
+ // Test will verify screen navigation
134
+ expect(container).toBeDefined();
135
+ });
136
+
137
+ it('should maintain state across screen transitions', async () => {
138
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
139
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
140
+
141
+ const onExit = vi.fn();
142
+ const { container } = render(<App onExit={onExit} />);
143
+
144
+ await waitFor(() => {
145
+ expect(container).toBeDefined();
146
+ });
147
+
148
+ // Test will verify state persistence
149
+ expect(container).toBeDefined();
150
+ });
151
+
152
+ it('should handle back navigation correctly', async () => {
153
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
154
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
155
+
156
+ const onExit = vi.fn();
157
+ const { container } = render(<App onExit={onExit} />);
158
+
159
+ await waitFor(() => {
160
+ expect(container).toBeDefined();
161
+ });
162
+
163
+ // Test will verify back navigation
164
+ expect(container).toBeDefined();
165
+ });
166
+
167
+ it('should handle navigation history', async () => {
168
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
169
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
170
+
171
+ const onExit = vi.fn();
172
+ const { container } = render(<App onExit={onExit} />);
173
+
174
+ await waitFor(() => {
175
+ expect(container).toBeDefined();
176
+ });
177
+
178
+ // Test will verify navigation history
179
+ expect(container).toBeDefined();
180
+ });
181
+
182
+ it('should display correct screen on navigation', async () => {
183
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
184
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
185
+
186
+ const onExit = vi.fn();
187
+ const { container } = render(<App onExit={onExit} />);
188
+
189
+ await waitFor(() => {
190
+ expect(container).toBeDefined();
191
+ });
192
+
193
+ // Test will verify correct screen rendering
194
+ expect(container).toBeDefined();
195
+ });
196
+
197
+ it('should call onExit when branch is selected', async () => {
198
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
199
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
200
+
201
+ const onExit = vi.fn();
202
+ const { container } = render(<App onExit={onExit} />);
203
+
204
+ await waitFor(() => {
205
+ expect(container).toBeDefined();
206
+ });
207
+
208
+ // Test will verify onExit is called
209
+ expect(container).toBeDefined();
210
+ });
211
+ });
212
+
213
+ describe('Protected Branch Navigation (T103)', () => {
214
+ const branchListProps: any[] = [];
215
+ const branchActionProps: any[] = [];
216
+ let branchListSpy: ReturnType<typeof vi.spyOn>;
217
+ let branchActionSpy: ReturnType<typeof vi.spyOn>;
218
+
219
+ const baseBranches: BranchInfo[] = [
220
+ {
221
+ name: 'main',
222
+ type: 'local',
223
+ branchType: 'main',
224
+ isCurrent: true,
225
+ },
226
+ {
227
+ name: 'feature/test',
228
+ type: 'local',
229
+ branchType: 'feature',
230
+ isCurrent: false,
231
+ },
232
+ ];
233
+
234
+ beforeEach(() => {
235
+ const window = new Window();
236
+ globalThis.window = window as any;
237
+ globalThis.document = window.document as any;
238
+ mockedGetAllBranches.mockReset();
239
+ mockedListAdditionalWorktrees.mockReset();
240
+ mockedGetRepositoryRoot.mockReset();
241
+ mockedDeleteBranch.mockReset();
242
+ mockedCreateWorktree.mockReset();
243
+ mockedGenerateWorktreePath.mockReset();
244
+ mockedGetMergedPRWorktrees.mockReset();
245
+ mockedRemoveWorktree.mockReset();
246
+ mockedIsProtectedBranchName.mockReset();
247
+ mockedSwitchToProtectedBranch.mockReset();
248
+ mockedGetRepositoryRoot.mockResolvedValue('/repo');
249
+ branchListProps.length = 0;
250
+ branchActionProps.length = 0;
251
+ aiToolScreenProps.length = 0;
252
+ branchListSpy = vi
253
+ .spyOn(BranchListScreenModule, 'BranchListScreen')
254
+ .mockImplementation((props: any) => {
255
+ branchListProps.push(props);
256
+ return React.createElement(originalBranchListScreen, props);
257
+ });
258
+ branchActionSpy = vi
259
+ .spyOn(BranchActionSelectorScreenModule, 'BranchActionSelectorScreen')
260
+ .mockImplementation((props: any) => {
261
+ branchActionProps.push(props);
262
+ return React.createElement(originalBranchActionSelectorScreen, props);
263
+ });
264
+
265
+ mockedIsProtectedBranchName.mockImplementation((name: string) =>
266
+ ['main', 'develop', 'origin/main', 'origin/develop'].includes(name)
267
+ );
268
+ mockedSwitchToProtectedBranch.mockResolvedValue('local');
269
+ mockedGetRepositoryRoot.mockResolvedValue('/repo');
270
+ });
271
+
272
+ afterEach(() => {
273
+ branchListSpy.mockRestore();
274
+ branchActionSpy.mockRestore();
275
+ });
276
+
277
+ it('switches local protected branches via root workflow and navigates to AI tool', async () => {
278
+ mockedGetAllBranches.mockResolvedValue(baseBranches);
279
+ mockedListAdditionalWorktrees.mockResolvedValue([]);
280
+
281
+ const onExit = vi.fn();
282
+ render(<App onExit={onExit} />);
283
+
284
+ await waitFor(() => {
285
+ expect(branchListProps.length).toBeGreaterThan(0);
286
+ });
287
+
288
+ await waitFor(() => {
289
+ const latest = branchListProps.at(-1);
290
+ const names = (latest?.branches as BranchItem[] | undefined)?.map(
291
+ (item) => item.name
292
+ );
293
+ expect(names).toBeDefined();
294
+ expect(names).toContain('main');
295
+ });
296
+
297
+ const latestProps = branchListProps.at(-1);
298
+ const protectedBranch = (latestProps?.branches as BranchItem[]).find(
299
+ (item) => item.name === 'main'
300
+ );
301
+ expect(protectedBranch).toBeDefined();
302
+
303
+ await act(async () => {
304
+ latestProps?.onSelect(protectedBranch);
305
+ await Promise.resolve();
306
+ });
307
+
308
+ await waitFor(() => {
309
+ expect(branchActionProps.length).toBeGreaterThan(0);
310
+ });
311
+
312
+ const actionProps = branchActionProps.at(-1);
313
+ expect(actionProps?.mode).toBe('protected');
314
+ expect(actionProps?.infoMessage).toContain('is a root branch');
315
+
316
+ await act(async () => {
317
+ await actionProps?.onUseExisting();
318
+ await Promise.resolve();
319
+ });
320
+
321
+ expect(mockedSwitchToProtectedBranch).toHaveBeenCalledWith({
322
+ branchName: 'main',
323
+ repoRoot: '/repo',
324
+ remoteRef: null,
325
+ });
326
+
327
+ await waitFor(() => {
328
+ expect(aiToolScreenProps.length).toBeGreaterThan(0);
329
+ });
330
+ });
331
+
332
+ it('creates tracking branch for remote protected selections before navigating to AI tool', async () => {
333
+ const remoteBranches: BranchInfo[] = [
334
+ {
335
+ name: 'origin/develop',
336
+ type: 'remote',
337
+ branchType: 'develop',
338
+ isCurrent: false,
339
+ },
340
+ {
341
+ name: 'feature/test',
342
+ type: 'local',
343
+ branchType: 'feature',
344
+ isCurrent: false,
345
+ },
346
+ ];
347
+ mockedGetAllBranches.mockResolvedValue(remoteBranches);
348
+ mockedListAdditionalWorktrees.mockResolvedValue([]);
349
+ mockedSwitchToProtectedBranch.mockResolvedValue('remote');
350
+
351
+ const onExit = vi.fn();
352
+ render(<App onExit={onExit} />);
353
+
354
+ await waitFor(() => {
355
+ expect(branchListProps.length).toBeGreaterThan(0);
356
+ });
357
+
358
+ await waitFor(() => {
359
+ const latest = branchListProps.at(-1);
360
+ const names = (latest?.branches as BranchItem[] | undefined)?.map(
361
+ (item) => item.name
362
+ );
363
+ expect(names).toBeDefined();
364
+ expect(names).toContain('origin/develop');
365
+ });
366
+
367
+ const latestProps = branchListProps.at(-1);
368
+ const protectedBranch = (latestProps?.branches as BranchItem[]).find(
369
+ (item) => item.name === 'origin/develop'
370
+ );
371
+ expect(protectedBranch).toBeDefined();
372
+
373
+ await act(async () => {
374
+ latestProps?.onSelect(protectedBranch);
375
+ await Promise.resolve();
376
+ });
377
+
378
+ await waitFor(() => {
379
+ expect(branchActionProps.length).toBeGreaterThan(0);
380
+ });
381
+
382
+ const actionProps = branchActionProps.at(-1);
383
+ expect(actionProps?.mode).toBe('protected');
384
+ expect(actionProps?.primaryLabel).toContain('root');
385
+
386
+ await act(async () => {
387
+ await actionProps?.onUseExisting();
388
+ await Promise.resolve();
389
+ });
390
+
391
+ expect(mockedSwitchToProtectedBranch).toHaveBeenCalledWith({
392
+ branchName: 'develop',
393
+ repoRoot: '/repo',
394
+ remoteRef: 'origin/develop',
395
+ });
396
+
397
+ await waitFor(() => {
398
+ expect(aiToolScreenProps.length).toBeGreaterThan(0);
399
+ });
400
+ });
401
+ });
402
+
403
+ afterAll(() => {
404
+ vi.restoreAllMocks();
405
+ });