ccmanager 2.7.0 → 2.9.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/cli.test.js +13 -2
- package/dist/components/App.js +125 -50
- package/dist/components/App.test.js +270 -0
- package/dist/components/ConfigureShortcuts.js +82 -8
- package/dist/components/DeleteWorktree.js +39 -5
- package/dist/components/DeleteWorktree.test.d.ts +1 -0
- package/dist/components/DeleteWorktree.test.js +128 -0
- package/dist/components/LoadingSpinner.d.ts +8 -0
- package/dist/components/LoadingSpinner.js +37 -0
- package/dist/components/LoadingSpinner.test.d.ts +1 -0
- package/dist/components/LoadingSpinner.test.js +187 -0
- package/dist/components/Menu.js +64 -16
- package/dist/components/Menu.recent-projects.test.js +32 -11
- package/dist/components/Menu.test.js +136 -4
- package/dist/components/MergeWorktree.js +79 -18
- package/dist/components/MergeWorktree.test.d.ts +1 -0
- package/dist/components/MergeWorktree.test.js +227 -0
- package/dist/components/NewWorktree.js +88 -9
- package/dist/components/NewWorktree.test.d.ts +1 -0
- package/dist/components/NewWorktree.test.js +244 -0
- package/dist/components/ProjectList.js +44 -13
- package/dist/components/ProjectList.recent-projects.test.js +8 -3
- package/dist/components/ProjectList.test.js +105 -8
- package/dist/components/RemoteBranchSelector.test.js +3 -1
- package/dist/hooks/useGitStatus.d.ts +11 -0
- package/dist/hooks/useGitStatus.js +70 -12
- package/dist/hooks/useGitStatus.test.js +30 -23
- package/dist/services/configurationManager.d.ts +75 -0
- package/dist/services/configurationManager.effect.test.d.ts +1 -0
- package/dist/services/configurationManager.effect.test.js +407 -0
- package/dist/services/configurationManager.js +246 -0
- package/dist/services/globalSessionOrchestrator.test.js +0 -8
- package/dist/services/projectManager.d.ts +98 -2
- package/dist/services/projectManager.js +228 -59
- package/dist/services/projectManager.test.js +242 -2
- package/dist/services/sessionManager.d.ts +44 -2
- package/dist/services/sessionManager.effect.test.d.ts +1 -0
- package/dist/services/sessionManager.effect.test.js +321 -0
- package/dist/services/sessionManager.js +216 -65
- package/dist/services/sessionManager.statePersistence.test.js +18 -9
- package/dist/services/sessionManager.test.js +40 -36
- package/dist/services/worktreeService.d.ts +356 -26
- package/dist/services/worktreeService.js +793 -353
- package/dist/services/worktreeService.test.js +294 -313
- package/dist/types/errors.d.ts +74 -0
- package/dist/types/errors.js +31 -0
- package/dist/types/errors.test.d.ts +1 -0
- package/dist/types/errors.test.js +201 -0
- package/dist/types/index.d.ts +5 -17
- package/dist/utils/claudeDir.d.ts +58 -6
- package/dist/utils/claudeDir.js +103 -8
- package/dist/utils/claudeDir.test.d.ts +1 -0
- package/dist/utils/claudeDir.test.js +108 -0
- package/dist/utils/concurrencyLimit.d.ts +5 -0
- package/dist/utils/concurrencyLimit.js +11 -0
- package/dist/utils/concurrencyLimit.test.js +40 -1
- package/dist/utils/gitStatus.d.ts +36 -8
- package/dist/utils/gitStatus.js +170 -88
- package/dist/utils/gitStatus.test.js +12 -9
- package/dist/utils/hookExecutor.d.ts +41 -6
- package/dist/utils/hookExecutor.js +75 -32
- package/dist/utils/hookExecutor.test.js +73 -20
- package/dist/utils/terminalCapabilities.d.ts +18 -0
- package/dist/utils/terminalCapabilities.js +81 -0
- package/dist/utils/terminalCapabilities.test.d.ts +1 -0
- package/dist/utils/terminalCapabilities.test.js +104 -0
- package/dist/utils/testHelpers.d.ts +106 -0
- package/dist/utils/testHelpers.js +153 -0
- package/dist/utils/testHelpers.test.d.ts +1 -0
- package/dist/utils/testHelpers.test.js +114 -0
- package/dist/utils/worktreeConfig.d.ts +77 -2
- package/dist/utils/worktreeConfig.js +156 -16
- package/dist/utils/worktreeConfig.test.d.ts +1 -0
- package/dist/utils/worktreeConfig.test.js +39 -0
- package/package.json +4 -4
- package/dist/integration-tests/devcontainer.integration.test.js +0 -101
- /package/dist/{integration-tests/devcontainer.integration.test.d.ts → components/App.test.d.ts} +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { WorktreeService } from './worktreeService.js';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import { existsSync, statSync } from 'fs';
|
|
5
5
|
import { configurationManager } from './configurationManager.js';
|
|
6
|
-
import {
|
|
6
|
+
import { Effect } from 'effect';
|
|
7
|
+
import { GitError } from '../types/errors.js';
|
|
7
8
|
// Mock child_process module
|
|
8
9
|
vi.mock('child_process');
|
|
9
10
|
// Mock fs module
|
|
@@ -31,7 +32,6 @@ const mockedExecSync = vi.mocked(execSync);
|
|
|
31
32
|
const mockedExistsSync = vi.mocked(existsSync);
|
|
32
33
|
const mockedStatSync = vi.mocked(statSync);
|
|
33
34
|
const mockedGetWorktreeHooks = vi.mocked(configurationManager.getWorktreeHooks);
|
|
34
|
-
const mockedExecuteHook = vi.mocked(executeWorktreePostCreationHook);
|
|
35
35
|
describe('WorktreeService', () => {
|
|
36
36
|
let service;
|
|
37
37
|
beforeEach(() => {
|
|
@@ -119,8 +119,8 @@ describe('WorktreeService', () => {
|
|
|
119
119
|
expect(result.startsWith('/')).toBe(true);
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
|
-
describe('
|
|
123
|
-
it('should return default branch from origin', () => {
|
|
122
|
+
describe('getDefaultBranchEffect', () => {
|
|
123
|
+
it('should return Effect with default branch from origin', async () => {
|
|
124
124
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
125
125
|
if (typeof cmd === 'string') {
|
|
126
126
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -132,7 +132,8 @@ describe('WorktreeService', () => {
|
|
|
132
132
|
}
|
|
133
133
|
throw new Error('Command not mocked: ' + cmd);
|
|
134
134
|
});
|
|
135
|
-
const
|
|
135
|
+
const effect = service.getDefaultBranchEffect();
|
|
136
|
+
const result = await Effect.runPromise(effect);
|
|
136
137
|
expect(result).toBe('main');
|
|
137
138
|
expect(execSync).toHaveBeenCalledWith("git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'", expect.objectContaining({
|
|
138
139
|
cwd: '/fake/path',
|
|
@@ -140,7 +141,7 @@ describe('WorktreeService', () => {
|
|
|
140
141
|
shell: '/bin/bash',
|
|
141
142
|
}));
|
|
142
143
|
});
|
|
143
|
-
it('should fallback to main if origin HEAD fails', () => {
|
|
144
|
+
it('should fallback to main if origin HEAD fails', async () => {
|
|
144
145
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
145
146
|
if (typeof cmd === 'string') {
|
|
146
147
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -155,12 +156,13 @@ describe('WorktreeService', () => {
|
|
|
155
156
|
}
|
|
156
157
|
throw new Error('Not found');
|
|
157
158
|
});
|
|
158
|
-
const
|
|
159
|
+
const effect = service.getDefaultBranchEffect();
|
|
160
|
+
const result = await Effect.runPromise(effect);
|
|
159
161
|
expect(result).toBe('main');
|
|
160
162
|
});
|
|
161
163
|
});
|
|
162
|
-
describe('
|
|
163
|
-
it('should return all branches without duplicates', () => {
|
|
164
|
+
describe('getAllBranchesEffect', () => {
|
|
165
|
+
it('should return Effect with all branches without duplicates', async () => {
|
|
164
166
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
165
167
|
if (typeof cmd === 'string') {
|
|
166
168
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -177,10 +179,11 @@ origin/feature/test
|
|
|
177
179
|
}
|
|
178
180
|
throw new Error('Command not mocked: ' + cmd);
|
|
179
181
|
});
|
|
180
|
-
const
|
|
182
|
+
const effect = service.getAllBranchesEffect();
|
|
183
|
+
const result = await Effect.runPromise(effect);
|
|
181
184
|
expect(result).toEqual(['main', 'feature/test', 'feature/remote']);
|
|
182
185
|
});
|
|
183
|
-
it('should return empty array on error', () => {
|
|
186
|
+
it('should return empty array on error', async () => {
|
|
184
187
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
185
188
|
if (typeof cmd === 'string' &&
|
|
186
189
|
cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -188,10 +191,67 @@ origin/feature/test
|
|
|
188
191
|
}
|
|
189
192
|
throw new Error('Git error');
|
|
190
193
|
});
|
|
191
|
-
const
|
|
194
|
+
const effect = service.getAllBranchesEffect();
|
|
195
|
+
const result = await Effect.runPromise(effect);
|
|
192
196
|
expect(result).toEqual([]);
|
|
193
197
|
});
|
|
194
198
|
});
|
|
199
|
+
describe('getCurrentBranchEffect', () => {
|
|
200
|
+
it('should return Effect with current branch name on success', async () => {
|
|
201
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
202
|
+
if (typeof cmd === 'string') {
|
|
203
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
204
|
+
return '/fake/path/.git\n';
|
|
205
|
+
}
|
|
206
|
+
if (cmd === 'git rev-parse --abbrev-ref HEAD') {
|
|
207
|
+
return 'feature-branch\n';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
211
|
+
});
|
|
212
|
+
const effect = service.getCurrentBranchEffect();
|
|
213
|
+
const result = await Effect.runPromise(effect);
|
|
214
|
+
expect(result).toBe('feature-branch');
|
|
215
|
+
expect(execSync).toHaveBeenCalledWith('git rev-parse --abbrev-ref HEAD', expect.objectContaining({
|
|
216
|
+
cwd: '/fake/path',
|
|
217
|
+
encoding: 'utf8',
|
|
218
|
+
}));
|
|
219
|
+
});
|
|
220
|
+
it('should return Effect with "unknown" when git command fails', async () => {
|
|
221
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
222
|
+
if (typeof cmd === 'string') {
|
|
223
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
224
|
+
return '/fake/path/.git\n';
|
|
225
|
+
}
|
|
226
|
+
if (cmd === 'git rev-parse --abbrev-ref HEAD') {
|
|
227
|
+
throw new Error('fatal: not a git repository');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
231
|
+
});
|
|
232
|
+
const effect = service.getCurrentBranchEffect();
|
|
233
|
+
const result = await Effect.runPromise(effect);
|
|
234
|
+
// Should fallback to 'unknown' instead of failing
|
|
235
|
+
expect(result).toBe('unknown');
|
|
236
|
+
});
|
|
237
|
+
it('should return Effect with "unknown" when branch name is empty', async () => {
|
|
238
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
239
|
+
if (typeof cmd === 'string') {
|
|
240
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
241
|
+
return '/fake/path/.git\n';
|
|
242
|
+
}
|
|
243
|
+
if (cmd === 'git rev-parse --abbrev-ref HEAD') {
|
|
244
|
+
return '\n'; // Empty branch name
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
248
|
+
});
|
|
249
|
+
const effect = service.getCurrentBranchEffect();
|
|
250
|
+
const result = await Effect.runPromise(effect);
|
|
251
|
+
// Should fallback to 'unknown' when no branch returned
|
|
252
|
+
expect(result).toBe('unknown');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
195
255
|
describe('resolveBranchReference', () => {
|
|
196
256
|
it('should return local branch when it exists', () => {
|
|
197
257
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
@@ -308,88 +368,8 @@ origin/feature/test
|
|
|
308
368
|
expect(result).toBe('foo/bar-xyz');
|
|
309
369
|
});
|
|
310
370
|
});
|
|
311
|
-
describe('
|
|
312
|
-
it('should
|
|
313
|
-
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
314
|
-
if (typeof cmd === 'string') {
|
|
315
|
-
if (cmd === 'git rev-parse --git-common-dir') {
|
|
316
|
-
return '/fake/path/.git\n';
|
|
317
|
-
}
|
|
318
|
-
if (cmd.includes('rev-parse --verify')) {
|
|
319
|
-
throw new Error('Branch not found');
|
|
320
|
-
}
|
|
321
|
-
return '';
|
|
322
|
-
}
|
|
323
|
-
throw new Error('Unexpected command');
|
|
324
|
-
});
|
|
325
|
-
const result = await service.createWorktree('/path/to/worktree', 'new-feature', 'develop');
|
|
326
|
-
expect(result).toEqual({ success: true });
|
|
327
|
-
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "develop"', expect.any(Object));
|
|
328
|
-
});
|
|
329
|
-
it('should create worktree without base branch when branch exists', async () => {
|
|
330
|
-
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
331
|
-
if (typeof cmd === 'string') {
|
|
332
|
-
if (cmd === 'git rev-parse --git-common-dir') {
|
|
333
|
-
return '/fake/path/.git\n';
|
|
334
|
-
}
|
|
335
|
-
if (cmd.includes('rev-parse --verify')) {
|
|
336
|
-
return 'hash';
|
|
337
|
-
}
|
|
338
|
-
return '';
|
|
339
|
-
}
|
|
340
|
-
throw new Error('Unexpected command');
|
|
341
|
-
});
|
|
342
|
-
const result = await service.createWorktree('/path/to/worktree', 'existing-feature', 'main');
|
|
343
|
-
expect(result).toEqual({ success: true });
|
|
344
|
-
expect(execSync).toHaveBeenCalledWith('git worktree add "/path/to/worktree" "existing-feature"', expect.any(Object));
|
|
345
|
-
});
|
|
346
|
-
it('should handle ambiguous branch error gracefully', async () => {
|
|
347
|
-
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
348
|
-
if (typeof cmd === 'string') {
|
|
349
|
-
if (cmd === 'git rev-parse --git-common-dir') {
|
|
350
|
-
return '/fake/path/.git\n';
|
|
351
|
-
}
|
|
352
|
-
if (cmd.includes('rev-parse --verify new-feature')) {
|
|
353
|
-
throw new Error('Branch not found');
|
|
354
|
-
}
|
|
355
|
-
if (cmd.includes('show-ref --verify --quiet refs/heads/foo/bar-xyz')) {
|
|
356
|
-
throw new Error('Local branch not found');
|
|
357
|
-
}
|
|
358
|
-
if (cmd === 'git remote') {
|
|
359
|
-
return 'origin\nupstream\n';
|
|
360
|
-
}
|
|
361
|
-
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/foo/bar-xyz') ||
|
|
362
|
-
cmd.includes('show-ref --verify --quiet refs/remotes/upstream/foo/bar-xyz')) {
|
|
363
|
-
return ''; // Both remotes have the branch
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
throw new Error('Command not mocked: ' + cmd);
|
|
367
|
-
});
|
|
368
|
-
const result = await service.createWorktree('/path/to/worktree', 'new-feature', 'foo/bar-xyz');
|
|
369
|
-
expect(result.success).toBe(false);
|
|
370
|
-
expect(result.error).toContain("Ambiguous branch 'foo/bar-xyz' found in multiple remotes");
|
|
371
|
-
expect(result.error).toContain('origin/foo/bar-xyz, upstream/foo/bar-xyz');
|
|
372
|
-
});
|
|
373
|
-
it('should create worktree from specified base branch when branch does not exist', async () => {
|
|
374
|
-
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
375
|
-
if (typeof cmd === 'string') {
|
|
376
|
-
if (cmd === 'git rev-parse --git-common-dir') {
|
|
377
|
-
return '/fake/path/.git\n';
|
|
378
|
-
}
|
|
379
|
-
if (cmd.includes('rev-parse --verify')) {
|
|
380
|
-
throw new Error('Branch not found');
|
|
381
|
-
}
|
|
382
|
-
return '';
|
|
383
|
-
}
|
|
384
|
-
throw new Error('Unexpected command');
|
|
385
|
-
});
|
|
386
|
-
const result = await service.createWorktree('/path/to/worktree', 'new-feature', 'main');
|
|
387
|
-
expect(result).toEqual({ success: true });
|
|
388
|
-
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "main"', expect.any(Object));
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
describe('hasClaudeDirectoryInBranch', () => {
|
|
392
|
-
it('should return true when .claude directory exists in branch worktree', () => {
|
|
371
|
+
describe('hasClaudeDirectoryInBranchEffect', () => {
|
|
372
|
+
it('should return Effect with true when .claude directory exists in branch worktree', async () => {
|
|
393
373
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
394
374
|
if (typeof cmd === 'string') {
|
|
395
375
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -414,12 +394,13 @@ branch refs/heads/feature-branch
|
|
|
414
394
|
mockedStatSync.mockImplementation(() => ({
|
|
415
395
|
isDirectory: () => true,
|
|
416
396
|
}));
|
|
417
|
-
const
|
|
397
|
+
const effect = service.hasClaudeDirectoryInBranchEffect('feature-branch');
|
|
398
|
+
const result = await Effect.runPromise(effect);
|
|
418
399
|
expect(result).toBe(true);
|
|
419
400
|
expect(existsSync).toHaveBeenCalledWith('/fake/path/feature-branch/.claude');
|
|
420
401
|
expect(statSync).toHaveBeenCalledWith('/fake/path/feature-branch/.claude');
|
|
421
402
|
});
|
|
422
|
-
it('should return false when .claude directory does not exist', () => {
|
|
403
|
+
it('should return Effect with false when .claude directory does not exist', async () => {
|
|
423
404
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
424
405
|
if (typeof cmd === 'string') {
|
|
425
406
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -439,11 +420,12 @@ branch refs/heads/feature-branch
|
|
|
439
420
|
throw new Error('Command not mocked: ' + cmd);
|
|
440
421
|
});
|
|
441
422
|
mockedExistsSync.mockReturnValue(false);
|
|
442
|
-
const
|
|
423
|
+
const effect = service.hasClaudeDirectoryInBranchEffect('feature-branch');
|
|
424
|
+
const result = await Effect.runPromise(effect);
|
|
443
425
|
expect(result).toBe(false);
|
|
444
426
|
expect(existsSync).toHaveBeenCalledWith('/fake/path/feature-branch/.claude');
|
|
445
427
|
});
|
|
446
|
-
it('should return false when .claude exists but is not a directory', () => {
|
|
428
|
+
it('should return Effect with false when .claude exists but is not a directory', async () => {
|
|
447
429
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
448
430
|
if (typeof cmd === 'string') {
|
|
449
431
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -466,10 +448,11 @@ branch refs/heads/feature-branch
|
|
|
466
448
|
mockedStatSync.mockImplementation(() => ({
|
|
467
449
|
isDirectory: () => false,
|
|
468
450
|
}));
|
|
469
|
-
const
|
|
451
|
+
const effect = service.hasClaudeDirectoryInBranchEffect('feature-branch');
|
|
452
|
+
const result = await Effect.runPromise(effect);
|
|
470
453
|
expect(result).toBe(false);
|
|
471
454
|
});
|
|
472
|
-
it('should fallback to default branch when branch worktree not found', () => {
|
|
455
|
+
it('should fallback to default branch when branch worktree not found', async () => {
|
|
473
456
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
474
457
|
if (typeof cmd === 'string') {
|
|
475
458
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -492,11 +475,12 @@ branch refs/heads/main
|
|
|
492
475
|
isDirectory: () => true,
|
|
493
476
|
}));
|
|
494
477
|
// When asking for main branch that doesn't have a separate worktree
|
|
495
|
-
const
|
|
478
|
+
const effect = service.hasClaudeDirectoryInBranchEffect('main');
|
|
479
|
+
const result = await Effect.runPromise(effect);
|
|
496
480
|
expect(result).toBe(true);
|
|
497
481
|
expect(existsSync).toHaveBeenCalledWith('/fake/path/.claude');
|
|
498
482
|
});
|
|
499
|
-
it('should return false when branch not found in any worktree', () => {
|
|
483
|
+
it('should return Effect with false when branch not found in any worktree', async () => {
|
|
500
484
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
501
485
|
if (typeof cmd === 'string') {
|
|
502
486
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -514,10 +498,11 @@ branch refs/heads/main
|
|
|
514
498
|
}
|
|
515
499
|
throw new Error('Command not mocked: ' + cmd);
|
|
516
500
|
});
|
|
517
|
-
const
|
|
501
|
+
const effect = service.hasClaudeDirectoryInBranchEffect('non-existent-branch');
|
|
502
|
+
const result = await Effect.runPromise(effect);
|
|
518
503
|
expect(result).toBe(false);
|
|
519
504
|
});
|
|
520
|
-
it('should check main worktree when branch is default branch', () => {
|
|
505
|
+
it('should check main worktree when branch is default branch', async () => {
|
|
521
506
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
522
507
|
if (typeof cmd === 'string') {
|
|
523
508
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -543,327 +528,323 @@ branch refs/heads/other-branch
|
|
|
543
528
|
mockedStatSync.mockImplementation(() => ({
|
|
544
529
|
isDirectory: () => true,
|
|
545
530
|
}));
|
|
546
|
-
const
|
|
531
|
+
const effect = service.hasClaudeDirectoryInBranchEffect('main');
|
|
532
|
+
const result = await Effect.runPromise(effect);
|
|
547
533
|
expect(result).toBe(true);
|
|
548
534
|
expect(existsSync).toHaveBeenCalledWith('/fake/path/.claude');
|
|
549
535
|
});
|
|
550
536
|
});
|
|
551
|
-
describe('
|
|
552
|
-
|
|
553
|
-
vi.clearAllMocks();
|
|
554
|
-
});
|
|
555
|
-
it('should execute post-creation hook when worktree is created', async () => {
|
|
556
|
-
// Arrange
|
|
557
|
-
const hookCommand = 'echo "Worktree created: $CCMANAGER_WORKTREE_PATH"';
|
|
558
|
-
mockedGetWorktreeHooks.mockReturnValue({
|
|
559
|
-
post_creation: {
|
|
560
|
-
command: hookCommand,
|
|
561
|
-
enabled: true,
|
|
562
|
-
},
|
|
563
|
-
});
|
|
564
|
-
mockedExecuteHook.mockResolvedValue(undefined);
|
|
537
|
+
describe('Effect-based getWorktrees', () => {
|
|
538
|
+
it('should return Effect with worktree array on success', async () => {
|
|
565
539
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
566
540
|
if (typeof cmd === 'string') {
|
|
567
541
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
568
542
|
return '/fake/path/.git\n';
|
|
569
543
|
}
|
|
570
|
-
if (cmd
|
|
571
|
-
return
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
544
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
545
|
+
return `worktree /fake/path
|
|
546
|
+
HEAD abcd1234
|
|
547
|
+
branch refs/heads/main
|
|
548
|
+
|
|
549
|
+
worktree /fake/path/feature
|
|
550
|
+
HEAD efgh5678
|
|
551
|
+
branch refs/heads/feature
|
|
552
|
+
`;
|
|
578
553
|
}
|
|
579
554
|
}
|
|
580
|
-
|
|
555
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
581
556
|
});
|
|
582
|
-
|
|
583
|
-
const result = await
|
|
584
|
-
|
|
585
|
-
expect(result
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
557
|
+
const effect = service.getWorktreesEffect();
|
|
558
|
+
const result = await Effect.runPromise(effect);
|
|
559
|
+
expect(result).toHaveLength(2);
|
|
560
|
+
expect(result[0]).toMatchObject({
|
|
561
|
+
path: '/fake/path',
|
|
562
|
+
branch: 'main',
|
|
563
|
+
isMainWorktree: true,
|
|
564
|
+
});
|
|
565
|
+
expect(result[1]).toMatchObject({
|
|
566
|
+
path: '/fake/path/feature',
|
|
567
|
+
branch: 'feature',
|
|
590
568
|
isMainWorktree: false,
|
|
591
|
-
hasSession: false,
|
|
592
|
-
}), '/fake/path', 'main');
|
|
593
|
-
});
|
|
594
|
-
it('should not execute hook when disabled', async () => {
|
|
595
|
-
// Arrange
|
|
596
|
-
mockedGetWorktreeHooks.mockReturnValue({
|
|
597
|
-
post_creation: {
|
|
598
|
-
command: 'echo "Should not run"',
|
|
599
|
-
enabled: false,
|
|
600
|
-
},
|
|
601
569
|
});
|
|
570
|
+
});
|
|
571
|
+
it('should return Effect that fails with GitError when git command fails', async () => {
|
|
602
572
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
603
573
|
if (typeof cmd === 'string') {
|
|
604
574
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
605
575
|
return '/fake/path/.git\n';
|
|
606
576
|
}
|
|
607
|
-
if (cmd
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
if (cmd.includes('git rev-parse --verify')) {
|
|
614
|
-
throw new Error('Branch not found');
|
|
577
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
578
|
+
const error = new Error('fatal: not a git repository');
|
|
579
|
+
error.status = 128;
|
|
580
|
+
error.stderr = 'fatal: not a git repository';
|
|
581
|
+
throw error;
|
|
615
582
|
}
|
|
616
583
|
}
|
|
617
|
-
|
|
584
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
618
585
|
});
|
|
619
|
-
|
|
620
|
-
const result = await
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
586
|
+
const effect = service.getWorktreesEffect();
|
|
587
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
588
|
+
if (result._tag === 'Left') {
|
|
589
|
+
expect(result.left).toBeInstanceOf(GitError);
|
|
590
|
+
expect(result.left.command).toBe('git worktree list --porcelain');
|
|
591
|
+
expect(result.left.exitCode).toBe(128);
|
|
592
|
+
expect(result.left.stderr).toContain('not a git repository');
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
expect.fail('Should have returned Left with GitError');
|
|
596
|
+
}
|
|
625
597
|
});
|
|
626
|
-
it('should
|
|
627
|
-
// Arrange
|
|
628
|
-
mockedGetWorktreeHooks.mockReturnValue({});
|
|
598
|
+
it('should fallback to single worktree when git worktree command not supported', async () => {
|
|
629
599
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
630
600
|
if (typeof cmd === 'string') {
|
|
631
601
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
632
602
|
return '/fake/path/.git\n';
|
|
633
603
|
}
|
|
634
|
-
if (cmd
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
604
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
605
|
+
const error = new Error('unknown command: worktree');
|
|
606
|
+
error.status = 1;
|
|
607
|
+
error.stderr = 'unknown command: worktree';
|
|
608
|
+
throw error;
|
|
639
609
|
}
|
|
640
|
-
if (cmd
|
|
641
|
-
|
|
610
|
+
if (cmd === 'git rev-parse --abbrev-ref HEAD') {
|
|
611
|
+
return 'main\n';
|
|
642
612
|
}
|
|
643
613
|
}
|
|
644
|
-
|
|
614
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
615
|
+
});
|
|
616
|
+
const effect = service.getWorktreesEffect();
|
|
617
|
+
const result = await Effect.runPromise(effect);
|
|
618
|
+
expect(result).toHaveLength(1);
|
|
619
|
+
expect(result[0]).toMatchObject({
|
|
620
|
+
path: '/fake/path',
|
|
621
|
+
branch: 'main',
|
|
622
|
+
isMainWorktree: true,
|
|
645
623
|
});
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
expect(mockedGetWorktreeHooks).toHaveBeenCalled();
|
|
651
|
-
expect(mockedExecuteHook).not.toHaveBeenCalled();
|
|
652
|
-
});
|
|
653
|
-
it('should not fail worktree creation if hook execution fails', async () => {
|
|
654
|
-
// Arrange
|
|
655
|
-
mockedGetWorktreeHooks.mockReturnValue({
|
|
656
|
-
post_creation: {
|
|
657
|
-
command: 'failing-command',
|
|
658
|
-
enabled: true,
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
// The real executeWorktreePostCreationHook doesn't throw, it catches errors internally
|
|
662
|
-
// So the mock should resolve, not reject
|
|
663
|
-
mockedExecuteHook.mockResolvedValue(undefined);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
describe('Effect-based createWorktree', () => {
|
|
627
|
+
it('should return Effect with Worktree on success', async () => {
|
|
664
628
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
665
629
|
if (typeof cmd === 'string') {
|
|
666
630
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
667
631
|
return '/fake/path/.git\n';
|
|
668
632
|
}
|
|
669
|
-
if (cmd.includes('
|
|
670
|
-
|
|
633
|
+
if (cmd.includes('rev-parse --verify')) {
|
|
634
|
+
throw new Error('Branch not found');
|
|
671
635
|
}
|
|
672
636
|
if (cmd.includes('git worktree add')) {
|
|
673
637
|
return '';
|
|
674
638
|
}
|
|
675
|
-
if (cmd.includes('git rev-parse --verify')) {
|
|
676
|
-
throw new Error('Branch not found');
|
|
677
|
-
}
|
|
678
639
|
}
|
|
679
640
|
return '';
|
|
680
641
|
});
|
|
681
|
-
|
|
682
|
-
const result = await
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
642
|
+
const effect = service.createWorktreeEffect('/path/to/worktree', 'new-feature', 'main');
|
|
643
|
+
const result = await Effect.runPromise(effect);
|
|
644
|
+
expect(result).toMatchObject({
|
|
645
|
+
path: '/path/to/worktree',
|
|
646
|
+
branch: 'new-feature',
|
|
647
|
+
isMainWorktree: false,
|
|
648
|
+
});
|
|
688
649
|
});
|
|
689
|
-
|
|
690
|
-
describe('AmbiguousBranchError Integration', () => {
|
|
691
|
-
it('should return error message when createWorktree encounters ambiguous branch', async () => {
|
|
650
|
+
it('should return Effect that fails with GitError on git command failure', async () => {
|
|
692
651
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
693
652
|
if (typeof cmd === 'string') {
|
|
694
653
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
695
654
|
return '/fake/path/.git\n';
|
|
696
655
|
}
|
|
697
|
-
if (cmd.includes('rev-parse --verify
|
|
656
|
+
if (cmd.includes('rev-parse --verify')) {
|
|
698
657
|
throw new Error('Branch not found');
|
|
699
658
|
}
|
|
700
|
-
if (cmd.includes('
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/ambiguous-branch') ||
|
|
707
|
-
cmd.includes('show-ref --verify --quiet refs/remotes/upstream/ambiguous-branch')) {
|
|
708
|
-
return ''; // Both remotes have the branch
|
|
659
|
+
if (cmd.includes('git worktree add')) {
|
|
660
|
+
const error = new Error('fatal: invalid reference: main');
|
|
661
|
+
error.status = 128;
|
|
662
|
+
error.stderr = 'fatal: invalid reference: main';
|
|
663
|
+
throw error;
|
|
709
664
|
}
|
|
710
665
|
}
|
|
711
666
|
throw new Error('Command not mocked: ' + cmd);
|
|
712
667
|
});
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
668
|
+
const effect = service.createWorktreeEffect('/path/to/worktree', 'new-feature', 'main');
|
|
669
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
670
|
+
if (result._tag === 'Left') {
|
|
671
|
+
expect(result.left).toBeInstanceOf(GitError);
|
|
672
|
+
expect(result.left.exitCode).toBe(128);
|
|
673
|
+
expect(result.left.stderr).toContain('invalid reference');
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
expect.fail('Should have returned Left with GitError');
|
|
677
|
+
}
|
|
718
678
|
});
|
|
719
|
-
|
|
679
|
+
});
|
|
680
|
+
describe('Effect-based deleteWorktree', () => {
|
|
681
|
+
it('should return Effect with void on success', async () => {
|
|
720
682
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
721
683
|
if (typeof cmd === 'string') {
|
|
722
684
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
723
685
|
return '/fake/path/.git\n';
|
|
724
686
|
}
|
|
725
|
-
if (cmd
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
687
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
688
|
+
return `worktree /fake/path
|
|
689
|
+
HEAD abcd1234
|
|
690
|
+
branch refs/heads/main
|
|
691
|
+
|
|
692
|
+
worktree /fake/path/feature
|
|
693
|
+
HEAD efgh5678
|
|
694
|
+
branch refs/heads/feature
|
|
695
|
+
`;
|
|
734
696
|
}
|
|
735
|
-
if (cmd.includes('
|
|
736
|
-
|
|
697
|
+
if (cmd.includes('git worktree remove')) {
|
|
698
|
+
return '';
|
|
737
699
|
}
|
|
738
|
-
|
|
739
|
-
if (cmd.includes('git worktree add -b "new-feature" "/path/to/worktree" "origin/ambiguous-branch"')) {
|
|
700
|
+
if (cmd.includes('git branch -D')) {
|
|
740
701
|
return '';
|
|
741
702
|
}
|
|
742
703
|
}
|
|
743
704
|
throw new Error('Command not mocked: ' + cmd);
|
|
744
705
|
});
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
expect(
|
|
748
|
-
expect(mockedExecSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "origin/ambiguous-branch"', { cwd: '/fake/path', encoding: 'utf8' });
|
|
706
|
+
const effect = service.deleteWorktreeEffect('/fake/path/feature');
|
|
707
|
+
await Effect.runPromise(effect);
|
|
708
|
+
expect(execSync).toHaveBeenCalledWith(expect.stringContaining('git worktree remove'), expect.any(Object));
|
|
749
709
|
});
|
|
750
|
-
it('should
|
|
710
|
+
it('should return Effect that fails with GitError when worktree not found', async () => {
|
|
751
711
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
752
712
|
if (typeof cmd === 'string') {
|
|
753
713
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
754
714
|
return '/fake/path/.git\n';
|
|
755
715
|
}
|
|
756
|
-
if (cmd
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
if (cmd === 'git remote') {
|
|
763
|
-
return 'origin\nupstream\nfork\n';
|
|
764
|
-
}
|
|
765
|
-
// All three remotes have the branch
|
|
766
|
-
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/three-way-branch') ||
|
|
767
|
-
cmd.includes('show-ref --verify --quiet refs/remotes/upstream/three-way-branch') ||
|
|
768
|
-
cmd.includes('show-ref --verify --quiet refs/remotes/fork/three-way-branch')) {
|
|
769
|
-
return '';
|
|
716
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
717
|
+
return `worktree /fake/path
|
|
718
|
+
HEAD abcd1234
|
|
719
|
+
branch refs/heads/main
|
|
720
|
+
`;
|
|
770
721
|
}
|
|
771
722
|
}
|
|
772
723
|
throw new Error('Command not mocked: ' + cmd);
|
|
773
724
|
});
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
725
|
+
const effect = service.deleteWorktreeEffect('/fake/path/nonexistent');
|
|
726
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
727
|
+
if (result._tag === 'Left') {
|
|
728
|
+
expect(result.left).toBeInstanceOf(GitError);
|
|
729
|
+
expect(result.left.stderr).toContain('Worktree not found');
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
expect.fail('Should have returned Left with GitError');
|
|
733
|
+
}
|
|
778
734
|
});
|
|
779
|
-
it('should
|
|
735
|
+
it('should return Effect that fails with GitError when trying to delete main worktree', async () => {
|
|
780
736
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
781
737
|
if (typeof cmd === 'string') {
|
|
782
738
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
783
739
|
return '/fake/path/.git\n';
|
|
784
740
|
}
|
|
785
|
-
if (cmd
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
if (cmd === 'git remote') {
|
|
792
|
-
return 'origin\nfork\n';
|
|
793
|
-
}
|
|
794
|
-
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/feature/sub/complex-name') ||
|
|
795
|
-
cmd.includes('show-ref --verify --quiet refs/remotes/fork/feature/sub/complex-name')) {
|
|
796
|
-
return '';
|
|
741
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
742
|
+
return `worktree /fake/path
|
|
743
|
+
HEAD abcd1234
|
|
744
|
+
branch refs/heads/main
|
|
745
|
+
`;
|
|
797
746
|
}
|
|
798
747
|
}
|
|
799
748
|
throw new Error('Command not mocked: ' + cmd);
|
|
800
749
|
});
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
750
|
+
const effect = service.deleteWorktreeEffect('/fake/path');
|
|
751
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
752
|
+
if (result._tag === 'Left') {
|
|
753
|
+
expect(result.left).toBeInstanceOf(GitError);
|
|
754
|
+
expect(result.left.stderr).toContain('Cannot delete the main worktree');
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
expect.fail('Should have returned Left with GitError');
|
|
758
|
+
}
|
|
805
759
|
});
|
|
806
|
-
|
|
760
|
+
});
|
|
761
|
+
describe('Effect-based mergeWorktree', () => {
|
|
762
|
+
it('should return Effect with void on successful merge', async () => {
|
|
807
763
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
808
764
|
if (typeof cmd === 'string') {
|
|
809
765
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
810
766
|
return '/fake/path/.git\n';
|
|
811
767
|
}
|
|
812
|
-
if (cmd
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
768
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
769
|
+
return `worktree /fake/path
|
|
770
|
+
HEAD abcd1234
|
|
771
|
+
branch refs/heads/main
|
|
772
|
+
|
|
773
|
+
worktree /fake/path/feature
|
|
774
|
+
HEAD efgh5678
|
|
775
|
+
branch refs/heads/feature
|
|
776
|
+
`;
|
|
820
777
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
return '';
|
|
778
|
+
if (cmd.includes('git merge')) {
|
|
779
|
+
return 'Merge successful';
|
|
824
780
|
}
|
|
825
|
-
|
|
826
|
-
|
|
781
|
+
}
|
|
782
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
783
|
+
});
|
|
784
|
+
const effect = service.mergeWorktreeEffect('feature', 'main', false);
|
|
785
|
+
await Effect.runPromise(effect);
|
|
786
|
+
expect(execSync).toHaveBeenCalledWith('git merge --no-ff "feature"', expect.any(Object));
|
|
787
|
+
});
|
|
788
|
+
it('should return Effect that fails with GitError when target branch not found', async () => {
|
|
789
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
790
|
+
if (typeof cmd === 'string') {
|
|
791
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
792
|
+
return '/fake/path/.git\n';
|
|
827
793
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
794
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
795
|
+
return `worktree /fake/path
|
|
796
|
+
HEAD abcd1234
|
|
797
|
+
branch refs/heads/main
|
|
798
|
+
`;
|
|
831
799
|
}
|
|
832
800
|
}
|
|
833
801
|
throw new Error('Command not mocked: ' + cmd);
|
|
834
802
|
});
|
|
835
|
-
|
|
836
|
-
const result = await
|
|
837
|
-
|
|
838
|
-
|
|
803
|
+
const effect = service.mergeWorktreeEffect('feature', 'nonexistent', false);
|
|
804
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
805
|
+
if (result._tag === 'Left') {
|
|
806
|
+
expect(result.left).toBeInstanceOf(GitError);
|
|
807
|
+
expect(result.left.stderr).toContain('Target branch worktree not found');
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
expect.fail('Should have returned Left with GitError');
|
|
811
|
+
}
|
|
839
812
|
});
|
|
840
|
-
it('should
|
|
813
|
+
it('should return Effect that fails with GitError on merge conflict', async () => {
|
|
841
814
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
842
815
|
if (typeof cmd === 'string') {
|
|
843
816
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
844
817
|
return '/fake/path/.git\n';
|
|
845
818
|
}
|
|
846
|
-
if (cmd
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
819
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
820
|
+
return `worktree /fake/path
|
|
821
|
+
HEAD abcd1234
|
|
822
|
+
branch refs/heads/main
|
|
823
|
+
|
|
824
|
+
worktree /fake/path/feature
|
|
825
|
+
HEAD efgh5678
|
|
826
|
+
branch refs/heads/feature
|
|
827
|
+
`;
|
|
852
828
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
829
|
+
if (cmd.includes('git merge')) {
|
|
830
|
+
const error = new Error('CONFLICT: Merge conflict');
|
|
831
|
+
error.status = 1;
|
|
832
|
+
error.stderr = 'CONFLICT: Merge conflict in file.txt';
|
|
833
|
+
throw error;
|
|
857
834
|
}
|
|
858
835
|
}
|
|
859
836
|
throw new Error('Command not mocked: ' + cmd);
|
|
860
837
|
});
|
|
861
|
-
|
|
862
|
-
const result = await
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
838
|
+
const effect = service.mergeWorktreeEffect('feature', 'main', false);
|
|
839
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
840
|
+
if (result._tag === 'Left') {
|
|
841
|
+
expect(result.left).toBeInstanceOf(GitError);
|
|
842
|
+
expect(result.left.exitCode).toBe(1);
|
|
843
|
+
expect(result.left.stderr).toContain('Merge conflict');
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
expect.fail('Should have returned Left with GitError');
|
|
847
|
+
}
|
|
867
848
|
});
|
|
868
849
|
});
|
|
869
850
|
});
|