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.
- package/README.md +11 -5
- package/dist/components/App.js +17 -3
- package/dist/components/App.test.js +5 -5
- package/dist/components/Configuration.d.ts +2 -0
- package/dist/components/Configuration.js +6 -2
- package/dist/components/ConfigureCommand.js +34 -11
- package/dist/components/ConfigureOther.js +18 -4
- package/dist/components/ConfigureOther.test.js +48 -12
- package/dist/components/ConfigureShortcuts.js +27 -85
- package/dist/components/ConfigureStatusHooks.js +19 -4
- package/dist/components/ConfigureStatusHooks.test.js +46 -12
- package/dist/components/ConfigureWorktree.js +18 -4
- package/dist/components/ConfigureWorktreeHooks.js +19 -4
- package/dist/components/ConfigureWorktreeHooks.test.js +49 -14
- package/dist/components/Menu.js +72 -14
- package/dist/components/Menu.recent-projects.test.js +2 -0
- package/dist/components/Menu.test.js +2 -0
- package/dist/components/NewWorktree.js +2 -2
- package/dist/components/NewWorktree.test.js +6 -6
- package/dist/components/PresetSelector.js +2 -2
- package/dist/constants/statusIcons.d.ts +4 -1
- package/dist/constants/statusIcons.js +10 -1
- package/dist/constants/statusIcons.test.js +42 -0
- package/dist/contexts/ConfigEditorContext.d.ts +21 -0
- package/dist/contexts/ConfigEditorContext.js +25 -0
- package/dist/services/autoApprovalVerifier.js +3 -3
- package/dist/services/autoApprovalVerifier.test.js +2 -2
- package/dist/services/config/configEditor.d.ts +46 -0
- package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js} +46 -49
- package/dist/services/config/configEditor.js +101 -0
- package/dist/services/{configurationManager.selectPresetOnStart.test.js → config/configEditor.selectPresetOnStart.test.js} +27 -19
- package/dist/services/config/configEditor.test.d.ts +1 -0
- package/dist/services/{configurationManager.test.js → config/configEditor.test.js} +60 -132
- package/dist/services/config/configReader.d.ts +28 -0
- package/dist/services/config/configReader.js +95 -0
- package/dist/services/config/configReader.multiProject.test.d.ts +1 -0
- package/dist/services/config/configReader.multiProject.test.js +136 -0
- package/dist/services/config/globalConfigManager.d.ts +30 -0
- package/dist/services/config/globalConfigManager.js +216 -0
- package/dist/services/config/index.d.ts +13 -0
- package/dist/services/config/index.js +13 -0
- package/dist/services/config/projectConfigManager.d.ts +41 -0
- package/dist/services/config/projectConfigManager.js +181 -0
- package/dist/services/config/projectConfigManager.test.d.ts +1 -0
- package/dist/services/config/projectConfigManager.test.js +105 -0
- package/dist/services/config/testUtils.d.ts +81 -0
- package/dist/services/config/testUtils.js +351 -0
- package/dist/services/sessionManager.autoApproval.test.js +9 -6
- package/dist/services/sessionManager.d.ts +2 -0
- package/dist/services/sessionManager.effect.test.js +27 -18
- package/dist/services/sessionManager.js +43 -40
- package/dist/services/sessionManager.statePersistence.test.js +5 -4
- package/dist/services/sessionManager.test.js +71 -49
- package/dist/services/shortcutManager.d.ts +0 -1
- package/dist/services/shortcutManager.js +5 -16
- package/dist/services/shortcutManager.test.js +2 -2
- package/dist/services/stateDetector/base.d.ts +1 -0
- package/dist/services/stateDetector/claude.d.ts +1 -0
- package/dist/services/stateDetector/claude.js +8 -0
- package/dist/services/stateDetector/claude.test.js +102 -0
- package/dist/services/stateDetector/cline.d.ts +1 -0
- package/dist/services/stateDetector/cline.js +3 -0
- package/dist/services/stateDetector/codex.d.ts +1 -0
- package/dist/services/stateDetector/codex.js +3 -0
- package/dist/services/stateDetector/cursor.d.ts +1 -0
- package/dist/services/stateDetector/cursor.js +3 -0
- package/dist/services/stateDetector/gemini.d.ts +1 -0
- package/dist/services/stateDetector/gemini.js +3 -0
- package/dist/services/stateDetector/github-copilot.d.ts +1 -0
- package/dist/services/stateDetector/github-copilot.js +3 -0
- package/dist/services/stateDetector/opencode.d.ts +1 -0
- package/dist/services/stateDetector/opencode.js +3 -0
- package/dist/services/stateDetector/types.d.ts +1 -0
- package/dist/services/worktreeService.d.ts +12 -0
- package/dist/services/worktreeService.js +24 -4
- package/dist/services/worktreeService.sort.test.js +105 -109
- package/dist/services/worktreeService.test.js +5 -5
- package/dist/types/index.d.ts +47 -7
- package/dist/utils/gitUtils.d.ts +8 -0
- package/dist/utils/gitUtils.js +32 -0
- package/dist/utils/hookExecutor.js +2 -2
- package/dist/utils/hookExecutor.test.js +13 -12
- package/dist/utils/mutex.d.ts +1 -0
- package/dist/utils/mutex.js +1 -0
- package/dist/utils/worktreeUtils.js +3 -2
- package/dist/utils/worktreeUtils.test.js +2 -1
- package/package.json +7 -7
- package/dist/services/configurationManager.d.ts +0 -121
- package/dist/services/configurationManager.js +0 -597
- /package/dist/{services/configurationManager.effect.test.d.ts → constants/statusIcons.test.d.ts} +0 -0
- /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
- /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: () => ({
|
|
18
|
+
createStateDetector: () => ({
|
|
19
|
+
detectState: detectStateMock,
|
|
20
|
+
detectBackgroundTask: () => false,
|
|
21
|
+
}),
|
|
19
22
|
}));
|
|
20
|
-
vi.mock('./
|
|
21
|
-
|
|
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
|
-
|
|
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('./
|
|
18
|
-
|
|
18
|
+
vi.mock('./config/configReader.js', () => ({
|
|
19
|
+
configReader: {
|
|
19
20
|
getDefaultPreset: vi.fn(),
|
|
20
|
-
|
|
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
|
|
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('./
|
|
92
|
+
const configManagerModule = await import('./config/configReader.js');
|
|
92
93
|
SessionManager = sessionManagerModule.SessionManager;
|
|
93
|
-
|
|
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(
|
|
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 -
|
|
121
|
-
vi.mocked(
|
|
122
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 -
|
|
248
|
-
vi.mocked(
|
|
249
|
-
|
|
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(
|
|
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(
|
|
317
|
+
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
309
318
|
id: '1',
|
|
310
319
|
name: 'Main',
|
|
311
320
|
command: 'claude',
|