ccmanager 3.9.0 → 3.11.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 (31) hide show
  1. package/dist/components/App.js +159 -44
  2. package/dist/components/App.test.js +96 -5
  3. package/dist/components/Dashboard.d.ts +12 -0
  4. package/dist/components/Dashboard.js +443 -0
  5. package/dist/components/Dashboard.test.js +348 -0
  6. package/dist/components/Menu.recent-projects.test.js +19 -19
  7. package/dist/components/NewWorktree.d.ts +20 -1
  8. package/dist/components/NewWorktree.js +103 -56
  9. package/dist/components/NewWorktree.test.js +17 -4
  10. package/dist/services/globalSessionOrchestrator.d.ts +1 -0
  11. package/dist/services/globalSessionOrchestrator.js +3 -0
  12. package/dist/services/projectManager.d.ts +7 -1
  13. package/dist/services/projectManager.js +26 -10
  14. package/dist/services/sessionManager.d.ts +3 -2
  15. package/dist/services/sessionManager.js +37 -40
  16. package/dist/services/sessionManager.test.js +38 -0
  17. package/dist/services/worktreeNameGenerator.d.ts +8 -0
  18. package/dist/services/worktreeNameGenerator.js +184 -0
  19. package/dist/services/worktreeNameGenerator.test.js +35 -0
  20. package/dist/utils/presetPrompt.d.ts +11 -0
  21. package/dist/utils/presetPrompt.js +71 -0
  22. package/dist/utils/presetPrompt.test.d.ts +1 -0
  23. package/dist/utils/presetPrompt.test.js +167 -0
  24. package/dist/utils/worktreeUtils.d.ts +1 -2
  25. package/package.json +6 -6
  26. package/dist/components/ProjectList.d.ts +0 -10
  27. package/dist/components/ProjectList.js +0 -233
  28. package/dist/components/ProjectList.recent-projects.test.js +0 -193
  29. package/dist/components/ProjectList.test.js +0 -620
  30. /package/dist/components/{ProjectList.recent-projects.test.d.ts → Dashboard.test.d.ts} +0 -0
  31. /package/dist/{components/ProjectList.test.d.ts → services/worktreeNameGenerator.test.d.ts} +0 -0
@@ -1,620 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render } from 'ink-testing-library';
3
- import { describe, it, expect, vi, beforeEach } from 'vitest';
4
- // Mock bunTerminal to avoid native module loading issues
5
- vi.mock('../services/bunTerminal.js', () => ({
6
- spawn: vi.fn(function () {
7
- return null;
8
- }),
9
- }));
10
- // Import the actual component code but skip the useInput hook
11
- vi.mock('ink', async () => {
12
- const actual = await vi.importActual('ink');
13
- return {
14
- ...actual,
15
- useInput: vi.fn(),
16
- };
17
- });
18
- // Mock SelectInput to render items as simple text
19
- vi.mock('ink-select-input', async () => {
20
- const React = await vi.importActual('react');
21
- const { Text, Box } = await vi.importActual('ink');
22
- return {
23
- default: ({ items }) => {
24
- return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
25
- },
26
- };
27
- });
28
- // Mock TextInputWrapper to render as simple text
29
- vi.mock('./TextInputWrapper.js', async () => {
30
- const React = await vi.importActual('react');
31
- const { Text } = await vi.importActual('ink');
32
- return {
33
- default: ({ value, placeholder }) => {
34
- return React.createElement(Text, {}, value || placeholder || '');
35
- },
36
- };
37
- });
38
- // Mock Effect for testing
39
- vi.mock('effect', async () => {
40
- const actual = await vi.importActual('effect');
41
- return actual;
42
- });
43
- // Mock the projectManager
44
- vi.mock('../services/projectManager.js', () => ({
45
- projectManager: {
46
- instance: {
47
- discoverProjectsEffect: vi.fn(),
48
- },
49
- getRecentProjects: vi.fn().mockReturnValue([]),
50
- },
51
- }));
52
- // Now import after mocking
53
- const { default: ProjectList } = await import('./ProjectList.js');
54
- const { projectManager } = await import('../services/projectManager.js');
55
- const { Effect } = await import('effect');
56
- describe('ProjectList', () => {
57
- const mockOnSelectProject = vi.fn();
58
- const mockOnDismissError = vi.fn();
59
- const mockProjects = [
60
- {
61
- name: 'project1',
62
- path: '/projects/project1',
63
- relativePath: 'project1',
64
- isValid: true,
65
- },
66
- {
67
- name: 'project2',
68
- path: '/projects/project2',
69
- relativePath: 'project2',
70
- isValid: true,
71
- },
72
- {
73
- name: 'project3',
74
- path: '/projects/project3',
75
- relativePath: 'project3',
76
- isValid: true,
77
- },
78
- ];
79
- beforeEach(() => {
80
- vi.clearAllMocks();
81
- vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
82
- });
83
- it('should render project list with correct title', () => {
84
- const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
85
- expect(lastFrame()).toContain('CCManager - Multi-Project Mode');
86
- expect(lastFrame()).toContain('Select a project:');
87
- });
88
- it('should display loading state initially', () => {
89
- // Create an Effect that never completes to keep loading state
90
- vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.async(() => { }));
91
- const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
92
- expect(lastFrame()).toContain('Loading projects...');
93
- });
94
- it('should display projects after loading', async () => {
95
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
96
- // Wait a bit for async operations
97
- await new Promise(resolve => setTimeout(resolve, 100));
98
- // Force rerender
99
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
100
- // Wait for SelectInput to render with our mock
101
- await vi.waitFor(() => {
102
- const frame = lastFrame();
103
- return frame && !frame.includes('Loading projects...');
104
- }, { timeout: 2000 });
105
- const frame = lastFrame();
106
- expect(frame).toContain('0 ❯ project1');
107
- expect(frame).toContain('1 ❯ project2');
108
- expect(frame).toContain('2 ❯ project3');
109
- });
110
- it('should display error when provided', () => {
111
- const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Failed to load projects", onDismissError: mockOnDismissError }));
112
- expect(lastFrame()).toContain('Error: Failed to load projects');
113
- expect(lastFrame()).toContain('Press any key to dismiss');
114
- });
115
- it('should handle project selection via menu', async () => {
116
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
117
- // Wait a bit for async operations
118
- await new Promise(resolve => setTimeout(resolve, 100));
119
- // Force rerender
120
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
121
- // Wait for component to update after async loading
122
- await vi.waitFor(() => {
123
- const frame = lastFrame();
124
- return frame && !frame.includes('Loading projects...');
125
- });
126
- // Verify menu structure
127
- const frame = lastFrame();
128
- expect(frame).toContain('0 ❯ project1');
129
- expect(frame).toContain('R');
130
- expect(frame).toContain('Refresh');
131
- expect(frame).toContain('Q');
132
- expect(frame).toContain('Exit');
133
- });
134
- it('should display number shortcuts for first 10 projects', async () => {
135
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
136
- // Wait a bit for async operations
137
- await new Promise(resolve => setTimeout(resolve, 100));
138
- // Force rerender
139
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
140
- // Wait for projects to load
141
- await vi.waitFor(() => {
142
- const frame = lastFrame();
143
- return frame && !frame.includes('Loading projects...');
144
- });
145
- // Verify number prefixes are shown
146
- const frame = lastFrame();
147
- expect(frame).toContain('0 ❯ project1');
148
- expect(frame).toContain('1 ❯ project2');
149
- expect(frame).toContain('2 ❯ project3');
150
- });
151
- it('should display exit option in menu', async () => {
152
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
153
- // Wait a bit for async operations
154
- await new Promise(resolve => setTimeout(resolve, 100));
155
- // Force rerender
156
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
157
- // Wait for projects to load
158
- await vi.waitFor(() => {
159
- const frame = lastFrame();
160
- return frame && !frame.includes('Loading projects...');
161
- });
162
- // Verify exit option is shown
163
- const frame = lastFrame();
164
- expect(frame).toContain('Q');
165
- expect(frame).toContain('Exit');
166
- });
167
- it('should display refresh option in menu', async () => {
168
- const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
169
- // Wait for projects to load
170
- await vi.waitFor(() => {
171
- return lastFrame()?.includes('project1') ?? false;
172
- });
173
- // Verify refresh option is shown
174
- const frame = lastFrame();
175
- expect(frame).toContain('R');
176
- expect(frame).toContain('Refresh');
177
- });
178
- it('should show empty state when no projects found', async () => {
179
- vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed([]));
180
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
181
- // Wait for loading to finish
182
- await new Promise(resolve => setTimeout(resolve, 100));
183
- // Force rerender
184
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
185
- // Wait for projects to load
186
- await vi.waitFor(() => {
187
- const frame = lastFrame();
188
- return frame && !frame.includes('Loading projects...');
189
- }, { timeout: 2000 });
190
- expect(lastFrame()).toContain('No git repositories found in /projects');
191
- });
192
- it('should display error message when error prop is provided', () => {
193
- const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
194
- expect(lastFrame()).toContain('Error: Test error');
195
- expect(lastFrame()).toContain('Press any key to dismiss');
196
- });
197
- describe('search functionality', () => {
198
- it.skip('should enter search mode when "/" key is pressed', async () => {
199
- const mockUseInput = vi.mocked(await import('ink')).useInput;
200
- const inputHandlers = [];
201
- mockUseInput.mockImplementation(handler => {
202
- inputHandlers.push(handler);
203
- });
204
- // Need to set up stdin.setRawMode for the test
205
- const originalSetRawMode = process.stdin.setRawMode;
206
- process.stdin.setRawMode = vi.fn();
207
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
208
- // Wait for projects to load
209
- await vi.waitFor(() => {
210
- return lastFrame()?.includes('project1') ?? false;
211
- });
212
- // Simulate pressing "/" key on all handlers (both from useSearchMode and ProjectList)
213
- inputHandlers.forEach(handler => {
214
- handler('/', {
215
- escape: false,
216
- return: false,
217
- leftArrow: false,
218
- rightArrow: false,
219
- upArrow: false,
220
- downArrow: false,
221
- pageDown: false,
222
- pageUp: false,
223
- ctrl: false,
224
- shift: false,
225
- tab: false,
226
- backspace: false,
227
- delete: false,
228
- meta: false,
229
- home: false,
230
- end: false,
231
- });
232
- });
233
- // Wait a bit for state update
234
- await new Promise(resolve => setTimeout(resolve, 50));
235
- // Force rerender to see updated state
236
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
237
- // Should show search input
238
- expect(lastFrame()).toContain('Search:');
239
- // Restore original
240
- process.stdin.setRawMode = originalSetRawMode;
241
- });
242
- it('should filter projects based on search query', async () => {
243
- const mockUseInput = vi.mocked(await import('ink')).useInput;
244
- let inputHandler = () => { };
245
- mockUseInput.mockImplementation(handler => {
246
- inputHandler = handler;
247
- });
248
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
249
- // Wait for projects to load
250
- await vi.waitFor(() => {
251
- return lastFrame()?.includes('project1') ?? false;
252
- });
253
- // Enter search mode
254
- inputHandler('/', {
255
- escape: false,
256
- return: false,
257
- leftArrow: false,
258
- rightArrow: false,
259
- upArrow: false,
260
- downArrow: false,
261
- pageDown: false,
262
- pageUp: false,
263
- ctrl: false,
264
- shift: false,
265
- tab: false,
266
- backspace: false,
267
- delete: false,
268
- meta: false,
269
- home: false,
270
- end: false,
271
- });
272
- // Force rerender with search active and query
273
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
274
- // Simulate typing "project2" in search
275
- // This would be handled by the TextInput component
276
- // We'll test the filtering logic separately
277
- });
278
- it.skip('should exit search mode but keep filter when ESC is pressed in search mode', async () => {
279
- const mockUseInput = vi.mocked(await import('ink')).useInput;
280
- let inputHandler = () => { };
281
- mockUseInput.mockImplementation(handler => {
282
- inputHandler = handler;
283
- });
284
- // Need to set up stdin.setRawMode for the test
285
- const originalSetRawMode = process.stdin.setRawMode;
286
- process.stdin.setRawMode = vi.fn();
287
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
288
- // Wait for projects to load
289
- await vi.waitFor(() => {
290
- return lastFrame()?.includes('project1') ?? false;
291
- });
292
- // Enter search mode
293
- inputHandler('/', {
294
- escape: false,
295
- return: false,
296
- leftArrow: false,
297
- rightArrow: false,
298
- upArrow: false,
299
- downArrow: false,
300
- pageDown: false,
301
- pageUp: false,
302
- ctrl: false,
303
- shift: false,
304
- tab: false,
305
- backspace: false,
306
- delete: false,
307
- meta: false,
308
- home: false,
309
- end: false,
310
- });
311
- // Wait a bit for state update
312
- await new Promise(resolve => setTimeout(resolve, 50));
313
- // Force rerender
314
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
315
- // Should be in search mode
316
- expect(lastFrame()).toContain('Search:');
317
- // Press ESC
318
- inputHandler('', {
319
- escape: true,
320
- return: false,
321
- leftArrow: false,
322
- rightArrow: false,
323
- upArrow: false,
324
- downArrow: false,
325
- pageDown: false,
326
- pageUp: false,
327
- ctrl: false,
328
- shift: false,
329
- tab: false,
330
- backspace: false,
331
- delete: false,
332
- meta: false,
333
- home: false,
334
- end: false,
335
- });
336
- // Wait a bit for state update
337
- await new Promise(resolve => setTimeout(resolve, 50));
338
- // Force rerender
339
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
340
- // Should exit search mode
341
- expect(lastFrame()).not.toContain('Search:');
342
- // Restore original
343
- process.stdin.setRawMode = originalSetRawMode;
344
- });
345
- it('should not enter search mode when "/" is pressed during error display', async () => {
346
- const mockUseInput = vi.mocked(await import('ink')).useInput;
347
- let inputHandler = () => { };
348
- mockUseInput.mockImplementation(handler => {
349
- inputHandler = handler;
350
- });
351
- // Need to set up stdin.setRawMode for the test
352
- const originalSetRawMode = process.stdin.setRawMode;
353
- process.stdin.setRawMode = vi.fn();
354
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
355
- // Press "/" key
356
- inputHandler('/', {
357
- escape: false,
358
- return: false,
359
- leftArrow: false,
360
- rightArrow: false,
361
- upArrow: false,
362
- downArrow: false,
363
- pageDown: false,
364
- pageUp: false,
365
- ctrl: false,
366
- shift: false,
367
- tab: false,
368
- backspace: false,
369
- delete: false,
370
- meta: false,
371
- home: false,
372
- end: false,
373
- });
374
- // Wait a bit for state update
375
- await new Promise(resolve => setTimeout(resolve, 50));
376
- // Force rerender
377
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
378
- // Should not show search input, should dismiss error instead
379
- expect(lastFrame()).not.toContain('Search:');
380
- expect(mockOnDismissError).toHaveBeenCalled();
381
- // Restore original
382
- process.stdin.setRawMode = originalSetRawMode;
383
- });
384
- it.skip('should exit search mode when Enter is pressed but keep filter', async () => {
385
- const mockUseInput = vi.mocked(await import('ink')).useInput;
386
- let inputHandler = () => { };
387
- mockUseInput.mockImplementation(handler => {
388
- inputHandler = handler;
389
- });
390
- // Need to set up stdin.setRawMode for the test
391
- const originalSetRawMode = process.stdin.setRawMode;
392
- process.stdin.setRawMode = vi.fn();
393
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
394
- // Wait for projects to load
395
- await vi.waitFor(() => {
396
- return lastFrame()?.includes('project1') ?? false;
397
- });
398
- // Enter search mode
399
- inputHandler('/', {
400
- escape: false,
401
- return: false,
402
- leftArrow: false,
403
- rightArrow: false,
404
- upArrow: false,
405
- downArrow: false,
406
- pageDown: false,
407
- pageUp: false,
408
- ctrl: false,
409
- shift: false,
410
- tab: false,
411
- backspace: false,
412
- delete: false,
413
- meta: false,
414
- home: false,
415
- end: false,
416
- });
417
- // Wait a bit for state update
418
- await new Promise(resolve => setTimeout(resolve, 50));
419
- // Force rerender
420
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
421
- // Should be in search mode
422
- expect(lastFrame()).toContain('Search:');
423
- // Press Enter
424
- inputHandler('', {
425
- escape: false,
426
- return: true,
427
- leftArrow: false,
428
- rightArrow: false,
429
- upArrow: false,
430
- downArrow: false,
431
- pageDown: false,
432
- pageUp: false,
433
- ctrl: false,
434
- shift: false,
435
- tab: false,
436
- backspace: false,
437
- delete: false,
438
- meta: false,
439
- home: false,
440
- end: false,
441
- });
442
- // Wait a bit for state update
443
- await new Promise(resolve => setTimeout(resolve, 50));
444
- // Force rerender
445
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
446
- // Should exit search mode
447
- expect(lastFrame()).not.toContain('Search:');
448
- // Should not have called onSelectProject
449
- expect(mockOnSelectProject).not.toHaveBeenCalled();
450
- // Restore original
451
- process.stdin.setRawMode = originalSetRawMode;
452
- });
453
- it('should clear filter when ESC is pressed outside search mode', async () => {
454
- const mockUseInput = vi.mocked(await import('ink')).useInput;
455
- let inputHandler = () => { };
456
- mockUseInput.mockImplementation(handler => {
457
- inputHandler = handler;
458
- });
459
- // Need to set up stdin.setRawMode for the test
460
- const originalSetRawMode = process.stdin.setRawMode;
461
- process.stdin.setRawMode = vi.fn();
462
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
463
- // Wait for projects to load
464
- await vi.waitFor(() => {
465
- return lastFrame()?.includes('project1') ?? false;
466
- });
467
- // Enter search mode
468
- inputHandler('/', {
469
- escape: false,
470
- return: false,
471
- leftArrow: false,
472
- rightArrow: false,
473
- upArrow: false,
474
- downArrow: false,
475
- pageDown: false,
476
- pageUp: false,
477
- ctrl: false,
478
- shift: false,
479
- tab: false,
480
- backspace: false,
481
- delete: false,
482
- meta: false,
483
- home: false,
484
- end: false,
485
- });
486
- await new Promise(resolve => setTimeout(resolve, 50));
487
- // Exit search mode with Enter (keeping filter)
488
- inputHandler('', {
489
- escape: false,
490
- return: true,
491
- leftArrow: false,
492
- rightArrow: false,
493
- upArrow: false,
494
- downArrow: false,
495
- pageDown: false,
496
- pageUp: false,
497
- ctrl: false,
498
- shift: false,
499
- tab: false,
500
- backspace: false,
501
- delete: false,
502
- meta: false,
503
- home: false,
504
- end: false,
505
- });
506
- await new Promise(resolve => setTimeout(resolve, 50));
507
- // Now press ESC outside search mode to clear filter
508
- inputHandler('', {
509
- escape: true,
510
- return: false,
511
- leftArrow: false,
512
- rightArrow: false,
513
- upArrow: false,
514
- downArrow: false,
515
- pageDown: false,
516
- pageUp: false,
517
- ctrl: false,
518
- shift: false,
519
- tab: false,
520
- backspace: false,
521
- delete: false,
522
- meta: false,
523
- home: false,
524
- end: false,
525
- });
526
- await new Promise(resolve => setTimeout(resolve, 50));
527
- // Force rerender
528
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
529
- // Should display all projects (filter cleared)
530
- expect(lastFrame()).toContain('project1');
531
- expect(lastFrame()).toContain('project2');
532
- expect(lastFrame()).toContain('project3');
533
- // Restore original
534
- process.stdin.setRawMode = originalSetRawMode;
535
- });
536
- });
537
- describe('Effect-based Project Discovery Error Handling', () => {
538
- it('should handle FileSystemError from discoverProjectsEffect gracefully', async () => {
539
- const { FileSystemError } = await import('../types/errors.js');
540
- // Mock discoverProjectsEffect to return a failed Effect with FileSystemError
541
- const fileSystemError = new FileSystemError({
542
- operation: 'read',
543
- path: '/projects',
544
- cause: 'Directory not accessible',
545
- });
546
- vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.fail(fileSystemError));
547
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
548
- // Wait for loading to finish
549
- await new Promise(resolve => setTimeout(resolve, 100));
550
- // Force rerender
551
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
552
- // Wait for projects to attempt loading
553
- await vi.waitFor(() => {
554
- const frame = lastFrame();
555
- return frame && !frame.includes('Loading projects...');
556
- }, { timeout: 2000 });
557
- // Should display error message with FileSystemError details
558
- const frame = lastFrame();
559
- expect(frame).toContain('Error:');
560
- });
561
- it.skip('should handle GitError from project validation failures', async () => {
562
- const { GitError } = await import('../types/errors.js');
563
- // Mock discoverProjectsEffect to return a failed Effect with GitError
564
- const gitError = new GitError({
565
- command: 'git rev-parse --show-toplevel',
566
- exitCode: 128,
567
- stderr: 'Not a git repository',
568
- });
569
- vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(
570
- // @ts-expect-error - Test uses wrong error type (should be FileSystemError)
571
- Effect.fail(gitError));
572
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
573
- // Wait for projects to attempt loading
574
- await vi.waitFor(() => {
575
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
576
- return !lastFrame()?.includes('Loading projects...');
577
- });
578
- // Should display error message
579
- const frame = lastFrame();
580
- expect(frame).toContain('Error:');
581
- });
582
- it('should implement cancellation flag for cleanup on unmount', async () => {
583
- vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.async(emit => {
584
- const timeout = setTimeout(() => {
585
- emit(Effect.succeed(mockProjects));
586
- }, 500);
587
- return Effect.sync(() => clearTimeout(timeout));
588
- }));
589
- const { unmount, lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
590
- // Wait a bit to ensure promise is pending
591
- await new Promise(resolve => setTimeout(resolve, 100));
592
- // Component should still be loading
593
- expect(lastFrame()).toContain('Loading projects...');
594
- // Unmount before promise resolves
595
- unmount();
596
- // Wait for promise to potentially resolve
597
- await new Promise(resolve => setTimeout(resolve, 500));
598
- // Component is unmounted, no state updates should occur
599
- // This test verifies the cancellation flag prevents state updates after unmount
600
- });
601
- it('should successfully load projects using Effect execution', async () => {
602
- vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
603
- const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
604
- // Wait for loading to finish
605
- await new Promise(resolve => setTimeout(resolve, 100));
606
- // Force rerender
607
- rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
608
- // Wait for projects to load
609
- await vi.waitFor(() => {
610
- const frame = lastFrame();
611
- return frame && frame.includes('project1');
612
- }, { timeout: 2000 });
613
- // Should display loaded projects
614
- const frame = lastFrame();
615
- expect(frame).toContain('0 ❯ project1');
616
- expect(frame).toContain('1 ❯ project2');
617
- expect(frame).toContain('2 ❯ project3');
618
- });
619
- });
620
- });