@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.
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +6 -2
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +103 -2
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -0
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +11 -8
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts +10 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js +44 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.d.ts +14 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.js +34 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/LogListScreen.d.ts +19 -0
- package/dist/cli/ui/components/screens/LogListScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogListScreen.js +107 -0
- package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -0
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +10 -3
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts +5 -0
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +18 -5
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/cli/ui/utils/clipboard.d.ts +7 -0
- package/dist/cli/ui/utils/clipboard.d.ts.map +1 -0
- package/dist/cli/ui/utils/clipboard.js +21 -0
- package/dist/cli/ui/utils/clipboard.js.map +1 -0
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +0 -1
- package/dist/codex.js.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +6 -3
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -81
- package/dist/index.js.map +1 -1
- package/dist/logging/formatter.d.ts +15 -0
- package/dist/logging/formatter.d.ts.map +1 -0
- package/dist/logging/formatter.js +81 -0
- package/dist/logging/formatter.js.map +1 -0
- package/dist/logging/reader.d.ts +12 -0
- package/dist/logging/reader.d.ts.map +1 -0
- package/dist/logging/reader.js +63 -0
- package/dist/logging/reader.js.map +1 -0
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +57 -0
- package/dist/worktree.js.map +1 -1
- package/package.json +2 -2
- package/src/claude.ts +7 -2
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +8 -4
- package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +17 -11
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +28 -2
- package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +57 -0
- package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +102 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts +197 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +84 -13
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +57 -37
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +105 -0
- package/src/cli/ui/__tests__/utils/clipboard.test.ts +65 -0
- package/src/cli/ui/components/App.tsx +178 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +11 -6
- package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +83 -0
- package/src/cli/ui/components/screens/LogDetailScreen.tsx +67 -0
- package/src/cli/ui/components/screens/LogListScreen.tsx +192 -0
- package/src/cli/ui/hooks/useGitData.ts +12 -3
- package/src/cli/ui/types.ts +3 -0
- package/src/cli/ui/utils/branchFormatter.ts +19 -5
- package/src/cli/ui/utils/clipboard.ts +31 -0
- package/src/codex.ts +0 -1
- package/src/gemini.ts +7 -3
- package/src/index.ts +147 -123
- package/src/logging/formatter.ts +106 -0
- package/src/logging/reader.ts +76 -0
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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",
|
|
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(<
|
|
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(<
|
|
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(<
|
|
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(<
|
|
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
|
|
90
|
-
const
|
|
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
|
});
|