bmad-fh 6.0.0-alpha.23 → 6.0.0-alpha.23.3b00cb36

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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * End-to-End Test: Multi-Scope Parallel Workflows
3
- *
3
+ *
4
4
  * Tests the complete flow of running parallel workflows in different scopes,
5
5
  * including artifact isolation, sync operations, and event notifications.
6
- *
6
+ *
7
7
  * Usage: node test/test-scope-e2e.js
8
8
  * Exit codes: 0 = all tests pass, 1 = test failures
9
9
  */
@@ -97,12 +97,12 @@ function assertFileContains(filePath, content, message = '') {
97
97
  // Create temporary test directory with BMAD structure
98
98
  function createTestProject() {
99
99
  const tmpDir = path.join(os.tmpdir(), `bmad-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`);
100
-
100
+
101
101
  // Create BMAD directory structure
102
102
  fs.mkdirSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true });
103
103
  fs.mkdirSync(path.join(tmpDir, '_bmad', '_events'), { recursive: true });
104
104
  fs.mkdirSync(path.join(tmpDir, '_bmad-output'), { recursive: true });
105
-
105
+
106
106
  return tmpDir;
107
107
  }
108
108
 
@@ -116,33 +116,33 @@ function cleanupTestProject(tmpDir) {
116
116
 
117
117
  async function testParallelScopeWorkflow() {
118
118
  console.log(`\n${colors.blue}E2E: Parallel Scope Workflow Simulation${colors.reset}`);
119
-
119
+
120
120
  const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
121
121
  const { ScopeContext } = require('../src/core/lib/scope/scope-context');
122
122
  const { ArtifactResolver } = require('../src/core/lib/scope/artifact-resolver');
123
123
  const { ScopeSync } = require('../src/core/lib/scope/scope-sync');
124
124
  const { EventLogger } = require('../src/core/lib/scope/event-logger');
125
-
125
+
126
126
  let tmpDir;
127
-
127
+
128
128
  try {
129
129
  tmpDir = createTestProject();
130
-
130
+
131
131
  // Initialize components
132
132
  const manager = new ScopeManager({ projectRoot: tmpDir });
133
133
  const context = new ScopeContext({ projectRoot: tmpDir });
134
-
134
+
135
135
  // ========================================
136
136
  // Step 1: Initialize scope system
137
137
  // ========================================
138
138
  await asyncTest('Initialize scope system', async () => {
139
139
  await manager.initialize();
140
-
140
+
141
141
  assertFileExists(path.join(tmpDir, '_bmad', '_config', 'scopes.yaml'));
142
142
  assertFileExists(path.join(tmpDir, '_bmad-output', '_shared'));
143
143
  assertFileExists(path.join(tmpDir, '_bmad', '_events'));
144
144
  });
145
-
145
+
146
146
  // ========================================
147
147
  // Step 2: Create two scopes (auth and payments)
148
148
  // ========================================
@@ -151,25 +151,25 @@ async function testParallelScopeWorkflow() {
151
151
  name: 'Authentication Service',
152
152
  description: 'User auth, SSO, authorization',
153
153
  });
154
-
154
+
155
155
  assertEqual(scope.id, 'auth');
156
156
  assertEqual(scope.status, 'active');
157
157
  assertFileExists(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts'));
158
158
  assertFileExists(path.join(tmpDir, '_bmad-output', 'auth', 'implementation-artifacts'));
159
159
  assertFileExists(path.join(tmpDir, '_bmad-output', 'auth', 'tests'));
160
160
  });
161
-
161
+
162
162
  await asyncTest('Create payments scope with dependency on auth', async () => {
163
163
  const scope = await manager.createScope('payments', {
164
164
  name: 'Payment Processing',
165
165
  description: 'Payment gateway integration',
166
166
  dependencies: ['auth'],
167
167
  });
168
-
168
+
169
169
  assertEqual(scope.id, 'payments');
170
170
  assertTrue(scope.dependencies.includes('auth'));
171
171
  });
172
-
172
+
173
173
  // ========================================
174
174
  // Step 3: Simulate parallel artifact creation
175
175
  // ========================================
@@ -177,39 +177,33 @@ async function testParallelScopeWorkflow() {
177
177
  // Create PRD in auth scope
178
178
  const prdPath = path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md');
179
179
  fs.writeFileSync(prdPath, '# Auth PRD\n\nAuthentication requirements...');
180
-
180
+
181
181
  assertFileExists(prdPath);
182
182
  assertFileContains(prdPath, 'Auth PRD');
183
183
  });
184
-
184
+
185
185
  await asyncTest('Simulate parallel PRD creation in payments scope', async () => {
186
186
  // Create PRD in payments scope
187
187
  const prdPath = path.join(tmpDir, '_bmad-output', 'payments', 'planning-artifacts', 'prd.md');
188
188
  fs.writeFileSync(prdPath, '# Payments PRD\n\nPayment processing requirements...');
189
-
189
+
190
190
  assertFileExists(prdPath);
191
191
  assertFileContains(prdPath, 'Payments PRD');
192
192
  });
193
-
193
+
194
194
  // ========================================
195
195
  // Step 4: Verify artifact isolation
196
196
  // ========================================
197
197
  await asyncTest('Verify artifacts are isolated', async () => {
198
- const authPrd = fs.readFileSync(
199
- path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md'),
200
- 'utf8'
201
- );
202
- const paymentsPrd = fs.readFileSync(
203
- path.join(tmpDir, '_bmad-output', 'payments', 'planning-artifacts', 'prd.md'),
204
- 'utf8'
205
- );
206
-
198
+ const authPrd = fs.readFileSync(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md'), 'utf8');
199
+ const paymentsPrd = fs.readFileSync(path.join(tmpDir, '_bmad-output', 'payments', 'planning-artifacts', 'prd.md'), 'utf8');
200
+
207
201
  assertTrue(authPrd.includes('Auth PRD'), 'Auth PRD should contain auth content');
208
202
  assertTrue(paymentsPrd.includes('Payments PRD'), 'Payments PRD should contain payments content');
209
203
  assertFalse(authPrd.includes('Payments'), 'Auth PRD should not contain payments content');
210
204
  assertFalse(paymentsPrd.includes('Auth'), 'Payments PRD should not contain auth content');
211
205
  });
212
-
206
+
213
207
  // ========================================
214
208
  // Step 5: Test ArtifactResolver access control
215
209
  // ========================================
@@ -219,14 +213,14 @@ async function testParallelScopeWorkflow() {
219
213
  basePath: '_bmad-output',
220
214
  projectRoot: tmpDir,
221
215
  });
222
-
223
- // Payments scope can read auth scope
216
+
217
+ // Payments scope can read auth scope - canRead returns {allowed, reason}
224
218
  assertTrue(
225
- resolver.canRead(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md')),
226
- 'Should allow cross-scope read'
219
+ resolver.canRead(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md')).allowed,
220
+ 'Should allow cross-scope read',
227
221
  );
228
222
  });
229
-
223
+
230
224
  await asyncTest('ArtifactResolver blocks cross-scope write', async () => {
231
225
  const resolver = new ArtifactResolver({
232
226
  currentScope: 'payments',
@@ -234,111 +228,106 @@ async function testParallelScopeWorkflow() {
234
228
  projectRoot: tmpDir,
235
229
  isolationMode: 'strict',
236
230
  });
237
-
238
- // Payments scope cannot write to auth scope
231
+
232
+ // Payments scope cannot write to auth scope - canWrite returns {allowed, reason, warning}
239
233
  assertFalse(
240
- resolver.canWrite(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'new.md')),
241
- 'Should block cross-scope write'
234
+ resolver.canWrite(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'new.md')).allowed,
235
+ 'Should block cross-scope write',
242
236
  );
243
237
  });
244
-
238
+
245
239
  // ========================================
246
240
  // Step 6: Test scope context session
247
241
  // ========================================
248
242
  await asyncTest('Session-sticky scope works', async () => {
249
243
  await context.setScope('auth');
250
-
244
+
251
245
  const currentScope = await context.getCurrentScope();
252
246
  assertEqual(currentScope, 'auth', 'Session scope should be auth');
253
-
247
+
254
248
  // Check .bmad-scope file was created
255
249
  assertFileExists(path.join(tmpDir, '.bmad-scope'));
256
250
  });
257
-
251
+
258
252
  await asyncTest('Session scope can be switched', async () => {
259
253
  await context.setScope('payments');
260
-
254
+
261
255
  const currentScope = await context.getCurrentScope();
262
256
  assertEqual(currentScope, 'payments', 'Session scope should be payments');
263
257
  });
264
-
258
+
265
259
  // ========================================
266
260
  // Step 7: Test sync-up (promote to shared)
267
261
  // ========================================
268
262
  await asyncTest('Sync-up promotes artifacts to shared layer', async () => {
269
263
  const sync = new ScopeSync({ projectRoot: tmpDir });
270
-
271
- // Create a promotable artifact (architecture.md)
272
- const archPath = path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'architecture.md');
264
+
265
+ // Create a promotable artifact matching pattern 'architecture/*.md'
266
+ const archPath = path.join(tmpDir, '_bmad-output', 'auth', 'architecture', 'overview.md');
273
267
  fs.mkdirSync(path.dirname(archPath), { recursive: true });
274
268
  fs.writeFileSync(archPath, '# Auth Architecture\n\nShared auth patterns...');
275
-
276
- // Create architecture directory in shared
277
- fs.mkdirSync(path.join(tmpDir, '_bmad-output', '_shared', 'auth'), { recursive: true });
278
-
269
+
279
270
  await sync.syncUp('auth');
280
-
281
- // Check artifact was promoted
271
+
272
+ // Check artifact was promoted to _shared/auth/architecture/overview.md
282
273
  assertFileExists(
283
- path.join(tmpDir, '_bmad-output', '_shared', 'auth', 'architecture.md'),
284
- 'Architecture should be promoted to shared'
274
+ path.join(tmpDir, '_bmad-output', '_shared', 'auth', 'architecture', 'overview.md'),
275
+ 'Architecture should be promoted to shared',
285
276
  );
286
277
  });
287
-
278
+
288
279
  // ========================================
289
280
  // Step 8: Test event logging
290
281
  // ========================================
291
282
  await asyncTest('Events are logged', async () => {
292
283
  const eventLogger = new EventLogger({ projectRoot: tmpDir });
293
-
294
- await eventLogger.log({
295
- type: 'artifact_created',
296
- scope: 'auth',
297
- artifact: 'prd.md',
298
- });
299
-
300
- const events = await eventLogger.getEvents({ scope: 'auth' });
284
+ await eventLogger.initialize();
285
+
286
+ // EventLogger uses logEvent(type, scopeId, data) not log({...})
287
+ await eventLogger.logEvent('artifact_created', 'auth', { artifact: 'prd.md' });
288
+
289
+ // getEvents takes (scopeId, options) not ({scope})
290
+ const events = await eventLogger.getEvents('auth');
301
291
  assertTrue(events.length > 0, 'Should have logged events');
302
292
  assertEqual(events[0].type, 'artifact_created');
303
293
  });
304
-
294
+
305
295
  // ========================================
306
296
  // Step 9: Test dependency tracking
307
297
  // ========================================
308
298
  await asyncTest('Dependent scopes can be found', async () => {
309
299
  const dependents = await manager.findDependentScopes('auth');
310
-
300
+
311
301
  assertTrue(dependents.includes('payments'), 'payments should depend on auth');
312
302
  });
313
-
303
+
314
304
  // ========================================
315
305
  // Step 10: Test scope archival
316
306
  // ========================================
317
307
  await asyncTest('Scope can be archived', async () => {
318
308
  await manager.archiveScope('auth');
319
-
309
+
320
310
  const scope = await manager.getScope('auth');
321
311
  assertEqual(scope.status, 'archived', 'Scope should be archived');
322
-
312
+
323
313
  // Re-activate for cleanup
324
314
  await manager.activateScope('auth');
325
315
  });
326
-
316
+
327
317
  // ========================================
328
318
  // Step 11: Verify final state
329
319
  // ========================================
330
320
  await asyncTest('Final state verification', async () => {
331
321
  const scopes = await manager.listScopes();
332
322
  assertEqual(scopes.length, 2, 'Should have 2 scopes');
333
-
323
+
334
324
  // Both scopes should have their artifacts
335
325
  assertFileExists(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md'));
336
326
  assertFileExists(path.join(tmpDir, '_bmad-output', 'payments', 'planning-artifacts', 'prd.md'));
337
-
327
+
338
328
  // Shared layer should have promoted artifacts
339
329
  assertFileExists(path.join(tmpDir, '_bmad-output', '_shared'));
340
330
  });
341
-
342
331
  } finally {
343
332
  if (tmpDir) {
344
333
  cleanupTestProject(tmpDir);
@@ -352,52 +341,645 @@ async function testParallelScopeWorkflow() {
352
341
 
353
342
  async function testConcurrentLockSimulation() {
354
343
  console.log(`\n${colors.blue}E2E: Concurrent Lock Simulation${colors.reset}`);
355
-
344
+
356
345
  const { StateLock } = require('../src/core/lib/scope/state-lock');
357
-
346
+
358
347
  let tmpDir;
359
-
348
+
360
349
  try {
361
350
  tmpDir = createTestProject();
362
351
  const lock = new StateLock();
363
352
  const lockPath = path.join(tmpDir, 'state.lock');
364
-
353
+
365
354
  // ========================================
366
355
  // Simulate concurrent access from two "terminals"
367
356
  // ========================================
368
357
  await asyncTest('Concurrent operations are serialized', async () => {
369
358
  const results = [];
370
359
  const startTime = Date.now();
371
-
360
+
372
361
  // Simulate Terminal 1 (auth scope)
373
362
  const terminal1 = lock.withLock(lockPath, async () => {
374
363
  results.push({ terminal: 1, action: 'start', time: Date.now() - startTime });
375
- await new Promise(r => setTimeout(r, 50)); // Simulate work
364
+ await new Promise((r) => setTimeout(r, 50)); // Simulate work
376
365
  results.push({ terminal: 1, action: 'end', time: Date.now() - startTime });
377
366
  return 'terminal1';
378
367
  });
379
-
368
+
380
369
  // Simulate Terminal 2 (payments scope) - starts slightly after
381
- await new Promise(r => setTimeout(r, 10));
370
+ await new Promise((r) => setTimeout(r, 10));
382
371
  const terminal2 = lock.withLock(lockPath, async () => {
383
372
  results.push({ terminal: 2, action: 'start', time: Date.now() - startTime });
384
- await new Promise(r => setTimeout(r, 50)); // Simulate work
373
+ await new Promise((r) => setTimeout(r, 50)); // Simulate work
385
374
  results.push({ terminal: 2, action: 'end', time: Date.now() - startTime });
386
375
  return 'terminal2';
387
376
  });
388
-
377
+
389
378
  await Promise.all([terminal1, terminal2]);
390
-
379
+
391
380
  // Terminal 2 should start after Terminal 1 ends
392
- const t1End = results.find(r => r.terminal === 1 && r.action === 'end');
393
- const t2Start = results.find(r => r.terminal === 2 && r.action === 'start');
394
-
395
- assertTrue(
396
- t2Start.time >= t1End.time,
397
- `Terminal 2 should start (${t2Start.time}ms) after Terminal 1 ends (${t1End.time}ms)`
398
- );
381
+ const t1End = results.find((r) => r.terminal === 1 && r.action === 'end');
382
+ const t2Start = results.find((r) => r.terminal === 2 && r.action === 'start');
383
+
384
+ assertTrue(t2Start.time >= t1End.time, `Terminal 2 should start (${t2Start.time}ms) after Terminal 1 ends (${t1End.time}ms)`);
385
+ });
386
+ } finally {
387
+ if (tmpDir) {
388
+ cleanupTestProject(tmpDir);
389
+ }
390
+ }
391
+ }
392
+
393
+ // ============================================================================
394
+ // E2E Test: Help Commands
395
+ // ============================================================================
396
+
397
+ async function testHelpCommandsE2E() {
398
+ console.log(`\n${colors.blue}E2E: Help Commands${colors.reset}`);
399
+
400
+ const { execSync } = require('node:child_process');
401
+ const cliPath = path.join(__dirname, '..', 'tools', 'cli', 'bmad-cli.js');
402
+
403
+ await asyncTest('scope --help shows subcommands', () => {
404
+ const output = execSync(`node ${cliPath} scope --help`, { encoding: 'utf8' });
405
+ assertTrue(output.includes('SUBCOMMANDS'), 'Should show SUBCOMMANDS section');
406
+ assertTrue(output.includes('init'), 'Should mention init');
407
+ assertTrue(output.includes('create'), 'Should mention create');
408
+ assertTrue(output.includes('list'), 'Should mention list');
409
+ });
410
+
411
+ await asyncTest('scope -h shows same as --help', () => {
412
+ const output = execSync(`node ${cliPath} scope -h`, { encoding: 'utf8' });
413
+ assertTrue(output.includes('SUBCOMMANDS'), 'Should show SUBCOMMANDS section');
414
+ });
415
+
416
+ await asyncTest('scope help shows comprehensive documentation', () => {
417
+ const output = execSync(`node ${cliPath} scope help`, { encoding: 'utf8' });
418
+ assertTrue(output.includes('OVERVIEW'), 'Should show OVERVIEW section');
419
+ assertTrue(output.includes('COMMANDS'), 'Should show COMMANDS section');
420
+ assertTrue(output.includes('QUICK START'), 'Should show QUICK START section');
421
+ });
422
+
423
+ await asyncTest('scope help create shows detailed create help', () => {
424
+ const output = execSync(`node ${cliPath} scope help create`, { encoding: 'utf8' });
425
+ assertTrue(output.includes('bmad scope create'), 'Should show create command title');
426
+ assertTrue(output.includes('ARGUMENTS'), 'Should show ARGUMENTS section');
427
+ assertTrue(output.includes('OPTIONS'), 'Should show OPTIONS section');
428
+ });
429
+
430
+ await asyncTest('scope help init shows detailed init help', () => {
431
+ const output = execSync(`node ${cliPath} scope help init`, { encoding: 'utf8' });
432
+ assertTrue(output.includes('bmad scope init'), 'Should show init command title');
433
+ assertTrue(output.includes('DESCRIPTION'), 'Should show DESCRIPTION section');
434
+ });
435
+
436
+ await asyncTest('scope help with invalid subcommand shows error', () => {
437
+ const output = execSync(`node ${cliPath} scope help invalidcommand`, { encoding: 'utf8' });
438
+ assertTrue(output.includes('Unknown command'), 'Should show unknown command error');
439
+ });
440
+
441
+ await asyncTest('scope help works with aliases', () => {
442
+ const output = execSync(`node ${cliPath} scope help ls`, { encoding: 'utf8' });
443
+ assertTrue(output.includes('bmad scope list'), 'Should show list help for ls alias');
444
+ });
445
+ }
446
+
447
+ // ============================================================================
448
+ // E2E Test: Error Handling and Edge Cases
449
+ // ============================================================================
450
+
451
+ async function testErrorHandlingE2E() {
452
+ console.log(`\n${colors.blue}E2E: Error Handling and Edge Cases${colors.reset}`);
453
+
454
+ const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
455
+ const { ScopeContext } = require('../src/core/lib/scope/scope-context');
456
+ const { ScopeSync } = require('../src/core/lib/scope/scope-sync');
457
+
458
+ let tmpDir;
459
+
460
+ try {
461
+ tmpDir = createTestProject();
462
+ const manager = new ScopeManager({ projectRoot: tmpDir });
463
+
464
+ // ========================================
465
+ // Error: Operations on uninitialized system
466
+ // ========================================
467
+ await asyncTest('List scopes on uninitialized system returns empty', async () => {
468
+ // Don't initialize, just try to list
469
+ let result = [];
470
+ try {
471
+ result = await manager.listScopes();
472
+ } catch {
473
+ // Expected - system not initialized
474
+ result = [];
475
+ }
476
+ assertEqual(result.length, 0, 'Should return empty or throw');
477
+ });
478
+
479
+ // Initialize for remaining tests
480
+ await manager.initialize();
481
+
482
+ // ========================================
483
+ // Error: Duplicate scope creation
484
+ // ========================================
485
+ await asyncTest('Creating duplicate scope throws meaningful error', async () => {
486
+ await manager.createScope('duptest', { name: 'Dup Test' });
487
+
488
+ let errorMsg = '';
489
+ try {
490
+ await manager.createScope('duptest', { name: 'Dup Test 2' });
491
+ } catch (error) {
492
+ errorMsg = error.message;
493
+ }
494
+ assertTrue(errorMsg.includes('already exists') || errorMsg.includes('duplicate'), `Error should mention duplicate: ${errorMsg}`);
495
+ });
496
+
497
+ // ========================================
498
+ // Error: Invalid operations on archived scope
499
+ // ========================================
500
+ await asyncTest('Operations on archived scope work correctly', async () => {
501
+ await manager.createScope('archtest', { name: 'Archive Test' });
502
+ await manager.archiveScope('archtest');
503
+
504
+ // Should still be able to get info
505
+ const scope = await manager.getScope('archtest');
506
+ assertEqual(scope.status, 'archived', 'Should get archived scope');
507
+
508
+ // Activate should work
509
+ await manager.activateScope('archtest');
510
+ const reactivated = await manager.getScope('archtest');
511
+ assertEqual(reactivated.status, 'active', 'Should be reactivated');
512
+ });
513
+
514
+ // ========================================
515
+ // Edge: Scope with maximum valid name length
516
+ // ========================================
517
+ await asyncTest('Scope with maximum length name', async () => {
518
+ const longName = 'A'.repeat(200); // Very long name
519
+ const scope = await manager.createScope('longname', {
520
+ name: longName,
521
+ description: 'B'.repeat(500),
522
+ });
523
+ assertEqual(scope.id, 'longname', 'Should create scope with long name');
524
+ });
525
+
526
+ // ========================================
527
+ // Edge: Scope with special characters in name/description
528
+ // ========================================
529
+ await asyncTest('Scope with special characters in metadata', async () => {
530
+ const scope = await manager.createScope('specialchars', {
531
+ name: 'Test <script>alert("xss")</script>',
532
+ description: 'Description with "quotes" and \'apostrophes\' and `backticks`',
533
+ });
534
+ assertEqual(scope.id, 'specialchars', 'Should create scope with special chars in metadata');
535
+ });
536
+
537
+ // ========================================
538
+ // Edge: Empty dependencies array
539
+ // ========================================
540
+ await asyncTest('Scope with empty dependencies array', async () => {
541
+ const scope = await manager.createScope('nodeps', {
542
+ name: 'No Deps',
543
+ dependencies: [],
544
+ });
545
+ assertEqual(scope.dependencies.length, 0, 'Should have no dependencies');
546
+ });
547
+
548
+ // ========================================
549
+ // Sync operations on non-existent scope - documents current behavior
550
+ // ========================================
551
+ await asyncTest('Sync-up on non-existent scope handles gracefully', async () => {
552
+ const sync = new ScopeSync({ projectRoot: tmpDir });
553
+
554
+ // Current implementation may return empty result or throw
555
+ // This documents actual behavior
556
+ let result = null;
557
+ try {
558
+ result = await sync.syncUp('nonexistent');
559
+ // If it doesn't throw, result should indicate no files synced
560
+ assertTrue(result.promoted.length === 0 || result.success !== false, 'Should handle gracefully with no files to sync');
561
+ } catch {
562
+ // Throwing is also acceptable behavior
563
+ assertTrue(true, 'Throws for non-existent scope');
564
+ }
565
+ });
566
+
567
+ // ========================================
568
+ // Edge: Rapid scope status changes
569
+ // ========================================
570
+ await asyncTest('Rapid archive/activate cycles', async () => {
571
+ await manager.createScope('rapidcycle', { name: 'Rapid Cycle' });
572
+
573
+ for (let i = 0; i < 5; i++) {
574
+ await manager.archiveScope('rapidcycle');
575
+ await manager.activateScope('rapidcycle');
576
+ }
577
+
578
+ const scope = await manager.getScope('rapidcycle');
579
+ assertEqual(scope.status, 'active', 'Should end up active after cycles');
580
+ });
581
+ } finally {
582
+ if (tmpDir) {
583
+ cleanupTestProject(tmpDir);
584
+ }
585
+ }
586
+ }
587
+
588
+ // ============================================================================
589
+ // E2E Test: Complex Dependency Scenarios
590
+ // ============================================================================
591
+
592
+ async function testComplexDependencyE2E() {
593
+ console.log(`\n${colors.blue}E2E: Complex Dependency Scenarios${colors.reset}`);
594
+
595
+ const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
596
+
597
+ let tmpDir;
598
+
599
+ try {
600
+ tmpDir = createTestProject();
601
+ const manager = new ScopeManager({ projectRoot: tmpDir });
602
+ await manager.initialize();
603
+
604
+ // ========================================
605
+ // Diamond dependency pattern
606
+ // ========================================
607
+ await asyncTest('Diamond dependency pattern works', async () => {
608
+ // core
609
+ // / \
610
+ // auth user
611
+ // \ /
612
+ // payments
613
+ await manager.createScope('core', { name: 'Core' });
614
+ await manager.createScope('auth', { name: 'Auth', dependencies: ['core'] });
615
+ await manager.createScope('user', { name: 'User', dependencies: ['core'] });
616
+ await manager.createScope('payments', { name: 'Payments', dependencies: ['auth', 'user'] });
617
+
618
+ const payments = await manager.getScope('payments');
619
+ assertTrue(payments.dependencies.includes('auth'), 'Should depend on auth');
620
+ assertTrue(payments.dependencies.includes('user'), 'Should depend on user');
621
+ });
622
+
623
+ // ========================================
624
+ // Finding all dependents in complex graph
625
+ // ========================================
626
+ await asyncTest('Finds all dependents in complex graph', async () => {
627
+ const coreDependents = await manager.findDependentScopes('core');
628
+ assertTrue(coreDependents.includes('auth'), 'auth should depend on core');
629
+ assertTrue(coreDependents.includes('user'), 'user should depend on core');
630
+ // Transitive dependents may or may not be included depending on implementation
631
+ });
632
+
633
+ // ========================================
634
+ // Removing scope in middle of dependency chain
635
+ // ========================================
636
+ await asyncTest('Cannot remove scope with dependents without force', async () => {
637
+ let threw = false;
638
+ try {
639
+ await manager.removeScope('auth'); // payments depends on auth
640
+ } catch {
641
+ threw = true;
642
+ }
643
+ assertTrue(threw, 'Should throw when removing scope with dependents');
644
+ });
645
+
646
+ // ========================================
647
+ // Adding dependency to existing scope
648
+ // ========================================
649
+ await asyncTest('Adding new dependency to existing scope', async () => {
650
+ await manager.createScope('notifications', { name: 'Notifications' });
651
+ await manager.updateScope('payments', {
652
+ dependencies: ['auth', 'user', 'notifications'],
653
+ });
654
+
655
+ const payments = await manager.getScope('payments');
656
+ assertTrue(payments.dependencies.includes('notifications'), 'Should have new dependency');
657
+ });
658
+
659
+ // ========================================
660
+ // Archiving scope in dependency chain
661
+ // ========================================
662
+ await asyncTest('Archiving scope in dependency chain', async () => {
663
+ await manager.archiveScope('auth');
664
+
665
+ // Payments should still exist and have auth as dependency
666
+ const payments = await manager.getScope('payments');
667
+ assertTrue(payments.dependencies.includes('auth'), 'Dependency should remain');
668
+
669
+ // Reactivate for cleanup
670
+ await manager.activateScope('auth');
671
+ });
672
+ } finally {
673
+ if (tmpDir) {
674
+ cleanupTestProject(tmpDir);
675
+ }
676
+ }
677
+ }
678
+
679
+ // ============================================================================
680
+ // E2E Test: Sync Operations Edge Cases
681
+ // ============================================================================
682
+
683
+ async function testSyncOperationsE2E() {
684
+ console.log(`\n${colors.blue}E2E: Sync Operations Edge Cases${colors.reset}`);
685
+
686
+ const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
687
+ const { ScopeSync } = require('../src/core/lib/scope/scope-sync');
688
+
689
+ let tmpDir;
690
+
691
+ try {
692
+ tmpDir = createTestProject();
693
+ const manager = new ScopeManager({ projectRoot: tmpDir });
694
+ await manager.initialize();
695
+
696
+ const sync = new ScopeSync({ projectRoot: tmpDir });
697
+
698
+ // Create test scope
699
+ await manager.createScope('synctest', { name: 'Sync Test' });
700
+
701
+ // ========================================
702
+ // Sync-up with no promotable files
703
+ // ========================================
704
+ await asyncTest('Sync-up with no promotable files', async () => {
705
+ // Create non-promotable file
706
+ const filePath = path.join(tmpDir, '_bmad-output', 'synctest', 'planning-artifacts', 'notes.md');
707
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
708
+ fs.writeFileSync(filePath, '# Random Notes');
709
+
710
+ const result = await sync.syncUp('synctest');
711
+ // Should succeed but with no files promoted
712
+ assertTrue(result.success || result.promoted.length === 0, 'Should handle no promotable files');
713
+ });
714
+
715
+ // ========================================
716
+ // Sync-up with empty architecture directory
717
+ // ========================================
718
+ await asyncTest('Sync-up with empty promotable directory', async () => {
719
+ // Create empty architecture directory
720
+ fs.mkdirSync(path.join(tmpDir, '_bmad-output', 'synctest', 'architecture'), { recursive: true });
721
+
722
+ const result = await sync.syncUp('synctest');
723
+ assertTrue(result.success !== false, 'Should handle empty directory');
724
+ });
725
+
726
+ // ========================================
727
+ // Sync-up with binary files (should skip)
728
+ // ========================================
729
+ await asyncTest('Sync-up skips binary files', async () => {
730
+ // Create a file that might be considered binary
731
+ const archPath = path.join(tmpDir, '_bmad-output', 'synctest', 'architecture', 'diagram.png');
732
+ fs.mkdirSync(path.dirname(archPath), { recursive: true });
733
+ fs.writeFileSync(archPath, Buffer.from([0x89, 0x50, 0x4e, 0x47])); // PNG header
734
+
735
+ const result = await sync.syncUp('synctest');
736
+ // Should succeed, binary might be skipped or included depending on implementation
737
+ assertTrue(result.success !== false, 'Should handle binary files gracefully');
738
+ });
739
+
740
+ // ========================================
741
+ // Create scope when directory already exists - safe by default
742
+ // ========================================
743
+ await asyncTest('Creating scope when directory exists throws by default', async () => {
744
+ // Pre-create the directory
745
+ fs.mkdirSync(path.join(tmpDir, '_bmad-output', 'preexist', 'planning-artifacts'), { recursive: true });
746
+ fs.writeFileSync(path.join(tmpDir, '_bmad-output', 'preexist', 'planning-artifacts', 'existing.md'), '# Existing File');
747
+
748
+ // Create scope - should throw because directory exists (safe default)
749
+ let threw = false;
750
+ let errorMsg = '';
751
+ try {
752
+ await manager.createScope('preexist', { name: 'Pre-existing' });
753
+ } catch (error) {
754
+ threw = true;
755
+ errorMsg = error.message;
756
+ }
757
+
758
+ assertTrue(threw, 'Should throw when directory exists');
759
+ assertTrue(errorMsg.includes('already exists'), 'Error should mention directory exists');
760
+
761
+ // Existing file should still be preserved
762
+ const existingContent = fs.readFileSync(path.join(tmpDir, '_bmad-output', 'preexist', 'planning-artifacts', 'existing.md'), 'utf8');
763
+ assertTrue(existingContent.includes('Existing File'), 'Should preserve existing files');
764
+ });
765
+
766
+ // ========================================
767
+ // Sync with very long file paths
768
+ // ========================================
769
+ await asyncTest('Sync handles deeply nested paths', async () => {
770
+ const deepPath = path.join(tmpDir, '_bmad-output', 'synctest', 'architecture', 'deep', 'nested', 'structure', 'document.md');
771
+ fs.mkdirSync(path.dirname(deepPath), { recursive: true });
772
+ fs.writeFileSync(deepPath, '# Deeply Nested');
773
+
774
+ const result = await sync.syncUp('synctest');
775
+ assertTrue(result.success !== false, 'Should handle deep paths');
776
+ });
777
+
778
+ // ========================================
779
+ // Sync with special characters in filename
780
+ // ========================================
781
+ await asyncTest('Sync handles special characters in filenames', async () => {
782
+ const specialPath = path.join(tmpDir, '_bmad-output', 'synctest', 'architecture', 'design (v2) [draft].md');
783
+ fs.mkdirSync(path.dirname(specialPath), { recursive: true });
784
+ fs.writeFileSync(specialPath, '# Design v2 Draft');
785
+
786
+ const result = await sync.syncUp('synctest');
787
+ assertTrue(result.success !== false, 'Should handle special chars in filenames');
788
+ });
789
+ } finally {
790
+ if (tmpDir) {
791
+ cleanupTestProject(tmpDir);
792
+ }
793
+ }
794
+ }
795
+
796
+ // ============================================================================
797
+ // E2E Test: File System Edge Cases
798
+ // ============================================================================
799
+
800
+ async function testFileSystemEdgeCasesE2E() {
801
+ console.log(`\n${colors.blue}E2E: File System Edge Cases${colors.reset}`);
802
+
803
+ const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
804
+ const { ScopeInitializer } = require('../src/core/lib/scope/scope-initializer');
805
+
806
+ let tmpDir;
807
+
808
+ try {
809
+ tmpDir = createTestProject();
810
+ const manager = new ScopeManager({ projectRoot: tmpDir });
811
+ const initializer = new ScopeInitializer({ projectRoot: tmpDir });
812
+ await manager.initialize();
813
+
814
+ // ========================================
815
+ // Remove scope with readonly files
816
+ // ========================================
817
+ await asyncTest('Remove scope handles readonly files', async () => {
818
+ await manager.createScope('readonly', { name: 'Readonly Test' });
819
+
820
+ // Make a file readonly
821
+ const filePath = path.join(tmpDir, '_bmad-output', 'readonly', 'planning-artifacts', 'locked.md');
822
+ fs.writeFileSync(filePath, '# Locked');
823
+ try {
824
+ fs.chmodSync(filePath, 0o444); // Read-only
825
+ } catch {
826
+ // Windows might not support chmod
827
+ }
828
+
829
+ // Remove should handle this gracefully
830
+ let removed = false;
831
+ try {
832
+ await initializer.removeScope('readonly', { backup: false });
833
+ await manager.removeScope('readonly', { force: true });
834
+ removed = true;
835
+ } catch {
836
+ // May fail on some systems, that's ok
837
+ // Clean up by making it writable again
838
+ try {
839
+ fs.chmodSync(filePath, 0o644);
840
+ await initializer.removeScope('readonly', { backup: false });
841
+ await manager.removeScope('readonly', { force: true });
842
+ removed = true;
843
+ } catch {
844
+ // Ignore cleanup errors
845
+ }
846
+ }
847
+ // Just verify it attempted the operation
848
+ assertTrue(true, 'Attempted removal of readonly files');
849
+ });
850
+
851
+ // ========================================
852
+ // Scope with symlinks (if supported)
853
+ // ========================================
854
+ await asyncTest('Scope handles symlinks gracefully', async () => {
855
+ await manager.createScope('symtest', { name: 'Symlink Test' });
856
+
857
+ const targetPath = path.join(tmpDir, '_bmad-output', 'symtest', 'planning-artifacts', 'target.md');
858
+ const linkPath = path.join(tmpDir, '_bmad-output', 'symtest', 'planning-artifacts', 'link.md');
859
+
860
+ fs.writeFileSync(targetPath, '# Target');
861
+
862
+ try {
863
+ fs.symlinkSync(targetPath, linkPath);
864
+
865
+ // Should be able to read through symlink
866
+ const content = fs.readFileSync(linkPath, 'utf8');
867
+ assertTrue(content.includes('Target'), 'Should read through symlink');
868
+ } catch {
869
+ // Symlinks may not be supported on all systems
870
+ assertTrue(true, 'Symlinks not supported on this system');
871
+ }
872
+ });
873
+
874
+ // ========================================
875
+ // Large number of files in scope
876
+ // ========================================
877
+ await asyncTest('Scope with many files', async () => {
878
+ await manager.createScope('manyfiles', { name: 'Many Files' });
879
+
880
+ const planningDir = path.join(tmpDir, '_bmad-output', 'manyfiles', 'planning-artifacts');
881
+
882
+ // Create 100 files
883
+ for (let i = 0; i < 100; i++) {
884
+ fs.writeFileSync(path.join(planningDir, `file-${i}.md`), `# File ${i}`);
885
+ }
886
+
887
+ // Should still be able to manage scope
888
+ const scope = await manager.getScope('manyfiles');
889
+ assertEqual(scope.id, 'manyfiles', 'Should manage scope with many files');
890
+ });
891
+ } finally {
892
+ if (tmpDir) {
893
+ cleanupTestProject(tmpDir);
894
+ }
895
+ }
896
+ }
897
+
898
+ // ============================================================================
899
+ // E2E Test: Concurrent Operations Stress Test
900
+ // ============================================================================
901
+
902
+ async function testConcurrentOperationsE2E() {
903
+ console.log(`\n${colors.blue}E2E: Concurrent Operations Stress Test${colors.reset}`);
904
+
905
+ const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
906
+ const { ScopeContext } = require('../src/core/lib/scope/scope-context');
907
+
908
+ let tmpDir;
909
+
910
+ try {
911
+ tmpDir = createTestProject();
912
+ const manager = new ScopeManager({ projectRoot: tmpDir });
913
+ await manager.initialize();
914
+
915
+ // ========================================
916
+ // Concurrent scope creation stress test
917
+ // ========================================
918
+ await asyncTest('Concurrent scope creations (stress test)', async () => {
919
+ const createPromises = [];
920
+ for (let i = 0; i < 20; i++) {
921
+ createPromises.push(
922
+ manager
923
+ .createScope(`concurrent-${i}`, { name: `Concurrent ${i}` })
924
+ .catch((error) => ({ error: error.message, id: `concurrent-${i}` })),
925
+ );
926
+ }
927
+
928
+ const results = await Promise.all(createPromises);
929
+
930
+ // Count successes
931
+ const successes = results.filter((r) => !r.error);
932
+ assertTrue(successes.length > 0, 'At least some concurrent creates should succeed');
933
+
934
+ // Verify all created scopes exist
935
+ const scopes = await manager.listScopes();
936
+ assertTrue(scopes.length >= successes.length, 'All successful creates should persist');
937
+ });
938
+
939
+ // ========================================
940
+ // Concurrent read/write operations
941
+ // ========================================
942
+ await asyncTest('Concurrent reads during writes', async () => {
943
+ await manager.createScope('rwtest', { name: 'Read/Write Test' });
944
+
945
+ const operations = [];
946
+
947
+ // Mix of reads and writes
948
+ for (let i = 0; i < 10; i++) {
949
+ if (i % 2 === 0) {
950
+ // Read
951
+ operations.push(manager.getScope('rwtest'));
952
+ } else {
953
+ // Write (update)
954
+ operations.push(manager.updateScope('rwtest', { description: `Update ${i}` }));
955
+ }
956
+ }
957
+
958
+ await Promise.all(operations);
959
+
960
+ // Verify scope is still valid
961
+ const scope = await manager.getScope('rwtest');
962
+ assertEqual(scope.id, 'rwtest', 'Scope should still be valid');
963
+ });
964
+
965
+ // ========================================
966
+ // Concurrent context switches
967
+ // ========================================
968
+ await asyncTest('Concurrent context switches', async () => {
969
+ const context1 = new ScopeContext({ projectRoot: tmpDir });
970
+ const context2 = new ScopeContext({ projectRoot: tmpDir });
971
+
972
+ // Both try to set different scopes
973
+ const [, scope1, scope2] = await Promise.all([
974
+ manager.createScope('ctx1', { name: 'Context 1' }),
975
+ context1.setScope('rwtest').then(() => context1.getCurrentScope()),
976
+ context2.setScope('rwtest').then(() => context2.getCurrentScope()),
977
+ ]);
978
+
979
+ // One should win (last write wins)
980
+ const finalScope = await context1.getCurrentScope();
981
+ assertTrue(finalScope === 'rwtest', 'Should have a valid scope set');
399
982
  });
400
-
401
983
  } finally {
402
984
  if (tmpDir) {
403
985
  cleanupTestProject(tmpDir);
@@ -417,6 +999,14 @@ async function main() {
417
999
  try {
418
1000
  await testParallelScopeWorkflow();
419
1001
  await testConcurrentLockSimulation();
1002
+
1003
+ // New comprehensive E2E tests
1004
+ await testHelpCommandsE2E();
1005
+ await testErrorHandlingE2E();
1006
+ await testComplexDependencyE2E();
1007
+ await testSyncOperationsE2E();
1008
+ await testFileSystemEdgeCasesE2E();
1009
+ await testConcurrentOperationsE2E();
420
1010
  } catch (error) {
421
1011
  console.log(`\n${colors.red}Fatal error: ${error.message}${colors.reset}`);
422
1012
  console.log(error.stack);