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.
- package/README.md +15 -0
- package/dist/index.js +22031 -10684
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
- package/src/cli/commandRegistrar.ts +785 -0
- package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
- package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
- package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
- package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
- package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
- package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
- package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
- package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
- package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
- package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
- package/src/cli/commands/aiCommand.ts +163 -0
- package/src/cli/commands/analyzeCommand.ts +219 -0
- package/src/cli/commands/checkCommand.ts +91 -98
- package/src/cli/commands/graphCommand.ts +475 -0
- package/src/cli/commands/initCommand.ts +64 -54
- package/src/cli/commands/rollbackCommand.ts +334 -0
- package/src/cli/commands/securityCommand.ts +165 -100
- package/src/cli/commands/themeCommand.ts +148 -0
- package/src/cli/commands/updateCommand.ts +215 -263
- package/src/cli/commands/workspaceCommand.ts +73 -0
- package/src/cli/constants/cliChoices.ts +93 -0
- package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
- package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
- package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
- package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
- package/src/cli/formatters/ciFormatter.ts +964 -0
- package/src/cli/formatters/colorUtils.ts +145 -0
- package/src/cli/formatters/outputFormatter.ts +615 -332
- package/src/cli/formatters/progressBar.ts +43 -52
- package/src/cli/formatters/versionFormatter.ts +132 -0
- package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
- package/src/cli/handlers/changelogHandler.ts +113 -0
- package/src/cli/handlers/index.ts +9 -0
- package/src/cli/handlers/installHandler.ts +130 -0
- package/src/cli/index.ts +175 -726
- package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
- package/src/cli/interactive/interactivePrompts.ts +189 -83
- package/src/cli/interactive/optionUtils.ts +89 -0
- package/src/cli/themes/colorTheme.ts +43 -16
- package/src/cli/utils/cliOutput.ts +118 -0
- package/src/cli/utils/commandHelpers.ts +249 -0
- package/src/cli/validators/commandValidator.ts +321 -336
- package/src/cli/validators/index.ts +37 -2
- package/src/cli/options/globalOptions.ts +0 -437
- 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
|
+
})
|