@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,775 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ formatBranchItem,
4
+ formatBranchItems,
5
+ } from "../../utils/branchFormatter.js";
6
+ import type { BranchInfo } from "../../types.js";
7
+
8
+ describe("branchFormatter", () => {
9
+ describe("formatBranchItem", () => {
10
+ it("should format a main branch", () => {
11
+ const branchInfo: BranchInfo = {
12
+ name: "main",
13
+ type: "local",
14
+ branchType: "main",
15
+ isCurrent: true,
16
+ };
17
+
18
+ const result = formatBranchItem(branchInfo);
19
+
20
+ expect(result.name).toBe("main");
21
+ expect(result.type).toBe("local");
22
+ expect(result.branchType).toBe("main");
23
+ expect(result.isCurrent).toBe(true);
24
+ expect(result.icons).toContain("⚡"); // main icon
25
+ expect(result.icons).toContain("⭐"); // current icon
26
+ expect(result.label).toContain("main");
27
+ expect(result.value).toBe("main");
28
+ expect(result.hasChanges).toBe(false);
29
+ });
30
+
31
+ it("should format a feature branch", () => {
32
+ const branchInfo: BranchInfo = {
33
+ name: "feature/new-ui",
34
+ type: "local",
35
+ branchType: "feature",
36
+ isCurrent: false,
37
+ };
38
+
39
+ const result = formatBranchItem(branchInfo);
40
+
41
+ expect(result.icons).toContain("✨"); // feature icon
42
+ expect(result.icons).not.toContain("⭐"); // not current
43
+ expect(result.label).toContain("feature/new-ui");
44
+ expect(result.value).toBe("feature/new-ui");
45
+ });
46
+
47
+ it("should format a hotfix branch", () => {
48
+ const branchInfo: BranchInfo = {
49
+ name: "hotfix/critical-bug",
50
+ type: "local",
51
+ branchType: "hotfix",
52
+ isCurrent: false,
53
+ };
54
+
55
+ const result = formatBranchItem(branchInfo);
56
+
57
+ expect(result.icons).toContain("🔥"); // hotfix icon
58
+ expect(result.label).toContain("hotfix/critical-bug");
59
+ });
60
+
61
+ it("should format a release branch", () => {
62
+ const branchInfo: BranchInfo = {
63
+ name: "release/v1.0.0",
64
+ type: "local",
65
+ branchType: "release",
66
+ isCurrent: false,
67
+ };
68
+
69
+ const result = formatBranchItem(branchInfo);
70
+
71
+ expect(result.icons).toContain("🚀"); // release icon
72
+ expect(result.label).toContain("release/v1.0.0");
73
+ });
74
+
75
+ it("should format a remote branch", () => {
76
+ const branchInfo: BranchInfo = {
77
+ name: "origin/main",
78
+ type: "remote",
79
+ branchType: "main",
80
+ isCurrent: false,
81
+ };
82
+
83
+ const result = formatBranchItem(branchInfo);
84
+
85
+ expect(result.icons).toContain("⚡"); // main icon
86
+ expect(result.icons).toContain("☁"); // remote icon
87
+ expect(result.type).toBe("remote");
88
+ expect(result.label).toContain("origin/main");
89
+ });
90
+
91
+ it("should align branch names regardless of remote icon presence", () => {
92
+ const localBranch: BranchInfo = {
93
+ name: "feature/foo",
94
+ type: "local",
95
+ branchType: "feature",
96
+ isCurrent: false,
97
+ };
98
+ const remoteBranch: BranchInfo = {
99
+ name: "origin/feature/foo",
100
+ type: "remote",
101
+ branchType: "feature",
102
+ isCurrent: false,
103
+ };
104
+
105
+ const localResult = formatBranchItem(localBranch);
106
+ const remoteResult = formatBranchItem(remoteBranch);
107
+
108
+ const localNameIndex = localResult.label.indexOf(localResult.name);
109
+ const remoteNameIndex = remoteResult.label.indexOf(remoteResult.name);
110
+
111
+ expect(localNameIndex).toBeGreaterThan(0);
112
+ expect(localNameIndex).toBe(remoteNameIndex);
113
+ expect(remoteResult.label).toMatch(/☁(?:️|︎)?\s+origin/);
114
+ });
115
+
116
+ it("should include worktree status icon when provided", () => {
117
+ const branchInfo: BranchInfo = {
118
+ name: "feature/test",
119
+ type: "local",
120
+ branchType: "feature",
121
+ isCurrent: false,
122
+ worktree: {
123
+ path: "/path/to/worktree",
124
+ locked: false,
125
+ prunable: false,
126
+ },
127
+ };
128
+
129
+ const result = formatBranchItem(branchInfo);
130
+
131
+ expect(result.icons).toContain("🟢"); // active worktree icon
132
+ expect(result.worktreeStatus).toBe("active");
133
+ });
134
+
135
+ it("should mark branch with changes", () => {
136
+ const branchInfo: BranchInfo = {
137
+ name: "feature/wip",
138
+ type: "local",
139
+ branchType: "feature",
140
+ isCurrent: false,
141
+ };
142
+
143
+ const result = formatBranchItem(branchInfo, { hasChanges: true });
144
+
145
+ expect(result.icons).toContain("✏️"); // changes icon
146
+ expect(result.hasChanges).toBe(true);
147
+ });
148
+
149
+ it("should show unpushed commits icon", () => {
150
+ const branchInfo: BranchInfo = {
151
+ name: "feature/unpushed",
152
+ type: "local",
153
+ branchType: "feature",
154
+ isCurrent: false,
155
+ hasUnpushedCommits: true,
156
+ };
157
+
158
+ const result = formatBranchItem(branchInfo);
159
+
160
+ expect(result.icons).toContain("⬆️"); // unpushed icon
161
+ expect(result.label).toContain("⬆️");
162
+ });
163
+
164
+ it("should show open PR icon", () => {
165
+ const branchInfo: BranchInfo = {
166
+ name: "feature/pr-open",
167
+ type: "local",
168
+ branchType: "feature",
169
+ isCurrent: false,
170
+ openPR: { number: 123, title: "Test PR" },
171
+ };
172
+
173
+ const result = formatBranchItem(branchInfo);
174
+
175
+ expect(result.icons).toContain("🔀"); // open PR icon
176
+ expect(result.label).toContain("🔀");
177
+ });
178
+
179
+ it("should show merged PR icon", () => {
180
+ const branchInfo: BranchInfo = {
181
+ name: "feature/pr-merged",
182
+ type: "local",
183
+ branchType: "feature",
184
+ isCurrent: false,
185
+ mergedPR: { number: 456, mergedAt: "2025-10-31T00:00:00Z" },
186
+ };
187
+
188
+ const result = formatBranchItem(branchInfo);
189
+
190
+ expect(result.icons).toContain("✅"); // merged PR icon
191
+ expect(result.label).toContain("✅");
192
+ });
193
+
194
+ it("should show warning icon for inaccessible worktree", () => {
195
+ const branchInfo: BranchInfo = {
196
+ name: "feature/broken-worktree",
197
+ type: "local",
198
+ branchType: "feature",
199
+ isCurrent: false,
200
+ worktree: {
201
+ path: "/path/to/worktree",
202
+ locked: false,
203
+ prunable: false,
204
+ isAccessible: false,
205
+ },
206
+ };
207
+
208
+ const result = formatBranchItem(branchInfo);
209
+
210
+ expect(result.icons).toContain("🟠"); // inaccessible worktree icon
211
+ expect(result.icons).toContain("⚠️"); // warning icon
212
+ expect(result.worktreeStatus).toBe("inaccessible");
213
+ });
214
+
215
+ it("should prioritize hasChanges over unpushed commits", () => {
216
+ const branchInfo: BranchInfo = {
217
+ name: "feature/both",
218
+ type: "local",
219
+ branchType: "feature",
220
+ isCurrent: false,
221
+ hasUnpushedCommits: true,
222
+ };
223
+
224
+ const resultWithChanges = formatBranchItem(branchInfo, {
225
+ hasChanges: true,
226
+ });
227
+ expect(resultWithChanges.icons).toContain("✏️");
228
+ expect(resultWithChanges.icons).not.toContain("⬆️");
229
+
230
+ const resultWithoutChanges = formatBranchItem(branchInfo);
231
+ expect(resultWithoutChanges.icons).toContain("⬆️");
232
+ expect(resultWithoutChanges.icons).not.toContain("✏️");
233
+ });
234
+
235
+ it("should prioritize unpushed over open PR", () => {
236
+ const branchInfo: BranchInfo = {
237
+ name: "feature/unpushed-pr",
238
+ type: "local",
239
+ branchType: "feature",
240
+ isCurrent: false,
241
+ hasUnpushedCommits: true,
242
+ openPR: { number: 123, title: "Test PR" },
243
+ };
244
+
245
+ const result = formatBranchItem(branchInfo);
246
+
247
+ expect(result.icons).toContain("⬆️");
248
+ expect(result.icons).not.toContain("🔀");
249
+ });
250
+
251
+ it("should prioritize open PR over merged PR", () => {
252
+ const branchInfo: BranchInfo = {
253
+ name: "feature/both-pr",
254
+ type: "local",
255
+ branchType: "feature",
256
+ isCurrent: false,
257
+ openPR: { number: 123, title: "Test PR" },
258
+ mergedPR: { number: 456, mergedAt: "2025-10-31T00:00:00Z" },
259
+ };
260
+
261
+ const result = formatBranchItem(branchInfo);
262
+
263
+ expect(result.icons).toContain("🔀");
264
+ expect(result.icons).not.toContain("✅");
265
+ });
266
+
267
+ it("should prioritize merged PR over warning", () => {
268
+ const branchInfo: BranchInfo = {
269
+ name: "feature/merged-broken",
270
+ type: "local",
271
+ branchType: "feature",
272
+ isCurrent: false,
273
+ mergedPR: { number: 456, mergedAt: "2025-10-31T00:00:00Z" },
274
+ worktree: {
275
+ path: "/path/to/worktree",
276
+ locked: false,
277
+ prunable: false,
278
+ isAccessible: false,
279
+ },
280
+ };
281
+
282
+ const result = formatBranchItem(branchInfo);
283
+
284
+ expect(result.icons).toContain("✅");
285
+ expect(result.icons).not.toContain("⚠️");
286
+ });
287
+
288
+ it("should prioritize warning over current branch icon", () => {
289
+ const branchInfo: BranchInfo = {
290
+ name: "feature/current-broken",
291
+ type: "local",
292
+ branchType: "feature",
293
+ isCurrent: true,
294
+ worktree: {
295
+ path: "/path/to/worktree",
296
+ locked: false,
297
+ prunable: false,
298
+ isAccessible: false,
299
+ },
300
+ };
301
+
302
+ const result = formatBranchItem(branchInfo);
303
+
304
+ expect(result.icons).toContain("⚠️");
305
+ expect(result.icons).not.toContain("⭐");
306
+ });
307
+
308
+ it("should handle develop branch", () => {
309
+ const branchInfo: BranchInfo = {
310
+ name: "develop",
311
+ type: "local",
312
+ branchType: "develop",
313
+ isCurrent: false,
314
+ };
315
+
316
+ const result = formatBranchItem(branchInfo);
317
+
318
+ expect(result.icons).toContain("⚡"); // develop icon (same as main)
319
+ expect(result.label).toContain("develop");
320
+ });
321
+
322
+ it("should handle other branch type", () => {
323
+ const branchInfo: BranchInfo = {
324
+ name: "custom-branch",
325
+ type: "local",
326
+ branchType: "other",
327
+ isCurrent: false,
328
+ };
329
+
330
+ const result = formatBranchItem(branchInfo);
331
+
332
+ expect(result.icons).toContain("📌"); // other icon
333
+ expect(result.label).toContain("custom-branch");
334
+ });
335
+ });
336
+
337
+ describe("formatBranchItems", () => {
338
+ it("should format multiple branches with sorting", () => {
339
+ const branches: BranchInfo[] = [
340
+ {
341
+ name: "main",
342
+ type: "local",
343
+ branchType: "main",
344
+ isCurrent: true,
345
+ },
346
+ {
347
+ name: "feature/test",
348
+ type: "local",
349
+ branchType: "feature",
350
+ isCurrent: false,
351
+ },
352
+ {
353
+ name: "origin/main",
354
+ type: "remote",
355
+ branchType: "main",
356
+ isCurrent: false,
357
+ },
358
+ ];
359
+
360
+ const results = formatBranchItems(branches);
361
+
362
+ expect(results).toHaveLength(3);
363
+ // Current branch (main) should be first
364
+ expect(results[0].name).toBe("main");
365
+ expect(results[0].isCurrent).toBe(true);
366
+ // origin/main is also main branch, so it comes second
367
+ expect(results[1].name).toBe("origin/main");
368
+ expect(results[1].type).toBe("remote");
369
+ // feature/test comes last
370
+ expect(results[2].name).toBe("feature/test");
371
+ });
372
+
373
+ it("should handle empty array", () => {
374
+ const results = formatBranchItems([]);
375
+
376
+ expect(results).toHaveLength(0);
377
+ });
378
+
379
+ it("should sort branches alphabetically when no other priority applies", () => {
380
+ const branches: BranchInfo[] = [
381
+ {
382
+ name: "z-branch",
383
+ type: "local",
384
+ branchType: "feature",
385
+ isCurrent: false,
386
+ },
387
+ {
388
+ name: "a-branch",
389
+ type: "local",
390
+ branchType: "feature",
391
+ isCurrent: false,
392
+ },
393
+ ];
394
+
395
+ const results = formatBranchItems(branches);
396
+
397
+ expect(results[0].name).toBe("a-branch");
398
+ expect(results[1].name).toBe("z-branch");
399
+ });
400
+ });
401
+
402
+ describe("formatBranchItems - sorting", () => {
403
+ it("should prioritize current branch at the top", () => {
404
+ const branches: BranchInfo[] = [
405
+ {
406
+ name: "feature/a",
407
+ type: "local",
408
+ branchType: "feature",
409
+ isCurrent: false,
410
+ },
411
+ {
412
+ name: "feature/current",
413
+ type: "local",
414
+ branchType: "feature",
415
+ isCurrent: true,
416
+ },
417
+ {
418
+ name: "main",
419
+ type: "local",
420
+ branchType: "main",
421
+ isCurrent: false,
422
+ },
423
+ ];
424
+
425
+ const results = formatBranchItems(branches);
426
+
427
+ expect(results[0].name).toBe("feature/current");
428
+ expect(results[0].isCurrent).toBe(true);
429
+ });
430
+
431
+ it("should prioritize main branch as second (after current)", () => {
432
+ const branches: BranchInfo[] = [
433
+ {
434
+ name: "feature/test",
435
+ type: "local",
436
+ branchType: "feature",
437
+ isCurrent: false,
438
+ },
439
+ {
440
+ name: "main",
441
+ type: "local",
442
+ branchType: "main",
443
+ isCurrent: false,
444
+ },
445
+ {
446
+ name: "develop",
447
+ type: "local",
448
+ branchType: "develop",
449
+ isCurrent: false,
450
+ },
451
+ ];
452
+
453
+ const results = formatBranchItems(branches);
454
+
455
+ expect(results[0].name).toBe("main");
456
+ expect(results[1].name).toBe("develop");
457
+ });
458
+
459
+ it("should prioritize develop branch after main (when main exists)", () => {
460
+ const branches: BranchInfo[] = [
461
+ {
462
+ name: "feature/test",
463
+ type: "local",
464
+ branchType: "feature",
465
+ isCurrent: false,
466
+ },
467
+ {
468
+ name: "develop",
469
+ type: "local",
470
+ branchType: "develop",
471
+ isCurrent: false,
472
+ },
473
+ {
474
+ name: "main",
475
+ type: "local",
476
+ branchType: "main",
477
+ isCurrent: false,
478
+ },
479
+ ];
480
+
481
+ const results = formatBranchItems(branches);
482
+
483
+ expect(results[0].name).toBe("main");
484
+ expect(results[1].name).toBe("develop");
485
+ expect(results[2].name).toBe("feature/test");
486
+ });
487
+
488
+ it("should NOT prioritize develop branch when main does not exist", () => {
489
+ const branches: BranchInfo[] = [
490
+ {
491
+ name: "feature/a",
492
+ type: "local",
493
+ branchType: "feature",
494
+ isCurrent: false,
495
+ },
496
+ {
497
+ name: "develop",
498
+ type: "local",
499
+ branchType: "develop",
500
+ isCurrent: false,
501
+ },
502
+ {
503
+ name: "feature/z",
504
+ type: "local",
505
+ branchType: "feature",
506
+ isCurrent: false,
507
+ },
508
+ ];
509
+
510
+ const results = formatBranchItems(branches);
511
+
512
+ // develop should be sorted alphabetically, not prioritized
513
+ expect(results[0].name).toBe("develop");
514
+ expect(results[1].name).toBe("feature/a");
515
+ expect(results[2].name).toBe("feature/z");
516
+ });
517
+
518
+ it("should prioritize branches with worktree", () => {
519
+ const worktreeMap = new Map([
520
+ [
521
+ "feature/with-worktree",
522
+ {
523
+ path: "/path/to/worktree",
524
+ locked: false,
525
+ prunable: false,
526
+ isAccessible: true,
527
+ },
528
+ ],
529
+ ]);
530
+
531
+ const branches: BranchInfo[] = [
532
+ {
533
+ name: "feature/no-worktree",
534
+ type: "local",
535
+ branchType: "feature",
536
+ isCurrent: false,
537
+ },
538
+ {
539
+ name: "feature/with-worktree",
540
+ type: "local",
541
+ branchType: "feature",
542
+ isCurrent: false,
543
+ worktree: {
544
+ path: "/path/to/worktree",
545
+ locked: false,
546
+ prunable: false,
547
+ isAccessible: true,
548
+ },
549
+ },
550
+ ];
551
+
552
+ const results = formatBranchItems(branches, worktreeMap);
553
+
554
+ expect(results[0].name).toBe("feature/with-worktree");
555
+ expect(results[1].name).toBe("feature/no-worktree");
556
+ });
557
+
558
+ it("should sort branches with worktree by latest commit timestamp", () => {
559
+ const worktreeMap = new Map([
560
+ [
561
+ "feature/recent",
562
+ {
563
+ path: "/path/to/recent",
564
+ locked: false,
565
+ prunable: false,
566
+ isAccessible: true,
567
+ },
568
+ ],
569
+ [
570
+ "feature/older",
571
+ {
572
+ path: "/path/to/older",
573
+ locked: false,
574
+ prunable: false,
575
+ isAccessible: true,
576
+ },
577
+ ],
578
+ ]);
579
+
580
+ const branches: BranchInfo[] = [
581
+ {
582
+ name: "feature/older",
583
+ type: "local",
584
+ branchType: "feature",
585
+ isCurrent: false,
586
+ worktree: {
587
+ path: "/path/to/older",
588
+ locked: false,
589
+ prunable: false,
590
+ isAccessible: true,
591
+ },
592
+ latestCommitTimestamp: 1_700_000_000,
593
+ },
594
+ {
595
+ name: "feature/recent",
596
+ type: "local",
597
+ branchType: "feature",
598
+ isCurrent: false,
599
+ worktree: {
600
+ path: "/path/to/recent",
601
+ locked: false,
602
+ prunable: false,
603
+ isAccessible: true,
604
+ },
605
+ latestCommitTimestamp: 1_800_000_000,
606
+ },
607
+ ];
608
+
609
+ const results = formatBranchItems(branches, worktreeMap);
610
+
611
+ expect(results[0].name).toBe("feature/recent");
612
+ expect(results[1].name).toBe("feature/older");
613
+ });
614
+
615
+ it("should prioritize local branches over remote branches", () => {
616
+ const branches: BranchInfo[] = [
617
+ {
618
+ name: "origin/feature/remote",
619
+ type: "remote",
620
+ branchType: "feature",
621
+ isCurrent: false,
622
+ },
623
+ {
624
+ name: "feature/local",
625
+ type: "local",
626
+ branchType: "feature",
627
+ isCurrent: false,
628
+ },
629
+ ];
630
+
631
+ const results = formatBranchItems(branches);
632
+
633
+ expect(results[0].name).toBe("feature/local");
634
+ expect(results[0].type).toBe("local");
635
+ expect(results[1].name).toBe("origin/feature/remote");
636
+ expect(results[1].type).toBe("remote");
637
+ });
638
+
639
+ it("should sort by latest commit timestamp when worktree status matches", () => {
640
+ const branches: BranchInfo[] = [
641
+ {
642
+ name: "origin/feature/newer",
643
+ type: "remote",
644
+ branchType: "feature",
645
+ isCurrent: false,
646
+ latestCommitTimestamp: 1_900_000_000,
647
+ },
648
+ {
649
+ name: "feature/local-older",
650
+ type: "local",
651
+ branchType: "feature",
652
+ isCurrent: false,
653
+ latestCommitTimestamp: 1_800_000_000,
654
+ },
655
+ ];
656
+
657
+ const results = formatBranchItems(branches);
658
+
659
+ expect(results[0].name).toBe("origin/feature/newer");
660
+ expect(results[1].name).toBe("feature/local-older");
661
+ });
662
+
663
+ it("should apply all sorting rules in correct priority order", () => {
664
+ const worktreeMap = new Map([
665
+ [
666
+ "feature/with-worktree",
667
+ {
668
+ path: "/path/to/worktree",
669
+ locked: false,
670
+ prunable: false,
671
+ isAccessible: true,
672
+ },
673
+ ],
674
+ ]);
675
+
676
+ const branches: BranchInfo[] = [
677
+ {
678
+ name: "origin/feature/z-remote",
679
+ type: "remote",
680
+ branchType: "feature",
681
+ isCurrent: false,
682
+ },
683
+ {
684
+ name: "feature/with-worktree",
685
+ type: "local",
686
+ branchType: "feature",
687
+ isCurrent: false,
688
+ worktree: {
689
+ path: "/path/to/worktree",
690
+ locked: false,
691
+ prunable: false,
692
+ isAccessible: true,
693
+ },
694
+ },
695
+ {
696
+ name: "feature/z-local-no-worktree",
697
+ type: "local",
698
+ branchType: "feature",
699
+ isCurrent: false,
700
+ },
701
+ {
702
+ name: "feature/a-local-no-worktree",
703
+ type: "local",
704
+ branchType: "feature",
705
+ isCurrent: false,
706
+ },
707
+ {
708
+ name: "develop",
709
+ type: "local",
710
+ branchType: "develop",
711
+ isCurrent: false,
712
+ },
713
+ {
714
+ name: "main",
715
+ type: "local",
716
+ branchType: "main",
717
+ isCurrent: false,
718
+ },
719
+ {
720
+ name: "feature/current",
721
+ type: "local",
722
+ branchType: "feature",
723
+ isCurrent: true,
724
+ },
725
+ ];
726
+
727
+ const results = formatBranchItems(branches, worktreeMap);
728
+
729
+ // Expected order:
730
+ // 1. Current branch
731
+ // 2. main
732
+ // 3. develop (because main exists)
733
+ // 4. Branches with worktree
734
+ // 5. Local branches (alphabetically)
735
+ // 6. Remote branches
736
+ expect(results[0].name).toBe("feature/current");
737
+ expect(results[1].name).toBe("main");
738
+ expect(results[2].name).toBe("develop");
739
+ expect(results[3].name).toBe("feature/with-worktree");
740
+ expect(results[4].name).toBe("feature/a-local-no-worktree");
741
+ expect(results[5].name).toBe("feature/z-local-no-worktree");
742
+ expect(results[6].name).toBe("origin/feature/z-remote");
743
+ });
744
+
745
+ it("should handle release and hotfix branches without special priority", () => {
746
+ const branches: BranchInfo[] = [
747
+ {
748
+ name: "feature/test",
749
+ type: "local",
750
+ branchType: "feature",
751
+ isCurrent: false,
752
+ },
753
+ {
754
+ name: "hotfix/urgent",
755
+ type: "local",
756
+ branchType: "hotfix",
757
+ isCurrent: false,
758
+ },
759
+ {
760
+ name: "release/v1.0",
761
+ type: "local",
762
+ branchType: "release",
763
+ isCurrent: false,
764
+ },
765
+ ];
766
+
767
+ const results = formatBranchItems(branches);
768
+
769
+ // Should be sorted alphabetically (no special priority)
770
+ expect(results[0].name).toBe("feature/test");
771
+ expect(results[1].name).toBe("hotfix/urgent");
772
+ expect(results[2].name).toBe("release/v1.0");
773
+ });
774
+ });
775
+ });