@akiojin/gwt 4.6.1 → 4.8.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 (83) hide show
  1. package/dist/claude.d.ts.map +1 -1
  2. package/dist/claude.js +6 -2
  3. package/dist/claude.js.map +1 -1
  4. package/dist/cli/ui/components/App.d.ts.map +1 -1
  5. package/dist/cli/ui/components/App.js +103 -2
  6. package/dist/cli/ui/components/App.js.map +1 -1
  7. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -0
  8. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchListScreen.js +11 -8
  10. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  11. package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts +10 -0
  12. package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts.map +1 -0
  13. package/dist/cli/ui/components/screens/LogDatePickerScreen.js +44 -0
  14. package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -0
  15. package/dist/cli/ui/components/screens/LogDetailScreen.d.ts +14 -0
  16. package/dist/cli/ui/components/screens/LogDetailScreen.d.ts.map +1 -0
  17. package/dist/cli/ui/components/screens/LogDetailScreen.js +34 -0
  18. package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -0
  19. package/dist/cli/ui/components/screens/LogListScreen.d.ts +19 -0
  20. package/dist/cli/ui/components/screens/LogListScreen.d.ts.map +1 -0
  21. package/dist/cli/ui/components/screens/LogListScreen.js +107 -0
  22. package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -0
  23. package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
  24. package/dist/cli/ui/hooks/useGitData.js +10 -3
  25. package/dist/cli/ui/hooks/useGitData.js.map +1 -1
  26. package/dist/cli/ui/types.d.ts +1 -1
  27. package/dist/cli/ui/types.d.ts.map +1 -1
  28. package/dist/cli/ui/utils/branchFormatter.d.ts +5 -0
  29. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  30. package/dist/cli/ui/utils/branchFormatter.js +18 -5
  31. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  32. package/dist/cli/ui/utils/clipboard.d.ts +7 -0
  33. package/dist/cli/ui/utils/clipboard.d.ts.map +1 -0
  34. package/dist/cli/ui/utils/clipboard.js +21 -0
  35. package/dist/cli/ui/utils/clipboard.js.map +1 -0
  36. package/dist/codex.d.ts.map +1 -1
  37. package/dist/codex.js +0 -1
  38. package/dist/codex.js.map +1 -1
  39. package/dist/gemini.d.ts.map +1 -1
  40. package/dist/gemini.js +6 -3
  41. package/dist/gemini.js.map +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +104 -81
  44. package/dist/index.js.map +1 -1
  45. package/dist/logging/formatter.d.ts +15 -0
  46. package/dist/logging/formatter.d.ts.map +1 -0
  47. package/dist/logging/formatter.js +81 -0
  48. package/dist/logging/formatter.js.map +1 -0
  49. package/dist/logging/reader.d.ts +12 -0
  50. package/dist/logging/reader.d.ts.map +1 -0
  51. package/dist/logging/reader.js +63 -0
  52. package/dist/logging/reader.js.map +1 -0
  53. package/dist/worktree.d.ts.map +1 -1
  54. package/dist/worktree.js +57 -0
  55. package/dist/worktree.js.map +1 -1
  56. package/package.json +2 -2
  57. package/src/claude.ts +7 -2
  58. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +8 -4
  59. package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
  60. package/src/cli/ui/__tests__/components/common/Select.test.tsx +17 -11
  61. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +28 -2
  62. package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +57 -0
  63. package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +102 -0
  64. package/src/cli/ui/__tests__/hooks/useGitData.test.ts +197 -0
  65. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +84 -13
  66. package/src/cli/ui/__tests__/integration/navigation.test.tsx +57 -37
  67. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +105 -0
  68. package/src/cli/ui/__tests__/utils/clipboard.test.ts +65 -0
  69. package/src/cli/ui/components/App.tsx +178 -1
  70. package/src/cli/ui/components/screens/BranchListScreen.tsx +11 -6
  71. package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +83 -0
  72. package/src/cli/ui/components/screens/LogDetailScreen.tsx +67 -0
  73. package/src/cli/ui/components/screens/LogListScreen.tsx +192 -0
  74. package/src/cli/ui/hooks/useGitData.ts +12 -3
  75. package/src/cli/ui/types.ts +3 -0
  76. package/src/cli/ui/utils/branchFormatter.ts +19 -5
  77. package/src/cli/ui/utils/clipboard.ts +31 -0
  78. package/src/codex.ts +0 -1
  79. package/src/gemini.ts +7 -3
  80. package/src/index.ts +147 -123
  81. package/src/logging/formatter.ts +106 -0
  82. package/src/logging/reader.ts +76 -0
  83. package/src/worktree.ts +77 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
5
+ import { renderHook, act, waitFor } from "@testing-library/react";
6
+ import { Window } from "happy-dom";
7
+
8
+ // モジュールをモック
9
+ vi.mock("../../../../git.js", () => ({
10
+ getAllBranches: vi.fn(),
11
+ hasUnpushedCommitsInRepo: vi.fn(),
12
+ getRepositoryRoot: vi.fn(),
13
+ fetchAllRemotes: vi.fn(),
14
+ collectUpstreamMap: vi.fn(),
15
+ getBranchDivergenceStatuses: vi.fn(),
16
+ hasUncommittedChanges: vi.fn(),
17
+ }));
18
+
19
+ vi.mock("../../../../worktree.js", () => ({
20
+ listAdditionalWorktrees: vi.fn(),
21
+ }));
22
+
23
+ vi.mock("../../../../github.js", () => ({
24
+ getPullRequestByBranch: vi.fn(),
25
+ }));
26
+
27
+ vi.mock("../../../../config/index.js", () => ({
28
+ getLastToolUsageMap: vi.fn(),
29
+ }));
30
+
31
+ import { useGitData } from "../../hooks/useGitData.js";
32
+ import {
33
+ getAllBranches,
34
+ getRepositoryRoot,
35
+ fetchAllRemotes,
36
+ collectUpstreamMap,
37
+ getBranchDivergenceStatuses,
38
+ } from "../../../../git.js";
39
+ import { listAdditionalWorktrees } from "../../../../worktree.js";
40
+ import { getLastToolUsageMap } from "../../../../config/index.js";
41
+
42
+ const mockGetAllBranches = getAllBranches as ReturnType<typeof vi.fn>;
43
+ const mockGetRepositoryRoot = getRepositoryRoot as ReturnType<typeof vi.fn>;
44
+ const mockFetchAllRemotes = fetchAllRemotes as ReturnType<typeof vi.fn>;
45
+ const mockCollectUpstreamMap = collectUpstreamMap as ReturnType<typeof vi.fn>;
46
+ const mockGetBranchDivergenceStatuses =
47
+ getBranchDivergenceStatuses as ReturnType<typeof vi.fn>;
48
+ const mockListAdditionalWorktrees = listAdditionalWorktrees as ReturnType<
49
+ typeof vi.fn
50
+ >;
51
+ const mockGetLastToolUsageMap = getLastToolUsageMap as ReturnType<typeof vi.fn>;
52
+
53
+ describe("useGitData", () => {
54
+ beforeEach(() => {
55
+ // Setup happy-dom
56
+ const window = new Window();
57
+ globalThis.window = window as unknown as typeof globalThis.window;
58
+ globalThis.document =
59
+ window.document as unknown as typeof globalThis.document;
60
+
61
+ // Reset all mocks
62
+ vi.clearAllMocks();
63
+
64
+ // Default mock implementations
65
+ mockGetRepositoryRoot.mockResolvedValue("/mock/repo");
66
+ mockFetchAllRemotes.mockResolvedValue(undefined);
67
+ mockGetAllBranches.mockResolvedValue([
68
+ {
69
+ name: "main",
70
+ type: "local",
71
+ isCurrent: true,
72
+ lastCommitDate: "2025-01-01",
73
+ },
74
+ {
75
+ name: "develop",
76
+ type: "local",
77
+ isCurrent: false,
78
+ lastCommitDate: "2025-01-02",
79
+ },
80
+ ]);
81
+ mockListAdditionalWorktrees.mockResolvedValue([]);
82
+ mockCollectUpstreamMap.mockResolvedValue(new Map());
83
+ mockGetBranchDivergenceStatuses.mockResolvedValue([]);
84
+ mockGetLastToolUsageMap.mockResolvedValue(new Map());
85
+ });
86
+
87
+ afterEach(() => {
88
+ vi.restoreAllMocks();
89
+ });
90
+
91
+ describe("キャッシュ機構", () => {
92
+ it("初回マウント時にGitデータを取得する", async () => {
93
+ const { result } = renderHook(() => useGitData());
94
+
95
+ // 初回ロード中
96
+ expect(result.current.loading).toBe(true);
97
+
98
+ await waitFor(() => {
99
+ expect(result.current.loading).toBe(false);
100
+ });
101
+
102
+ // Gitデータが取得されていることを確認
103
+ expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
104
+ expect(result.current.branches).toHaveLength(2);
105
+ });
106
+
107
+ it("refresh()を呼び出すとGitデータを再取得する", async () => {
108
+ const { result } = renderHook(() => useGitData());
109
+
110
+ await waitFor(() => {
111
+ expect(result.current.loading).toBe(false);
112
+ });
113
+
114
+ // 初回ロードで1回呼ばれている
115
+ expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
116
+
117
+ // refresh()を呼び出す
118
+ await act(async () => {
119
+ result.current.refresh();
120
+ });
121
+
122
+ await waitFor(() => {
123
+ expect(result.current.loading).toBe(false);
124
+ });
125
+
126
+ // refresh後に再度呼ばれている(forceRefresh=true)
127
+ expect(mockGetAllBranches).toHaveBeenCalledTimes(2);
128
+ });
129
+
130
+ it("キャッシュ済みの場合、再マウント時にGitデータを再取得しない", async () => {
131
+ // 注: useGitData は内部で useRef を使ってキャッシュ状態を管理
132
+ // 同一コンポーネント内での再レンダリングではキャッシュが効く
133
+ const { result, rerender } = renderHook(() => useGitData());
134
+
135
+ await waitFor(() => {
136
+ expect(result.current.loading).toBe(false);
137
+ });
138
+
139
+ // 初回ロードで1回呼ばれている
140
+ expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
141
+
142
+ // 再レンダリング
143
+ rerender();
144
+
145
+ // キャッシュされているため追加の呼び出しはない
146
+ expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
147
+ });
148
+ });
149
+
150
+ describe("lastUpdated", () => {
151
+ it("データ取得成功後にlastUpdatedが更新される", async () => {
152
+ const { result } = renderHook(() => useGitData());
153
+
154
+ // 初期状態ではnull
155
+ expect(result.current.lastUpdated).toBeNull();
156
+
157
+ await waitFor(() => {
158
+ expect(result.current.loading).toBe(false);
159
+ });
160
+
161
+ // ロード完了後にlastUpdatedが設定される
162
+ expect(result.current.lastUpdated).toBeInstanceOf(Date);
163
+ });
164
+ });
165
+
166
+ describe("エラーハンドリング", () => {
167
+ it("getAllBranchesがエラーを投げた場合、フォールバック値(空配列)が使用される", async () => {
168
+ // withTimeout がエラーをキャッチしてフォールバック値を返すため、
169
+ // エラー状態にはならず、空配列が設定される
170
+ mockGetAllBranches.mockRejectedValue(new Error("Git error"));
171
+
172
+ const { result } = renderHook(() => useGitData());
173
+
174
+ await waitFor(() => {
175
+ expect(result.current.loading).toBe(false);
176
+ });
177
+
178
+ // エラーはキャッチされ、フォールバック値が使用される
179
+ expect(result.current.error).toBeNull();
180
+ expect(result.current.branches).toHaveLength(0);
181
+ });
182
+
183
+ it("fetchAllRemotesがエラーを投げてもローカル表示は継続される", async () => {
184
+ mockFetchAllRemotes.mockRejectedValue(new Error("Network error"));
185
+
186
+ const { result } = renderHook(() => useGitData());
187
+
188
+ await waitFor(() => {
189
+ expect(result.current.loading).toBe(false);
190
+ });
191
+
192
+ // リモート取得失敗でもローカルブランチは表示される
193
+ expect(result.current.branches).toHaveLength(2);
194
+ expect(result.current.error).toBeNull();
195
+ });
196
+ });
197
+ });
@@ -5,6 +5,7 @@
5
5
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
6
6
  import { render } from "@testing-library/react";
7
7
  import React from "react";
8
+ import { App as AppComponent } from "../../components/App.js";
8
9
  import * as BranchListScreenModule from "../../components/screens/BranchListScreen.js";
9
10
  import type { BranchListScreenProps } from "../../components/screens/BranchListScreen.js";
10
11
  import { Window } from "happy-dom";
@@ -13,12 +14,51 @@ import type { BranchInfo, BranchItem, Statistics } from "../../types.js";
13
14
  const mockRefresh = vi.fn();
14
15
  const branchListProps: BranchListScreenProps[] = [];
15
16
  const useGitDataMock = vi.fn();
16
- let App: typeof import("../../components/App.js").App;
17
+ const useProfilesMock = vi.fn();
18
+ const useToolStatusMock = vi.fn();
19
+ const originalStdoutRows = process.stdout.rows;
20
+ const originalStdoutColumns = process.stdout.columns;
21
+
22
+ vi.mock("ink", async () => {
23
+ const ReactImport = await import("react");
24
+ const Box = ({ children }: { children?: ReactImport.ReactNode }) =>
25
+ ReactImport.createElement("div", null, children);
26
+ const Text = ({ children }: { children?: ReactImport.ReactNode }) =>
27
+ ReactImport.createElement("span", null, children);
28
+ return {
29
+ Box,
30
+ Text,
31
+ useApp: () => ({ exit: vi.fn() }),
32
+ useInput: () => {},
33
+ useStdout: () => ({ stdout: process.stdout, write: vi.fn() }),
34
+ };
35
+ });
17
36
 
18
37
  vi.mock("../../hooks/useGitData.js", () => ({
19
38
  useGitData: (...args: unknown[]) => useGitDataMock(...args),
20
39
  }));
21
40
 
41
+ vi.mock("../../hooks/useProfiles.js", () => ({
42
+ useProfiles: (...args: unknown[]) => useProfilesMock(...args),
43
+ }));
44
+
45
+ vi.mock("../../hooks/useToolStatus.js", () => ({
46
+ useToolStatus: (...args: unknown[]) => useToolStatusMock(...args),
47
+ }));
48
+
49
+ vi.mock("../../../utils.js", () => ({
50
+ getPackageVersion: vi.fn(async () => "0.0.0-test"),
51
+ }));
52
+
53
+ vi.mock("../../../git.js", () => ({
54
+ getRepositoryRoot: vi.fn(async () => "/repo"),
55
+ deleteBranch: vi.fn(async () => undefined),
56
+ }));
57
+
58
+ vi.mock("../../../config/index.js", () => ({
59
+ loadSession: vi.fn(async () => null),
60
+ }));
61
+
22
62
  vi.mock("../../components/screens/BranchListScreen.js", () => ({
23
63
  BranchListScreen: (props: BranchListScreenProps) => {
24
64
  branchListProps.push(props);
@@ -42,7 +82,7 @@ vi.mock("../../components/screens/BranchListScreen.js", () => ({
42
82
  }));
43
83
 
44
84
  describe("Edge Cases Integration Tests", () => {
45
- beforeEach(async () => {
85
+ beforeEach(() => {
46
86
  // Setup happy-dom
47
87
  const window = new Window();
48
88
  globalThis.window = window as unknown as typeof globalThis.window;
@@ -52,8 +92,44 @@ describe("Edge Cases Integration Tests", () => {
52
92
  // Reset mocks
53
93
  vi.clearAllMocks();
54
94
  useGitDataMock.mockReset();
95
+ useProfilesMock.mockReset();
96
+ useToolStatusMock.mockReset();
55
97
  branchListProps.length = 0;
56
- App = (await import("../../components/App.js")).App;
98
+ useGitDataMock.mockReturnValue({
99
+ branches: [],
100
+ worktrees: [],
101
+ loading: false,
102
+ error: null,
103
+ refresh: mockRefresh,
104
+ lastUpdated: null,
105
+ });
106
+
107
+ useProfilesMock.mockReturnValue({
108
+ profiles: null,
109
+ loading: false,
110
+ error: null,
111
+ activeProfileName: null,
112
+ activeProfile: null,
113
+ refresh: vi.fn(),
114
+ setActiveProfile: vi.fn(),
115
+ createProfile: vi.fn(),
116
+ updateProfile: vi.fn(),
117
+ deleteProfile: vi.fn(),
118
+ updateEnvVar: vi.fn(),
119
+ deleteEnvVar: vi.fn(),
120
+ });
121
+
122
+ useToolStatusMock.mockReturnValue({
123
+ tools: [],
124
+ loading: false,
125
+ error: null,
126
+ refresh: vi.fn(),
127
+ });
128
+ });
129
+
130
+ afterEach(() => {
131
+ process.stdout.rows = originalStdoutRows;
132
+ process.stdout.columns = originalStdoutColumns;
57
133
  });
58
134
 
59
135
  /**
@@ -218,7 +294,7 @@ describe("Edge Cases Integration Tests", () => {
218
294
  * testing unreliable in testing-library. The error is thrown but not caught
219
295
  * by the test framework correctly.
220
296
  */
221
- it.skip("[T093] should catch errors in App component", async () => {
297
+ it.skip("[T093] should catch errors in App component", () => {
222
298
  // Mock useGitData to throw an error after initial render
223
299
  let callCount = 0;
224
300
  useGitDataMock.mockImplementation(() => {
@@ -237,7 +313,7 @@ describe("Edge Cases Integration Tests", () => {
237
313
  });
238
314
 
239
315
  const onExit = vi.fn();
240
- const { container } = render(<App onExit={onExit} />);
316
+ const { container } = render(<AppComponent onExit={onExit} />);
241
317
 
242
318
  // Initial render should work
243
319
  expect(container).toBeDefined();
@@ -255,7 +331,7 @@ describe("Edge Cases Integration Tests", () => {
255
331
  });
256
332
 
257
333
  const onExit = vi.fn();
258
- const { getByText } = render(<App onExit={onExit} />);
334
+ const { getByText } = render(<AppComponent onExit={onExit} />);
259
335
 
260
336
  expect(branchListProps).not.toHaveLength(0);
261
337
  expect(branchListProps.at(-1)?.error).toBe(testError);
@@ -275,7 +351,7 @@ describe("Edge Cases Integration Tests", () => {
275
351
  });
276
352
 
277
353
  const onExit = vi.fn();
278
- const { container } = render(<App onExit={onExit} />);
354
+ const { container } = render(<AppComponent onExit={onExit} />);
279
355
 
280
356
  // Should render without error even with no branches
281
357
  expect(container).toBeDefined();
@@ -307,7 +383,7 @@ describe("Edge Cases Integration Tests", () => {
307
383
  });
308
384
 
309
385
  const onExit = vi.fn();
310
- const { container } = render(<App onExit={onExit} />);
386
+ const { container } = render(<AppComponent onExit={onExit} />);
311
387
 
312
388
  expect(container).toBeDefined();
313
389
  });
@@ -357,9 +433,4 @@ describe("Edge Cases Integration Tests", () => {
357
433
 
358
434
  process.stdout.rows = originalRows;
359
435
  });
360
-
361
- afterEach(() => {
362
- useGitDataMock.mockReset();
363
- branchListProps.length = 0;
364
- });
365
436
  });
@@ -1,15 +1,7 @@
1
1
  /**
2
2
  * @vitest-environment happy-dom
3
3
  */
4
- import {
5
- describe,
6
- it,
7
- expect,
8
- beforeEach,
9
- afterEach,
10
- afterAll,
11
- vi,
12
- } from "vitest";
4
+ import { describe, it, expect, beforeEach, afterAll, vi } from "vitest";
13
5
  import type { Mock } from "vitest";
14
6
  import { render, waitFor } from "@testing-library/react";
15
7
  import { act } from "react-dom/test-utils";
@@ -17,11 +9,12 @@ import React from "react";
17
9
  import { App } from "../../components/App.js";
18
10
  import { Window } from "happy-dom";
19
11
  import type { BranchInfo, BranchItem } from "../../types.js";
20
- import * as BranchListScreenModule from "../../components/screens/BranchListScreen.js";
21
12
  import type { BranchListScreenProps } from "../../components/screens/BranchListScreen.js";
22
- import * as BranchActionSelectorScreenModule from "../../screens/BranchActionSelectorScreen.js";
23
13
  import type { BranchActionSelectorScreenProps } from "../../screens/BranchActionSelectorScreen.js";
24
14
 
15
+ const branchListProps: BranchListScreenProps[] = [];
16
+ const branchActionProps: BranchActionSelectorScreenProps[] = [];
17
+
25
18
  vi.mock("../../../../git.ts", () => ({
26
19
  __esModule: true,
27
20
  getAllBranches: vi.fn(),
@@ -32,6 +25,45 @@ vi.mock("../../../../git.ts", () => ({
32
25
  getBranchDivergenceStatuses: vi.fn(async () => []),
33
26
  }));
34
27
 
28
+ vi.mock("../../../../config/index.js", () => ({
29
+ loadSession: vi.fn(),
30
+ getLastToolUsageMap: vi.fn(),
31
+ }));
32
+
33
+ vi.mock(
34
+ "../../components/screens/BranchListScreen.js",
35
+ async (importOriginal) => {
36
+ const actual =
37
+ await importOriginal<
38
+ typeof import("../../components/screens/BranchListScreen.js")
39
+ >();
40
+ return {
41
+ ...actual,
42
+ BranchListScreen: (props: BranchListScreenProps) => {
43
+ branchListProps.push(props);
44
+ return React.createElement(actual.BranchListScreen, props);
45
+ },
46
+ };
47
+ },
48
+ );
49
+
50
+ vi.mock(
51
+ "../../screens/BranchActionSelectorScreen.js",
52
+ async (importOriginal) => {
53
+ const actual =
54
+ await importOriginal<
55
+ typeof import("../../screens/BranchActionSelectorScreen.js")
56
+ >();
57
+ return {
58
+ ...actual,
59
+ BranchActionSelectorScreen: (props: BranchActionSelectorScreenProps) => {
60
+ branchActionProps.push(props);
61
+ return React.createElement(actual.BranchActionSelectorScreen, props);
62
+ },
63
+ };
64
+ },
65
+ );
66
+
35
67
  const { mockIsProtectedBranchName, mockSwitchToProtectedBranch } = vi.hoisted(
36
68
  () => ({
37
69
  mockIsProtectedBranchName: vi.fn(() => false),
@@ -74,6 +106,7 @@ import {
74
106
  getMergedPRWorktrees,
75
107
  removeWorktree,
76
108
  } from "../../../../worktree.ts";
109
+ import { loadSession, getLastToolUsageMap } from "../../../../config/index.js";
77
110
 
78
111
  const mockedGetAllBranches = getAllBranches as Mock;
79
112
  const mockedGetRepositoryRoot = getRepositoryRoot as Mock;
@@ -86,9 +119,8 @@ const mockedGetMergedPRWorktrees = getMergedPRWorktrees as Mock;
86
119
  const mockedRemoveWorktree = removeWorktree as Mock;
87
120
  const mockedIsProtectedBranchName = mockIsProtectedBranchName as Mock;
88
121
  const mockedSwitchToProtectedBranch = mockSwitchToProtectedBranch as Mock;
89
- const originalBranchListScreen = BranchListScreenModule.BranchListScreen;
90
- const originalBranchActionSelectorScreen =
91
- BranchActionSelectorScreenModule.BranchActionSelectorScreen;
122
+ const mockedLoadSession = loadSession as Mock;
123
+ const mockedGetLastToolUsageMap = getLastToolUsageMap as Mock;
92
124
 
93
125
  describe("Navigation Integration Tests", () => {
94
126
  beforeEach(() => {
@@ -110,8 +142,15 @@ describe("Navigation Integration Tests", () => {
110
142
  mockedRemoveWorktree.mockReset();
111
143
  mockedIsProtectedBranchName.mockReset();
112
144
  mockedSwitchToProtectedBranch.mockReset();
145
+ mockedLoadSession.mockReset();
146
+ mockedGetLastToolUsageMap.mockReset();
147
+ branchListProps.length = 0;
148
+ branchActionProps.length = 0;
149
+ aiToolScreenProps.length = 0;
113
150
  mockedGetRepositoryRoot.mockResolvedValue("/repo");
114
151
  mockedSwitchToProtectedBranch.mockResolvedValue("local");
152
+ mockedLoadSession.mockResolvedValue({ history: [] });
153
+ mockedGetLastToolUsageMap.mockResolvedValue(new Map());
115
154
  });
116
155
 
117
156
  const mockBranches: BranchInfo[] = [
@@ -248,11 +287,6 @@ describe("Navigation Integration Tests", () => {
248
287
  });
249
288
 
250
289
  describe("Protected Branch Navigation (T103)", () => {
251
- const branchListProps: BranchListScreenProps[] = [];
252
- const branchActionProps: BranchActionSelectorScreenProps[] = [];
253
- let branchListSpy: ReturnType<typeof vi.spyOn>;
254
- let branchActionSpy: ReturnType<typeof vi.spyOn>;
255
-
256
290
  const baseBranches: BranchInfo[] = [
257
291
  {
258
292
  name: "main",
@@ -284,33 +318,19 @@ describe("Protected Branch Navigation (T103)", () => {
284
318
  mockedRemoveWorktree.mockReset();
285
319
  mockedIsProtectedBranchName.mockReset();
286
320
  mockedSwitchToProtectedBranch.mockReset();
321
+ mockedLoadSession.mockReset();
322
+ mockedGetLastToolUsageMap.mockReset();
287
323
  mockedGetRepositoryRoot.mockResolvedValue("/repo");
288
324
  branchListProps.length = 0;
289
325
  branchActionProps.length = 0;
290
326
  aiToolScreenProps.length = 0;
291
- branchListSpy = vi
292
- .spyOn(BranchListScreenModule, "BranchListScreen")
293
- .mockImplementation((props: BranchListScreenProps) => {
294
- branchListProps.push(props);
295
- return React.createElement(originalBranchListScreen, props);
296
- });
297
- branchActionSpy = vi
298
- .spyOn(BranchActionSelectorScreenModule, "BranchActionSelectorScreen")
299
- .mockImplementation((props: BranchActionSelectorScreenProps) => {
300
- branchActionProps.push(props);
301
- return React.createElement(originalBranchActionSelectorScreen, props);
302
- });
303
-
304
327
  mockedIsProtectedBranchName.mockImplementation((name: string) =>
305
328
  ["main", "develop", "origin/main", "origin/develop"].includes(name),
306
329
  );
307
330
  mockedSwitchToProtectedBranch.mockResolvedValue("local");
308
331
  mockedGetRepositoryRoot.mockResolvedValue("/repo");
309
- });
310
-
311
- afterEach(() => {
312
- branchListSpy.mockRestore();
313
- branchActionSpy.mockRestore();
332
+ mockedLoadSession.mockResolvedValue({ history: [] });
333
+ mockedGetLastToolUsageMap.mockResolvedValue(new Map());
314
334
  });
315
335
 
316
336
  it("prefills AI tool selector with last used tool for the branch", async () => {
@@ -848,5 +848,110 @@ describe("branchFormatter", () => {
848
848
  expect(results[1].name).toBe("hotfix/urgent");
849
849
  expect(results[2].name).toBe("release/v1.0");
850
850
  });
851
+
852
+ it("should sort by latest activity time (max of git commit and tool usage)", () => {
853
+ const branches: BranchInfo[] = [
854
+ {
855
+ name: "feature/git-newer",
856
+ type: "local",
857
+ branchType: "feature",
858
+ isCurrent: false,
859
+ latestCommitTimestamp: 1_800_000_000, // git commit is newer
860
+ lastToolUsage: {
861
+ branch: "feature/git-newer",
862
+ worktreePath: "/tmp/wt1",
863
+ toolId: "claude-code",
864
+ toolLabel: "Claude",
865
+ timestamp: 1_700_000_000_000, // 1_700_000_000 seconds (older)
866
+ },
867
+ },
868
+ {
869
+ name: "feature/tool-newer",
870
+ type: "local",
871
+ branchType: "feature",
872
+ isCurrent: false,
873
+ latestCommitTimestamp: 1_700_000_000, // git commit is older
874
+ lastToolUsage: {
875
+ branch: "feature/tool-newer",
876
+ worktreePath: "/tmp/wt2",
877
+ toolId: "claude-code",
878
+ toolLabel: "Claude",
879
+ timestamp: 1_800_000_000_000, // 1_800_000_000 seconds (newer)
880
+ },
881
+ },
882
+ ];
883
+
884
+ const results = formatBranchItems(branches);
885
+
886
+ // Both have same latest activity time (1_800_000_000), so alphabetical
887
+ // feature/git-newer: max(1_800_000_000, 1_700_000_000) = 1_800_000_000
888
+ // feature/tool-newer: max(1_700_000_000, 1_800_000_000) = 1_800_000_000
889
+ expect(results[0].name).toBe("feature/git-newer");
890
+ expect(results[1].name).toBe("feature/tool-newer");
891
+ });
892
+
893
+ it("should prioritize branch with tool usage over branch with only git commit when tool is newer", () => {
894
+ const branches: BranchInfo[] = [
895
+ {
896
+ name: "feature/git-only",
897
+ type: "local",
898
+ branchType: "feature",
899
+ isCurrent: false,
900
+ latestCommitTimestamp: 1_700_000_000,
901
+ },
902
+ {
903
+ name: "feature/with-tool",
904
+ type: "local",
905
+ branchType: "feature",
906
+ isCurrent: false,
907
+ latestCommitTimestamp: 1_600_000_000,
908
+ lastToolUsage: {
909
+ branch: "feature/with-tool",
910
+ worktreePath: "/tmp/wt",
911
+ toolId: "claude-code",
912
+ toolLabel: "Claude",
913
+ timestamp: 1_800_000_000_000, // 1_800_000_000 seconds (newest)
914
+ },
915
+ },
916
+ ];
917
+
918
+ const results = formatBranchItems(branches);
919
+
920
+ // feature/with-tool has newer activity (tool usage at 1_800_000_000)
921
+ expect(results[0].name).toBe("feature/with-tool");
922
+ expect(results[1].name).toBe("feature/git-only");
923
+ });
924
+
925
+ it("should prioritize branch with newer git commit over branch with older tool usage", () => {
926
+ const branches: BranchInfo[] = [
927
+ {
928
+ name: "feature/old-tool",
929
+ type: "local",
930
+ branchType: "feature",
931
+ isCurrent: false,
932
+ latestCommitTimestamp: 1_600_000_000,
933
+ lastToolUsage: {
934
+ branch: "feature/old-tool",
935
+ worktreePath: "/tmp/wt",
936
+ toolId: "claude-code",
937
+ toolLabel: "Claude",
938
+ timestamp: 1_650_000_000_000, // 1_650_000_000 seconds
939
+ },
940
+ },
941
+ {
942
+ name: "feature/new-git",
943
+ type: "local",
944
+ branchType: "feature",
945
+ isCurrent: false,
946
+ latestCommitTimestamp: 1_800_000_000, // newest
947
+ },
948
+ ];
949
+
950
+ const results = formatBranchItems(branches);
951
+
952
+ // feature/new-git has newer activity (git commit at 1_800_000_000)
953
+ expect(results[0].name).toBe("feature/new-git");
954
+ expect(results[1].name).toBe("feature/old-tool");
955
+ });
851
956
  });
852
957
  });