ccmanager 2.8.0 → 2.9.1

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 (82) hide show
  1. package/dist/cli.test.js +13 -2
  2. package/dist/components/App.js +125 -50
  3. package/dist/components/App.test.js +270 -0
  4. package/dist/components/ConfigureShortcuts.js +82 -8
  5. package/dist/components/DeleteWorktree.js +39 -5
  6. package/dist/components/DeleteWorktree.test.d.ts +1 -0
  7. package/dist/components/DeleteWorktree.test.js +128 -0
  8. package/dist/components/LoadingSpinner.d.ts +8 -0
  9. package/dist/components/LoadingSpinner.js +37 -0
  10. package/dist/components/LoadingSpinner.test.d.ts +1 -0
  11. package/dist/components/LoadingSpinner.test.js +187 -0
  12. package/dist/components/Menu.js +64 -16
  13. package/dist/components/Menu.recent-projects.test.js +32 -11
  14. package/dist/components/Menu.test.js +136 -4
  15. package/dist/components/MergeWorktree.js +79 -18
  16. package/dist/components/MergeWorktree.test.d.ts +1 -0
  17. package/dist/components/MergeWorktree.test.js +227 -0
  18. package/dist/components/NewWorktree.js +88 -9
  19. package/dist/components/NewWorktree.test.d.ts +1 -0
  20. package/dist/components/NewWorktree.test.js +244 -0
  21. package/dist/components/ProjectList.js +44 -13
  22. package/dist/components/ProjectList.recent-projects.test.js +8 -3
  23. package/dist/components/ProjectList.test.js +105 -8
  24. package/dist/components/RemoteBranchSelector.test.js +3 -1
  25. package/dist/components/Session.js +11 -6
  26. package/dist/hooks/useGitStatus.d.ts +11 -0
  27. package/dist/hooks/useGitStatus.js +70 -12
  28. package/dist/hooks/useGitStatus.test.js +30 -23
  29. package/dist/services/configurationManager.d.ts +75 -0
  30. package/dist/services/configurationManager.effect.test.d.ts +1 -0
  31. package/dist/services/configurationManager.effect.test.js +407 -0
  32. package/dist/services/configurationManager.js +246 -0
  33. package/dist/services/globalSessionOrchestrator.test.js +0 -8
  34. package/dist/services/projectManager.d.ts +98 -2
  35. package/dist/services/projectManager.js +228 -59
  36. package/dist/services/projectManager.test.js +242 -2
  37. package/dist/services/sessionManager.d.ts +44 -2
  38. package/dist/services/sessionManager.effect.test.d.ts +1 -0
  39. package/dist/services/sessionManager.effect.test.js +321 -0
  40. package/dist/services/sessionManager.js +216 -65
  41. package/dist/services/sessionManager.statePersistence.test.js +18 -9
  42. package/dist/services/sessionManager.test.js +40 -36
  43. package/dist/services/shortcutManager.d.ts +2 -0
  44. package/dist/services/shortcutManager.js +53 -0
  45. package/dist/services/shortcutManager.test.d.ts +1 -0
  46. package/dist/services/shortcutManager.test.js +30 -0
  47. package/dist/services/worktreeService.d.ts +356 -26
  48. package/dist/services/worktreeService.js +793 -353
  49. package/dist/services/worktreeService.test.js +294 -313
  50. package/dist/types/errors.d.ts +74 -0
  51. package/dist/types/errors.js +31 -0
  52. package/dist/types/errors.test.d.ts +1 -0
  53. package/dist/types/errors.test.js +201 -0
  54. package/dist/types/index.d.ts +5 -17
  55. package/dist/utils/claudeDir.d.ts +58 -6
  56. package/dist/utils/claudeDir.js +103 -8
  57. package/dist/utils/claudeDir.test.d.ts +1 -0
  58. package/dist/utils/claudeDir.test.js +108 -0
  59. package/dist/utils/concurrencyLimit.d.ts +5 -0
  60. package/dist/utils/concurrencyLimit.js +11 -0
  61. package/dist/utils/concurrencyLimit.test.js +40 -1
  62. package/dist/utils/gitStatus.d.ts +36 -8
  63. package/dist/utils/gitStatus.js +170 -88
  64. package/dist/utils/gitStatus.test.js +12 -9
  65. package/dist/utils/hookExecutor.d.ts +41 -6
  66. package/dist/utils/hookExecutor.js +75 -32
  67. package/dist/utils/hookExecutor.test.js +73 -20
  68. package/dist/utils/terminalCapabilities.d.ts +18 -0
  69. package/dist/utils/terminalCapabilities.js +81 -0
  70. package/dist/utils/terminalCapabilities.test.d.ts +1 -0
  71. package/dist/utils/terminalCapabilities.test.js +104 -0
  72. package/dist/utils/testHelpers.d.ts +106 -0
  73. package/dist/utils/testHelpers.js +153 -0
  74. package/dist/utils/testHelpers.test.d.ts +1 -0
  75. package/dist/utils/testHelpers.test.js +114 -0
  76. package/dist/utils/worktreeConfig.d.ts +77 -2
  77. package/dist/utils/worktreeConfig.js +156 -16
  78. package/dist/utils/worktreeConfig.test.d.ts +1 -0
  79. package/dist/utils/worktreeConfig.test.js +39 -0
  80. package/package.json +4 -4
  81. package/dist/integration-tests/devcontainer.integration.test.js +0 -101
  82. /package/dist/{integration-tests/devcontainer.integration.test.d.ts → components/App.test.d.ts} +0 -0
@@ -1,4 +1,6 @@
1
+ import { Effect, Either } from 'effect';
1
2
  import { ConfigurationData, StatusHookConfig, WorktreeHookConfig, ShortcutConfig, WorktreeConfig, CommandConfig, CommandPreset, CommandPresetsConfig } from '../types/index.js';
3
+ import { FileSystemError, ConfigError, ValidationError } from '../types/errors.js';
2
4
  export declare class ConfigurationManager {
3
5
  private configPath;
4
6
  private legacyShortcutsPath;
@@ -30,5 +32,78 @@ export declare class ConfigurationManager {
30
32
  setDefaultPreset(id: string): void;
31
33
  getSelectPresetOnStart(): boolean;
32
34
  setSelectPresetOnStart(enabled: boolean): void;
35
+ /**
36
+ * Load configuration from file with Effect-based error handling
37
+ *
38
+ * @returns {Effect.Effect<ConfigurationData, FileSystemError | ConfigError, never>} Configuration data on success, errors on failure
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const result = await Effect.runPromise(
43
+ * configManager.loadConfigEffect()
44
+ * );
45
+ * ```
46
+ */
47
+ loadConfigEffect(): Effect.Effect<ConfigurationData, FileSystemError | ConfigError, never>;
48
+ /**
49
+ * Save configuration to file with Effect-based error handling
50
+ *
51
+ * @returns {Effect.Effect<void, FileSystemError, never>} Void on success, FileSystemError on write failure
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * await Effect.runPromise(
56
+ * configManager.saveConfigEffect(config)
57
+ * );
58
+ * ```
59
+ */
60
+ saveConfigEffect(config: ConfigurationData): Effect.Effect<void, FileSystemError, never>;
61
+ /**
62
+ * Validate configuration structure
63
+ * Synchronous validation using Either
64
+ */
65
+ validateConfig(config: unknown): Either.Either<ValidationError, ConfigurationData>;
66
+ /**
67
+ * Get preset by ID with Either-based error handling
68
+ * Synchronous lookup using Either
69
+ */
70
+ getPresetByIdEffect(id: string): Either.Either<ValidationError, CommandPreset>;
71
+ /**
72
+ * Set shortcuts with Effect-based error handling
73
+ *
74
+ * @returns {Effect.Effect<void, FileSystemError, never>} Void on success, FileSystemError on save failure
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * await Effect.runPromise(
79
+ * configManager.setShortcutsEffect(shortcuts)
80
+ * );
81
+ * ```
82
+ */
83
+ setShortcutsEffect(shortcuts: ShortcutConfig): Effect.Effect<void, FileSystemError, never>;
84
+ /**
85
+ * Set command presets with Effect-based error handling
86
+ */
87
+ setCommandPresetsEffect(presets: CommandPresetsConfig): Effect.Effect<void, FileSystemError, never>;
88
+ /**
89
+ * Add or update preset with Effect-based error handling
90
+ */
91
+ addPresetEffect(preset: CommandPreset): Effect.Effect<void, FileSystemError, never>;
92
+ /**
93
+ * Delete preset with Effect-based error handling
94
+ */
95
+ deletePresetEffect(id: string): Effect.Effect<void, ValidationError | FileSystemError, never>;
96
+ /**
97
+ * Set default preset with Effect-based error handling
98
+ */
99
+ setDefaultPresetEffect(id: string): Effect.Effect<void, ValidationError | FileSystemError, never>;
100
+ /**
101
+ * Apply default values to configuration
102
+ */
103
+ private applyDefaults;
104
+ /**
105
+ * Synchronous legacy shortcuts migration helper
106
+ */
107
+ private migrateLegacyShortcutsSync;
33
108
  }
34
109
  export declare const configurationManager: ConfigurationManager;
@@ -0,0 +1,407 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { Effect, Either } from 'effect';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { ConfigurationManager } from './configurationManager.js';
5
+ // Mock fs module
6
+ vi.mock('fs', () => ({
7
+ existsSync: vi.fn(),
8
+ mkdirSync: vi.fn(),
9
+ readFileSync: vi.fn(),
10
+ writeFileSync: vi.fn(),
11
+ }));
12
+ // Mock os module
13
+ vi.mock('os', () => ({
14
+ homedir: vi.fn(() => '/home/test'),
15
+ }));
16
+ describe('ConfigurationManager - Effect-based operations', () => {
17
+ let configManager;
18
+ let mockConfigData;
19
+ beforeEach(() => {
20
+ // Reset all mocks
21
+ vi.clearAllMocks();
22
+ // Default mock config data
23
+ mockConfigData = {
24
+ shortcuts: {
25
+ returnToMenu: { ctrl: true, key: 'e' },
26
+ cancel: { key: 'escape' },
27
+ },
28
+ command: {
29
+ command: 'claude',
30
+ args: ['--existing'],
31
+ },
32
+ };
33
+ // Mock file system operations
34
+ existsSync.mockImplementation((path) => {
35
+ return path.includes('config.json');
36
+ });
37
+ readFileSync.mockImplementation(() => {
38
+ return JSON.stringify(mockConfigData);
39
+ });
40
+ mkdirSync.mockImplementation(() => { });
41
+ writeFileSync.mockImplementation(() => { });
42
+ // Create new instance for each test
43
+ configManager = new ConfigurationManager();
44
+ });
45
+ afterEach(() => {
46
+ vi.resetAllMocks();
47
+ });
48
+ describe('loadConfigEffect', () => {
49
+ it('should return Effect with ConfigurationData on success', async () => {
50
+ const result = await Effect.runPromise(configManager.loadConfigEffect());
51
+ expect(result).toBeDefined();
52
+ expect(result.shortcuts).toBeDefined();
53
+ expect(result.command).toBeDefined();
54
+ });
55
+ it('should fail with FileSystemError when file read fails', async () => {
56
+ readFileSync.mockImplementation(() => {
57
+ throw new Error('EACCES: permission denied');
58
+ });
59
+ configManager = new ConfigurationManager();
60
+ const result = await Effect.runPromise(Effect.either(configManager.loadConfigEffect()));
61
+ expect(Either.isLeft(result)).toBe(true);
62
+ if (Either.isLeft(result)) {
63
+ expect(result.left._tag).toBe('FileSystemError');
64
+ expect(result.left.operation).toBe('read');
65
+ expect(result.left.cause).toContain('permission denied');
66
+ }
67
+ });
68
+ it('should fail with ConfigError when JSON parsing fails', async () => {
69
+ readFileSync.mockImplementation(() => {
70
+ return 'invalid json{';
71
+ });
72
+ configManager = new ConfigurationManager();
73
+ const result = await Effect.runPromise(Effect.either(configManager.loadConfigEffect()));
74
+ expect(Either.isLeft(result)).toBe(true);
75
+ if (Either.isLeft(result)) {
76
+ expect(result.left._tag).toBe('ConfigError');
77
+ expect(result.left.reason).toBe('parse');
78
+ }
79
+ });
80
+ it('should migrate legacy shortcuts and return success', async () => {
81
+ existsSync.mockImplementation((path) => {
82
+ if (path.includes('shortcuts.json'))
83
+ return true;
84
+ if (path.includes('config.json'))
85
+ return false;
86
+ return true;
87
+ });
88
+ const legacyShortcuts = {
89
+ returnToMenu: { ctrl: true, key: 'b' },
90
+ };
91
+ readFileSync.mockImplementation((path) => {
92
+ if (path.includes('shortcuts.json')) {
93
+ return JSON.stringify(legacyShortcuts);
94
+ }
95
+ return '{}';
96
+ });
97
+ configManager = new ConfigurationManager();
98
+ const result = await Effect.runPromise(configManager.loadConfigEffect());
99
+ expect(result.shortcuts).toEqual(legacyShortcuts);
100
+ expect(writeFileSync).toHaveBeenCalled();
101
+ });
102
+ });
103
+ describe('saveConfigEffect', () => {
104
+ it('should return Effect<void> on successful save', async () => {
105
+ const newConfig = {
106
+ shortcuts: {
107
+ returnToMenu: { ctrl: true, key: 'z' },
108
+ cancel: { key: 'escape' },
109
+ },
110
+ };
111
+ await Effect.runPromise(configManager.saveConfigEffect(newConfig));
112
+ expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('config.json'), expect.any(String));
113
+ });
114
+ it('should fail with FileSystemError when file write fails', async () => {
115
+ writeFileSync.mockImplementation(() => {
116
+ throw new Error('ENOSPC: no space left on device');
117
+ });
118
+ const newConfig = {
119
+ shortcuts: {
120
+ returnToMenu: { ctrl: true, key: 'z' },
121
+ cancel: { key: 'escape' },
122
+ },
123
+ };
124
+ const result = await Effect.runPromise(Effect.either(configManager.saveConfigEffect(newConfig)));
125
+ expect(Either.isLeft(result)).toBe(true);
126
+ if (Either.isLeft(result)) {
127
+ expect(result.left._tag).toBe('FileSystemError');
128
+ expect(result.left.operation).toBe('write');
129
+ expect(result.left.cause).toContain('no space left on device');
130
+ }
131
+ });
132
+ });
133
+ describe('validateConfig', () => {
134
+ it('should return Right with valid ConfigurationData', () => {
135
+ const validConfig = {
136
+ shortcuts: {
137
+ returnToMenu: { ctrl: true, key: 'e' },
138
+ cancel: { key: 'escape' },
139
+ },
140
+ };
141
+ const result = configManager.validateConfig(validConfig);
142
+ expect(Either.isRight(result)).toBe(true);
143
+ if (Either.isRight(result)) {
144
+ expect(result.right).toEqual(validConfig);
145
+ }
146
+ });
147
+ it('should return Left with ValidationError for invalid config', () => {
148
+ const invalidConfig = {
149
+ shortcuts: 'not an object',
150
+ };
151
+ const result = configManager.validateConfig(invalidConfig);
152
+ expect(Either.isLeft(result)).toBe(true);
153
+ if (Either.isLeft(result)) {
154
+ const error = result.left;
155
+ expect(error._tag).toBe('ValidationError');
156
+ expect(error.field).toBe('config');
157
+ }
158
+ });
159
+ it('should return Left for null config', () => {
160
+ const result = configManager.validateConfig(null);
161
+ expect(Either.isLeft(result)).toBe(true);
162
+ if (Either.isLeft(result)) {
163
+ const error = result.left;
164
+ expect(error._tag).toBe('ValidationError');
165
+ }
166
+ });
167
+ });
168
+ describe('getPresetByIdEffect', () => {
169
+ it('should return Right with preset when found', () => {
170
+ mockConfigData.commandPresets = {
171
+ presets: [
172
+ { id: '1', name: 'Main', command: 'claude' },
173
+ { id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
174
+ ],
175
+ defaultPresetId: '1',
176
+ };
177
+ configManager = new ConfigurationManager();
178
+ const result = configManager.getPresetByIdEffect('2');
179
+ expect(Either.isRight(result)).toBe(true);
180
+ if (Either.isRight(result)) {
181
+ expect(result.right).toEqual({
182
+ id: '2',
183
+ name: 'Custom',
184
+ command: 'claude',
185
+ args: ['--custom'],
186
+ });
187
+ }
188
+ });
189
+ it('should return Left with ValidationError when preset not found', () => {
190
+ mockConfigData.commandPresets = {
191
+ presets: [{ id: '1', name: 'Main', command: 'claude' }],
192
+ defaultPresetId: '1',
193
+ };
194
+ configManager = new ConfigurationManager();
195
+ const result = configManager.getPresetByIdEffect('999');
196
+ expect(Either.isLeft(result)).toBe(true);
197
+ if (Either.isLeft(result)) {
198
+ const error = result.left;
199
+ expect(error._tag).toBe('ValidationError');
200
+ expect(error.field).toBe('presetId');
201
+ expect(error.receivedValue).toBe('999');
202
+ }
203
+ });
204
+ it('should include constraint in ValidationError', () => {
205
+ mockConfigData.commandPresets = {
206
+ presets: [{ id: '1', name: 'Main', command: 'claude' }],
207
+ defaultPresetId: '1',
208
+ };
209
+ configManager = new ConfigurationManager();
210
+ const result = configManager.getPresetByIdEffect('invalid-id');
211
+ expect(Either.isLeft(result)).toBe(true);
212
+ if (Either.isLeft(result)) {
213
+ const error = result.left;
214
+ expect(error.constraint).toContain('not found');
215
+ }
216
+ });
217
+ });
218
+ describe('setShortcutsEffect', () => {
219
+ it('should return Effect<void> on successful update', async () => {
220
+ const newShortcuts = {
221
+ returnToMenu: { ctrl: true, key: 'z' },
222
+ cancel: { key: 'escape' },
223
+ };
224
+ await Effect.runPromise(configManager.setShortcutsEffect(newShortcuts));
225
+ expect(writeFileSync).toHaveBeenCalled();
226
+ });
227
+ it('should fail with FileSystemError when save fails', async () => {
228
+ writeFileSync.mockImplementation(() => {
229
+ throw new Error('Write failed');
230
+ });
231
+ const newShortcuts = {
232
+ returnToMenu: { ctrl: true, key: 'z' },
233
+ cancel: { key: 'escape' },
234
+ };
235
+ const result = await Effect.runPromise(Effect.either(configManager.setShortcutsEffect(newShortcuts)));
236
+ expect(Either.isLeft(result)).toBe(true);
237
+ if (Either.isLeft(result)) {
238
+ expect(result.left._tag).toBe('FileSystemError');
239
+ }
240
+ });
241
+ });
242
+ describe('setCommandPresetsEffect', () => {
243
+ it('should return Effect<void> on successful update', async () => {
244
+ const newPresets = {
245
+ presets: [
246
+ { id: '1', name: 'Main', command: 'claude' },
247
+ { id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
248
+ ],
249
+ defaultPresetId: '2',
250
+ };
251
+ await Effect.runPromise(configManager.setCommandPresetsEffect(newPresets));
252
+ expect(writeFileSync).toHaveBeenCalled();
253
+ });
254
+ it('should fail with FileSystemError when save fails', async () => {
255
+ writeFileSync.mockImplementation(() => {
256
+ throw new Error('Disk full');
257
+ });
258
+ const newPresets = {
259
+ presets: [{ id: '1', name: 'Main', command: 'claude' }],
260
+ defaultPresetId: '1',
261
+ };
262
+ const result = await Effect.runPromise(Effect.either(configManager.setCommandPresetsEffect(newPresets)));
263
+ expect(Either.isLeft(result)).toBe(true);
264
+ if (Either.isLeft(result)) {
265
+ expect(result.left._tag).toBe('FileSystemError');
266
+ }
267
+ });
268
+ });
269
+ describe('addPresetEffect', () => {
270
+ it('should add new preset and return Effect<void>', async () => {
271
+ mockConfigData.commandPresets = {
272
+ presets: [{ id: '1', name: 'Main', command: 'claude' }],
273
+ defaultPresetId: '1',
274
+ };
275
+ configManager = new ConfigurationManager();
276
+ const newPreset = {
277
+ id: '2',
278
+ name: 'New',
279
+ command: 'claude',
280
+ args: ['--new'],
281
+ };
282
+ await Effect.runPromise(configManager.addPresetEffect(newPreset));
283
+ expect(writeFileSync).toHaveBeenCalled();
284
+ });
285
+ it('should replace existing preset with same id', async () => {
286
+ mockConfigData.commandPresets = {
287
+ presets: [{ id: '1', name: 'Main', command: 'claude' }],
288
+ defaultPresetId: '1',
289
+ };
290
+ configManager = new ConfigurationManager();
291
+ const updatedPreset = {
292
+ id: '1',
293
+ name: 'Updated',
294
+ command: 'claude',
295
+ args: ['--updated'],
296
+ };
297
+ await Effect.runPromise(configManager.addPresetEffect(updatedPreset));
298
+ expect(writeFileSync).toHaveBeenCalled();
299
+ });
300
+ it('should fail with FileSystemError when save fails', async () => {
301
+ writeFileSync.mockImplementation(() => {
302
+ throw new Error('Save failed');
303
+ });
304
+ const newPreset = {
305
+ id: '2',
306
+ name: 'New',
307
+ command: 'claude',
308
+ };
309
+ const result = await Effect.runPromise(Effect.either(configManager.addPresetEffect(newPreset)));
310
+ expect(Either.isLeft(result)).toBe(true);
311
+ if (Either.isLeft(result)) {
312
+ expect(result.left._tag).toBe('FileSystemError');
313
+ }
314
+ });
315
+ });
316
+ describe('deletePresetEffect', () => {
317
+ it('should delete preset and return Effect<void>', async () => {
318
+ mockConfigData.commandPresets = {
319
+ presets: [
320
+ { id: '1', name: 'Main', command: 'claude' },
321
+ { id: '2', name: 'Custom', command: 'claude' },
322
+ ],
323
+ defaultPresetId: '1',
324
+ };
325
+ configManager = new ConfigurationManager();
326
+ await Effect.runPromise(configManager.deletePresetEffect('2'));
327
+ expect(writeFileSync).toHaveBeenCalled();
328
+ });
329
+ it('should fail with ValidationError when deleting last preset', async () => {
330
+ mockConfigData.commandPresets = {
331
+ presets: [{ id: '1', name: 'Main', command: 'claude' }],
332
+ defaultPresetId: '1',
333
+ };
334
+ configManager = new ConfigurationManager();
335
+ const result = await Effect.runPromise(Effect.either(configManager.deletePresetEffect('1')));
336
+ expect(Either.isLeft(result)).toBe(true);
337
+ if (Either.isLeft(result)) {
338
+ expect(result.left._tag).toBe('ValidationError');
339
+ expect(result.left.field).toBe('presetId');
340
+ expect(result.left.constraint).toContain('Cannot delete last preset');
341
+ }
342
+ });
343
+ it('should fail with FileSystemError when save fails', async () => {
344
+ mockConfigData.commandPresets = {
345
+ presets: [
346
+ { id: '1', name: 'Main', command: 'claude' },
347
+ { id: '2', name: 'Custom', command: 'claude' },
348
+ ],
349
+ defaultPresetId: '1',
350
+ };
351
+ configManager = new ConfigurationManager();
352
+ writeFileSync.mockImplementation(() => {
353
+ throw new Error('Save failed');
354
+ });
355
+ const result = await Effect.runPromise(Effect.either(configManager.deletePresetEffect('2')));
356
+ expect(Either.isLeft(result)).toBe(true);
357
+ if (Either.isLeft(result)) {
358
+ expect(result.left._tag).toBe('FileSystemError');
359
+ }
360
+ });
361
+ });
362
+ describe('setDefaultPresetEffect', () => {
363
+ it('should update default preset and return Effect<void>', async () => {
364
+ mockConfigData.commandPresets = {
365
+ presets: [
366
+ { id: '1', name: 'Main', command: 'claude' },
367
+ { id: '2', name: 'Custom', command: 'claude' },
368
+ ],
369
+ defaultPresetId: '1',
370
+ };
371
+ configManager = new ConfigurationManager();
372
+ await Effect.runPromise(configManager.setDefaultPresetEffect('2'));
373
+ expect(writeFileSync).toHaveBeenCalled();
374
+ });
375
+ it('should fail with ValidationError when preset id does not exist', async () => {
376
+ mockConfigData.commandPresets = {
377
+ presets: [{ id: '1', name: 'Main', command: 'claude' }],
378
+ defaultPresetId: '1',
379
+ };
380
+ configManager = new ConfigurationManager();
381
+ const result = await Effect.runPromise(Effect.either(configManager.setDefaultPresetEffect('999')));
382
+ expect(Either.isLeft(result)).toBe(true);
383
+ if (Either.isLeft(result)) {
384
+ expect(result.left._tag).toBe('ValidationError');
385
+ expect(result.left.field).toBe('presetId');
386
+ }
387
+ });
388
+ it('should fail with FileSystemError when save fails', async () => {
389
+ mockConfigData.commandPresets = {
390
+ presets: [
391
+ { id: '1', name: 'Main', command: 'claude' },
392
+ { id: '2', name: 'Custom', command: 'claude' },
393
+ ],
394
+ defaultPresetId: '1',
395
+ };
396
+ configManager = new ConfigurationManager();
397
+ writeFileSync.mockImplementation(() => {
398
+ throw new Error('Save failed');
399
+ });
400
+ const result = await Effect.runPromise(Effect.either(configManager.setDefaultPresetEffect('2')));
401
+ expect(Either.isLeft(result)).toBe(true);
402
+ if (Either.isLeft(result)) {
403
+ expect(result.left._tag).toBe('FileSystemError');
404
+ }
405
+ });
406
+ });
407
+ });