bbk-cli 1.0.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 (76) hide show
  1. package/.claude/bitbucket-config.local.md.example +58 -0
  2. package/.eslintcache +1 -0
  3. package/.github/dependabot.yml +15 -0
  4. package/.github/workflows/convetional-commit.yml +24 -0
  5. package/.github/workflows/publish-on-tag.yml +47 -0
  6. package/.github/workflows/release-please.yml +21 -0
  7. package/.github/workflows/run-tests.yml +75 -0
  8. package/.nvmrc +1 -0
  9. package/.prettierignore +2 -0
  10. package/.prettierrc.cjs +17 -0
  11. package/.release-please-manifest.json +3 -0
  12. package/CHANGELOG.md +21 -0
  13. package/LICENSE +202 -0
  14. package/README.md +381 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +2 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/cli/wrapper.d.ts +38 -0
  20. package/dist/cli/wrapper.d.ts.map +1 -0
  21. package/dist/cli/wrapper.js +326 -0
  22. package/dist/cli/wrapper.js.map +1 -0
  23. package/dist/commands/helpers.d.ts +11 -0
  24. package/dist/commands/helpers.d.ts.map +1 -0
  25. package/dist/commands/helpers.js +40 -0
  26. package/dist/commands/helpers.js.map +1 -0
  27. package/dist/commands/index.d.ts +3 -0
  28. package/dist/commands/index.d.ts.map +1 -0
  29. package/dist/commands/index.js +3 -0
  30. package/dist/commands/index.js.map +1 -0
  31. package/dist/commands/runner.d.ts +7 -0
  32. package/dist/commands/runner.d.ts.map +1 -0
  33. package/dist/commands/runner.js +126 -0
  34. package/dist/commands/runner.js.map +1 -0
  35. package/dist/config/constants.d.ts +16 -0
  36. package/dist/config/constants.d.ts.map +1 -0
  37. package/dist/config/constants.js +171 -0
  38. package/dist/config/constants.js.map +1 -0
  39. package/dist/config/index.d.ts +2 -0
  40. package/dist/config/index.d.ts.map +1 -0
  41. package/dist/config/index.js +2 -0
  42. package/dist/config/index.js.map +1 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +24 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/utils/arg-parser.d.ts +7 -0
  48. package/dist/utils/arg-parser.d.ts.map +1 -0
  49. package/dist/utils/arg-parser.js +67 -0
  50. package/dist/utils/arg-parser.js.map +1 -0
  51. package/dist/utils/bitbucket-client.d.ts +122 -0
  52. package/dist/utils/bitbucket-client.d.ts.map +1 -0
  53. package/dist/utils/bitbucket-client.js +182 -0
  54. package/dist/utils/bitbucket-client.js.map +1 -0
  55. package/dist/utils/bitbucket-utils.d.ts +110 -0
  56. package/dist/utils/bitbucket-utils.d.ts.map +1 -0
  57. package/dist/utils/bitbucket-utils.js +491 -0
  58. package/dist/utils/bitbucket-utils.js.map +1 -0
  59. package/dist/utils/config-loader.d.ts +41 -0
  60. package/dist/utils/config-loader.d.ts.map +1 -0
  61. package/dist/utils/config-loader.js +76 -0
  62. package/dist/utils/config-loader.js.map +1 -0
  63. package/dist/utils/index.d.ts +5 -0
  64. package/dist/utils/index.d.ts.map +1 -0
  65. package/dist/utils/index.js +4 -0
  66. package/dist/utils/index.js.map +1 -0
  67. package/eslint.config.ts +15 -0
  68. package/package.json +62 -0
  69. package/release-please-config.json +33 -0
  70. package/tests/integration/cli-integration.test.ts +528 -0
  71. package/tests/unit/cli/wrapper.test.ts +727 -0
  72. package/tests/unit/commands/helpers.test.ts +268 -0
  73. package/tests/unit/commands/runner.test.ts +758 -0
  74. package/tests/unit/utils/arg-parser.test.ts +350 -0
  75. package/tests/unit/utils/config-loader.test.ts +158 -0
  76. package/vitest.config.ts +22 -0
@@ -0,0 +1,528 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ // Mock the Bitbucket API functions
7
+ vi.mock('../../src/utils/bitbucket-client.js', () => ({
8
+ listRepositories: vi.fn(),
9
+ getRepository: vi.fn(),
10
+ listPullRequests: vi.fn(),
11
+ getPullRequest: vi.fn(),
12
+ createPullRequest: vi.fn(),
13
+ listBranches: vi.fn(),
14
+ listCommits: vi.fn(),
15
+ listIssues: vi.fn(),
16
+ getIssue: vi.fn(),
17
+ createIssue: vi.fn(),
18
+ listPipelines: vi.fn(),
19
+ getUser: vi.fn(),
20
+ testConnection: vi.fn(),
21
+ clearClients: vi.fn(),
22
+ }));
23
+
24
+ // Mock helper functions - only where we need to spy on them
25
+ vi.mock('../../src/commands/helpers.js', async () => {
26
+ const actual = await vi.importActual('../../src/commands/helpers.js');
27
+ return {
28
+ ...actual,
29
+ printAvailableCommands: vi.fn(actual.printAvailableCommands),
30
+ printCommandDetail: vi.fn(actual.printCommandDetail),
31
+ getCurrentVersion: vi.fn(() => '0.0.0'),
32
+ };
33
+ });
34
+
35
+ // Mock config-loader to spy on loadConfig
36
+ vi.mock('../../src/utils/config-loader.js', async () => {
37
+ const actual = await vi.importActual('../../src/utils/config-loader.js');
38
+ return {
39
+ ...actual,
40
+ loadConfig: vi.fn(actual.loadConfig),
41
+ };
42
+ });
43
+
44
+ // Integration tests that test the entire flow through multiple modules
45
+
46
+ describe('CLI Integration', () => {
47
+ let testDir: string;
48
+ let configPath: string;
49
+
50
+ beforeEach(() => {
51
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bbk-cli-integration-'));
52
+ fs.mkdirSync(path.join(testDir, '.claude'));
53
+ configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
54
+
55
+ // Write valid config
56
+ const configContent = `---
57
+ profiles:
58
+ cloud:
59
+ email: test@test.com
60
+ apiToken: test_token_123
61
+ staging:
62
+ email: staging@test.com
63
+ apiToken: staging_token_456
64
+
65
+ defaultProfile: cloud
66
+ defaultFormat: json
67
+ ---
68
+
69
+ # Test Config`;
70
+ fs.writeFileSync(configPath, configContent);
71
+
72
+ // Clear all mocks before each test
73
+ vi.clearAllMocks();
74
+ });
75
+
76
+ afterEach(() => {
77
+ fs.rmSync(testDir, { recursive: true, force: true });
78
+ });
79
+
80
+ describe('Config Loading Integration', () => {
81
+ it('should load and parse configuration file', async () => {
82
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
83
+
84
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
85
+ const config = await loadConfig(testDir);
86
+
87
+ expect(config).toBeDefined();
88
+ expect(config.profiles).toBeDefined();
89
+ expect(config.profiles.cloud).toBeDefined();
90
+ expect(config.profiles.cloud.email).toBe('test@test.com');
91
+ expect(config.profiles.cloud.apiToken).toBe('test_token_123');
92
+ expect(config.defaultProfile).toBe('cloud');
93
+ expect(config.defaultFormat).toBe('json');
94
+ });
95
+
96
+ it('should support multiple profiles', async () => {
97
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
98
+ const config = await loadConfig(testDir);
99
+
100
+ expect(Object.keys(config.profiles)).toHaveLength(2);
101
+ expect(config.profiles.cloud).toBeDefined();
102
+ expect(config.profiles.staging).toBeDefined();
103
+ });
104
+
105
+ it('should validate required profile fields', async () => {
106
+ const invalidConfig = `---
107
+ profiles:
108
+ incomplete:
109
+ email: test@test.com
110
+ # Missing apiToken
111
+ ---
112
+ `;
113
+ fs.writeFileSync(configPath, invalidConfig);
114
+
115
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
116
+
117
+ expect(() => loadConfig(testDir)).toThrow('must have both "email" and "apiToken"');
118
+ });
119
+ });
120
+
121
+ describe('Command Runner Integration', () => {
122
+ it('should parse command line arguments and execute', async () => {
123
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
124
+
125
+ const { parseArguments } = await import('../../src/utils/arg-parser.js');
126
+ const { listRepositories } = await import('../../src/utils/bitbucket-client.js');
127
+
128
+ // Mock the Bitbucket API call
129
+ listRepositories.mockResolvedValue({
130
+ success: true,
131
+ result: JSON.stringify({ repositories: [{ slug: 'my-repo', name: 'My Repository' }] }),
132
+ });
133
+
134
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
135
+ throw new Error('process.exit called');
136
+ });
137
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
138
+
139
+ try {
140
+ await parseArguments(['list-repositories', '{"workspace":"test-workspace"}']);
141
+ } catch {
142
+ // Expected
143
+ }
144
+
145
+ expect(listRepositories).toHaveBeenCalled();
146
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('repositories'));
147
+ expect(exitSpy).toHaveBeenCalledWith(0);
148
+
149
+ exitSpy.mockRestore();
150
+ consoleLogSpy.mockRestore();
151
+ });
152
+
153
+ it('should handle --version flag', async () => {
154
+ const { parseArguments } = await import('../../src/utils/arg-parser.js');
155
+
156
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
157
+ throw new Error('process.exit called');
158
+ });
159
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
160
+
161
+ try {
162
+ await parseArguments(['--version']);
163
+ } catch {
164
+ // Expected
165
+ }
166
+
167
+ expect(consoleLogSpy).toHaveBeenCalledWith('0.0.0');
168
+ expect(exitSpy).toHaveBeenCalledWith(0);
169
+
170
+ exitSpy.mockRestore();
171
+ consoleLogSpy.mockRestore();
172
+ });
173
+
174
+ it('should handle --commands flag', async () => {
175
+ const { parseArguments } = await import('../../src/utils/arg-parser.js');
176
+ const { printAvailableCommands } = await import('../../src/commands/helpers.js');
177
+
178
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
179
+ throw new Error('process.exit called');
180
+ });
181
+
182
+ try {
183
+ await parseArguments(['--commands']);
184
+ } catch {
185
+ // Expected
186
+ }
187
+
188
+ expect(printAvailableCommands).toHaveBeenCalled();
189
+ expect(exitSpy).toHaveBeenCalledWith(0);
190
+
191
+ exitSpy.mockRestore();
192
+ });
193
+
194
+ it('should handle command-specific help', async () => {
195
+ const { parseArguments } = await import('../../src/utils/arg-parser.js');
196
+ const { printCommandDetail } = await import('../../src/commands/helpers.js');
197
+
198
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
199
+ throw new Error('process.exit called');
200
+ });
201
+
202
+ try {
203
+ await parseArguments(['list-repositories', '-h']);
204
+ } catch {
205
+ // Expected
206
+ }
207
+
208
+ expect(printCommandDetail).toHaveBeenCalledWith('list-repositories');
209
+ expect(exitSpy).toHaveBeenCalledWith(0);
210
+
211
+ exitSpy.mockRestore();
212
+ });
213
+ });
214
+
215
+ describe('Bitbucket API Integration', () => {
216
+ it('should initialize Bitbucket client with profile', async () => {
217
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
218
+
219
+ const { getBitbucketClientOptions } = await import('../../src/utils/config-loader.js');
220
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
221
+
222
+ const config = await loadConfig(testDir);
223
+ const options = getBitbucketClientOptions(config, 'cloud');
224
+
225
+ expect(options.auth).toBeDefined();
226
+ expect(options.auth?.email).toBe('test@test.com');
227
+ expect(options.auth?.apiToken).toBe('test_token_123');
228
+ });
229
+
230
+ it('should handle different profiles', async () => {
231
+ const { getBitbucketClientOptions } = await import('../../src/utils/config-loader.js');
232
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
233
+
234
+ const config = await loadConfig(testDir);
235
+ const cloudOptions = getBitbucketClientOptions(config, 'cloud');
236
+ const stagingOptions = getBitbucketClientOptions(config, 'staging');
237
+
238
+ expect(cloudOptions.auth?.email).toBe('test@test.com');
239
+ expect(stagingOptions.auth?.email).toBe('staging@test.com');
240
+ expect(cloudOptions).not.toEqual(stagingOptions);
241
+ });
242
+ });
243
+
244
+ describe('Command Help Integration', () => {
245
+ it('should display all available commands', async () => {
246
+ const { printAvailableCommands } = await import('../../src/commands/helpers.js');
247
+ const { COMMANDS } = await import('../../src/config/constants.js');
248
+
249
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
250
+
251
+ printAvailableCommands();
252
+
253
+ expect(consoleLogSpy).toHaveBeenCalledWith('\nAvailable commands:');
254
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining(`1. ${COMMANDS[0]}`));
255
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining(`10. ${COMMANDS[9]}`));
256
+
257
+ consoleLogSpy.mockRestore();
258
+ });
259
+
260
+ it('should display detailed help for each command', async () => {
261
+ const { printCommandDetail } = await import('../../src/commands/helpers.js');
262
+ const { COMMANDS, COMMANDS_INFO } = await import('../../src/config/constants.js');
263
+
264
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
265
+
266
+ COMMANDS.forEach((command, index) => {
267
+ consoleLogSpy.mockClear();
268
+ printCommandDetail(command);
269
+
270
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining(command));
271
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining(COMMANDS_INFO[index]));
272
+ });
273
+
274
+ consoleLogSpy.mockRestore();
275
+ });
276
+ });
277
+
278
+ describe('CLI Wrapper Integration', () => {
279
+ it('should initialize CLI with config', async () => {
280
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
281
+
282
+ const { wrapper } = await import('../../src/cli/wrapper.js');
283
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
284
+
285
+ const cli = new wrapper();
286
+
287
+ await cli.connect();
288
+
289
+ expect(loadConfig).toHaveBeenCalledWith(testDir);
290
+ });
291
+
292
+ it('should handle profile switching', async () => {
293
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
294
+
295
+ const { wrapper } = await import('../../src/cli/wrapper.js');
296
+ const cli = new wrapper();
297
+
298
+ await cli.connect();
299
+
300
+ // Simulate profile switch
301
+ const handleCommand = cli['handleCommand'].bind(cli);
302
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
303
+
304
+ await handleCommand('profile staging');
305
+
306
+ expect(consoleLogSpy).toHaveBeenCalledWith('Switched to profile: staging');
307
+
308
+ consoleLogSpy.mockRestore();
309
+ });
310
+
311
+ it('should handle format switching', async () => {
312
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
313
+
314
+ const { wrapper } = await import('../../src/cli/wrapper.js');
315
+ const cli = new wrapper();
316
+
317
+ await cli.connect();
318
+
319
+ const handleCommand = cli['handleCommand'].bind(cli);
320
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
321
+
322
+ await handleCommand('format toon');
323
+
324
+ expect(consoleLogSpy).toHaveBeenCalledWith('Output format set to: toon');
325
+
326
+ consoleLogSpy.mockRestore();
327
+ });
328
+ });
329
+
330
+ describe('Error Handling Integration', () => {
331
+ it('should handle missing config file', async () => {
332
+ fs.rmSync(configPath);
333
+
334
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
335
+
336
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
337
+
338
+ expect(() => loadConfig(testDir)).toThrow('Configuration file not found');
339
+ });
340
+
341
+ it('should handle invalid config format', async () => {
342
+ const invalidConfig = `# Invalid Config
343
+
344
+ This is just markdown without frontmatter
345
+ `;
346
+ fs.writeFileSync(configPath, invalidConfig);
347
+
348
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
349
+
350
+ expect(() => loadConfig(testDir)).toThrow('Invalid configuration file format');
351
+ });
352
+
353
+ it('should handle missing profile', async () => {
354
+ const { getBitbucketClientOptions } = await import('../../src/utils/config-loader.js');
355
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
356
+
357
+ const config = await loadConfig(testDir);
358
+
359
+ expect(() => getBitbucketClientOptions(config, 'nonexistent')).toThrow('Profile "nonexistent" not found');
360
+ });
361
+
362
+ it('should handle invalid email format', async () => {
363
+ const invalidConfig = `---
364
+ profiles:
365
+ invalid:
366
+ email: invalid-email
367
+ apiToken: token
368
+ ---
369
+ `;
370
+ fs.writeFileSync(configPath, invalidConfig);
371
+
372
+ const { loadConfig } = await import('../../src/utils/config-loader.js');
373
+
374
+ expect(() => loadConfig(testDir)).toThrow('Profile "invalid" has invalid email format: "invalid-email"');
375
+ });
376
+ });
377
+
378
+ describe('End-to-End Workflows', () => {
379
+ it('should execute list-repositories workflow', async () => {
380
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
381
+
382
+ const { runCommand } = await import('../../src/commands/runner.js');
383
+ const { listRepositories } = await import('../../src/utils/bitbucket-client.js');
384
+
385
+ listRepositories.mockResolvedValue({
386
+ success: true,
387
+ result: JSON.stringify({
388
+ repositories: [
389
+ { slug: 'docs-repo', name: 'Documentation' },
390
+ { slug: 'eng-repo', name: 'Engineering' },
391
+ ],
392
+ }),
393
+ });
394
+
395
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
396
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
397
+ throw new Error('process.exit called');
398
+ });
399
+
400
+ try {
401
+ await runCommand('list-repositories', '{"workspace":"test-workspace"}', null);
402
+ } catch {
403
+ // Expected
404
+ }
405
+
406
+ expect(listRepositories).toHaveBeenCalledWith('cloud', 'test-workspace', 'json');
407
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('docs-repo'));
408
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Engineering'));
409
+
410
+ exitSpy.mockRestore();
411
+ consoleLogSpy.mockRestore();
412
+ });
413
+
414
+ it('should execute create-issue workflow', async () => {
415
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
416
+
417
+ const { runCommand } = await import('../../src/commands/runner.js');
418
+ const { createIssue } = await import('../../src/utils/bitbucket-client.js');
419
+
420
+ createIssue.mockResolvedValue({
421
+ success: true,
422
+ result: JSON.stringify({
423
+ id: '12345',
424
+ title: 'New Issue',
425
+ kind: 'bug',
426
+ }),
427
+ });
428
+
429
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
430
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
431
+ throw new Error('process.exit called');
432
+ });
433
+
434
+ try {
435
+ await runCommand(
436
+ 'create-issue',
437
+ JSON.stringify({
438
+ workspace: 'test-workspace',
439
+ repoSlug: 'my-repo',
440
+ title: 'New Issue',
441
+ content: 'Issue description',
442
+ }),
443
+ null
444
+ );
445
+ } catch {
446
+ // Expected
447
+ }
448
+
449
+ expect(createIssue).toHaveBeenCalledWith(
450
+ 'cloud',
451
+ 'test-workspace',
452
+ 'my-repo',
453
+ 'New Issue',
454
+ 'Issue description',
455
+ undefined,
456
+ undefined,
457
+ 'json'
458
+ );
459
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('12345'));
460
+
461
+ exitSpy.mockRestore();
462
+ consoleLogSpy.mockRestore();
463
+ });
464
+
465
+ it('should execute get-user workflow', async () => {
466
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
467
+
468
+ const { runCommand } = await import('../../src/commands/runner.js');
469
+ const { getUser } = await import('../../src/utils/bitbucket-client.js');
470
+
471
+ getUser.mockResolvedValue({
472
+ success: true,
473
+ result: JSON.stringify({
474
+ uuid: '{5b10a2844c20165700ede21g}',
475
+ display_name: 'John Doe',
476
+ username: 'johndoe',
477
+ }),
478
+ });
479
+
480
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
481
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
482
+ throw new Error('process.exit called');
483
+ });
484
+
485
+ try {
486
+ await runCommand('get-user', '{"username":"johndoe"}', null);
487
+ } catch {
488
+ // Expected
489
+ }
490
+
491
+ expect(getUser).toHaveBeenCalledWith('cloud', 'johndoe', 'json');
492
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('John Doe'));
493
+
494
+ exitSpy.mockRestore();
495
+ consoleLogSpy.mockRestore();
496
+ });
497
+
498
+ it('should execute test-connection workflow', async () => {
499
+ process.env.CLAUDE_PROJECT_ROOT = testDir;
500
+
501
+ const { runCommand } = await import('../../src/commands/runner.js');
502
+ const { testConnection } = await import('../../src/utils/bitbucket-client.js');
503
+
504
+ testConnection.mockResolvedValue({
505
+ success: true,
506
+ result: 'Connection successful!\n\nProfile: cloud\nLogged in as: John Doe (johndoe)',
507
+ });
508
+
509
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
510
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
511
+ throw new Error('process.exit called');
512
+ });
513
+
514
+ try {
515
+ await runCommand('test-connection', null, null);
516
+ } catch {
517
+ // Expected
518
+ }
519
+
520
+ expect(testConnection).toHaveBeenCalledWith('cloud');
521
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Connection successful'));
522
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('John Doe'));
523
+
524
+ exitSpy.mockRestore();
525
+ consoleLogSpy.mockRestore();
526
+ });
527
+ });
528
+ });