ccmanager 3.3.2 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +11 -5
  2. package/dist/components/App.js +17 -3
  3. package/dist/components/App.test.js +5 -5
  4. package/dist/components/Configuration.d.ts +2 -0
  5. package/dist/components/Configuration.js +6 -2
  6. package/dist/components/ConfigureCommand.js +34 -11
  7. package/dist/components/ConfigureOther.js +18 -4
  8. package/dist/components/ConfigureOther.test.js +48 -12
  9. package/dist/components/ConfigureShortcuts.js +27 -85
  10. package/dist/components/ConfigureStatusHooks.js +19 -4
  11. package/dist/components/ConfigureStatusHooks.test.js +46 -12
  12. package/dist/components/ConfigureWorktree.js +18 -4
  13. package/dist/components/ConfigureWorktreeHooks.js +19 -4
  14. package/dist/components/ConfigureWorktreeHooks.test.js +49 -14
  15. package/dist/components/Menu.js +72 -14
  16. package/dist/components/Menu.recent-projects.test.js +2 -0
  17. package/dist/components/Menu.test.js +2 -0
  18. package/dist/components/NewWorktree.js +2 -2
  19. package/dist/components/NewWorktree.test.js +6 -6
  20. package/dist/components/PresetSelector.js +2 -2
  21. package/dist/constants/statusIcons.d.ts +4 -1
  22. package/dist/constants/statusIcons.js +10 -1
  23. package/dist/constants/statusIcons.test.js +42 -0
  24. package/dist/contexts/ConfigEditorContext.d.ts +21 -0
  25. package/dist/contexts/ConfigEditorContext.js +25 -0
  26. package/dist/services/autoApprovalVerifier.js +3 -3
  27. package/dist/services/autoApprovalVerifier.test.js +2 -2
  28. package/dist/services/config/configEditor.d.ts +46 -0
  29. package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js} +46 -49
  30. package/dist/services/config/configEditor.js +101 -0
  31. package/dist/services/{configurationManager.selectPresetOnStart.test.js → config/configEditor.selectPresetOnStart.test.js} +27 -19
  32. package/dist/services/config/configEditor.test.d.ts +1 -0
  33. package/dist/services/{configurationManager.test.js → config/configEditor.test.js} +60 -132
  34. package/dist/services/config/configReader.d.ts +28 -0
  35. package/dist/services/config/configReader.js +95 -0
  36. package/dist/services/config/configReader.multiProject.test.d.ts +1 -0
  37. package/dist/services/config/configReader.multiProject.test.js +136 -0
  38. package/dist/services/config/globalConfigManager.d.ts +30 -0
  39. package/dist/services/config/globalConfigManager.js +216 -0
  40. package/dist/services/config/index.d.ts +13 -0
  41. package/dist/services/config/index.js +13 -0
  42. package/dist/services/config/projectConfigManager.d.ts +41 -0
  43. package/dist/services/config/projectConfigManager.js +181 -0
  44. package/dist/services/config/projectConfigManager.test.d.ts +1 -0
  45. package/dist/services/config/projectConfigManager.test.js +105 -0
  46. package/dist/services/config/testUtils.d.ts +81 -0
  47. package/dist/services/config/testUtils.js +351 -0
  48. package/dist/services/sessionManager.autoApproval.test.js +9 -6
  49. package/dist/services/sessionManager.d.ts +2 -0
  50. package/dist/services/sessionManager.effect.test.js +27 -18
  51. package/dist/services/sessionManager.js +43 -40
  52. package/dist/services/sessionManager.statePersistence.test.js +5 -4
  53. package/dist/services/sessionManager.test.js +71 -49
  54. package/dist/services/shortcutManager.d.ts +0 -1
  55. package/dist/services/shortcutManager.js +5 -16
  56. package/dist/services/shortcutManager.test.js +2 -2
  57. package/dist/services/stateDetector/base.d.ts +1 -0
  58. package/dist/services/stateDetector/claude.d.ts +1 -0
  59. package/dist/services/stateDetector/claude.js +8 -0
  60. package/dist/services/stateDetector/claude.test.js +102 -0
  61. package/dist/services/stateDetector/cline.d.ts +1 -0
  62. package/dist/services/stateDetector/cline.js +3 -0
  63. package/dist/services/stateDetector/codex.d.ts +1 -0
  64. package/dist/services/stateDetector/codex.js +3 -0
  65. package/dist/services/stateDetector/cursor.d.ts +1 -0
  66. package/dist/services/stateDetector/cursor.js +3 -0
  67. package/dist/services/stateDetector/gemini.d.ts +1 -0
  68. package/dist/services/stateDetector/gemini.js +3 -0
  69. package/dist/services/stateDetector/github-copilot.d.ts +1 -0
  70. package/dist/services/stateDetector/github-copilot.js +3 -0
  71. package/dist/services/stateDetector/opencode.d.ts +1 -0
  72. package/dist/services/stateDetector/opencode.js +3 -0
  73. package/dist/services/stateDetector/types.d.ts +1 -0
  74. package/dist/services/worktreeService.d.ts +12 -0
  75. package/dist/services/worktreeService.js +24 -4
  76. package/dist/services/worktreeService.sort.test.js +105 -109
  77. package/dist/services/worktreeService.test.js +5 -5
  78. package/dist/types/index.d.ts +47 -7
  79. package/dist/utils/gitUtils.d.ts +8 -0
  80. package/dist/utils/gitUtils.js +32 -0
  81. package/dist/utils/hookExecutor.js +2 -2
  82. package/dist/utils/hookExecutor.test.js +13 -12
  83. package/dist/utils/mutex.d.ts +1 -0
  84. package/dist/utils/mutex.js +1 -0
  85. package/dist/utils/worktreeUtils.js +3 -2
  86. package/dist/utils/worktreeUtils.test.js +2 -1
  87. package/package.json +7 -7
  88. package/dist/services/configurationManager.d.ts +0 -121
  89. package/dist/services/configurationManager.js +0 -597
  90. /package/dist/{services/configurationManager.effect.test.d.ts → constants/statusIcons.test.d.ts} +0 -0
  91. /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
  92. /package/dist/services/{configurationManager.test.d.ts → config/configEditor.selectPresetOnStart.test.d.ts} +0 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * @fileoverview Test utilities for config module
3
+ *
4
+ * WARNING: This file is intended for TEST USE ONLY.
5
+ * Do not import from production code.
6
+ *
7
+ * These functions provide Effect-based wrappers for testing config operations.
8
+ */
9
+ import { Effect, Either } from 'effect';
10
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
11
+ import { DEFAULT_SHORTCUTS, } from '../../types/index.js';
12
+ import { FileSystemError, ConfigError, ValidationError, } from '../../types/errors.js';
13
+ /**
14
+ * TEST ONLY: Load configuration from file with Effect-based error handling
15
+ *
16
+ * @param configPath - Path to the config file
17
+ * @param legacyShortcutsPath - Path to legacy shortcuts file for migration
18
+ * @returns Effect with ConfigurationData on success, errors on failure
19
+ */
20
+ export function loadConfigEffect(configPath, legacyShortcutsPath) {
21
+ return Effect.try({
22
+ try: () => {
23
+ if (existsSync(configPath)) {
24
+ const configData = readFileSync(configPath, 'utf-8');
25
+ const parsedConfig = JSON.parse(configData);
26
+ return applyDefaults(parsedConfig);
27
+ }
28
+ else {
29
+ const migratedConfig = migrateLegacyShortcutsSync(configPath, legacyShortcutsPath);
30
+ return applyDefaults(migratedConfig || {});
31
+ }
32
+ },
33
+ catch: (error) => {
34
+ if (error instanceof SyntaxError) {
35
+ return new ConfigError({
36
+ configPath,
37
+ reason: 'parse',
38
+ details: String(error),
39
+ });
40
+ }
41
+ return new FileSystemError({
42
+ operation: 'read',
43
+ path: configPath,
44
+ cause: String(error),
45
+ });
46
+ },
47
+ });
48
+ }
49
+ /**
50
+ * Type guard to check if value is a non-null object
51
+ */
52
+ function isObject(value) {
53
+ return value !== null && typeof value === 'object';
54
+ }
55
+ function validationError(constraint, receivedValue) {
56
+ return Either.left(new ValidationError({
57
+ field: 'config',
58
+ constraint,
59
+ receivedValue,
60
+ }));
61
+ }
62
+ function validationSuccess(config) {
63
+ return Either.right(config);
64
+ }
65
+ /**
66
+ * TEST ONLY: Validate configuration structure
67
+ */
68
+ export function validateConfig(config) {
69
+ if (!isObject(config)) {
70
+ return validationError('must be a valid configuration object', config);
71
+ }
72
+ const shortcuts = config['shortcuts'];
73
+ if (shortcuts !== undefined && !isObject(shortcuts)) {
74
+ return validationError('shortcuts must be a valid object', config);
75
+ }
76
+ return validationSuccess(config);
77
+ }
78
+ /**
79
+ * Apply default values to configuration
80
+ */
81
+ function applyDefaults(config) {
82
+ if (!config.shortcuts) {
83
+ config.shortcuts = DEFAULT_SHORTCUTS;
84
+ }
85
+ if (!config.statusHooks) {
86
+ config.statusHooks = {};
87
+ }
88
+ if (!config.worktreeHooks) {
89
+ config.worktreeHooks = {};
90
+ }
91
+ if (!config.worktree) {
92
+ config.worktree = {
93
+ autoDirectory: false,
94
+ copySessionData: true,
95
+ sortByLastSession: false,
96
+ };
97
+ }
98
+ if (!Object.prototype.hasOwnProperty.call(config.worktree, 'copySessionData')) {
99
+ config.worktree.copySessionData = true;
100
+ }
101
+ if (!Object.prototype.hasOwnProperty.call(config.worktree, 'sortByLastSession')) {
102
+ config.worktree.sortByLastSession = false;
103
+ }
104
+ if (!config.autoApproval) {
105
+ config.autoApproval = {
106
+ enabled: false,
107
+ timeout: 30,
108
+ };
109
+ }
110
+ else {
111
+ if (!Object.prototype.hasOwnProperty.call(config.autoApproval, 'enabled')) {
112
+ config.autoApproval.enabled = false;
113
+ }
114
+ if (!Object.prototype.hasOwnProperty.call(config.autoApproval, 'timeout')) {
115
+ config.autoApproval.timeout = 30;
116
+ }
117
+ }
118
+ return config;
119
+ }
120
+ /**
121
+ * Synchronous legacy shortcuts migration helper
122
+ */
123
+ function migrateLegacyShortcutsSync(configPath, legacyShortcutsPath) {
124
+ if (existsSync(legacyShortcutsPath)) {
125
+ try {
126
+ const shortcutsData = readFileSync(legacyShortcutsPath, 'utf-8');
127
+ const shortcuts = JSON.parse(shortcutsData);
128
+ if (shortcuts && typeof shortcuts === 'object') {
129
+ const config = { shortcuts };
130
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
131
+ console.log('Migrated shortcuts from legacy shortcuts.json to config.json');
132
+ return config;
133
+ }
134
+ }
135
+ catch (error) {
136
+ console.error('Failed to migrate legacy shortcuts:', error);
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+ // ============================================================================
142
+ // Test-only helper functions for GlobalConfigManager
143
+ // These functions were moved from GlobalConfigManager class to reduce its API
144
+ // surface while keeping tests functional.
145
+ // ============================================================================
146
+ /**
147
+ * TEST ONLY: Add or update a preset in the config manager
148
+ */
149
+ export function addPreset(configManager, preset) {
150
+ const presets = configManager.getCommandPresets();
151
+ // Replace if exists, otherwise add
152
+ const existingIndex = presets.presets.findIndex(p => p.id === preset.id);
153
+ if (existingIndex >= 0) {
154
+ presets.presets[existingIndex] = preset;
155
+ }
156
+ else {
157
+ presets.presets.push(preset);
158
+ }
159
+ configManager.setCommandPresets(presets);
160
+ }
161
+ /**
162
+ * TEST ONLY: Delete a preset by ID
163
+ */
164
+ export function deletePreset(configManager, id) {
165
+ const presets = configManager.getCommandPresets();
166
+ // Don't delete if it's the last preset
167
+ if (presets.presets.length <= 1) {
168
+ return;
169
+ }
170
+ // Remove the preset
171
+ presets.presets = presets.presets.filter(p => p.id !== id);
172
+ // Update default if needed
173
+ if (presets.defaultPresetId === id && presets.presets.length > 0) {
174
+ presets.defaultPresetId = presets.presets[0].id;
175
+ }
176
+ configManager.setCommandPresets(presets);
177
+ }
178
+ /**
179
+ * TEST ONLY: Set the default preset ID
180
+ */
181
+ export function setDefaultPreset(configManager, id) {
182
+ const presets = configManager.getCommandPresets();
183
+ // Only update if preset exists
184
+ if (presets.presets.some(p => p.id === id)) {
185
+ presets.defaultPresetId = id;
186
+ configManager.setCommandPresets(presets);
187
+ }
188
+ }
189
+ /**
190
+ * TEST ONLY: Save configuration to file with Effect-based error handling
191
+ */
192
+ export function saveConfigEffect(configManager, config, configPath) {
193
+ return Effect.try({
194
+ try: () => {
195
+ configManager.setCommandPresets(config.commandPresets || configManager.getCommandPresets());
196
+ if (config.shortcuts) {
197
+ configManager.setShortcuts(config.shortcuts);
198
+ }
199
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
200
+ },
201
+ catch: (error) => {
202
+ return new FileSystemError({
203
+ operation: 'write',
204
+ path: configPath,
205
+ cause: String(error),
206
+ });
207
+ },
208
+ });
209
+ }
210
+ /**
211
+ * TEST ONLY: Set shortcuts with Effect-based error handling
212
+ */
213
+ export function setShortcutsEffect(configManager, shortcuts, configPath) {
214
+ return Effect.try({
215
+ try: () => {
216
+ configManager.setShortcuts(shortcuts);
217
+ const config = {
218
+ ...{},
219
+ shortcuts,
220
+ commandPresets: configManager.getCommandPresets(),
221
+ };
222
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
223
+ },
224
+ catch: (error) => {
225
+ return new FileSystemError({
226
+ operation: 'write',
227
+ path: configPath,
228
+ cause: String(error),
229
+ });
230
+ },
231
+ });
232
+ }
233
+ /**
234
+ * TEST ONLY: Set command presets with Effect-based error handling
235
+ */
236
+ export function setCommandPresetsEffect(configManager, presets, configPath) {
237
+ return Effect.try({
238
+ try: () => {
239
+ configManager.setCommandPresets(presets);
240
+ const config = {
241
+ ...{},
242
+ commandPresets: presets,
243
+ };
244
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
245
+ },
246
+ catch: (error) => {
247
+ return new FileSystemError({
248
+ operation: 'write',
249
+ path: configPath,
250
+ cause: String(error),
251
+ });
252
+ },
253
+ });
254
+ }
255
+ /**
256
+ * TEST ONLY: Add or update preset with Effect-based error handling
257
+ */
258
+ export function addPresetEffect(configManager, preset, configPath) {
259
+ const presets = configManager.getCommandPresets();
260
+ // Replace if exists, otherwise add
261
+ const existingIndex = presets.presets.findIndex(p => p.id === preset.id);
262
+ if (existingIndex >= 0) {
263
+ presets.presets[existingIndex] = preset;
264
+ }
265
+ else {
266
+ presets.presets.push(preset);
267
+ }
268
+ return setCommandPresetsEffect(configManager, presets, configPath);
269
+ }
270
+ /**
271
+ * TEST ONLY: Delete preset with Effect-based error handling
272
+ */
273
+ export function deletePresetEffect(configManager, id, configPath) {
274
+ const presets = configManager.getCommandPresets();
275
+ // Don't delete if it's the last preset
276
+ if (presets.presets.length <= 1) {
277
+ return Effect.fail(new ValidationError({
278
+ field: 'presetId',
279
+ constraint: 'Cannot delete last preset',
280
+ receivedValue: id,
281
+ }));
282
+ }
283
+ // Remove the preset
284
+ presets.presets = presets.presets.filter(p => p.id !== id);
285
+ // Update default if needed
286
+ if (presets.defaultPresetId === id && presets.presets.length > 0) {
287
+ presets.defaultPresetId = presets.presets[0].id;
288
+ }
289
+ return setCommandPresetsEffect(configManager, presets, configPath);
290
+ }
291
+ /**
292
+ * TEST ONLY: Set default preset with Effect-based error handling
293
+ */
294
+ export function setDefaultPresetEffect(configManager, id, configPath) {
295
+ const presets = configManager.getCommandPresets();
296
+ // Only update if preset exists
297
+ if (!presets.presets.some(p => p.id === id)) {
298
+ return Effect.fail(new ValidationError({
299
+ field: 'presetId',
300
+ constraint: 'Preset not found',
301
+ receivedValue: id,
302
+ }));
303
+ }
304
+ presets.defaultPresetId = id;
305
+ return setCommandPresetsEffect(configManager, presets, configPath);
306
+ }
307
+ /**
308
+ * TEST ONLY: Get the default preset
309
+ */
310
+ export function getDefaultPreset(configManager) {
311
+ const presets = configManager.getCommandPresets();
312
+ const defaultPreset = presets.presets.find(p => p.id === presets.defaultPresetId);
313
+ return defaultPreset || presets.presets[0];
314
+ }
315
+ /**
316
+ * TEST ONLY: Get whether to select preset on start
317
+ */
318
+ export function getSelectPresetOnStart(configManager) {
319
+ const presets = configManager.getCommandPresets();
320
+ return presets.selectPresetOnStart ?? false;
321
+ }
322
+ /**
323
+ * TEST ONLY: Set whether to select preset on start
324
+ */
325
+ export function setSelectPresetOnStart(configManager, enabled) {
326
+ const presets = configManager.getCommandPresets();
327
+ presets.selectPresetOnStart = enabled;
328
+ configManager.setCommandPresets(presets);
329
+ }
330
+ /**
331
+ * TEST ONLY: Get whether auto-approval is enabled
332
+ */
333
+ export function isAutoApprovalEnabled(configManager) {
334
+ const config = configManager.getAutoApprovalConfig();
335
+ return config?.enabled ?? false;
336
+ }
337
+ /**
338
+ * TEST ONLY: Get preset by ID with Either-based error handling
339
+ */
340
+ export function getPresetByIdEffect(configManager, id) {
341
+ const presets = configManager.getCommandPresets();
342
+ const preset = presets.presets.find(p => p.id === id);
343
+ if (!preset) {
344
+ return Either.left(new ValidationError({
345
+ field: 'presetId',
346
+ constraint: 'Preset not found',
347
+ receivedValue: id,
348
+ }));
349
+ }
350
+ return Either.right(preset);
351
+ }
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { EventEmitter } from 'events';
3
3
  import { spawn } from './bunTerminal.js';
4
4
  import { STATE_CHECK_INTERVAL_MS, STATE_PERSISTENCE_DURATION_MS, } from '../constants/statePersistence.js';
5
- import { Effect } from 'effect';
5
+ import { Effect, Either } from 'effect';
6
6
  const detectStateMock = vi.fn();
7
7
  // Create a deferred promise pattern for controllable mock
8
8
  let verifyResolve = null;
@@ -15,10 +15,13 @@ vi.mock('./bunTerminal.js', () => ({
15
15
  }),
16
16
  }));
17
17
  vi.mock('./stateDetector/index.js', () => ({
18
- createStateDetector: () => ({ detectState: detectStateMock }),
18
+ createStateDetector: () => ({
19
+ detectState: detectStateMock,
20
+ detectBackgroundTask: () => false,
21
+ }),
19
22
  }));
20
- vi.mock('./configurationManager.js', () => ({
21
- configurationManager: {
23
+ vi.mock('./config/configReader.js', () => ({
24
+ configReader: {
22
25
  getConfig: vi.fn().mockReturnValue({
23
26
  commands: [
24
27
  {
@@ -30,12 +33,12 @@ vi.mock('./configurationManager.js', () => ({
30
33
  ],
31
34
  defaultCommandId: 'test',
32
35
  }),
33
- getPresetById: vi.fn().mockReturnValue({
36
+ getPresetByIdEffect: vi.fn().mockReturnValue(Either.right({
34
37
  id: 'test',
35
38
  name: 'Test',
36
39
  command: 'test',
37
40
  args: [],
38
- }),
41
+ })),
39
42
  getDefaultPreset: vi.fn().mockReturnValue({
40
43
  id: 'test',
41
44
  name: 'Test',
@@ -8,6 +8,7 @@ export interface SessionCounts {
8
8
  waiting_input: number;
9
9
  pending_auto_approval: number;
10
10
  total: number;
11
+ backgroundTasks: number;
11
12
  }
12
13
  export declare class SessionManager extends EventEmitter implements ISessionManager {
13
14
  sessions: Map<string, Session>;
@@ -15,6 +16,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
15
16
  private busyTimers;
16
17
  private spawn;
17
18
  detectTerminalState(session: Session): SessionState;
19
+ detectBackgroundTask(session: Session): boolean;
18
20
  private getTerminalContent;
19
21
  private handleAutoApproval;
20
22
  private cancelAutoApprovalVerification;
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { Effect, Either } from 'effect';
3
3
  import { spawn } from './bunTerminal.js';
4
4
  import { EventEmitter } from 'events';
5
+ import { ValidationError } from '../types/errors.js';
5
6
  // Mock bunTerminal
6
7
  vi.mock('./bunTerminal.js', () => ({
7
8
  spawn: vi.fn(function () {
@@ -14,10 +15,10 @@ vi.mock('child_process', () => ({
14
15
  execFile: vi.fn(),
15
16
  }));
16
17
  // Mock configuration manager
17
- vi.mock('./configurationManager.js', () => ({
18
- configurationManager: {
18
+ vi.mock('./config/configReader.js', () => ({
19
+ configReader: {
19
20
  getDefaultPreset: vi.fn(),
20
- getPresetById: vi.fn(),
21
+ getPresetByIdEffect: vi.fn(),
21
22
  setWorktreeLastOpened: vi.fn(),
22
23
  getWorktreeLastOpenedTime: vi.fn(),
23
24
  getWorktreeLastOpened: vi.fn(() => ({})),
@@ -83,14 +84,14 @@ describe('SessionManager Effect-based Operations', () => {
83
84
  let sessionManager;
84
85
  let mockPty;
85
86
  let SessionManager;
86
- let configurationManager;
87
+ let configReader;
87
88
  beforeEach(async () => {
88
89
  vi.clearAllMocks();
89
90
  // Dynamically import after mocks are set up
90
91
  const sessionManagerModule = await import('./sessionManager.js');
91
- const configManagerModule = await import('./configurationManager.js');
92
+ const configManagerModule = await import('./config/configReader.js');
92
93
  SessionManager = sessionManagerModule.SessionManager;
93
- configurationManager = configManagerModule.configurationManager;
94
+ configReader = configManagerModule.configReader;
94
95
  sessionManager = new SessionManager();
95
96
  mockPty = new MockPty();
96
97
  });
@@ -100,7 +101,7 @@ describe('SessionManager Effect-based Operations', () => {
100
101
  describe('createSessionWithPreset returning Effect', () => {
101
102
  it('should return Effect that succeeds with Session', async () => {
102
103
  // Setup mock preset
103
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
104
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue({
104
105
  id: '1',
105
106
  name: 'Main',
106
107
  command: 'claude',
@@ -117,9 +118,13 @@ describe('SessionManager Effect-based Operations', () => {
117
118
  expect(session.stateMutex.getSnapshot().state).toBe('busy');
118
119
  });
119
120
  it('should return Effect that fails with ConfigError when preset not found', async () => {
120
- // Setup mocks - both return null/undefined
121
- vi.mocked(configurationManager.getPresetById).mockReturnValue(undefined);
122
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue(undefined);
121
+ // Setup mocks - getPresetByIdEffect returns Left, getDefaultPreset returns undefined
122
+ vi.mocked(configReader.getPresetByIdEffect).mockReturnValue(Either.left(new ValidationError({
123
+ field: 'presetId',
124
+ constraint: 'Preset not found',
125
+ receivedValue: 'invalid-preset',
126
+ })));
127
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue(undefined);
123
128
  // Create session with non-existent preset - should return Effect
124
129
  const effect = sessionManager.createSessionWithPresetEffect('/test/worktree', 'invalid-preset');
125
130
  // Execute the Effect and expect it to fail with ConfigError
@@ -135,7 +140,7 @@ describe('SessionManager Effect-based Operations', () => {
135
140
  });
136
141
  it('should return Effect that fails with ProcessError when spawn fails', async () => {
137
142
  // Setup mock preset
138
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
143
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue({
139
144
  id: '1',
140
145
  name: 'Main',
141
146
  command: 'invalid-command',
@@ -160,7 +165,7 @@ describe('SessionManager Effect-based Operations', () => {
160
165
  });
161
166
  it('should return existing session without creating new Effect', async () => {
162
167
  // Setup mock preset
163
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
168
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue({
164
169
  id: '1',
165
170
  name: 'Main',
166
171
  command: 'claude',
@@ -181,7 +186,7 @@ describe('SessionManager Effect-based Operations', () => {
181
186
  describe('createSessionWithDevcontainer returning Effect', () => {
182
187
  it('should return Effect that succeeds with Session', async () => {
183
188
  // Setup mock preset
184
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
189
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue({
185
190
  id: '1',
186
191
  name: 'Main',
187
192
  command: 'claude',
@@ -244,9 +249,13 @@ describe('SessionManager Effect-based Operations', () => {
244
249
  }
245
250
  });
246
251
  it('should return Effect that fails with ConfigError when preset not found', async () => {
247
- // Setup mocks - both return null/undefined
248
- vi.mocked(configurationManager.getPresetById).mockReturnValue(undefined);
249
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue(undefined);
252
+ // Setup mocks - getPresetByIdEffect returns Left, getDefaultPreset returns undefined
253
+ vi.mocked(configReader.getPresetByIdEffect).mockReturnValue(Either.left(new ValidationError({
254
+ field: 'presetId',
255
+ constraint: 'Preset not found',
256
+ receivedValue: 'invalid-preset',
257
+ })));
258
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue(undefined);
250
259
  // Mock exec to succeed (devcontainer up)
251
260
  const { exec } = await import('child_process');
252
261
  const mockExec = vi.mocked(exec);
@@ -276,7 +285,7 @@ describe('SessionManager Effect-based Operations', () => {
276
285
  describe('terminateSession returning Effect', () => {
277
286
  it('should return Effect that succeeds when session exists', async () => {
278
287
  // Setup mock preset and create a session first
279
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
288
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue({
280
289
  id: '1',
281
290
  name: 'Main',
282
291
  command: 'claude',
@@ -305,7 +314,7 @@ describe('SessionManager Effect-based Operations', () => {
305
314
  });
306
315
  it('should return Effect that succeeds even when process kill fails', async () => {
307
316
  // Setup mock preset and create a session
308
- vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
317
+ vi.mocked(configReader.getDefaultPreset).mockReturnValue({
309
318
  id: '1',
310
319
  name: 'Main',
311
320
  command: 'claude',