bmad-fh 6.0.0-alpha.23.66f19588 → 6.0.0-alpha.23.6874ced1

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.
@@ -0,0 +1,686 @@
1
+ /**
2
+ * CLI Argument Handling Test Suite
3
+ *
4
+ * Tests for proper handling of CLI arguments, especially:
5
+ * - Arguments containing spaces
6
+ * - Arguments with special characters
7
+ * - The npx wrapper's argument preservation
8
+ * - Various quoting scenarios
9
+ *
10
+ * This test suite was created to prevent regression of the bug where
11
+ * the npx wrapper used args.join(' ') which broke arguments containing spaces.
12
+ *
13
+ * Usage: node test/test-cli-arguments.js
14
+ * Exit codes: 0 = all tests pass, 1 = test failures
15
+ */
16
+
17
+ const fs = require('fs-extra');
18
+ const path = require('node:path');
19
+ const os = require('node:os');
20
+ const { spawnSync } = require('node:child_process');
21
+
22
+ // ANSI color codes
23
+ const colors = {
24
+ reset: '\u001B[0m',
25
+ green: '\u001B[32m',
26
+ red: '\u001B[31m',
27
+ yellow: '\u001B[33m',
28
+ blue: '\u001B[34m',
29
+ cyan: '\u001B[36m',
30
+ dim: '\u001B[2m',
31
+ bold: '\u001B[1m',
32
+ };
33
+
34
+ // Test utilities
35
+ let testCount = 0;
36
+ let passCount = 0;
37
+ let failCount = 0;
38
+ let skipCount = 0;
39
+ const failures = [];
40
+
41
+ function test(name, fn) {
42
+ testCount++;
43
+ try {
44
+ fn();
45
+ passCount++;
46
+ console.log(` ${colors.green}✓${colors.reset} ${name}`);
47
+ } catch (error) {
48
+ failCount++;
49
+ console.log(` ${colors.red}✗${colors.reset} ${name}`);
50
+ console.log(` ${colors.red}${error.message}${colors.reset}`);
51
+ failures.push({ name, error: error.message });
52
+ }
53
+ }
54
+
55
+ async function testAsync(name, fn) {
56
+ testCount++;
57
+ try {
58
+ await fn();
59
+ passCount++;
60
+ console.log(` ${colors.green}✓${colors.reset} ${name}`);
61
+ } catch (error) {
62
+ failCount++;
63
+ console.log(` ${colors.red}✗${colors.reset} ${name}`);
64
+ console.log(` ${colors.red}${error.message}${colors.reset}`);
65
+ failures.push({ name, error: error.message });
66
+ }
67
+ }
68
+
69
+ function skip(name, reason = '') {
70
+ skipCount++;
71
+ console.log(` ${colors.yellow}○${colors.reset} ${name} ${colors.dim}(skipped${reason ? ': ' + reason : ''})${colors.reset}`);
72
+ }
73
+
74
+ function assertEqual(actual, expected, message = '') {
75
+ if (actual !== expected) {
76
+ throw new Error(`${message}\n Expected: ${JSON.stringify(expected)}\n Actual: ${JSON.stringify(actual)}`);
77
+ }
78
+ }
79
+
80
+ function assertTrue(value, message = 'Expected true') {
81
+ if (!value) {
82
+ throw new Error(message);
83
+ }
84
+ }
85
+
86
+ function assertFalse(value, message = 'Expected false') {
87
+ if (value) {
88
+ throw new Error(message);
89
+ }
90
+ }
91
+
92
+ function assertContains(str, substring, message = '') {
93
+ if (!str.includes(substring)) {
94
+ throw new Error(`${message}\n Expected to contain: "${substring}"\n Actual: "${str.slice(0, 500)}..."`);
95
+ }
96
+ }
97
+
98
+ function assertNotContains(str, substring, message = '') {
99
+ if (str.includes(substring)) {
100
+ throw new Error(`${message}\n Expected NOT to contain: "${substring}"`);
101
+ }
102
+ }
103
+
104
+ function assertExists(filePath, message = '') {
105
+ if (!fs.existsSync(filePath)) {
106
+ throw new Error(`${message || 'File does not exist'}: ${filePath}`);
107
+ }
108
+ }
109
+
110
+ // Create temporary test directory with BMAD structure
111
+ function createTestProject() {
112
+ const tmpDir = path.join(os.tmpdir(), `bmad-cli-args-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
113
+ fs.mkdirSync(tmpDir, { recursive: true });
114
+
115
+ // Create minimal BMAD structure
116
+ fs.mkdirSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true });
117
+ fs.mkdirSync(path.join(tmpDir, '_bmad-output'), { recursive: true });
118
+
119
+ return tmpDir;
120
+ }
121
+
122
+ function cleanupTestProject(tmpDir) {
123
+ try {
124
+ fs.rmSync(tmpDir, { recursive: true, force: true });
125
+ } catch {
126
+ // Ignore cleanup errors
127
+ }
128
+ }
129
+
130
+ // Paths to CLI entry points
131
+ const CLI_PATH = path.join(__dirname, '..', 'tools', 'cli', 'bmad-cli.js');
132
+ const NPX_WRAPPER_PATH = path.join(__dirname, '..', 'tools', 'bmad-npx-wrapper.js');
133
+
134
+ /**
135
+ * Execute CLI command using spawnSync with an array of arguments.
136
+ * This properly preserves argument boundaries, just like the shell does.
137
+ *
138
+ * @param {string[]} args - Array of arguments (NOT a joined string)
139
+ * @param {string} cwd - Working directory
140
+ * @param {Object} options - Additional options
141
+ * @returns {Object} Result with success, output, stderr, exitCode
142
+ */
143
+ function runCliArray(args, cwd, options = {}) {
144
+ const result = spawnSync('node', [CLI_PATH, ...args], {
145
+ cwd,
146
+ encoding: 'utf8',
147
+ timeout: options.timeout || 30_000,
148
+ env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
149
+ });
150
+
151
+ return {
152
+ success: result.status === 0,
153
+ output: result.stdout || '',
154
+ stderr: result.stderr || '',
155
+ exitCode: result.status || 0,
156
+ error: result.error ? result.error.message : null,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Execute CLI command via the npx wrapper using spawnSync.
162
+ * This tests the actual npx execution path.
163
+ *
164
+ * @param {string[]} args - Array of arguments
165
+ * @param {string} cwd - Working directory
166
+ * @param {Object} options - Additional options
167
+ * @returns {Object} Result with success, output, stderr, exitCode
168
+ */
169
+ function runNpxWrapper(args, cwd, options = {}) {
170
+ const result = spawnSync('node', [NPX_WRAPPER_PATH, ...args], {
171
+ cwd,
172
+ encoding: 'utf8',
173
+ timeout: options.timeout || 30_000,
174
+ env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
175
+ });
176
+
177
+ return {
178
+ success: result.status === 0,
179
+ output: result.stdout || '',
180
+ stderr: result.stderr || '',
181
+ exitCode: result.status || 0,
182
+ error: result.error ? result.error.message : null,
183
+ };
184
+ }
185
+
186
+ // ============================================================================
187
+ // Arguments with Spaces Tests
188
+ // ============================================================================
189
+
190
+ function testArgumentsWithSpaces() {
191
+ console.log(`\n${colors.blue}${colors.bold}Arguments with Spaces Tests${colors.reset}`);
192
+
193
+ test('scope create with description containing spaces (direct CLI)', () => {
194
+ const tmpDir = createTestProject();
195
+ try {
196
+ runCliArray(['scope', 'init'], tmpDir);
197
+ const result = runCliArray(
198
+ ['scope', 'create', 'test-scope', '--name', 'Test Scope', '--description', 'This is a description with multiple words'],
199
+ tmpDir,
200
+ );
201
+ assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
202
+ assertContains(result.output, "Scope 'test-scope' created successfully");
203
+
204
+ // Verify the description was saved correctly
205
+ const infoResult = runCliArray(['scope', 'info', 'test-scope'], tmpDir);
206
+ assertContains(infoResult.output, 'This is a description with multiple words');
207
+ } finally {
208
+ cleanupTestProject(tmpDir);
209
+ }
210
+ });
211
+
212
+ test('scope create with description containing spaces (via npx wrapper)', () => {
213
+ const tmpDir = createTestProject();
214
+ try {
215
+ runNpxWrapper(['scope', 'init'], tmpDir);
216
+ const result = runNpxWrapper(
217
+ ['scope', 'create', 'test-scope', '--name', 'Test Scope', '--description', 'This is a description with multiple words'],
218
+ tmpDir,
219
+ );
220
+ assertTrue(result.success, `Create should succeed via wrapper: ${result.stderr || result.error}`);
221
+ assertContains(result.output, "Scope 'test-scope' created successfully");
222
+
223
+ // Verify the description was saved correctly
224
+ const infoResult = runNpxWrapper(['scope', 'info', 'test-scope'], tmpDir);
225
+ assertContains(infoResult.output, 'This is a description with multiple words');
226
+ } finally {
227
+ cleanupTestProject(tmpDir);
228
+ }
229
+ });
230
+
231
+ test('scope create with long description (many spaces)', () => {
232
+ const tmpDir = createTestProject();
233
+ try {
234
+ runCliArray(['scope', 'init'], tmpDir);
235
+ const longDesc = 'PRD Auto queue for not inbound yet products with special handling for edge cases';
236
+ const result = runCliArray(['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', longDesc], tmpDir);
237
+ assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
238
+
239
+ const infoResult = runCliArray(['scope', 'info', 'auto-queue'], tmpDir);
240
+ assertContains(infoResult.output, 'PRD Auto queue for not inbound yet products');
241
+ } finally {
242
+ cleanupTestProject(tmpDir);
243
+ }
244
+ });
245
+
246
+ test('scope create with name containing spaces', () => {
247
+ const tmpDir = createTestProject();
248
+ try {
249
+ runCliArray(['scope', 'init'], tmpDir);
250
+ const result = runCliArray(
251
+ ['scope', 'create', 'auth', '--name', 'User Authentication Service', '--description', 'Handles user auth'],
252
+ tmpDir,
253
+ );
254
+ assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
255
+
256
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
257
+ assertContains(infoResult.output, 'User Authentication Service');
258
+ } finally {
259
+ cleanupTestProject(tmpDir);
260
+ }
261
+ });
262
+ }
263
+
264
+ // ============================================================================
265
+ // Special Characters Tests
266
+ // ============================================================================
267
+
268
+ function testSpecialCharacters() {
269
+ console.log(`\n${colors.blue}${colors.bold}Special Characters Tests${colors.reset}`);
270
+
271
+ test('scope create with name containing ampersand', () => {
272
+ const tmpDir = createTestProject();
273
+ try {
274
+ runCliArray(['scope', 'init'], tmpDir);
275
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth & Users', '--description', ''], tmpDir);
276
+ assertTrue(result.success, 'Should handle ampersand');
277
+
278
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
279
+ assertContains(infoResult.output, 'Auth & Users');
280
+ } finally {
281
+ cleanupTestProject(tmpDir);
282
+ }
283
+ });
284
+
285
+ test('scope create with name containing parentheses', () => {
286
+ const tmpDir = createTestProject();
287
+ try {
288
+ runCliArray(['scope', 'init'], tmpDir);
289
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth Service (v2)', '--description', ''], tmpDir);
290
+ assertTrue(result.success, 'Should handle parentheses');
291
+
292
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
293
+ assertContains(infoResult.output, 'Auth Service (v2)');
294
+ } finally {
295
+ cleanupTestProject(tmpDir);
296
+ }
297
+ });
298
+
299
+ test('scope create with description containing quotes', () => {
300
+ const tmpDir = createTestProject();
301
+ try {
302
+ runCliArray(['scope', 'init'], tmpDir);
303
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Handle "special" cases'], tmpDir);
304
+ assertTrue(result.success, 'Should handle quotes in description');
305
+
306
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
307
+ assertContains(infoResult.output, 'Handle "special" cases');
308
+ } finally {
309
+ cleanupTestProject(tmpDir);
310
+ }
311
+ });
312
+
313
+ test('scope create with description containing single quotes', () => {
314
+ const tmpDir = createTestProject();
315
+ try {
316
+ runCliArray(['scope', 'init'], tmpDir);
317
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', "Handle user's authentication"], tmpDir);
318
+ assertTrue(result.success, 'Should handle single quotes');
319
+
320
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
321
+ assertContains(infoResult.output, "user's");
322
+ } finally {
323
+ cleanupTestProject(tmpDir);
324
+ }
325
+ });
326
+
327
+ test('scope create with description containing colons', () => {
328
+ const tmpDir = createTestProject();
329
+ try {
330
+ runCliArray(['scope', 'init'], tmpDir);
331
+ const result = runCliArray(
332
+ ['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Features: login, logout, sessions'],
333
+ tmpDir,
334
+ );
335
+ assertTrue(result.success, 'Should handle colons');
336
+
337
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
338
+ assertContains(infoResult.output, 'Features: login, logout, sessions');
339
+ } finally {
340
+ cleanupTestProject(tmpDir);
341
+ }
342
+ });
343
+
344
+ test('scope create with description containing hyphens and dashes', () => {
345
+ const tmpDir = createTestProject();
346
+ try {
347
+ runCliArray(['scope', 'init'], tmpDir);
348
+ const result = runCliArray(
349
+ ['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Multi-factor auth - two-step verification'],
350
+ tmpDir,
351
+ );
352
+ assertTrue(result.success, 'Should handle hyphens and dashes');
353
+
354
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
355
+ assertContains(infoResult.output, 'Multi-factor auth - two-step verification');
356
+ } finally {
357
+ cleanupTestProject(tmpDir);
358
+ }
359
+ });
360
+
361
+ test('scope create with description containing slashes', () => {
362
+ const tmpDir = createTestProject();
363
+ try {
364
+ runCliArray(['scope', 'init'], tmpDir);
365
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Handles /api/auth/* endpoints'], tmpDir);
366
+ assertTrue(result.success, 'Should handle slashes');
367
+
368
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
369
+ assertContains(infoResult.output, '/api/auth/*');
370
+ } finally {
371
+ cleanupTestProject(tmpDir);
372
+ }
373
+ });
374
+ }
375
+
376
+ // ============================================================================
377
+ // NPX Wrapper Specific Tests
378
+ // ============================================================================
379
+
380
+ function testNpxWrapperBehavior() {
381
+ console.log(`\n${colors.blue}${colors.bold}NPX Wrapper Behavior Tests${colors.reset}`);
382
+
383
+ test('npx wrapper preserves argument boundaries', () => {
384
+ const tmpDir = createTestProject();
385
+ try {
386
+ runNpxWrapper(['scope', 'init'], tmpDir);
387
+
388
+ // This was the exact failing case: description with multiple words
389
+ const result = runNpxWrapper(
390
+ ['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', 'PRD Auto queue for not inbound yet products'],
391
+ tmpDir,
392
+ );
393
+ assertTrue(result.success, `NPX wrapper should preserve spaces: ${result.stderr || result.output}`);
394
+
395
+ // Verify full description was saved
396
+ const infoResult = runNpxWrapper(['scope', 'info', 'auto-queue'], tmpDir);
397
+ assertContains(infoResult.output, 'PRD Auto queue for not inbound yet products');
398
+ } finally {
399
+ cleanupTestProject(tmpDir);
400
+ }
401
+ });
402
+
403
+ test('npx wrapper handles multiple space-containing arguments', () => {
404
+ const tmpDir = createTestProject();
405
+ try {
406
+ runNpxWrapper(['scope', 'init'], tmpDir);
407
+
408
+ const result = runNpxWrapper(
409
+ ['scope', 'create', 'test-scope', '--name', 'My Test Scope Name', '--description', 'A long description with many words and spaces'],
410
+ tmpDir,
411
+ );
412
+ assertTrue(result.success, 'Should handle multiple space-containing args');
413
+
414
+ const infoResult = runNpxWrapper(['scope', 'info', 'test-scope'], tmpDir);
415
+ assertContains(infoResult.output, 'My Test Scope Name');
416
+ assertContains(infoResult.output, 'A long description with many words and spaces');
417
+ } finally {
418
+ cleanupTestProject(tmpDir);
419
+ }
420
+ });
421
+
422
+ test('npx wrapper handles help commands', () => {
423
+ const tmpDir = createTestProject();
424
+ try {
425
+ const result = runNpxWrapper(['scope', 'help'], tmpDir);
426
+ assertTrue(result.success, 'Help should work via wrapper');
427
+ assertContains(result.output, 'BMAD Scope Management');
428
+ } finally {
429
+ cleanupTestProject(tmpDir);
430
+ }
431
+ });
432
+
433
+ test('npx wrapper handles subcommand help', () => {
434
+ const tmpDir = createTestProject();
435
+ try {
436
+ const result = runNpxWrapper(['scope', 'help', 'create'], tmpDir);
437
+ assertTrue(result.success, 'Subcommand help should work via wrapper');
438
+ assertContains(result.output, 'bmad scope create');
439
+ } finally {
440
+ cleanupTestProject(tmpDir);
441
+ }
442
+ });
443
+
444
+ test('npx wrapper preserves exit codes on failure', () => {
445
+ const tmpDir = createTestProject();
446
+ try {
447
+ runNpxWrapper(['scope', 'init'], tmpDir);
448
+ const result = runNpxWrapper(['scope', 'info', 'nonexistent'], tmpDir);
449
+ assertFalse(result.success, 'Should fail for non-existent scope');
450
+ assertTrue(result.exitCode !== 0, 'Exit code should be non-zero');
451
+ } finally {
452
+ cleanupTestProject(tmpDir);
453
+ }
454
+ });
455
+ }
456
+
457
+ // ============================================================================
458
+ // Edge Cases Tests
459
+ // ============================================================================
460
+
461
+ function testEdgeCases() {
462
+ console.log(`\n${colors.blue}${colors.bold}Edge Cases Tests${colors.reset}`);
463
+
464
+ test('empty description argument', () => {
465
+ const tmpDir = createTestProject();
466
+ try {
467
+ runCliArray(['scope', 'init'], tmpDir);
468
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', ''], tmpDir);
469
+ assertTrue(result.success, 'Should handle empty description');
470
+ } finally {
471
+ cleanupTestProject(tmpDir);
472
+ }
473
+ });
474
+
475
+ test('description with only spaces', () => {
476
+ const tmpDir = createTestProject();
477
+ try {
478
+ runCliArray(['scope', 'init'], tmpDir);
479
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', ' '], tmpDir);
480
+ assertTrue(result.success, 'Should handle whitespace-only description');
481
+ } finally {
482
+ cleanupTestProject(tmpDir);
483
+ }
484
+ });
485
+
486
+ test('name with leading and trailing spaces', () => {
487
+ const tmpDir = createTestProject();
488
+ try {
489
+ runCliArray(['scope', 'init'], tmpDir);
490
+ const result = runCliArray(['scope', 'create', 'auth', '--name', ' Spaced Name ', '--description', ''], tmpDir);
491
+ assertTrue(result.success, 'Should handle leading/trailing spaces in name');
492
+ } finally {
493
+ cleanupTestProject(tmpDir);
494
+ }
495
+ });
496
+
497
+ test('mixed flags and positional arguments', () => {
498
+ const tmpDir = createTestProject();
499
+ try {
500
+ runCliArray(['scope', 'init'], tmpDir);
501
+ // Some CLI parsers are sensitive to flag ordering
502
+ const result = runCliArray(['scope', 'create', '--name', 'Auth Service', 'auth', '--description', 'User authentication'], tmpDir);
503
+ // Depending on Commander.js behavior, this might fail or succeed
504
+ // The important thing is it doesn't crash unexpectedly
505
+ // Note: Commander.js is strict about positional arg ordering, so this may fail
506
+ } finally {
507
+ cleanupTestProject(tmpDir);
508
+ }
509
+ });
510
+
511
+ test('very long description', () => {
512
+ const tmpDir = createTestProject();
513
+ try {
514
+ runCliArray(['scope', 'init'], tmpDir);
515
+ const longDesc = 'A '.repeat(100) + 'very long description';
516
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', longDesc], tmpDir);
517
+ assertTrue(result.success, 'Should handle very long description');
518
+
519
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
520
+ assertContains(infoResult.output, 'very long description');
521
+ } finally {
522
+ cleanupTestProject(tmpDir);
523
+ }
524
+ });
525
+
526
+ test('description with newline-like content', () => {
527
+ const tmpDir = createTestProject();
528
+ try {
529
+ runCliArray(['scope', 'init'], tmpDir);
530
+ // Note: actual newlines would be handled by the shell, this tests the literal string
531
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', String.raw`Line1\nLine2`], tmpDir);
532
+ assertTrue(result.success, 'Should handle backslash-n in description');
533
+ } finally {
534
+ cleanupTestProject(tmpDir);
535
+ }
536
+ });
537
+
538
+ test('description with unicode characters', () => {
539
+ const tmpDir = createTestProject();
540
+ try {
541
+ runCliArray(['scope', 'init'], tmpDir);
542
+ const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Handles authentication 认证 🔐'], tmpDir);
543
+ assertTrue(result.success, 'Should handle unicode in description');
544
+
545
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
546
+ assertContains(infoResult.output, '认证');
547
+ } finally {
548
+ cleanupTestProject(tmpDir);
549
+ }
550
+ });
551
+ }
552
+
553
+ // ============================================================================
554
+ // Argument Count Tests (Regression tests for "too many arguments" error)
555
+ // ============================================================================
556
+
557
+ function testArgumentCounts() {
558
+ console.log(`\n${colors.blue}${colors.bold}Argument Count Tests (Regression)${colors.reset}`);
559
+
560
+ test('9-word description does not cause "too many arguments" error', () => {
561
+ const tmpDir = createTestProject();
562
+ try {
563
+ runCliArray(['scope', 'init'], tmpDir);
564
+ // This was the exact case that failed: 9 words became 9 separate arguments
565
+ const result = runCliArray(
566
+ ['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', 'PRD Auto queue for not inbound yet products'],
567
+ tmpDir,
568
+ );
569
+ assertTrue(result.success, `Should not fail with "too many arguments": ${result.stderr}`);
570
+ assertNotContains(result.stderr || '', 'too many arguments');
571
+ } finally {
572
+ cleanupTestProject(tmpDir);
573
+ }
574
+ });
575
+
576
+ test('20-word description works correctly', () => {
577
+ const tmpDir = createTestProject();
578
+ try {
579
+ runCliArray(['scope', 'init'], tmpDir);
580
+ const desc =
581
+ 'This is a very long description with exactly twenty words to test that argument parsing works correctly for descriptions';
582
+ const result = runCliArray(['scope', 'create', 'test', '--name', 'Test', '--description', desc], tmpDir);
583
+ assertTrue(result.success, 'Should handle 20-word description');
584
+ } finally {
585
+ cleanupTestProject(tmpDir);
586
+ }
587
+ });
588
+
589
+ test('multiple flag values with spaces all preserved', () => {
590
+ const tmpDir = createTestProject();
591
+ try {
592
+ runNpxWrapper(['scope', 'init'], tmpDir);
593
+ const result = runNpxWrapper(
594
+ ['scope', 'create', 'my-scope', '--name', 'My Scope Name Here', '--description', 'This is a description with many spaces'],
595
+ tmpDir,
596
+ );
597
+ assertTrue(result.success, 'All spaced arguments should be preserved');
598
+
599
+ const infoResult = runNpxWrapper(['scope', 'info', 'my-scope'], tmpDir);
600
+ assertContains(infoResult.output, 'My Scope Name Here');
601
+ assertContains(infoResult.output, 'This is a description with many spaces');
602
+ } finally {
603
+ cleanupTestProject(tmpDir);
604
+ }
605
+ });
606
+ }
607
+
608
+ // ============================================================================
609
+ // Install Command Tests (for completeness)
610
+ // ============================================================================
611
+
612
+ function testInstallCommand() {
613
+ console.log(`\n${colors.blue}${colors.bold}Install Command Tests${colors.reset}`);
614
+
615
+ test('install --help works via npx wrapper', () => {
616
+ const tmpDir = createTestProject();
617
+ try {
618
+ const result = runNpxWrapper(['install', '--help'], tmpDir);
619
+ assertTrue(result.success || result.output.includes('Install'), 'Install help should work');
620
+ } finally {
621
+ cleanupTestProject(tmpDir);
622
+ }
623
+ });
624
+
625
+ test('install --debug flag works', () => {
626
+ const tmpDir = createTestProject();
627
+ try {
628
+ // Just verify the flag is recognized, don't actually run full install
629
+ const result = runNpxWrapper(['install', '--help'], tmpDir);
630
+ // If we got here without crashing, the CLI is working
631
+ assertTrue(true, 'Install command accepts flags');
632
+ } finally {
633
+ cleanupTestProject(tmpDir);
634
+ }
635
+ });
636
+ }
637
+
638
+ // ============================================================================
639
+ // Main Test Runner
640
+ // ============================================================================
641
+
642
+ function main() {
643
+ console.log(`\n${colors.bold}BMAD CLI Argument Handling Test Suite${colors.reset}`);
644
+ console.log(colors.dim + '═'.repeat(70) + colors.reset);
645
+ console.log(colors.cyan + 'Testing proper preservation of argument boundaries,' + colors.reset);
646
+ console.log(colors.cyan + 'especially for arguments containing spaces.' + colors.reset);
647
+
648
+ const startTime = Date.now();
649
+
650
+ // Run all test suites
651
+ testArgumentsWithSpaces();
652
+ testSpecialCharacters();
653
+ testNpxWrapperBehavior();
654
+ testEdgeCases();
655
+ testArgumentCounts();
656
+ testInstallCommand();
657
+
658
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
659
+
660
+ // Summary
661
+ console.log(`\n${colors.dim}${'─'.repeat(70)}${colors.reset}`);
662
+ console.log(`\n${colors.bold}Test Results${colors.reset}`);
663
+ console.log(` Total: ${testCount}`);
664
+ console.log(` ${colors.green}Passed: ${passCount}${colors.reset}`);
665
+ if (failCount > 0) {
666
+ console.log(` ${colors.red}Failed: ${failCount}${colors.reset}`);
667
+ }
668
+ if (skipCount > 0) {
669
+ console.log(` ${colors.yellow}Skipped: ${skipCount}${colors.reset}`);
670
+ }
671
+ console.log(` Time: ${duration}s`);
672
+
673
+ if (failures.length > 0) {
674
+ console.log(`\n${colors.red}${colors.bold}Failures:${colors.reset}`);
675
+ for (const { name, error } of failures) {
676
+ console.log(`\n ${colors.red}✗${colors.reset} ${name}`);
677
+ console.log(` ${colors.dim}${error}${colors.reset}`);
678
+ }
679
+ process.exit(1);
680
+ }
681
+
682
+ console.log(`\n${colors.green}${colors.bold}All tests passed!${colors.reset}\n`);
683
+ process.exit(0);
684
+ }
685
+
686
+ main();