|
1 | | -import { describe, test, expect, beforeEach, afterEach } from "@jest/globals"; |
2 | | -import * as fs from "fs/promises"; |
3 | | -import * as path from "path"; |
4 | | -import { execSync } from "child_process"; |
5 | | -import { removeWorktreeSafe, isWorktreeClean, hasSubmodules } from "./gitService"; |
6 | | -import { createWorktree, detectDefaultTrunkBranch } from "@/git"; |
7 | | -import type { Config } from "@/config"; |
| 1 | +import { describe } from "@jest/globals"; |
8 | 2 |
|
9 | | -// Helper to create a test git repo |
10 | | -async function createTestRepo(basePath: string): Promise<string> { |
11 | | - const repoPath = path.join(basePath, "test-repo"); |
12 | | - await fs.mkdir(repoPath, { recursive: true }); |
| 3 | +// gitService.ts exports removeWorktree() and pruneWorktrees() which are used by ipcMain. |
| 4 | +// These functions are thin wrappers around git commands and are better tested via |
| 5 | +// integration tests that exercise the full Runtime.deleteWorkspace() flow. |
13 | 6 |
|
14 | | - execSync("git init", { cwd: repoPath }); |
15 | | - execSync("git config user.email 'test@test.com'", { cwd: repoPath }); |
16 | | - execSync("git config user.name 'Test User'", { cwd: repoPath }); |
17 | | - |
18 | | - // Create initial commit |
19 | | - await fs.writeFile(path.join(repoPath, "README.md"), "# Test Repo"); |
20 | | - execSync("git add .", { cwd: repoPath }); |
21 | | - execSync('git commit -m "Initial commit"', { cwd: repoPath }); |
22 | | - |
23 | | - return repoPath; |
24 | | -} |
25 | | - |
26 | | -// Helper to create mock config with unique temp directory |
27 | | -function createMockConfig(tempDir: string): Config { |
28 | | - return { |
29 | | - srcDir: path.join(tempDir, "test-workspaces"), |
30 | | - getWorkspacePath: (projectPath: string, branchName: string) => { |
31 | | - return path.join(path.dirname(projectPath), "workspaces", branchName); |
32 | | - }, |
33 | | - } as unknown as Config; |
34 | | -} |
35 | | - |
36 | | -describe("removeWorktreeSafe", () => { |
37 | | - let tempDir: string; |
38 | | - let repoPath: string; |
39 | | - let defaultBranch: string; |
40 | | - let mockConfig: Config; |
41 | | - |
42 | | - beforeEach(async () => { |
43 | | - tempDir = await fs.mkdtemp(path.join(__dirname, "..", "test-temp-")); |
44 | | - mockConfig = createMockConfig(tempDir); |
45 | | - repoPath = await createTestRepo(tempDir); |
46 | | - defaultBranch = await detectDefaultTrunkBranch(repoPath); |
47 | | - }); |
48 | | - |
49 | | - afterEach(async () => { |
50 | | - try { |
51 | | - await fs.rm(tempDir, { recursive: true, force: true }); |
52 | | - } catch { |
53 | | - // Ignore cleanup errors |
54 | | - } |
55 | | - }); |
56 | | - |
57 | | - test("should instantly remove clean worktree via rename", async () => { |
58 | | - // Create a worktree |
59 | | - const result = await createWorktree(mockConfig, repoPath, "test-branch", { |
60 | | - trunkBranch: defaultBranch, |
61 | | - }); |
62 | | - expect(result.success).toBe(true); |
63 | | - const worktreePath = result.path!; |
64 | | - |
65 | | - // Verify worktree exists |
66 | | - const existsBefore = await fs |
67 | | - .access(worktreePath) |
68 | | - .then(() => true) |
69 | | - .catch(() => false); |
70 | | - expect(existsBefore).toBe(true); |
71 | | - |
72 | | - // Remove it (should be instant since it's clean) |
73 | | - const startTime = Date.now(); |
74 | | - const removeResult = await removeWorktreeSafe(repoPath, worktreePath); |
75 | | - const duration = Date.now() - startTime; |
76 | | - |
77 | | - expect(removeResult.success).toBe(true); |
78 | | - |
79 | | - // Should complete quickly (<200ms accounting for CI overhead) |
80 | | - expect(duration).toBeLessThan(200); |
81 | | - |
82 | | - // Worktree should be gone immediately |
83 | | - const existsAfter = await fs |
84 | | - .access(worktreePath) |
85 | | - .then(() => true) |
86 | | - .catch(() => false); |
87 | | - expect(existsAfter).toBe(false); |
88 | | - }); |
89 | | - |
90 | | - test("should block removal of dirty worktree", async () => { |
91 | | - // Create a worktree |
92 | | - const result = await createWorktree(mockConfig, repoPath, "dirty-branch", { |
93 | | - trunkBranch: defaultBranch, |
94 | | - }); |
95 | | - expect(result.success).toBe(true); |
96 | | - const worktreePath = result.path!; |
97 | | - |
98 | | - // Make it dirty by adding uncommitted changes |
99 | | - await fs.writeFile(path.join(worktreePath, "new-file.txt"), "uncommitted content"); |
100 | | - |
101 | | - // Verify it's dirty |
102 | | - const isClean = await isWorktreeClean(worktreePath); |
103 | | - expect(isClean).toBe(false); |
104 | | - |
105 | | - // Try to remove it - should fail due to uncommitted changes |
106 | | - const removeResult = await removeWorktreeSafe(repoPath, worktreePath); |
107 | | - |
108 | | - expect(removeResult.success).toBe(false); |
109 | | - expect(removeResult.error).toMatch(/modified|untracked|changes/i); |
110 | | - |
111 | | - // Worktree should still exist |
112 | | - const existsAfter = await fs |
113 | | - .access(worktreePath) |
114 | | - .then(() => true) |
115 | | - .catch(() => false); |
116 | | - expect(existsAfter).toBe(true); |
117 | | - }); |
118 | | - |
119 | | - test("should handle already-deleted worktree gracefully", async () => { |
120 | | - // Create a worktree |
121 | | - const result = await createWorktree(mockConfig, repoPath, "temp-branch", { |
122 | | - trunkBranch: defaultBranch, |
123 | | - }); |
124 | | - expect(result.success).toBe(true); |
125 | | - const worktreePath = result.path!; |
126 | | - |
127 | | - // Manually delete it (simulating external deletion) |
128 | | - await fs.rm(worktreePath, { recursive: true, force: true }); |
129 | | - |
130 | | - // Remove via removeWorktreeSafe - should succeed and prune git records |
131 | | - const removeResult = await removeWorktreeSafe(repoPath, worktreePath); |
132 | | - |
133 | | - expect(removeResult.success).toBe(true); |
134 | | - }); |
135 | | - |
136 | | - test("should remove clean worktree with staged changes using git", async () => { |
137 | | - // Create a worktree |
138 | | - const result = await createWorktree(mockConfig, repoPath, "staged-branch", { |
139 | | - trunkBranch: defaultBranch, |
140 | | - }); |
141 | | - expect(result.success).toBe(true); |
142 | | - const worktreePath = result.path!; |
143 | | - |
144 | | - // Add staged changes |
145 | | - await fs.writeFile(path.join(worktreePath, "staged.txt"), "staged content"); |
146 | | - execSync("git add .", { cwd: worktreePath }); |
147 | | - |
148 | | - // Verify it's dirty (staged changes count as dirty) |
149 | | - const isClean = await isWorktreeClean(worktreePath); |
150 | | - expect(isClean).toBe(false); |
151 | | - |
152 | | - // Try to remove it - should fail |
153 | | - const removeResult = await removeWorktreeSafe(repoPath, worktreePath); |
154 | | - |
155 | | - expect(removeResult.success).toBe(false); |
156 | | - }); |
157 | | - |
158 | | - test("should call onBackgroundDelete callback on errors", async () => { |
159 | | - // Create a worktree |
160 | | - const result = await createWorktree(mockConfig, repoPath, "callback-branch", { |
161 | | - trunkBranch: defaultBranch, |
162 | | - }); |
163 | | - expect(result.success).toBe(true); |
164 | | - const worktreePath = result.path!; |
165 | | - |
166 | | - const errors: Array<{ tempDir: string; error?: Error }> = []; |
167 | | - |
168 | | - // Remove it |
169 | | - const removeResult = await removeWorktreeSafe(repoPath, worktreePath, { |
170 | | - onBackgroundDelete: (tempDir, error) => { |
171 | | - errors.push({ tempDir, error }); |
172 | | - }, |
173 | | - }); |
174 | | - |
175 | | - expect(removeResult.success).toBe(true); |
176 | | - |
177 | | - // Wait a bit for background deletion to complete |
178 | | - await new Promise((resolve) => setTimeout(resolve, 100)); |
179 | | - |
180 | | - // Callback should be called for successful background deletion |
181 | | - // (or not called at all if deletion succeeds without error) |
182 | | - // This test mainly ensures the callback doesn't crash |
183 | | - }); |
184 | | -}); |
185 | | - |
186 | | -describe("isWorktreeClean", () => { |
187 | | - let tempDir: string; |
188 | | - let repoPath: string; |
189 | | - let defaultBranch: string; |
190 | | - let mockConfig: Config; |
191 | | - |
192 | | - beforeEach(async () => { |
193 | | - tempDir = await fs.mkdtemp(path.join(__dirname, "..", "test-temp-")); |
194 | | - mockConfig = createMockConfig(tempDir); |
195 | | - repoPath = await createTestRepo(tempDir); |
196 | | - defaultBranch = await detectDefaultTrunkBranch(repoPath); |
197 | | - }); |
198 | | - |
199 | | - afterEach(async () => { |
200 | | - try { |
201 | | - await fs.rm(tempDir, { recursive: true, force: true }); |
202 | | - } catch { |
203 | | - // Ignore cleanup errors |
204 | | - } |
205 | | - }); |
206 | | - |
207 | | - test("should return true for clean worktree", async () => { |
208 | | - const result = await createWorktree(mockConfig, repoPath, "clean-check", { |
209 | | - trunkBranch: defaultBranch, |
210 | | - }); |
211 | | - expect(result.success).toBe(true); |
212 | | - |
213 | | - const isClean = await isWorktreeClean(result.path!); |
214 | | - expect(isClean).toBe(true); |
215 | | - }); |
216 | | - |
217 | | - test("should return false for worktree with uncommitted changes", async () => { |
218 | | - const result = await createWorktree(mockConfig, repoPath, "dirty-check", { |
219 | | - trunkBranch: defaultBranch, |
220 | | - }); |
221 | | - expect(result.success).toBe(true); |
222 | | - const worktreePath = result.path!; |
223 | | - |
224 | | - // Add uncommitted file |
225 | | - await fs.writeFile(path.join(worktreePath, "uncommitted.txt"), "content"); |
226 | | - |
227 | | - const isClean = await isWorktreeClean(worktreePath); |
228 | | - expect(isClean).toBe(false); |
229 | | - }); |
230 | | - |
231 | | - test("should return false for non-existent path", async () => { |
232 | | - const isClean = await isWorktreeClean("/non/existent/path"); |
233 | | - expect(isClean).toBe(false); |
234 | | - }); |
235 | | -}); |
236 | | - |
237 | | -describe("hasSubmodules", () => { |
238 | | - let tempDir: string; |
239 | | - |
240 | | - beforeEach(async () => { |
241 | | - tempDir = await fs.mkdtemp(path.join(__dirname, "..", "test-temp-")); |
242 | | - }); |
243 | | - |
244 | | - afterEach(async () => { |
245 | | - try { |
246 | | - await fs.rm(tempDir, { recursive: true, force: true }); |
247 | | - } catch { |
248 | | - // Ignore cleanup errors |
249 | | - } |
250 | | - }); |
251 | | - |
252 | | - test("should return true when .gitmodules exists", async () => { |
253 | | - const testDir = path.join(tempDir, "with-submodule"); |
254 | | - await fs.mkdir(testDir, { recursive: true }); |
255 | | - await fs.writeFile(path.join(testDir, ".gitmodules"), '[submodule "test"]\n\tpath = test\n'); |
256 | | - |
257 | | - const result = await hasSubmodules(testDir); |
258 | | - expect(result).toBe(true); |
259 | | - }); |
260 | | - |
261 | | - test("should return false when .gitmodules does not exist", async () => { |
262 | | - const testDir = path.join(tempDir, "no-submodule"); |
263 | | - await fs.mkdir(testDir, { recursive: true }); |
264 | | - |
265 | | - const result = await hasSubmodules(testDir); |
266 | | - expect(result).toBe(false); |
267 | | - }); |
268 | | - |
269 | | - test("should return false for non-existent path", async () => { |
270 | | - const result = await hasSubmodules("/non/existent/path"); |
271 | | - expect(result).toBe(false); |
272 | | - }); |
| 7 | +describe("gitService", () => { |
| 8 | + // Placeholder describe block to keep test file structure |
| 9 | + // Add unit tests here if needed for removeWorktree() or pruneWorktrees() |
273 | 10 | }); |
0 commit comments