bmad-fh 6.0.0-alpha.23.96db56c9 → 6.0.0-alpha.23.b9802f7d

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,1475 @@
1
+ /**
2
+ * Scope CLI Test Suite
3
+ *
4
+ * Comprehensive tests for the scope CLI command including:
5
+ * - All subcommands (init, create, list, info, set, unset, remove, archive, activate, sync-up, sync-down)
6
+ * - Help system (main help and subcommand-specific help)
7
+ * - Error handling and edge cases
8
+ * - Integration with ScopeManager, ScopeSync, and other components
9
+ *
10
+ * Usage: node test/test-scope-cli.js
11
+ * Exit codes: 0 = all tests pass, 1 = test failures
12
+ */
13
+
14
+ const fs = require('fs-extra');
15
+ const path = require('node:path');
16
+ const os = require('node:os');
17
+ const { execSync, spawnSync } = require('node:child_process');
18
+
19
+ // ANSI color codes
20
+ const colors = {
21
+ reset: '\u001B[0m',
22
+ green: '\u001B[32m',
23
+ red: '\u001B[31m',
24
+ yellow: '\u001B[33m',
25
+ blue: '\u001B[34m',
26
+ cyan: '\u001B[36m',
27
+ dim: '\u001B[2m',
28
+ bold: '\u001B[1m',
29
+ };
30
+
31
+ // Test utilities
32
+ let testCount = 0;
33
+ let passCount = 0;
34
+ let failCount = 0;
35
+ let skipCount = 0;
36
+ const failures = [];
37
+
38
+ function test(name, fn) {
39
+ testCount++;
40
+ try {
41
+ fn();
42
+ passCount++;
43
+ console.log(` ${colors.green}✓${colors.reset} ${name}`);
44
+ } catch (error) {
45
+ failCount++;
46
+ console.log(` ${colors.red}✗${colors.reset} ${name}`);
47
+ console.log(` ${colors.red}${error.message}${colors.reset}`);
48
+ failures.push({ name, error: error.message });
49
+ }
50
+ }
51
+
52
+ async function testAsync(name, fn) {
53
+ testCount++;
54
+ try {
55
+ await fn();
56
+ passCount++;
57
+ console.log(` ${colors.green}✓${colors.reset} ${name}`);
58
+ } catch (error) {
59
+ failCount++;
60
+ console.log(` ${colors.red}✗${colors.reset} ${name}`);
61
+ console.log(` ${colors.red}${error.message}${colors.reset}`);
62
+ failures.push({ name, error: error.message });
63
+ }
64
+ }
65
+
66
+ function skip(name, reason = '') {
67
+ skipCount++;
68
+ console.log(` ${colors.yellow}○${colors.reset} ${name} ${colors.dim}(skipped${reason ? ': ' + reason : ''})${colors.reset}`);
69
+ }
70
+
71
+ function assertEqual(actual, expected, message = '') {
72
+ if (actual !== expected) {
73
+ throw new Error(`${message}\n Expected: ${JSON.stringify(expected)}\n Actual: ${JSON.stringify(actual)}`);
74
+ }
75
+ }
76
+
77
+ function assertTrue(value, message = 'Expected true') {
78
+ if (!value) {
79
+ throw new Error(message);
80
+ }
81
+ }
82
+
83
+ function assertFalse(value, message = 'Expected false') {
84
+ if (value) {
85
+ throw new Error(message);
86
+ }
87
+ }
88
+
89
+ function assertContains(str, substring, message = '') {
90
+ if (!str.includes(substring)) {
91
+ throw new Error(`${message}\n Expected to contain: "${substring}"\n Actual: "${str.slice(0, 200)}..."`);
92
+ }
93
+ }
94
+
95
+ function assertNotContains(str, substring, message = '') {
96
+ if (str.includes(substring)) {
97
+ throw new Error(`${message}\n Expected NOT to contain: "${substring}"`);
98
+ }
99
+ }
100
+
101
+ function assertExists(filePath, message = '') {
102
+ if (!fs.existsSync(filePath)) {
103
+ throw new Error(`${message || 'File does not exist'}: ${filePath}`);
104
+ }
105
+ }
106
+
107
+ function assertNotExists(filePath, message = '') {
108
+ if (fs.existsSync(filePath)) {
109
+ throw new Error(`${message || 'File should not exist'}: ${filePath}`);
110
+ }
111
+ }
112
+
113
+ // Create temporary test directory with BMAD structure
114
+ function createTestProject() {
115
+ const tmpDir = path.join(os.tmpdir(), `bmad-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
116
+ fs.mkdirSync(tmpDir, { recursive: true });
117
+
118
+ // Create minimal BMAD structure
119
+ fs.mkdirSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true });
120
+ fs.mkdirSync(path.join(tmpDir, '_bmad-output'), { recursive: true });
121
+
122
+ return tmpDir;
123
+ }
124
+
125
+ function cleanupTestProject(tmpDir) {
126
+ try {
127
+ fs.rmSync(tmpDir, { recursive: true, force: true });
128
+ } catch {
129
+ // Ignore cleanup errors
130
+ }
131
+ }
132
+
133
+ // Get path to CLI
134
+ const CLI_PATH = path.join(__dirname, '..', 'tools', 'cli', 'bmad-cli.js');
135
+
136
+ // Execute CLI command and capture output (string-based, for simple cases)
137
+ function runCli(args, cwd, options = {}) {
138
+ const cmd = `node "${CLI_PATH}" ${args}`;
139
+ try {
140
+ const output = execSync(cmd, {
141
+ cwd,
142
+ encoding: 'utf8',
143
+ timeout: options.timeout || 30_000,
144
+ env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
145
+ });
146
+ return { success: true, output, exitCode: 0 };
147
+ } catch (error) {
148
+ return {
149
+ success: false,
150
+ output: error.stdout || '',
151
+ stderr: error.stderr || '',
152
+ exitCode: error.status || 1,
153
+ error: error.message,
154
+ };
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Execute CLI command using spawnSync with an array of arguments.
160
+ * This properly preserves argument boundaries, essential for arguments with spaces.
161
+ *
162
+ * @param {string[]} args - Array of arguments (NOT a joined string)
163
+ * @param {string} cwd - Working directory
164
+ * @param {Object} options - Additional options
165
+ * @returns {Object} Result with success, output, stderr, exitCode
166
+ */
167
+ function runCliArray(args, cwd, options = {}) {
168
+ const result = spawnSync('node', [CLI_PATH, ...args], {
169
+ cwd,
170
+ encoding: 'utf8',
171
+ timeout: options.timeout || 30_000,
172
+ env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
173
+ });
174
+
175
+ return {
176
+ success: result.status === 0,
177
+ output: result.stdout || '',
178
+ stderr: result.stderr || '',
179
+ exitCode: result.status || 0,
180
+ error: result.error ? result.error.message : null,
181
+ };
182
+ }
183
+
184
+ // ============================================================================
185
+ // Help System Tests
186
+ // ============================================================================
187
+
188
+ function testHelpSystem() {
189
+ console.log(`\n${colors.blue}${colors.bold}Help System Tests${colors.reset}`);
190
+
191
+ const tmpDir = createTestProject();
192
+
193
+ try {
194
+ // Main help
195
+ test('scope help shows overview', () => {
196
+ const result = runCli('scope help', tmpDir);
197
+ assertContains(result.output, 'BMAD Scope Management');
198
+ assertContains(result.output, 'OVERVIEW');
199
+ assertContains(result.output, 'COMMANDS');
200
+ });
201
+
202
+ test('scope help shows all commands', () => {
203
+ const result = runCli('scope help', tmpDir);
204
+ assertContains(result.output, 'init');
205
+ assertContains(result.output, 'list');
206
+ assertContains(result.output, 'create');
207
+ assertContains(result.output, 'info');
208
+ assertContains(result.output, 'set');
209
+ assertContains(result.output, 'unset');
210
+ assertContains(result.output, 'remove');
211
+ assertContains(result.output, 'archive');
212
+ assertContains(result.output, 'activate');
213
+ assertContains(result.output, 'sync-up');
214
+ assertContains(result.output, 'sync-down');
215
+ });
216
+
217
+ test('scope help shows options', () => {
218
+ const result = runCli('scope help', tmpDir);
219
+ assertContains(result.output, 'OPTIONS');
220
+ assertContains(result.output, '--name');
221
+ assertContains(result.output, '--description');
222
+ assertContains(result.output, '--force');
223
+ assertContains(result.output, '--dry-run');
224
+ assertContains(result.output, '--resolution');
225
+ });
226
+
227
+ test('scope help shows quick start', () => {
228
+ const result = runCli('scope help', tmpDir);
229
+ assertContains(result.output, 'QUICK START');
230
+ assertContains(result.output, 'scope init');
231
+ assertContains(result.output, 'scope create');
232
+ assertContains(result.output, 'scope set');
233
+ });
234
+
235
+ test('scope help shows directory structure', () => {
236
+ const result = runCli('scope help', tmpDir);
237
+ assertContains(result.output, 'DIRECTORY STRUCTURE');
238
+ assertContains(result.output, '_bmad-output');
239
+ assertContains(result.output, '_shared');
240
+ assertContains(result.output, 'scopes.yaml');
241
+ });
242
+
243
+ test('scope help shows access model', () => {
244
+ const result = runCli('scope help', tmpDir);
245
+ assertContains(result.output, 'ACCESS MODEL');
246
+ assertContains(result.output, 'read-any');
247
+ assertContains(result.output, 'write-own');
248
+ });
249
+
250
+ test('scope help shows troubleshooting', () => {
251
+ const result = runCli('scope help', tmpDir);
252
+ assertContains(result.output, 'TROUBLESHOOTING');
253
+ });
254
+
255
+ // Subcommand-specific help
256
+ test('scope help init shows detailed help', () => {
257
+ const result = runCli('scope help init', tmpDir);
258
+ assertContains(result.output, 'bmad scope init');
259
+ assertContains(result.output, 'DESCRIPTION');
260
+ assertContains(result.output, 'USAGE');
261
+ assertContains(result.output, 'WHAT IT CREATES');
262
+ });
263
+
264
+ test('scope help create shows detailed help', () => {
265
+ const result = runCli('scope help create', tmpDir);
266
+ assertContains(result.output, 'bmad scope create');
267
+ assertContains(result.output, 'ARGUMENTS');
268
+ assertContains(result.output, 'OPTIONS');
269
+ assertContains(result.output, '--name');
270
+ assertContains(result.output, '--deps');
271
+ assertContains(result.output, 'SCOPE ID RULES');
272
+ });
273
+
274
+ test('scope help list shows detailed help', () => {
275
+ const result = runCli('scope help list', tmpDir);
276
+ assertContains(result.output, 'bmad scope list');
277
+ assertContains(result.output, '--status');
278
+ assertContains(result.output, 'OUTPUT COLUMNS');
279
+ });
280
+
281
+ test('scope help info shows detailed help', () => {
282
+ const result = runCli('scope help info', tmpDir);
283
+ assertContains(result.output, 'bmad scope info');
284
+ assertContains(result.output, 'DISPLAYED INFORMATION');
285
+ });
286
+
287
+ test('scope help set shows detailed help', () => {
288
+ const result = runCli('scope help set', tmpDir);
289
+ assertContains(result.output, 'bmad scope set');
290
+ assertContains(result.output, '.bmad-scope');
291
+ assertContains(result.output, 'BMAD_SCOPE');
292
+ assertContains(result.output, 'FILE FORMAT');
293
+ });
294
+
295
+ test('scope help unset shows detailed help', () => {
296
+ const result = runCli('scope help unset', tmpDir);
297
+ assertContains(result.output, 'bmad scope unset');
298
+ assertContains(result.output, 'Clear');
299
+ });
300
+
301
+ test('scope help remove shows detailed help', () => {
302
+ const result = runCli('scope help remove', tmpDir);
303
+ assertContains(result.output, 'bmad scope remove');
304
+ assertContains(result.output, '--force');
305
+ assertContains(result.output, '--no-backup');
306
+ assertContains(result.output, 'BACKUP LOCATION');
307
+ });
308
+
309
+ test('scope help archive shows detailed help', () => {
310
+ const result = runCli('scope help archive', tmpDir);
311
+ assertContains(result.output, 'bmad scope archive');
312
+ assertContains(result.output, 'BEHAVIOR');
313
+ });
314
+
315
+ test('scope help activate shows detailed help', () => {
316
+ const result = runCli('scope help activate', tmpDir);
317
+ assertContains(result.output, 'bmad scope activate');
318
+ assertContains(result.output, 'Reactivate');
319
+ });
320
+
321
+ test('scope help sync-up shows detailed help', () => {
322
+ const result = runCli('scope help sync-up', tmpDir);
323
+ assertContains(result.output, 'bmad scope sync-up');
324
+ assertContains(result.output, 'WHAT GETS PROMOTED');
325
+ assertContains(result.output, '--dry-run');
326
+ assertContains(result.output, '--resolution');
327
+ });
328
+
329
+ test('scope help sync-down shows detailed help', () => {
330
+ const result = runCli('scope help sync-down', tmpDir);
331
+ assertContains(result.output, 'bmad scope sync-down');
332
+ assertContains(result.output, '--dry-run');
333
+ assertContains(result.output, 'keep-local');
334
+ assertContains(result.output, 'keep-shared');
335
+ });
336
+
337
+ // Alias help
338
+ test('scope help ls shows list help', () => {
339
+ const result = runCli('scope help ls', tmpDir);
340
+ assertContains(result.output, 'bmad scope list');
341
+ });
342
+
343
+ test('scope help use shows set help', () => {
344
+ const result = runCli('scope help use', tmpDir);
345
+ assertContains(result.output, 'bmad scope set');
346
+ });
347
+
348
+ test('scope help clear shows unset help', () => {
349
+ const result = runCli('scope help clear', tmpDir);
350
+ assertContains(result.output, 'bmad scope unset');
351
+ });
352
+
353
+ test('scope help rm shows remove help', () => {
354
+ const result = runCli('scope help rm', tmpDir);
355
+ assertContains(result.output, 'bmad scope remove');
356
+ });
357
+
358
+ test('scope help syncup shows sync-up help', () => {
359
+ const result = runCli('scope help syncup', tmpDir);
360
+ assertContains(result.output, 'bmad scope sync-up');
361
+ });
362
+
363
+ // Unknown command help
364
+ test('scope help unknown-cmd shows error', () => {
365
+ const result = runCli('scope help foobar', tmpDir);
366
+ assertContains(result.output, 'Unknown command');
367
+ assertContains(result.output, 'foobar');
368
+ });
369
+
370
+ // No args shows help
371
+ test('scope with no args shows help', () => {
372
+ const result = runCli('scope', tmpDir);
373
+ assertContains(result.output, 'BMAD Scope Management');
374
+ });
375
+ } finally {
376
+ cleanupTestProject(tmpDir);
377
+ }
378
+ }
379
+
380
+ // ============================================================================
381
+ // Init Command Tests
382
+ // ============================================================================
383
+
384
+ function testInitCommand() {
385
+ console.log(`\n${colors.blue}${colors.bold}Init Command Tests${colors.reset}`);
386
+
387
+ test('scope init creates configuration', () => {
388
+ const tmpDir = createTestProject();
389
+ try {
390
+ const result = runCli('scope init', tmpDir);
391
+ assertTrue(result.success, `Init should succeed: ${result.stderr || result.error}`);
392
+ assertContains(result.output, 'initialized successfully');
393
+
394
+ // Check files created
395
+ assertExists(path.join(tmpDir, '_bmad', '_config', 'scopes.yaml'));
396
+ assertExists(path.join(tmpDir, '_bmad-output', '_shared'));
397
+ assertExists(path.join(tmpDir, '_bmad', '_events'));
398
+ } finally {
399
+ cleanupTestProject(tmpDir);
400
+ }
401
+ });
402
+
403
+ test('scope init is idempotent', () => {
404
+ const tmpDir = createTestProject();
405
+ try {
406
+ // Run init twice
407
+ runCli('scope init', tmpDir);
408
+ const result = runCli('scope init', tmpDir);
409
+ assertTrue(result.success, 'Second init should succeed');
410
+ } finally {
411
+ cleanupTestProject(tmpDir);
412
+ }
413
+ });
414
+ }
415
+
416
+ // ============================================================================
417
+ // Create Command Tests
418
+ // ============================================================================
419
+
420
+ function testCreateCommand() {
421
+ console.log(`\n${colors.blue}${colors.bold}Create Command Tests${colors.reset}`);
422
+
423
+ test('scope create with all options', () => {
424
+ const tmpDir = createTestProject();
425
+ try {
426
+ runCli('scope init', tmpDir);
427
+ const result = runCli('scope create auth --name "Authentication" --description "User auth"', tmpDir);
428
+ assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
429
+ assertContains(result.output, "Scope 'auth' created successfully");
430
+
431
+ // Check directories created
432
+ assertExists(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts'));
433
+ assertExists(path.join(tmpDir, '_bmad-output', 'auth', 'implementation-artifacts'));
434
+ assertExists(path.join(tmpDir, '_bmad-output', 'auth', 'tests'));
435
+ } finally {
436
+ cleanupTestProject(tmpDir);
437
+ }
438
+ });
439
+
440
+ test('scope create with dependencies', () => {
441
+ const tmpDir = createTestProject();
442
+ try {
443
+ runCli('scope init', tmpDir);
444
+ runCli('scope create users --name "Users" --description ""', tmpDir);
445
+ const result = runCli('scope create auth --name "Auth" --description "" --deps users', tmpDir);
446
+ assertTrue(result.success, 'Create with deps should succeed');
447
+ } finally {
448
+ cleanupTestProject(tmpDir);
449
+ }
450
+ });
451
+
452
+ test('scope create with --context flag', () => {
453
+ const tmpDir = createTestProject();
454
+ try {
455
+ runCli('scope init', tmpDir);
456
+ const result = runCli('scope create auth --name "Auth" --description "" --context', tmpDir);
457
+ assertTrue(result.success, 'Create with context should succeed');
458
+ // Note: project-context.md creation depends on ScopeInitializer implementation
459
+ } finally {
460
+ cleanupTestProject(tmpDir);
461
+ }
462
+ });
463
+
464
+ test('scope create auto-initializes if needed', () => {
465
+ const tmpDir = createTestProject();
466
+ try {
467
+ // Don't run init, but create should auto-init
468
+ const result = runCli('scope create auth --name "Auth" --description ""', tmpDir);
469
+ assertTrue(result.success, 'Create should auto-init');
470
+ assertExists(path.join(tmpDir, '_bmad', '_config', 'scopes.yaml'));
471
+ } finally {
472
+ cleanupTestProject(tmpDir);
473
+ }
474
+ });
475
+
476
+ test('scope create rejects invalid ID (uppercase)', () => {
477
+ const tmpDir = createTestProject();
478
+ try {
479
+ runCli('scope init', tmpDir);
480
+ const result = runCli('scope create Auth --name "Auth"', tmpDir);
481
+ assertFalse(result.success, 'Should reject uppercase');
482
+ assertContains(result.output + result.stderr, 'Error');
483
+ } finally {
484
+ cleanupTestProject(tmpDir);
485
+ }
486
+ });
487
+
488
+ test('scope create rejects invalid ID (underscore)', () => {
489
+ const tmpDir = createTestProject();
490
+ try {
491
+ runCli('scope init', tmpDir);
492
+ const result = runCli('scope create user_auth --name "Auth" --description ""', tmpDir);
493
+ assertFalse(result.success, 'Should reject underscore');
494
+ } finally {
495
+ cleanupTestProject(tmpDir);
496
+ }
497
+ });
498
+
499
+ test('scope create rejects reserved name _shared', () => {
500
+ const tmpDir = createTestProject();
501
+ try {
502
+ runCli('scope init', tmpDir);
503
+ const result = runCli('scope create _shared --name "Shared" --description ""', tmpDir);
504
+ assertFalse(result.success, 'Should reject _shared');
505
+ } finally {
506
+ cleanupTestProject(tmpDir);
507
+ }
508
+ });
509
+
510
+ test('scope new is alias for create', () => {
511
+ const tmpDir = createTestProject();
512
+ try {
513
+ runCli('scope init', tmpDir);
514
+ const result = runCli('scope new auth --name "Auth"', tmpDir);
515
+ assertTrue(result.success, 'new alias should work');
516
+ } finally {
517
+ cleanupTestProject(tmpDir);
518
+ }
519
+ });
520
+ }
521
+
522
+ // ============================================================================
523
+ // List Command Tests
524
+ // ============================================================================
525
+
526
+ function testListCommand() {
527
+ console.log(`\n${colors.blue}${colors.bold}List Command Tests${colors.reset}`);
528
+
529
+ test('scope list shows no scopes initially', () => {
530
+ const tmpDir = createTestProject();
531
+ try {
532
+ runCli('scope init', tmpDir);
533
+ const result = runCli('scope list', tmpDir);
534
+ assertContains(result.output, 'No scopes found');
535
+ } finally {
536
+ cleanupTestProject(tmpDir);
537
+ }
538
+ });
539
+
540
+ test('scope list shows created scopes', () => {
541
+ const tmpDir = createTestProject();
542
+ try {
543
+ runCli('scope init', tmpDir);
544
+ runCli('scope create auth --name "Authentication" --description ""', tmpDir);
545
+ runCli('scope create payments --name "Payments" --description ""', tmpDir);
546
+
547
+ const result = runCli('scope list', tmpDir);
548
+ assertContains(result.output, 'auth');
549
+ assertContains(result.output, 'payments');
550
+ assertContains(result.output, 'Authentication');
551
+ assertContains(result.output, 'Payments');
552
+ } finally {
553
+ cleanupTestProject(tmpDir);
554
+ }
555
+ });
556
+
557
+ test('scope list --status active filters', () => {
558
+ const tmpDir = createTestProject();
559
+ try {
560
+ runCli('scope init', tmpDir);
561
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
562
+ runCli('scope create old --name "Old" --description ""', tmpDir);
563
+ runCli('scope archive old', tmpDir);
564
+
565
+ const result = runCli('scope list --status active', tmpDir);
566
+ assertContains(result.output, 'auth');
567
+ assertNotContains(result.output, 'old');
568
+ } finally {
569
+ cleanupTestProject(tmpDir);
570
+ }
571
+ });
572
+
573
+ test('scope list --status archived filters', () => {
574
+ const tmpDir = createTestProject();
575
+ try {
576
+ runCli('scope init', tmpDir);
577
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
578
+ runCli('scope create old --name "Old" --description ""', tmpDir);
579
+ runCli('scope archive old', tmpDir);
580
+
581
+ const result = runCli('scope list --status archived', tmpDir);
582
+ assertContains(result.output, 'old');
583
+ } finally {
584
+ cleanupTestProject(tmpDir);
585
+ }
586
+ });
587
+
588
+ test('scope ls is alias for list', () => {
589
+ const tmpDir = createTestProject();
590
+ try {
591
+ runCli('scope init', tmpDir);
592
+ const result = runCli('scope ls', tmpDir);
593
+ assertTrue(result.success, 'ls alias should work');
594
+ } finally {
595
+ cleanupTestProject(tmpDir);
596
+ }
597
+ });
598
+
599
+ test('scope list without init shows helpful message', () => {
600
+ const tmpDir = createTestProject();
601
+ try {
602
+ // Remove the _config directory to simulate uninitialized
603
+ fs.rmSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true, force: true });
604
+
605
+ const result = runCli('scope list', tmpDir);
606
+ assertContains(result.output, 'not initialized');
607
+ } finally {
608
+ cleanupTestProject(tmpDir);
609
+ }
610
+ });
611
+ }
612
+
613
+ // ============================================================================
614
+ // Info Command Tests
615
+ // ============================================================================
616
+
617
+ function testInfoCommand() {
618
+ console.log(`\n${colors.blue}${colors.bold}Info Command Tests${colors.reset}`);
619
+
620
+ test('scope info shows scope details', () => {
621
+ const tmpDir = createTestProject();
622
+ try {
623
+ runCli('scope init', tmpDir);
624
+ runCli('scope create auth --name "Authentication" --description "User auth system"', tmpDir);
625
+
626
+ const result = runCli('scope info auth', tmpDir);
627
+ assertTrue(result.success, 'Info should succeed');
628
+ assertContains(result.output, 'auth');
629
+ assertContains(result.output, 'Authentication');
630
+ assertContains(result.output, 'active');
631
+ assertContains(result.output, 'planning-artifacts');
632
+ } finally {
633
+ cleanupTestProject(tmpDir);
634
+ }
635
+ });
636
+
637
+ test('scope info shows dependencies', () => {
638
+ const tmpDir = createTestProject();
639
+ try {
640
+ runCli('scope init', tmpDir);
641
+ runCli('scope create users --name "Users" --description ""', tmpDir);
642
+ runCli('scope create auth --name "Auth" --description "" --deps users', tmpDir);
643
+
644
+ const result = runCli('scope info auth', tmpDir);
645
+ assertContains(result.output, 'Dependencies');
646
+ assertContains(result.output, 'users');
647
+ } finally {
648
+ cleanupTestProject(tmpDir);
649
+ }
650
+ });
651
+
652
+ test('scope info on non-existent scope fails', () => {
653
+ const tmpDir = createTestProject();
654
+ try {
655
+ runCli('scope init', tmpDir);
656
+ const result = runCli('scope info nonexistent', tmpDir);
657
+ assertFalse(result.success, 'Should fail for non-existent scope');
658
+ assertContains(result.output + result.stderr, 'not found');
659
+ } finally {
660
+ cleanupTestProject(tmpDir);
661
+ }
662
+ });
663
+
664
+ test('scope info requires ID', () => {
665
+ const tmpDir = createTestProject();
666
+ try {
667
+ runCli('scope init', tmpDir);
668
+ const result = runCli('scope info', tmpDir);
669
+ assertFalse(result.success, 'Should require ID');
670
+ assertContains(result.output + result.stderr, 'required');
671
+ } finally {
672
+ cleanupTestProject(tmpDir);
673
+ }
674
+ });
675
+
676
+ test('scope show is alias for info', () => {
677
+ const tmpDir = createTestProject();
678
+ try {
679
+ runCli('scope init', tmpDir);
680
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
681
+ const result = runCli('scope show auth', tmpDir);
682
+ assertTrue(result.success, 'show alias should work');
683
+ } finally {
684
+ cleanupTestProject(tmpDir);
685
+ }
686
+ });
687
+
688
+ test('scope <id> shorthand shows info', () => {
689
+ const tmpDir = createTestProject();
690
+ try {
691
+ runCli('scope init', tmpDir);
692
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
693
+ const result = runCli('scope auth', tmpDir);
694
+ assertTrue(result.success, 'shorthand should work');
695
+ assertContains(result.output, 'auth');
696
+ } finally {
697
+ cleanupTestProject(tmpDir);
698
+ }
699
+ });
700
+ }
701
+
702
+ // ============================================================================
703
+ // Set/Unset Command Tests
704
+ // ============================================================================
705
+
706
+ function testSetUnsetCommands() {
707
+ console.log(`\n${colors.blue}${colors.bold}Set/Unset Command Tests${colors.reset}`);
708
+
709
+ test('scope set creates .bmad-scope file', () => {
710
+ const tmpDir = createTestProject();
711
+ try {
712
+ runCli('scope init', tmpDir);
713
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
714
+
715
+ const result = runCli('scope set auth', tmpDir);
716
+ assertTrue(result.success, `Set should succeed: ${result.stderr || result.error}`);
717
+ assertContains(result.output, "Active scope set to 'auth'");
718
+
719
+ // Check file created
720
+ const scopeFile = path.join(tmpDir, '.bmad-scope');
721
+ assertExists(scopeFile);
722
+
723
+ const content = fs.readFileSync(scopeFile, 'utf8');
724
+ assertContains(content, 'active_scope: auth');
725
+ } finally {
726
+ cleanupTestProject(tmpDir);
727
+ }
728
+ });
729
+
730
+ test('scope set validates scope exists', () => {
731
+ const tmpDir = createTestProject();
732
+ try {
733
+ runCli('scope init', tmpDir);
734
+ const result = runCli('scope set nonexistent', tmpDir);
735
+ assertFalse(result.success, 'Should fail for non-existent scope');
736
+ assertContains(result.output + result.stderr, 'not found');
737
+ } finally {
738
+ cleanupTestProject(tmpDir);
739
+ }
740
+ });
741
+
742
+ test('scope set warns for archived scope', () => {
743
+ const tmpDir = createTestProject();
744
+ try {
745
+ runCli('scope init', tmpDir);
746
+ runCli('scope create old --name "Old" --description ""', tmpDir);
747
+ runCli('scope archive old', tmpDir);
748
+
749
+ // This will prompt for confirmation - we can't easily test interactive mode
750
+ // Just verify it doesn't crash with the scope being archived
751
+ const result = runCli('scope info old', tmpDir);
752
+ assertContains(result.output, 'archived');
753
+ } finally {
754
+ cleanupTestProject(tmpDir);
755
+ }
756
+ });
757
+
758
+ test('scope unset removes .bmad-scope file', () => {
759
+ const tmpDir = createTestProject();
760
+ try {
761
+ runCli('scope init', tmpDir);
762
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
763
+ runCli('scope set auth', tmpDir);
764
+
765
+ const scopeFile = path.join(tmpDir, '.bmad-scope');
766
+ assertExists(scopeFile);
767
+
768
+ const result = runCli('scope unset', tmpDir);
769
+ assertTrue(result.success, 'Unset should succeed');
770
+ assertContains(result.output, 'Active scope cleared');
771
+ assertNotExists(scopeFile);
772
+ } finally {
773
+ cleanupTestProject(tmpDir);
774
+ }
775
+ });
776
+
777
+ test('scope unset when no scope is set', () => {
778
+ const tmpDir = createTestProject();
779
+ try {
780
+ runCli('scope init', tmpDir);
781
+ const result = runCli('scope unset', tmpDir);
782
+ assertTrue(result.success, 'Unset should succeed even if no scope');
783
+ assertContains(result.output, 'No active scope');
784
+ } finally {
785
+ cleanupTestProject(tmpDir);
786
+ }
787
+ });
788
+
789
+ test('scope use is alias for set', () => {
790
+ const tmpDir = createTestProject();
791
+ try {
792
+ runCli('scope init', tmpDir);
793
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
794
+ const result = runCli('scope use auth', tmpDir);
795
+ assertTrue(result.success, 'use alias should work');
796
+ } finally {
797
+ cleanupTestProject(tmpDir);
798
+ }
799
+ });
800
+
801
+ test('scope clear is alias for unset', () => {
802
+ const tmpDir = createTestProject();
803
+ try {
804
+ runCli('scope init', tmpDir);
805
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
806
+ runCli('scope set auth', tmpDir);
807
+ const result = runCli('scope clear', tmpDir);
808
+ assertTrue(result.success, 'clear alias should work');
809
+ } finally {
810
+ cleanupTestProject(tmpDir);
811
+ }
812
+ });
813
+ }
814
+
815
+ // ============================================================================
816
+ // Archive/Activate Command Tests
817
+ // ============================================================================
818
+
819
+ function testArchiveActivateCommands() {
820
+ console.log(`\n${colors.blue}${colors.bold}Archive/Activate Command Tests${colors.reset}`);
821
+
822
+ test('scope archive changes status', () => {
823
+ const tmpDir = createTestProject();
824
+ try {
825
+ runCli('scope init', tmpDir);
826
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
827
+
828
+ const result = runCli('scope archive auth', tmpDir);
829
+ assertTrue(result.success, 'Archive should succeed');
830
+ assertContains(result.output, 'archived');
831
+
832
+ // Verify status changed
833
+ const infoResult = runCli('scope info auth', tmpDir);
834
+ assertContains(infoResult.output, 'archived');
835
+ } finally {
836
+ cleanupTestProject(tmpDir);
837
+ }
838
+ });
839
+
840
+ test('scope archive requires ID', () => {
841
+ const tmpDir = createTestProject();
842
+ try {
843
+ runCli('scope init', tmpDir);
844
+ const result = runCli('scope archive', tmpDir);
845
+ assertFalse(result.success, 'Should require ID');
846
+ } finally {
847
+ cleanupTestProject(tmpDir);
848
+ }
849
+ });
850
+
851
+ test('scope activate reactivates archived scope', () => {
852
+ const tmpDir = createTestProject();
853
+ try {
854
+ runCli('scope init', tmpDir);
855
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
856
+ runCli('scope archive auth', tmpDir);
857
+
858
+ const result = runCli('scope activate auth', tmpDir);
859
+ assertTrue(result.success, 'Activate should succeed');
860
+ assertContains(result.output, 'activated');
861
+
862
+ // Verify status changed back
863
+ const infoResult = runCli('scope info auth', tmpDir);
864
+ assertContains(infoResult.output, 'active');
865
+ } finally {
866
+ cleanupTestProject(tmpDir);
867
+ }
868
+ });
869
+
870
+ test('scope activate requires ID', () => {
871
+ const tmpDir = createTestProject();
872
+ try {
873
+ runCli('scope init', tmpDir);
874
+ const result = runCli('scope activate', tmpDir);
875
+ assertFalse(result.success, 'Should require ID');
876
+ } finally {
877
+ cleanupTestProject(tmpDir);
878
+ }
879
+ });
880
+ }
881
+
882
+ // ============================================================================
883
+ // Remove Command Tests
884
+ // ============================================================================
885
+
886
+ function testRemoveCommand() {
887
+ console.log(`\n${colors.blue}${colors.bold}Remove Command Tests${colors.reset}`);
888
+
889
+ test('scope remove --force removes scope', () => {
890
+ const tmpDir = createTestProject();
891
+ try {
892
+ runCli('scope init', tmpDir);
893
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
894
+
895
+ const result = runCli('scope remove auth --force', tmpDir);
896
+ assertTrue(result.success, 'Remove should succeed');
897
+ assertContains(result.output, 'removed successfully');
898
+
899
+ // Verify scope is gone
900
+ const listResult = runCli('scope list', tmpDir);
901
+ assertNotContains(listResult.output, 'auth');
902
+ } finally {
903
+ cleanupTestProject(tmpDir);
904
+ }
905
+ });
906
+
907
+ test('scope remove creates backup by default', () => {
908
+ const tmpDir = createTestProject();
909
+ try {
910
+ runCli('scope init', tmpDir);
911
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
912
+
913
+ const result = runCli('scope remove auth --force', tmpDir);
914
+ assertContains(result.output, 'backup');
915
+ } finally {
916
+ cleanupTestProject(tmpDir);
917
+ }
918
+ });
919
+
920
+ test('scope remove --force --no-backup skips backup', () => {
921
+ const tmpDir = createTestProject();
922
+ try {
923
+ runCli('scope init', tmpDir);
924
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
925
+
926
+ const result = runCli('scope remove auth --force --no-backup', tmpDir);
927
+ assertTrue(result.success, 'Remove should succeed');
928
+ assertNotContains(result.output, 'backup was created');
929
+ } finally {
930
+ cleanupTestProject(tmpDir);
931
+ }
932
+ });
933
+
934
+ test('scope remove requires ID', () => {
935
+ const tmpDir = createTestProject();
936
+ try {
937
+ runCli('scope init', tmpDir);
938
+ const result = runCli('scope remove --force', tmpDir);
939
+ assertFalse(result.success, 'Should require ID');
940
+ } finally {
941
+ cleanupTestProject(tmpDir);
942
+ }
943
+ });
944
+
945
+ test('scope remove on non-existent scope fails', () => {
946
+ const tmpDir = createTestProject();
947
+ try {
948
+ runCli('scope init', tmpDir);
949
+ const result = runCli('scope remove nonexistent --force', tmpDir);
950
+ assertFalse(result.success, 'Should fail');
951
+ assertContains(result.output + result.stderr, 'not found');
952
+ } finally {
953
+ cleanupTestProject(tmpDir);
954
+ }
955
+ });
956
+
957
+ test('scope rm is alias for remove', () => {
958
+ const tmpDir = createTestProject();
959
+ try {
960
+ runCli('scope init', tmpDir);
961
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
962
+ const result = runCli('scope rm auth --force', tmpDir);
963
+ assertTrue(result.success, 'rm alias should work');
964
+ } finally {
965
+ cleanupTestProject(tmpDir);
966
+ }
967
+ });
968
+
969
+ test('scope delete is alias for remove', () => {
970
+ const tmpDir = createTestProject();
971
+ try {
972
+ runCli('scope init', tmpDir);
973
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
974
+ const result = runCli('scope delete auth --force', tmpDir);
975
+ assertTrue(result.success, 'delete alias should work');
976
+ } finally {
977
+ cleanupTestProject(tmpDir);
978
+ }
979
+ });
980
+ }
981
+
982
+ // ============================================================================
983
+ // Sync Command Tests
984
+ // ============================================================================
985
+
986
+ function testSyncCommands() {
987
+ console.log(`\n${colors.blue}${colors.bold}Sync Command Tests${colors.reset}`);
988
+
989
+ test('scope sync-up requires scope ID', () => {
990
+ const tmpDir = createTestProject();
991
+ try {
992
+ runCli('scope init', tmpDir);
993
+ const result = runCli('scope sync-up', tmpDir);
994
+ assertFalse(result.success, 'Should require ID');
995
+ assertContains(result.output + result.stderr, 'required');
996
+ } finally {
997
+ cleanupTestProject(tmpDir);
998
+ }
999
+ });
1000
+
1001
+ test('scope sync-up validates scope exists', () => {
1002
+ const tmpDir = createTestProject();
1003
+ try {
1004
+ runCli('scope init', tmpDir);
1005
+ const result = runCli('scope sync-up nonexistent', tmpDir);
1006
+ assertFalse(result.success, 'Should fail for non-existent scope');
1007
+ } finally {
1008
+ cleanupTestProject(tmpDir);
1009
+ }
1010
+ });
1011
+
1012
+ test('scope sync-up --dry-run shows analysis', () => {
1013
+ const tmpDir = createTestProject();
1014
+ try {
1015
+ runCli('scope init', tmpDir);
1016
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
1017
+
1018
+ const result = runCli('scope sync-up auth --dry-run', tmpDir);
1019
+ assertTrue(result.success, 'Dry run should succeed');
1020
+ assertContains(result.output, 'Dry Run');
1021
+ assertContains(result.output, 'patterns');
1022
+ } finally {
1023
+ cleanupTestProject(tmpDir);
1024
+ }
1025
+ });
1026
+
1027
+ test('scope sync-up runs without errors', () => {
1028
+ const tmpDir = createTestProject();
1029
+ try {
1030
+ runCli('scope init', tmpDir);
1031
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
1032
+
1033
+ const result = runCli('scope sync-up auth', tmpDir);
1034
+ assertTrue(result.success, `Sync-up should succeed: ${result.stderr || result.error}`);
1035
+ } finally {
1036
+ cleanupTestProject(tmpDir);
1037
+ }
1038
+ });
1039
+
1040
+ test('scope sync-down requires scope ID', () => {
1041
+ const tmpDir = createTestProject();
1042
+ try {
1043
+ runCli('scope init', tmpDir);
1044
+ const result = runCli('scope sync-down', tmpDir);
1045
+ assertFalse(result.success, 'Should require ID');
1046
+ } finally {
1047
+ cleanupTestProject(tmpDir);
1048
+ }
1049
+ });
1050
+
1051
+ test('scope sync-down validates scope exists', () => {
1052
+ const tmpDir = createTestProject();
1053
+ try {
1054
+ runCli('scope init', tmpDir);
1055
+ const result = runCli('scope sync-down nonexistent', tmpDir);
1056
+ assertFalse(result.success, 'Should fail for non-existent scope');
1057
+ } finally {
1058
+ cleanupTestProject(tmpDir);
1059
+ }
1060
+ });
1061
+
1062
+ test('scope sync-down --dry-run shows analysis', () => {
1063
+ const tmpDir = createTestProject();
1064
+ try {
1065
+ runCli('scope init', tmpDir);
1066
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
1067
+
1068
+ const result = runCli('scope sync-down auth --dry-run', tmpDir);
1069
+ assertTrue(result.success, 'Dry run should succeed');
1070
+ assertContains(result.output, 'Dry Run');
1071
+ } finally {
1072
+ cleanupTestProject(tmpDir);
1073
+ }
1074
+ });
1075
+
1076
+ test('scope sync-down runs without errors', () => {
1077
+ const tmpDir = createTestProject();
1078
+ try {
1079
+ runCli('scope init', tmpDir);
1080
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
1081
+
1082
+ const result = runCli('scope sync-down auth', tmpDir);
1083
+ assertTrue(result.success, `Sync-down should succeed: ${result.stderr || result.error}`);
1084
+ } finally {
1085
+ cleanupTestProject(tmpDir);
1086
+ }
1087
+ });
1088
+
1089
+ test('scope syncup is alias for sync-up', () => {
1090
+ const tmpDir = createTestProject();
1091
+ try {
1092
+ runCli('scope init', tmpDir);
1093
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
1094
+ const result = runCli('scope syncup auth --dry-run', tmpDir);
1095
+ assertTrue(result.success, 'syncup alias should work');
1096
+ } finally {
1097
+ cleanupTestProject(tmpDir);
1098
+ }
1099
+ });
1100
+
1101
+ test('scope syncdown is alias for sync-down', () => {
1102
+ const tmpDir = createTestProject();
1103
+ try {
1104
+ runCli('scope init', tmpDir);
1105
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
1106
+ const result = runCli('scope syncdown auth --dry-run', tmpDir);
1107
+ assertTrue(result.success, 'syncdown alias should work');
1108
+ } finally {
1109
+ cleanupTestProject(tmpDir);
1110
+ }
1111
+ });
1112
+ }
1113
+
1114
+ // ============================================================================
1115
+ // Edge Cases and Error Handling Tests
1116
+ // ============================================================================
1117
+
1118
+ function testEdgeCases() {
1119
+ console.log(`\n${colors.blue}${colors.bold}Edge Cases and Error Handling Tests${colors.reset}`);
1120
+
1121
+ test('handles special characters in scope name', () => {
1122
+ const tmpDir = createTestProject();
1123
+ try {
1124
+ runCli('scope init', tmpDir);
1125
+ const result = runCli('scope create auth --name "Auth & Users (v2)" --description ""', tmpDir);
1126
+ assertTrue(result.success, 'Should handle special chars in name');
1127
+ } finally {
1128
+ cleanupTestProject(tmpDir);
1129
+ }
1130
+ });
1131
+
1132
+ test('handles empty description', () => {
1133
+ const tmpDir = createTestProject();
1134
+ try {
1135
+ runCli('scope init', tmpDir);
1136
+ const result = runCli('scope create auth --name "Auth" --description "" --description ""', tmpDir);
1137
+ assertTrue(result.success, 'Should handle empty description');
1138
+ } finally {
1139
+ cleanupTestProject(tmpDir);
1140
+ }
1141
+ });
1142
+
1143
+ test('handles multiple dependencies', () => {
1144
+ const tmpDir = createTestProject();
1145
+ try {
1146
+ runCli('scope init', tmpDir);
1147
+ runCli('scope create users --name "Users" --description ""', tmpDir);
1148
+ runCli('scope create notifications --name "Notifications" --description ""', tmpDir);
1149
+ runCli('scope create logging --name "Logging" --description ""', tmpDir);
1150
+
1151
+ const result = runCli('scope create auth --name "Auth" --description "" --deps users,notifications,logging', tmpDir);
1152
+ assertTrue(result.success, 'Should handle multiple deps');
1153
+ } finally {
1154
+ cleanupTestProject(tmpDir);
1155
+ }
1156
+ });
1157
+
1158
+ test('handles long scope ID', () => {
1159
+ const tmpDir = createTestProject();
1160
+ try {
1161
+ runCli('scope init', tmpDir);
1162
+ const longId = 'a'.repeat(50);
1163
+ const result = runCli(`scope create ${longId} --name "Long ID"`, tmpDir);
1164
+ assertTrue(result.success, 'Should handle long ID');
1165
+ } finally {
1166
+ cleanupTestProject(tmpDir);
1167
+ }
1168
+ });
1169
+
1170
+ test('rejects too long scope ID', () => {
1171
+ const tmpDir = createTestProject();
1172
+ try {
1173
+ runCli('scope init', tmpDir);
1174
+ const tooLongId = 'a'.repeat(51);
1175
+ const result = runCli(`scope create ${tooLongId} --name "Too Long"`, tmpDir);
1176
+ assertFalse(result.success, 'Should reject too long ID');
1177
+ } finally {
1178
+ cleanupTestProject(tmpDir);
1179
+ }
1180
+ });
1181
+
1182
+ test('DEBUG env var enables verbose output', () => {
1183
+ const tmpDir = createTestProject();
1184
+ try {
1185
+ runCli('scope init', tmpDir);
1186
+ runCli('scope create auth --name "Auth" --description ""', tmpDir);
1187
+
1188
+ // Trigger an error with DEBUG enabled
1189
+ const result = runCli('scope info nonexistent', tmpDir, { env: { DEBUG: 'true' } });
1190
+ // Just verify it doesn't crash with DEBUG enabled
1191
+ assertFalse(result.success);
1192
+ } finally {
1193
+ cleanupTestProject(tmpDir);
1194
+ }
1195
+ });
1196
+ }
1197
+
1198
+ // ============================================================================
1199
+ // Integration Tests
1200
+ // ============================================================================
1201
+
1202
+ function testIntegration() {
1203
+ console.log(`\n${colors.blue}${colors.bold}Integration Tests${colors.reset}`);
1204
+
1205
+ test('full workflow: init -> create -> set -> list -> archive -> activate -> remove', () => {
1206
+ const tmpDir = createTestProject();
1207
+ try {
1208
+ // Init
1209
+ let result = runCli('scope init', tmpDir);
1210
+ assertTrue(result.success, 'Init failed');
1211
+
1212
+ // Create scopes
1213
+ result = runCli('scope create auth --name "Authentication" --description ""', tmpDir);
1214
+ assertTrue(result.success, 'Create auth failed');
1215
+
1216
+ result = runCli('scope create payments --name "Payments" --description "" --deps auth', tmpDir);
1217
+ assertTrue(result.success, 'Create payments failed');
1218
+
1219
+ // Set active scope
1220
+ result = runCli('scope set auth', tmpDir);
1221
+ assertTrue(result.success, 'Set failed');
1222
+
1223
+ // List scopes
1224
+ result = runCli('scope list', tmpDir);
1225
+ assertTrue(result.success, 'List failed');
1226
+ assertContains(result.output, 'auth');
1227
+ assertContains(result.output, 'payments');
1228
+
1229
+ // Archive
1230
+ result = runCli('scope archive auth', tmpDir);
1231
+ assertTrue(result.success, 'Archive failed');
1232
+
1233
+ // Activate
1234
+ result = runCli('scope activate auth', tmpDir);
1235
+ assertTrue(result.success, 'Activate failed');
1236
+
1237
+ // Unset
1238
+ result = runCli('scope unset', tmpDir);
1239
+ assertTrue(result.success, 'Unset failed');
1240
+
1241
+ // Remove
1242
+ result = runCli('scope remove payments --force', tmpDir);
1243
+ assertTrue(result.success, 'Remove payments failed');
1244
+
1245
+ result = runCli('scope remove auth --force', tmpDir);
1246
+ assertTrue(result.success, 'Remove auth failed');
1247
+
1248
+ // Verify all gone
1249
+ result = runCli('scope list', tmpDir);
1250
+ assertContains(result.output, 'No scopes found');
1251
+ } finally {
1252
+ cleanupTestProject(tmpDir);
1253
+ }
1254
+ });
1255
+
1256
+ test('parallel scopes simulation', () => {
1257
+ const tmpDir = createTestProject();
1258
+ try {
1259
+ runCli('scope init', tmpDir);
1260
+
1261
+ // Create multiple scopes (simulating parallel development)
1262
+ runCli('scope create frontend --name "Frontend" --description ""', tmpDir);
1263
+ runCli('scope create backend --name "Backend" --description ""', tmpDir);
1264
+ runCli('scope create mobile --name "Mobile" --description "" --deps backend', tmpDir);
1265
+
1266
+ // Verify all exist
1267
+ const result = runCli('scope list', tmpDir);
1268
+ assertContains(result.output, 'frontend');
1269
+ assertContains(result.output, 'backend');
1270
+ assertContains(result.output, 'mobile');
1271
+
1272
+ // Check dependencies
1273
+ const infoResult = runCli('scope info mobile', tmpDir);
1274
+ assertContains(infoResult.output, 'backend');
1275
+ } finally {
1276
+ cleanupTestProject(tmpDir);
1277
+ }
1278
+ });
1279
+ }
1280
+
1281
+ // ============================================================================
1282
+ // Argument Handling Tests (using runCliArray for proper boundary preservation)
1283
+ // ============================================================================
1284
+
1285
+ function testArgumentHandling() {
1286
+ console.log(`\n${colors.blue}${colors.bold}Argument Handling Tests${colors.reset}`);
1287
+
1288
+ test('scope create with multi-word description (array args)', () => {
1289
+ const tmpDir = createTestProject();
1290
+ try {
1291
+ runCliArray(['scope', 'init'], tmpDir);
1292
+ const result = runCliArray(
1293
+ ['scope', 'create', 'auth', '--name', 'Auth Service', '--description', 'Handles user authentication and sessions'],
1294
+ tmpDir,
1295
+ );
1296
+ assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
1297
+
1298
+ const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
1299
+ assertContains(infoResult.output, 'Auth Service');
1300
+ assertContains(infoResult.output, 'Handles user authentication and sessions');
1301
+ } finally {
1302
+ cleanupTestProject(tmpDir);
1303
+ }
1304
+ });
1305
+
1306
+ test('scope create with 9-word description (regression test)', () => {
1307
+ const tmpDir = createTestProject();
1308
+ try {
1309
+ runCliArray(['scope', 'init'], tmpDir);
1310
+ // This exact case caused "too many arguments" error before the fix
1311
+ const result = runCliArray(
1312
+ ['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', 'PRD Auto queue for not inbound yet products'],
1313
+ tmpDir,
1314
+ );
1315
+ assertTrue(result.success, `Should not fail with "too many arguments": ${result.stderr}`);
1316
+ assertNotContains(result.stderr || '', 'too many arguments');
1317
+
1318
+ const infoResult = runCliArray(['scope', 'info', 'auto-queue'], tmpDir);
1319
+ assertContains(infoResult.output, 'PRD Auto queue for not inbound yet products');
1320
+ } finally {
1321
+ cleanupTestProject(tmpDir);
1322
+ }
1323
+ });
1324
+
1325
+ test('all subcommands work with array args', () => {
1326
+ const tmpDir = createTestProject();
1327
+ try {
1328
+ // init
1329
+ let result = runCliArray(['scope', 'init'], tmpDir);
1330
+ assertTrue(result.success, 'init should work');
1331
+
1332
+ // create
1333
+ result = runCliArray(['scope', 'create', 'test', '--name', 'Test Scope', '--description', 'A test scope'], tmpDir);
1334
+ assertTrue(result.success, 'create should work');
1335
+
1336
+ // list
1337
+ result = runCliArray(['scope', 'list'], tmpDir);
1338
+ assertTrue(result.success, 'list should work');
1339
+ assertContains(result.output, 'test');
1340
+
1341
+ // info
1342
+ result = runCliArray(['scope', 'info', 'test'], tmpDir);
1343
+ assertTrue(result.success, 'info should work');
1344
+
1345
+ // set
1346
+ result = runCliArray(['scope', 'set', 'test'], tmpDir);
1347
+ assertTrue(result.success, 'set should work');
1348
+
1349
+ // archive
1350
+ result = runCliArray(['scope', 'archive', 'test'], tmpDir);
1351
+ assertTrue(result.success, 'archive should work');
1352
+
1353
+ // activate
1354
+ result = runCliArray(['scope', 'activate', 'test'], tmpDir);
1355
+ assertTrue(result.success, 'activate should work');
1356
+
1357
+ // sync-up
1358
+ result = runCliArray(['scope', 'sync-up', 'test', '--dry-run'], tmpDir);
1359
+ assertTrue(result.success, 'sync-up should work');
1360
+
1361
+ // sync-down
1362
+ result = runCliArray(['scope', 'sync-down', 'test', '--dry-run'], tmpDir);
1363
+ assertTrue(result.success, 'sync-down should work');
1364
+
1365
+ // unset
1366
+ result = runCliArray(['scope', 'unset'], tmpDir);
1367
+ assertTrue(result.success, 'unset should work');
1368
+
1369
+ // remove
1370
+ result = runCliArray(['scope', 'remove', 'test', '--force'], tmpDir);
1371
+ assertTrue(result.success, 'remove should work');
1372
+
1373
+ // help
1374
+ result = runCliArray(['scope', 'help'], tmpDir);
1375
+ assertTrue(result.success, 'help should work');
1376
+ } finally {
1377
+ cleanupTestProject(tmpDir);
1378
+ }
1379
+ });
1380
+
1381
+ test('subcommand aliases work with array args', () => {
1382
+ const tmpDir = createTestProject();
1383
+ try {
1384
+ runCliArray(['scope', 'init'], tmpDir);
1385
+
1386
+ // new (alias for create) - include --description to avoid interactive prompt
1387
+ let result = runCliArray(['scope', 'new', 'test', '--name', 'Test', '--description', ''], tmpDir);
1388
+ assertTrue(result.success, 'new alias should work');
1389
+
1390
+ // ls (alias for list)
1391
+ result = runCliArray(['scope', 'ls'], tmpDir);
1392
+ assertTrue(result.success, 'ls alias should work');
1393
+
1394
+ // show (alias for info)
1395
+ result = runCliArray(['scope', 'show', 'test'], tmpDir);
1396
+ assertTrue(result.success, 'show alias should work');
1397
+
1398
+ // use (alias for set)
1399
+ result = runCliArray(['scope', 'use', 'test'], tmpDir);
1400
+ assertTrue(result.success, 'use alias should work');
1401
+
1402
+ // clear (alias for unset)
1403
+ result = runCliArray(['scope', 'clear'], tmpDir);
1404
+ assertTrue(result.success, 'clear alias should work');
1405
+
1406
+ // syncup (alias for sync-up)
1407
+ result = runCliArray(['scope', 'syncup', 'test', '--dry-run'], tmpDir);
1408
+ assertTrue(result.success, 'syncup alias should work');
1409
+
1410
+ // syncdown (alias for sync-down)
1411
+ result = runCliArray(['scope', 'syncdown', 'test', '--dry-run'], tmpDir);
1412
+ assertTrue(result.success, 'syncdown alias should work');
1413
+
1414
+ // rm (alias for remove)
1415
+ result = runCliArray(['scope', 'rm', 'test', '--force'], tmpDir);
1416
+ assertTrue(result.success, 'rm alias should work');
1417
+ } finally {
1418
+ cleanupTestProject(tmpDir);
1419
+ }
1420
+ });
1421
+ }
1422
+
1423
+ // ============================================================================
1424
+ // Main Test Runner
1425
+ // ============================================================================
1426
+
1427
+ function main() {
1428
+ console.log(`\n${colors.bold}BMAD Scope CLI Test Suite${colors.reset}`);
1429
+ console.log(colors.dim + '═'.repeat(70) + colors.reset);
1430
+
1431
+ const startTime = Date.now();
1432
+
1433
+ // Run all test suites
1434
+ testHelpSystem();
1435
+ testInitCommand();
1436
+ testCreateCommand();
1437
+ testListCommand();
1438
+ testInfoCommand();
1439
+ testSetUnsetCommands();
1440
+ testArchiveActivateCommands();
1441
+ testRemoveCommand();
1442
+ testSyncCommands();
1443
+ testEdgeCases();
1444
+ testIntegration();
1445
+ testArgumentHandling();
1446
+
1447
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
1448
+
1449
+ // Summary
1450
+ console.log(`\n${colors.dim}${'─'.repeat(70)}${colors.reset}`);
1451
+ console.log(`\n${colors.bold}Test Results${colors.reset}`);
1452
+ console.log(` Total: ${testCount}`);
1453
+ console.log(` ${colors.green}Passed: ${passCount}${colors.reset}`);
1454
+ if (failCount > 0) {
1455
+ console.log(` ${colors.red}Failed: ${failCount}${colors.reset}`);
1456
+ }
1457
+ if (skipCount > 0) {
1458
+ console.log(` ${colors.yellow}Skipped: ${skipCount}${colors.reset}`);
1459
+ }
1460
+ console.log(` Time: ${duration}s`);
1461
+
1462
+ if (failures.length > 0) {
1463
+ console.log(`\n${colors.red}${colors.bold}Failures:${colors.reset}`);
1464
+ for (const { name, error } of failures) {
1465
+ console.log(`\n ${colors.red}✗${colors.reset} ${name}`);
1466
+ console.log(` ${colors.dim}${error}${colors.reset}`);
1467
+ }
1468
+ process.exit(1);
1469
+ }
1470
+
1471
+ console.log(`\n${colors.green}${colors.bold}All tests passed!${colors.reset}\n`);
1472
+ process.exit(0);
1473
+ }
1474
+
1475
+ main();