pnpm-catalog-updates 1.0.3 → 1.1.2

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 (51) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +22031 -10684
  3. package/dist/index.js.map +1 -1
  4. package/package.json +7 -2
  5. package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
  6. package/src/cli/commandRegistrar.ts +785 -0
  7. package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
  8. package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
  9. package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
  10. package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
  11. package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
  12. package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
  13. package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
  14. package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
  15. package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
  16. package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
  17. package/src/cli/commands/aiCommand.ts +163 -0
  18. package/src/cli/commands/analyzeCommand.ts +219 -0
  19. package/src/cli/commands/checkCommand.ts +91 -98
  20. package/src/cli/commands/graphCommand.ts +475 -0
  21. package/src/cli/commands/initCommand.ts +64 -54
  22. package/src/cli/commands/rollbackCommand.ts +334 -0
  23. package/src/cli/commands/securityCommand.ts +165 -100
  24. package/src/cli/commands/themeCommand.ts +148 -0
  25. package/src/cli/commands/updateCommand.ts +215 -263
  26. package/src/cli/commands/workspaceCommand.ts +73 -0
  27. package/src/cli/constants/cliChoices.ts +93 -0
  28. package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
  29. package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
  30. package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
  31. package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
  32. package/src/cli/formatters/ciFormatter.ts +964 -0
  33. package/src/cli/formatters/colorUtils.ts +145 -0
  34. package/src/cli/formatters/outputFormatter.ts +615 -332
  35. package/src/cli/formatters/progressBar.ts +43 -52
  36. package/src/cli/formatters/versionFormatter.ts +132 -0
  37. package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
  38. package/src/cli/handlers/changelogHandler.ts +113 -0
  39. package/src/cli/handlers/index.ts +9 -0
  40. package/src/cli/handlers/installHandler.ts +130 -0
  41. package/src/cli/index.ts +175 -726
  42. package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
  43. package/src/cli/interactive/interactivePrompts.ts +189 -83
  44. package/src/cli/interactive/optionUtils.ts +89 -0
  45. package/src/cli/themes/colorTheme.ts +43 -16
  46. package/src/cli/utils/cliOutput.ts +118 -0
  47. package/src/cli/utils/commandHelpers.ts +249 -0
  48. package/src/cli/validators/commandValidator.ts +321 -336
  49. package/src/cli/validators/index.ts +37 -2
  50. package/src/cli/options/globalOptions.ts +0 -437
  51. package/src/cli/options/index.ts +0 -5
@@ -0,0 +1,720 @@
1
+ /**
2
+ * Update Command Tests
3
+ */
4
+
5
+ import type {
6
+ CatalogUpdateService,
7
+ IPackageManagerService,
8
+ UpdatePlan,
9
+ UpdateResult,
10
+ WorkspaceService,
11
+ } from '@pcu/core'
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
13
+ import { createMockCliOutput, resetCliOutput, setCliOutput } from '../../utils/cliOutput.js'
14
+
15
+ // Use vi.hoisted to ensure mocks are available during vi.mock hoisting
16
+ const mocks = vi.hoisted(() => {
17
+ // Create a chainable chalk mock that supports all color combinations
18
+ const createChalkMock = () => {
19
+ const createColorFn = (text: string) => text
20
+ // Create a function that returns itself for chaining
21
+ const chainableFn = Object.assign(createColorFn, {
22
+ bold: Object.assign((text: string) => text, {
23
+ cyan: (text: string) => text,
24
+ white: (text: string) => text,
25
+ red: (text: string) => text,
26
+ green: (text: string) => text,
27
+ yellow: (text: string) => text,
28
+ blue: (text: string) => text,
29
+ }),
30
+ dim: Object.assign((text: string) => text, {
31
+ white: (text: string) => text,
32
+ }),
33
+ red: Object.assign((text: string) => text, {
34
+ bold: (text: string) => text,
35
+ }),
36
+ green: Object.assign((text: string) => text, {
37
+ bold: (text: string) => text,
38
+ }),
39
+ yellow: Object.assign((text: string) => text, {
40
+ bold: (text: string) => text,
41
+ }),
42
+ blue: Object.assign((text: string) => text, {
43
+ bold: (text: string) => text,
44
+ }),
45
+ cyan: Object.assign((text: string) => text, {
46
+ bold: (text: string) => text,
47
+ }),
48
+ gray: (text: string) => text,
49
+ white: (text: string) => text,
50
+ })
51
+ return chainableFn
52
+ }
53
+
54
+ return {
55
+ loadConfig: vi.fn(),
56
+ selectPackages: vi.fn(),
57
+ analyzeWithChunking: vi.fn(),
58
+ getWorkspaceInfo: vi.fn(),
59
+ formatUpdatePlan: vi.fn().mockReturnValue('Formatted plan'),
60
+ formatUpdateResult: vi.fn().mockReturnValue('Formatted result'),
61
+ // Package manager service mocks
62
+ packageManagerInstall: vi.fn(),
63
+ packageManagerGetName: vi.fn().mockReturnValue('pnpm'),
64
+ createChalkMock,
65
+ }
66
+ })
67
+
68
+ // Mock @pcu/utils
69
+ vi.mock('@pcu/utils', () => ({
70
+ logger: {
71
+ error: vi.fn(),
72
+ warn: vi.fn(),
73
+ info: vi.fn(),
74
+ debug: vi.fn(),
75
+ },
76
+ Logger: {
77
+ setGlobalLevel: vi.fn(),
78
+ },
79
+ ConfigLoader: {
80
+ loadConfig: mocks.loadConfig,
81
+ },
82
+ t: (key: string, params?: Record<string, unknown>) => {
83
+ if (params) {
84
+ let result = key
85
+ for (const [k, v] of Object.entries(params)) {
86
+ result = result.replace(`{{${k}}}`, String(v))
87
+ }
88
+ return result
89
+ }
90
+ return key
91
+ },
92
+ // Include async utilities that are used by the code
93
+ timeout: vi.fn().mockImplementation((promise: Promise<unknown>) => promise),
94
+ delay: vi.fn().mockResolvedValue(undefined),
95
+ retry: vi.fn().mockImplementation((fn: () => Promise<unknown>) => fn()),
96
+ // Include validation utilities
97
+ createValidationResult: (isValid = true, errors: string[] = [], warnings: string[] = []) => ({
98
+ isValid,
99
+ errors,
100
+ warnings,
101
+ }),
102
+ }))
103
+
104
+ // Mock OutputFormatter - needs to be a proper class constructor
105
+ vi.mock('../../formatters/outputFormatter.js', () => {
106
+ return {
107
+ OutputFormatter: class MockOutputFormatter {
108
+ formatUpdatePlan = mocks.formatUpdatePlan
109
+ formatUpdateResult = mocks.formatUpdateResult
110
+ formatOutdatedReport = vi.fn()
111
+ formatSecurityReport = vi.fn()
112
+ formatImpactAnalysis = vi.fn()
113
+ },
114
+ }
115
+ })
116
+
117
+ // Mock ProgressBar - needs to be a proper class constructor
118
+ vi.mock('../../formatters/progressBar.js', () => ({
119
+ ProgressBar: class MockProgressBar {
120
+ start = vi.fn()
121
+ update = vi.fn()
122
+ stop = vi.fn()
123
+ succeed = vi.fn()
124
+ fail = vi.fn()
125
+ warn = vi.fn()
126
+ },
127
+ }))
128
+
129
+ // Mock ThemeManager and StyledText
130
+ vi.mock('../../themes/colorTheme.js', () => ({
131
+ ThemeManager: {
132
+ setTheme: vi.fn(),
133
+ getTheme: vi.fn().mockReturnValue({
134
+ major: (text: string) => text,
135
+ minor: (text: string) => text,
136
+ patch: (text: string) => text,
137
+ }),
138
+ },
139
+ StyledText: {
140
+ iconAnalysis: (text: string) => `[analysis]${text}`,
141
+ iconSuccess: (text: string) => `[success]${text}`,
142
+ iconInfo: (text: string) => `[info]${text}`,
143
+ iconError: (text: string) => `[error]${text}`,
144
+ iconWarning: (text: string) => `[warning]${text}`,
145
+ iconUpdate: (text: string) => `[update]${text}`,
146
+ iconPackage: (text: string) => `[package]${text}`,
147
+ iconComplete: (text: string) => `[complete]${text}`,
148
+ muted: (text: string) => `[muted]${text}`,
149
+ error: (text: string) => `[error]${text}`,
150
+ },
151
+ }))
152
+
153
+ // Mock chalk with chainable functions from hoisted mock
154
+ vi.mock('chalk', () => ({
155
+ default: mocks.createChalkMock(),
156
+ }))
157
+
158
+ // Mock interactive prompts
159
+ vi.mock('../../interactive/interactivePrompts.js', () => {
160
+ const InteractivePromptsMock = vi.fn(function (this: Record<string, unknown>) {
161
+ this.selectPackages = mocks.selectPackages
162
+ })
163
+ return { InteractivePrompts: InteractivePromptsMock }
164
+ })
165
+
166
+ // Mock @pcu/core - use class syntax for proper constructor mocking
167
+ vi.mock('@pcu/core', async (importOriginal) => {
168
+ const actual = await importOriginal()
169
+
170
+ const AIAnalysisServiceMock = vi.fn(function (this: Record<string, unknown>) {
171
+ this.analyzeWithChunking = mocks.analyzeWithChunking
172
+ })
173
+
174
+ const WorkspaceServiceMock = vi.fn(function (this: Record<string, unknown>) {
175
+ this.getWorkspaceInfo = mocks.getWorkspaceInfo
176
+ })
177
+
178
+ const FileSystemServiceMock = vi.fn()
179
+ const FileWorkspaceRepositoryMock = vi.fn()
180
+
181
+ return {
182
+ ...(actual as object),
183
+ AIAnalysisService: AIAnalysisServiceMock,
184
+ WorkspaceService: WorkspaceServiceMock,
185
+ FileSystemService: FileSystemServiceMock,
186
+ FileWorkspaceRepository: FileWorkspaceRepositoryMock,
187
+ }
188
+ })
189
+
190
+ // Import after mock setup
191
+ const { UpdateCommand } = await import('../updateCommand.js')
192
+
193
+ describe('UpdateCommand', () => {
194
+ let command: InstanceType<typeof UpdateCommand>
195
+ let mockCatalogUpdateService: CatalogUpdateService
196
+ let mockWorkspaceService: WorkspaceService
197
+ let mockPackageManagerService: IPackageManagerService
198
+ // Use cliOutput mock instead of console spies for better testability
199
+ let cliMock: ReturnType<typeof createMockCliOutput>
200
+
201
+ const mockUpdatePlan: UpdatePlan = {
202
+ totalUpdates: 2,
203
+ updates: [
204
+ {
205
+ packageName: 'lodash',
206
+ catalogName: 'default',
207
+ currentVersion: '4.17.20',
208
+ newVersion: '4.17.21',
209
+ updateType: 'patch',
210
+ },
211
+ {
212
+ packageName: 'typescript',
213
+ catalogName: 'default',
214
+ currentVersion: '5.0.0',
215
+ newVersion: '5.3.0',
216
+ updateType: 'minor',
217
+ },
218
+ ],
219
+ hasSecurityUpdates: false,
220
+ hasMajorUpdates: false,
221
+ hasMinorUpdates: true,
222
+ hasPatchUpdates: true,
223
+ }
224
+
225
+ const mockUpdateResult: UpdateResult = {
226
+ success: true,
227
+ appliedUpdates: mockUpdatePlan.updates,
228
+ failedUpdates: [],
229
+ backupPath: null,
230
+ errors: [],
231
+ }
232
+
233
+ beforeEach(() => {
234
+ vi.clearAllMocks()
235
+
236
+ // Set up ConfigLoader mock
237
+ mocks.loadConfig.mockReturnValue({
238
+ defaults: {
239
+ target: 'latest',
240
+ format: 'table',
241
+ interactive: false,
242
+ dryRun: false,
243
+ createBackup: false,
244
+ },
245
+ include: [],
246
+ exclude: [],
247
+ })
248
+
249
+ // Set up default mock return values
250
+ mocks.selectPackages.mockResolvedValue(['lodash', 'typescript'])
251
+ mocks.analyzeWithChunking.mockResolvedValue({
252
+ provider: 'rule-engine',
253
+ confidence: 0.85,
254
+ summary: 'Test analysis summary',
255
+ recommendations: [],
256
+ processingTimeMs: 100,
257
+ })
258
+ mocks.getWorkspaceInfo.mockResolvedValue({
259
+ path: '/test/workspace',
260
+ name: 'test-workspace',
261
+ packageCount: 5,
262
+ catalogCount: 2,
263
+ })
264
+
265
+ // Use cliOutput mock instead of console spies
266
+ cliMock = createMockCliOutput()
267
+ setCliOutput(cliMock.mock)
268
+
269
+ // Create mock catalog update service
270
+ mockCatalogUpdateService = {
271
+ planUpdates: vi.fn().mockResolvedValue(mockUpdatePlan),
272
+ executeUpdates: vi.fn().mockResolvedValue(mockUpdateResult),
273
+ checkOutdatedDependencies: vi.fn(),
274
+ findCatalogForPackage: vi.fn(),
275
+ analyzeImpact: vi.fn(),
276
+ } as unknown as CatalogUpdateService
277
+
278
+ // Create mock workspace service
279
+ mockWorkspaceService = {
280
+ getWorkspaceInfo: mocks.getWorkspaceInfo,
281
+ discoverWorkspace: vi.fn(),
282
+ validateWorkspace: vi.fn(),
283
+ getCatalogs: vi.fn(),
284
+ getPackages: vi.fn(),
285
+ usesCatalogs: vi.fn(),
286
+ getPackagesUsingCatalog: vi.fn(),
287
+ getWorkspaceStats: vi.fn(),
288
+ findWorkspaces: vi.fn(),
289
+ checkHealth: vi.fn(),
290
+ } as unknown as WorkspaceService
291
+
292
+ // Create mock package manager service
293
+ mocks.packageManagerInstall.mockResolvedValue({
294
+ success: true,
295
+ code: 0,
296
+ stdout: '',
297
+ stderr: '',
298
+ })
299
+ mockPackageManagerService = {
300
+ install: mocks.packageManagerInstall,
301
+ getName: mocks.packageManagerGetName,
302
+ } as unknown as IPackageManagerService
303
+
304
+ command = new UpdateCommand(
305
+ mockCatalogUpdateService,
306
+ mockWorkspaceService,
307
+ mockPackageManagerService
308
+ )
309
+ })
310
+
311
+ afterEach(() => {
312
+ // Reset cliOutput to default implementation
313
+ resetCliOutput()
314
+ vi.resetAllMocks()
315
+ })
316
+
317
+ describe('execute', () => {
318
+ it('should plan and execute updates', async () => {
319
+ await command.execute({})
320
+
321
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalled()
322
+ expect(mockCatalogUpdateService.executeUpdates).toHaveBeenCalled()
323
+ expect(cliMock.prints.length).toBeGreaterThan(0)
324
+ })
325
+
326
+ it('should use specified workspace path', async () => {
327
+ await command.execute({ workspace: '/custom/workspace' })
328
+
329
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalledWith(
330
+ expect.objectContaining({
331
+ workspacePath: '/custom/workspace',
332
+ })
333
+ )
334
+ })
335
+
336
+ it('should filter by catalog name', async () => {
337
+ await command.execute({ catalog: 'react17' })
338
+
339
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalledWith(
340
+ expect.objectContaining({
341
+ catalogName: 'react17',
342
+ })
343
+ )
344
+ })
345
+
346
+ it('should use specified target', async () => {
347
+ await command.execute({ target: 'minor' })
348
+
349
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalledWith(
350
+ expect.objectContaining({
351
+ target: 'minor',
352
+ })
353
+ )
354
+ })
355
+
356
+ it('should not execute updates in dry-run mode', async () => {
357
+ await command.execute({ dryRun: true })
358
+
359
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalled()
360
+ expect(mockCatalogUpdateService.executeUpdates).not.toHaveBeenCalled()
361
+ })
362
+
363
+ it('should show message when no updates found', async () => {
364
+ mockCatalogUpdateService.planUpdates = vi.fn().mockResolvedValue({
365
+ totalUpdates: 0,
366
+ updates: [],
367
+ hasSecurityUpdates: false,
368
+ hasMajorUpdates: false,
369
+ hasMinorUpdates: false,
370
+ hasPatchUpdates: false,
371
+ })
372
+
373
+ await command.execute({})
374
+
375
+ expect(cliMock.prints.length).toBeGreaterThan(0)
376
+ expect(mockCatalogUpdateService.executeUpdates).not.toHaveBeenCalled()
377
+ })
378
+
379
+ it('should use interactive selection when interactive mode is enabled', async () => {
380
+ mocks.selectPackages.mockResolvedValue(['lodash'])
381
+
382
+ await command.execute({ interactive: true })
383
+
384
+ expect(mocks.selectPackages).toHaveBeenCalled()
385
+ expect(mockCatalogUpdateService.executeUpdates).toHaveBeenCalled()
386
+ })
387
+
388
+ it('should not execute when no packages selected in interactive mode', async () => {
389
+ mocks.selectPackages.mockResolvedValue([])
390
+
391
+ await command.execute({ interactive: true })
392
+
393
+ expect(mockCatalogUpdateService.executeUpdates).not.toHaveBeenCalled()
394
+ })
395
+
396
+ it('should perform AI analysis when ai option is enabled', async () => {
397
+ await command.execute({ ai: true })
398
+
399
+ expect(mocks.analyzeWithChunking).toHaveBeenCalled()
400
+ expect(cliMock.prints.length).toBeGreaterThan(0)
401
+ })
402
+
403
+ it('should continue without AI when analysis fails', async () => {
404
+ mocks.analyzeWithChunking.mockRejectedValue(new Error('AI analysis failed'))
405
+
406
+ await command.execute({ ai: true })
407
+
408
+ expect(cliMock.warns.length).toBeGreaterThan(0)
409
+ expect(mockCatalogUpdateService.executeUpdates).toHaveBeenCalled()
410
+ })
411
+
412
+ it('should handle errors gracefully', async () => {
413
+ mockCatalogUpdateService.planUpdates = vi.fn().mockRejectedValue(new Error('Plan failed'))
414
+
415
+ await expect(command.execute({})).rejects.toThrow('Plan failed')
416
+ expect(cliMock.errors.length).toBeGreaterThan(0)
417
+ })
418
+
419
+ it('should apply include patterns', async () => {
420
+ await command.execute({ include: ['lodash', 'react*'] })
421
+
422
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalledWith(
423
+ expect.objectContaining({
424
+ include: ['lodash', 'react*'],
425
+ })
426
+ )
427
+ })
428
+
429
+ it('should apply exclude patterns', async () => {
430
+ await command.execute({ exclude: ['@types/*'] })
431
+
432
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalledWith(
433
+ expect.objectContaining({
434
+ exclude: ['@types/*'],
435
+ })
436
+ )
437
+ })
438
+
439
+ it('should create backup when specified', async () => {
440
+ await command.execute({ createBackup: true })
441
+
442
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalledWith(
443
+ expect.objectContaining({
444
+ createBackup: true,
445
+ })
446
+ )
447
+ })
448
+ })
449
+
450
+ describe('validateOptions', () => {
451
+ it('should return no errors for valid options', () => {
452
+ const errors = UpdateCommand.validateOptions({
453
+ format: 'json',
454
+ target: 'minor',
455
+ })
456
+
457
+ expect(errors).toHaveLength(0)
458
+ })
459
+
460
+ it('should return error for invalid format', () => {
461
+ const errors = UpdateCommand.validateOptions({
462
+ format: 'invalid' as never,
463
+ })
464
+
465
+ expect(errors).toContain('validation.invalidFormat')
466
+ })
467
+
468
+ it('should return error for invalid target', () => {
469
+ const errors = UpdateCommand.validateOptions({
470
+ target: 'invalid' as never,
471
+ })
472
+
473
+ expect(errors).toContain('validation.invalidTarget')
474
+ })
475
+
476
+ it('should return error when interactive and dry-run are both set', () => {
477
+ const errors = UpdateCommand.validateOptions({
478
+ interactive: true,
479
+ dryRun: true,
480
+ })
481
+
482
+ expect(errors).toContain('validation.interactiveWithDryRun')
483
+ })
484
+ })
485
+
486
+ describe('getHelpText', () => {
487
+ it('should return help text with all options', () => {
488
+ const helpText = UpdateCommand.getHelpText()
489
+
490
+ expect(helpText).toContain('Update catalog dependencies')
491
+ expect(helpText).toContain('--workspace')
492
+ expect(helpText).toContain('--catalog')
493
+ expect(helpText).toContain('--format')
494
+ expect(helpText).toContain('--target')
495
+ expect(helpText).toContain('--interactive')
496
+ expect(helpText).toContain('--dry-run')
497
+ expect(helpText).toContain('--force')
498
+ expect(helpText).toContain('--include')
499
+ expect(helpText).toContain('--exclude')
500
+ expect(helpText).toContain('--create-backup')
501
+ })
502
+ })
503
+
504
+ // TEST-001: Boundary condition and error path tests
505
+ describe('boundary conditions and error handling', () => {
506
+ it('should handle network timeout during plan', async () => {
507
+ const timeoutError = new Error('ETIMEDOUT: Connection timed out')
508
+ timeoutError.name = 'TimeoutError'
509
+ mockCatalogUpdateService.planUpdates = vi.fn().mockRejectedValue(timeoutError)
510
+
511
+ await expect(command.execute({})).rejects.toThrow('ETIMEDOUT')
512
+ expect(cliMock.errors.length).toBeGreaterThan(0)
513
+ })
514
+
515
+ it('should handle network connection refused', async () => {
516
+ const connectionError = new Error('ECONNREFUSED: Connection refused')
517
+ mockCatalogUpdateService.planUpdates = vi.fn().mockRejectedValue(connectionError)
518
+
519
+ await expect(command.execute({})).rejects.toThrow('ECONNREFUSED')
520
+ expect(cliMock.errors.length).toBeGreaterThan(0)
521
+ })
522
+
523
+ it('should handle empty workspace path gracefully', async () => {
524
+ await command.execute({ workspace: '' })
525
+
526
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalledWith(
527
+ expect.objectContaining({
528
+ workspacePath: '',
529
+ })
530
+ )
531
+ })
532
+
533
+ it('should handle undefined options without crashing', async () => {
534
+ await command.execute(undefined as unknown as Record<string, unknown>)
535
+
536
+ expect(mockCatalogUpdateService.planUpdates).toHaveBeenCalled()
537
+ })
538
+
539
+ it('should handle user cancellation (Ctrl+C) gracefully', async () => {
540
+ const cancelError = new Error('User cancelled')
541
+ cancelError.name = 'ExitPromptError'
542
+ mockCatalogUpdateService.planUpdates = vi.fn().mockRejectedValue(cancelError)
543
+
544
+ // Should not throw for user cancellation
545
+ await expect(command.execute({})).resolves.not.toThrow()
546
+ // Check if any print output contains 'cancelled' or 'warning'
547
+ // prints is unknown[][], so we need to flatten and check string content
548
+ const hasExpectedOutput = cliMock.prints
549
+ .flat()
550
+ .some(
551
+ (item) =>
552
+ typeof item === 'string' && (item.includes('cancelled') || item.includes('warning'))
553
+ )
554
+ expect(hasExpectedOutput).toBe(true)
555
+ })
556
+
557
+ it('should handle force closed prompt gracefully', async () => {
558
+ const forceCloseError = new Error('Prompt was force closed')
559
+ mockCatalogUpdateService.planUpdates = vi.fn().mockRejectedValue(forceCloseError)
560
+
561
+ await expect(command.execute({})).resolves.not.toThrow()
562
+ })
563
+
564
+ it('should handle very large update plan', async () => {
565
+ const largeUpdates = Array.from({ length: 500 }, (_, i) => ({
566
+ packageName: `package-${i}`,
567
+ catalogName: 'default',
568
+ currentVersion: '1.0.0',
569
+ newVersion: '2.0.0',
570
+ updateType: 'major' as const,
571
+ }))
572
+
573
+ mockCatalogUpdateService.planUpdates = vi.fn().mockResolvedValue({
574
+ totalUpdates: 500,
575
+ updates: largeUpdates,
576
+ hasSecurityUpdates: false,
577
+ hasMajorUpdates: true,
578
+ hasMinorUpdates: false,
579
+ hasPatchUpdates: false,
580
+ })
581
+
582
+ await command.execute({})
583
+
584
+ expect(mockCatalogUpdateService.executeUpdates).toHaveBeenCalled()
585
+ })
586
+
587
+ it('should handle special characters in package names', async () => {
588
+ mockCatalogUpdateService.planUpdates = vi.fn().mockResolvedValue({
589
+ totalUpdates: 1,
590
+ updates: [
591
+ {
592
+ packageName: '@scope/package-name.test',
593
+ catalogName: 'default',
594
+ currentVersion: '1.0.0',
595
+ newVersion: '2.0.0',
596
+ updateType: 'major',
597
+ },
598
+ ],
599
+ hasSecurityUpdates: false,
600
+ hasMajorUpdates: true,
601
+ hasMinorUpdates: false,
602
+ hasPatchUpdates: false,
603
+ })
604
+
605
+ await command.execute({})
606
+
607
+ expect(mockCatalogUpdateService.executeUpdates).toHaveBeenCalled()
608
+ })
609
+
610
+ it('should handle concurrent execution attempts', async () => {
611
+ // Simulate slow execution
612
+ mockCatalogUpdateService.planUpdates = vi
613
+ .fn()
614
+ .mockImplementation(
615
+ () => new Promise((resolve) => setTimeout(() => resolve(mockUpdatePlan), 100))
616
+ )
617
+
618
+ // Start two concurrent executions
619
+ const execution1 = command.execute({})
620
+ const execution2 = command.execute({})
621
+
622
+ // Both should complete without errors
623
+ await expect(Promise.all([execution1, execution2])).resolves.not.toThrow()
624
+ })
625
+
626
+ it('should handle partial update failure in result', async () => {
627
+ mockCatalogUpdateService.executeUpdates = vi.fn().mockResolvedValue({
628
+ success: false,
629
+ appliedUpdates: [mockUpdatePlan.updates[0]],
630
+ failedUpdates: [mockUpdatePlan.updates[1]],
631
+ backupPath: null,
632
+ errors: ['Failed to update typescript'],
633
+ })
634
+
635
+ await command.execute({})
636
+
637
+ expect(cliMock.prints.length).toBeGreaterThan(0)
638
+ })
639
+
640
+ it('should handle invalid semver versions gracefully', async () => {
641
+ mockCatalogUpdateService.planUpdates = vi.fn().mockResolvedValue({
642
+ totalUpdates: 1,
643
+ updates: [
644
+ {
645
+ packageName: 'test-pkg',
646
+ catalogName: 'default',
647
+ currentVersion: 'invalid',
648
+ newVersion: 'also-invalid',
649
+ updateType: 'unknown',
650
+ },
651
+ ],
652
+ hasSecurityUpdates: false,
653
+ hasMajorUpdates: false,
654
+ hasMinorUpdates: false,
655
+ hasPatchUpdates: false,
656
+ })
657
+
658
+ await command.execute({})
659
+
660
+ expect(mockCatalogUpdateService.executeUpdates).toHaveBeenCalled()
661
+ })
662
+ })
663
+
664
+ describe('package manager integration', () => {
665
+ it('should call package manager install after successful update', async () => {
666
+ await command.execute({ install: true })
667
+
668
+ expect(mocks.packageManagerInstall).toHaveBeenCalledWith(
669
+ expect.objectContaining({
670
+ cwd: expect.any(String),
671
+ })
672
+ )
673
+ })
674
+
675
+ it('should skip install when install option is false', async () => {
676
+ await command.execute({ install: false })
677
+
678
+ expect(mocks.packageManagerInstall).not.toHaveBeenCalled()
679
+ })
680
+
681
+ it('should continue successfully when package manager install fails', async () => {
682
+ mocks.packageManagerInstall.mockResolvedValue({
683
+ success: false,
684
+ code: 1,
685
+ stdout: '',
686
+ stderr: 'ERESOLVE unable to resolve dependency tree',
687
+ })
688
+
689
+ // Should not throw even if install fails
690
+ await expect(command.execute({})).resolves.not.toThrow()
691
+ expect(cliMock.prints.length).toBeGreaterThan(0)
692
+ })
693
+
694
+ it('should handle install timeout gracefully', async () => {
695
+ mocks.packageManagerInstall.mockResolvedValue({
696
+ success: false,
697
+ code: null,
698
+ stdout: '',
699
+ stderr: '',
700
+ error: new Error('Command timed out after 300000ms'),
701
+ })
702
+
703
+ // Should not throw even if install times out
704
+ await expect(command.execute({})).resolves.not.toThrow()
705
+ })
706
+
707
+ it('should get package manager name for logging', async () => {
708
+ mocks.packageManagerInstall.mockResolvedValue({
709
+ success: false,
710
+ code: 1,
711
+ stdout: '',
712
+ stderr: 'Error',
713
+ })
714
+
715
+ await command.execute({})
716
+
717
+ expect(mocks.packageManagerGetName).toHaveBeenCalled()
718
+ })
719
+ })
720
+ })