bmad-fh 6.0.0-alpha.23 → 6.0.0-alpha.23.6fbcf839
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish-multi-artifact.yaml +6 -2
- package/eslint.config.mjs +2 -2
- package/package.json +2 -2
- package/src/bmm/module.yaml +2 -2
- package/src/core/lib/scope/artifact-resolver.js +26 -26
- package/src/core/lib/scope/event-logger.js +34 -45
- package/src/core/lib/scope/index.js +3 -3
- package/src/core/lib/scope/scope-context.js +22 -28
- package/src/core/lib/scope/scope-initializer.js +29 -31
- package/src/core/lib/scope/scope-manager.js +21 -21
- package/src/core/lib/scope/scope-migrator.js +44 -52
- package/src/core/lib/scope/scope-sync.js +42 -48
- package/src/core/lib/scope/scope-validator.js +16 -21
- package/src/core/lib/scope/state-lock.js +37 -43
- package/src/core/module.yaml +2 -2
- package/test/test-scope-e2e.js +65 -76
- package/test/test-scope-system.js +66 -72
- package/tools/cli/commands/scope.js +73 -73
- package/tools/cli/installers/lib/modules/manager.js +2 -2
- package/tools/cli/scripts/migrate-workflows.js +43 -51
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scope System Test Suite
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Tests for multi-scope parallel artifacts system including:
|
|
5
5
|
* - ScopeValidator: ID validation, schema validation, circular dependency detection
|
|
6
6
|
* - ScopeManager: CRUD operations, path resolution
|
|
7
7
|
* - ArtifactResolver: Read/write access control
|
|
8
8
|
* - StateLock: File locking and optimistic versioning
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* Usage: node test/test-scope-system.js
|
|
11
11
|
* Exit codes: 0 = all tests pass, 1 = test failures
|
|
12
12
|
*/
|
|
@@ -104,7 +104,7 @@ function cleanupTempDir(tmpDir) {
|
|
|
104
104
|
|
|
105
105
|
function testScopeValidator() {
|
|
106
106
|
console.log(`\n${colors.blue}ScopeValidator Tests${colors.reset}`);
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
const { ScopeValidator } = require('../src/core/lib/scope/scope-validator');
|
|
109
109
|
const validator = new ScopeValidator();
|
|
110
110
|
|
|
@@ -209,11 +209,11 @@ function testScopeValidator() {
|
|
|
209
209
|
|
|
210
210
|
function testScopeManager() {
|
|
211
211
|
console.log(`\n${colors.blue}ScopeManager Tests${colors.reset}`);
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
|
|
214
|
-
|
|
214
|
+
|
|
215
215
|
let tmpDir;
|
|
216
|
-
|
|
216
|
+
|
|
217
217
|
// Setup/teardown for each test
|
|
218
218
|
function setup() {
|
|
219
219
|
tmpDir = createTempDir();
|
|
@@ -222,7 +222,7 @@ function testScopeManager() {
|
|
|
222
222
|
fs.mkdirSync(path.join(tmpDir, '_bmad-output'), { recursive: true });
|
|
223
223
|
return new ScopeManager({ projectRoot: tmpDir });
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
function teardown() {
|
|
227
227
|
if (tmpDir) {
|
|
228
228
|
cleanupTempDir(tmpDir);
|
|
@@ -260,7 +260,7 @@ function testScopeManager() {
|
|
|
260
260
|
try {
|
|
261
261
|
await manager.initialize();
|
|
262
262
|
await manager.createScope('auth', { name: 'Auth' });
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
const scopePath = path.join(tmpDir, '_bmad-output', 'auth');
|
|
265
265
|
assertTrue(fs.existsSync(scopePath), 'Scope directory should exist');
|
|
266
266
|
assertTrue(fs.existsSync(path.join(scopePath, 'planning-artifacts')), 'planning-artifacts should exist');
|
|
@@ -310,7 +310,7 @@ function testScopeManager() {
|
|
|
310
310
|
try {
|
|
311
311
|
await manager.initialize();
|
|
312
312
|
await manager.createScope('auth', { name: 'Authentication', description: 'Auth service' });
|
|
313
|
-
|
|
313
|
+
|
|
314
314
|
const scope = await manager.getScope('auth');
|
|
315
315
|
assertEqual(scope.id, 'auth', 'ID should match');
|
|
316
316
|
assertEqual(scope.name, 'Authentication', 'Name should match');
|
|
@@ -338,7 +338,7 @@ function testScopeManager() {
|
|
|
338
338
|
await manager.initialize();
|
|
339
339
|
await manager.createScope('auth', { name: 'Auth' });
|
|
340
340
|
await manager.createScope('payments', { name: 'Payments' });
|
|
341
|
-
|
|
341
|
+
|
|
342
342
|
const scopes = await manager.listScopes();
|
|
343
343
|
assertEqual(scopes.length, 2, 'Should have 2 scopes');
|
|
344
344
|
} finally {
|
|
@@ -353,7 +353,7 @@ function testScopeManager() {
|
|
|
353
353
|
await manager.createScope('auth', { name: 'Auth' });
|
|
354
354
|
await manager.createScope('legacy', { name: 'Legacy' });
|
|
355
355
|
await manager.archiveScope('legacy');
|
|
356
|
-
|
|
356
|
+
|
|
357
357
|
const activeScopes = await manager.listScopes({ status: 'active' });
|
|
358
358
|
assertEqual(activeScopes.length, 1, 'Should have 1 active scope');
|
|
359
359
|
assertEqual(activeScopes[0].id, 'auth', 'Active scope should be auth');
|
|
@@ -368,9 +368,9 @@ function testScopeManager() {
|
|
|
368
368
|
try {
|
|
369
369
|
await manager.initialize();
|
|
370
370
|
await manager.createScope('auth', { name: 'Auth', description: 'Old desc' });
|
|
371
|
-
|
|
371
|
+
|
|
372
372
|
await manager.updateScope('auth', { description: 'New description' });
|
|
373
|
-
|
|
373
|
+
|
|
374
374
|
const scope = await manager.getScope('auth');
|
|
375
375
|
assertEqual(scope.description, 'New description', 'Description should be updated');
|
|
376
376
|
} finally {
|
|
@@ -384,9 +384,9 @@ function testScopeManager() {
|
|
|
384
384
|
try {
|
|
385
385
|
await manager.initialize();
|
|
386
386
|
await manager.createScope('auth', { name: 'Auth' });
|
|
387
|
-
|
|
387
|
+
|
|
388
388
|
await manager.archiveScope('auth');
|
|
389
|
-
|
|
389
|
+
|
|
390
390
|
const scope = await manager.getScope('auth');
|
|
391
391
|
assertEqual(scope.status, 'archived', 'Status should be archived');
|
|
392
392
|
} finally {
|
|
@@ -400,9 +400,9 @@ function testScopeManager() {
|
|
|
400
400
|
await manager.initialize();
|
|
401
401
|
await manager.createScope('auth', { name: 'Auth' });
|
|
402
402
|
await manager.archiveScope('auth');
|
|
403
|
-
|
|
403
|
+
|
|
404
404
|
await manager.activateScope('auth');
|
|
405
|
-
|
|
405
|
+
|
|
406
406
|
const scope = await manager.getScope('auth');
|
|
407
407
|
assertEqual(scope.status, 'active', 'Status should be active');
|
|
408
408
|
} finally {
|
|
@@ -416,7 +416,7 @@ function testScopeManager() {
|
|
|
416
416
|
try {
|
|
417
417
|
await manager.initialize();
|
|
418
418
|
await manager.createScope('auth', { name: 'Auth' });
|
|
419
|
-
|
|
419
|
+
|
|
420
420
|
const paths = await manager.getScopePaths('auth');
|
|
421
421
|
assertTrue(paths.root.includes('auth'), 'Root path should contain scope ID');
|
|
422
422
|
assertTrue(paths.planning.includes('planning-artifacts'), 'Should have planning path');
|
|
@@ -434,7 +434,7 @@ function testScopeManager() {
|
|
|
434
434
|
await manager.initialize();
|
|
435
435
|
await manager.createScope('auth', { name: 'Auth' });
|
|
436
436
|
await manager.createScope('payments', { name: 'Payments', dependencies: ['auth'] });
|
|
437
|
-
|
|
437
|
+
|
|
438
438
|
const scope = await manager.getScope('payments');
|
|
439
439
|
assertArrayEqual(scope.dependencies, ['auth'], 'Dependencies should be set');
|
|
440
440
|
} finally {
|
|
@@ -449,7 +449,7 @@ function testScopeManager() {
|
|
|
449
449
|
await manager.createScope('auth', { name: 'Auth' });
|
|
450
450
|
await manager.createScope('payments', { name: 'Payments', dependencies: ['auth'] });
|
|
451
451
|
await manager.createScope('orders', { name: 'Orders', dependencies: ['auth'] });
|
|
452
|
-
|
|
452
|
+
|
|
453
453
|
const dependents = await manager.findDependentScopes('auth');
|
|
454
454
|
assertEqual(dependents.length, 2, 'Should have 2 dependents');
|
|
455
455
|
assertTrue(dependents.includes('payments'), 'payments should depend on auth');
|
|
@@ -466,7 +466,7 @@ function testScopeManager() {
|
|
|
466
466
|
|
|
467
467
|
function testArtifactResolver() {
|
|
468
468
|
console.log(`\n${colors.blue}ArtifactResolver Tests${colors.reset}`);
|
|
469
|
-
|
|
469
|
+
|
|
470
470
|
const { ArtifactResolver } = require('../src/core/lib/scope/artifact-resolver');
|
|
471
471
|
|
|
472
472
|
test('allows read from any scope', () => {
|
|
@@ -474,7 +474,7 @@ function testArtifactResolver() {
|
|
|
474
474
|
currentScope: 'auth',
|
|
475
475
|
basePath: '_bmad-output',
|
|
476
476
|
});
|
|
477
|
-
|
|
477
|
+
|
|
478
478
|
assertTrue(resolver.canRead('_bmad-output/payments/planning-artifacts/prd.md'), 'Should allow cross-scope read');
|
|
479
479
|
assertTrue(resolver.canRead('_bmad-output/auth/planning-artifacts/prd.md'), 'Should allow own-scope read');
|
|
480
480
|
assertTrue(resolver.canRead('_bmad-output/_shared/project-context.md'), 'Should allow shared read');
|
|
@@ -485,7 +485,7 @@ function testArtifactResolver() {
|
|
|
485
485
|
currentScope: 'auth',
|
|
486
486
|
basePath: '_bmad-output',
|
|
487
487
|
});
|
|
488
|
-
|
|
488
|
+
|
|
489
489
|
assertTrue(resolver.canWrite('_bmad-output/auth/planning-artifacts/prd.md'), 'Should allow own-scope write');
|
|
490
490
|
});
|
|
491
491
|
|
|
@@ -495,7 +495,7 @@ function testArtifactResolver() {
|
|
|
495
495
|
basePath: '_bmad-output',
|
|
496
496
|
isolationMode: 'strict',
|
|
497
497
|
});
|
|
498
|
-
|
|
498
|
+
|
|
499
499
|
assertFalse(resolver.canWrite('_bmad-output/payments/planning-artifacts/prd.md'), 'Should block cross-scope write');
|
|
500
500
|
});
|
|
501
501
|
|
|
@@ -504,7 +504,7 @@ function testArtifactResolver() {
|
|
|
504
504
|
currentScope: 'auth',
|
|
505
505
|
basePath: '_bmad-output',
|
|
506
506
|
});
|
|
507
|
-
|
|
507
|
+
|
|
508
508
|
assertFalse(resolver.canWrite('_bmad-output/_shared/project-context.md'), 'Should block _shared write');
|
|
509
509
|
});
|
|
510
510
|
|
|
@@ -513,7 +513,7 @@ function testArtifactResolver() {
|
|
|
513
513
|
currentScope: 'auth',
|
|
514
514
|
basePath: '_bmad-output',
|
|
515
515
|
});
|
|
516
|
-
|
|
516
|
+
|
|
517
517
|
assertEqual(resolver.extractScopeFromPath('_bmad-output/payments/planning-artifacts/prd.md'), 'payments');
|
|
518
518
|
assertEqual(resolver.extractScopeFromPath('_bmad-output/auth/tests/unit.js'), 'auth');
|
|
519
519
|
assertEqual(resolver.extractScopeFromPath('_bmad-output/_shared/context.md'), '_shared');
|
|
@@ -525,11 +525,8 @@ function testArtifactResolver() {
|
|
|
525
525
|
basePath: '_bmad-output',
|
|
526
526
|
isolationMode: 'strict',
|
|
527
527
|
});
|
|
528
|
-
|
|
529
|
-
assertThrows(
|
|
530
|
-
() => resolver.validateWrite('_bmad-output/payments/prd.md'),
|
|
531
|
-
'Cannot write to scope'
|
|
532
|
-
);
|
|
528
|
+
|
|
529
|
+
assertThrows(() => resolver.validateWrite('_bmad-output/payments/prd.md'), 'Cannot write to scope');
|
|
533
530
|
});
|
|
534
531
|
|
|
535
532
|
test('warns on cross-scope write in warn mode', () => {
|
|
@@ -538,7 +535,7 @@ function testArtifactResolver() {
|
|
|
538
535
|
basePath: '_bmad-output',
|
|
539
536
|
isolationMode: 'warn',
|
|
540
537
|
});
|
|
541
|
-
|
|
538
|
+
|
|
542
539
|
// Should not throw in warn mode
|
|
543
540
|
const result = resolver.canWrite('_bmad-output/payments/prd.md');
|
|
544
541
|
// In warn mode, it may return true but log a warning
|
|
@@ -552,16 +549,16 @@ function testArtifactResolver() {
|
|
|
552
549
|
|
|
553
550
|
function testStateLock() {
|
|
554
551
|
console.log(`\n${colors.blue}StateLock Tests${colors.reset}`);
|
|
555
|
-
|
|
552
|
+
|
|
556
553
|
const { StateLock } = require('../src/core/lib/scope/state-lock');
|
|
557
|
-
|
|
554
|
+
|
|
558
555
|
let tmpDir;
|
|
559
|
-
|
|
556
|
+
|
|
560
557
|
function setup() {
|
|
561
558
|
tmpDir = createTempDir();
|
|
562
559
|
return new StateLock();
|
|
563
560
|
}
|
|
564
|
-
|
|
561
|
+
|
|
565
562
|
function teardown() {
|
|
566
563
|
if (tmpDir) {
|
|
567
564
|
cleanupTempDir(tmpDir);
|
|
@@ -572,11 +569,11 @@ function testStateLock() {
|
|
|
572
569
|
const lock = setup();
|
|
573
570
|
try {
|
|
574
571
|
const lockPath = path.join(tmpDir, 'test.lock');
|
|
575
|
-
|
|
572
|
+
|
|
576
573
|
const result = await lock.withLock(lockPath, async () => {
|
|
577
574
|
return 'success';
|
|
578
575
|
});
|
|
579
|
-
|
|
576
|
+
|
|
580
577
|
assertEqual(result, 'success', 'Should return operation result');
|
|
581
578
|
} finally {
|
|
582
579
|
teardown();
|
|
@@ -588,24 +585,24 @@ function testStateLock() {
|
|
|
588
585
|
try {
|
|
589
586
|
const lockPath = path.join(tmpDir, 'test.lock');
|
|
590
587
|
const order = [];
|
|
591
|
-
|
|
588
|
+
|
|
592
589
|
// Start first operation (holds lock)
|
|
593
590
|
const op1 = lock.withLock(lockPath, async () => {
|
|
594
591
|
order.push('op1-start');
|
|
595
|
-
await new Promise(r => setTimeout(r, 100));
|
|
592
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
596
593
|
order.push('op1-end');
|
|
597
594
|
return 'op1';
|
|
598
595
|
});
|
|
599
|
-
|
|
596
|
+
|
|
600
597
|
// Start second operation immediately (should wait)
|
|
601
|
-
await new Promise(r => setTimeout(r, 10));
|
|
598
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
602
599
|
const op2 = lock.withLock(lockPath, async () => {
|
|
603
600
|
order.push('op2');
|
|
604
601
|
return 'op2';
|
|
605
602
|
});
|
|
606
|
-
|
|
603
|
+
|
|
607
604
|
await Promise.all([op1, op2]);
|
|
608
|
-
|
|
605
|
+
|
|
609
606
|
// op2 should start after op1 ends
|
|
610
607
|
assertTrue(order.indexOf('op1-end') < order.indexOf('op2'), 'op2 should run after op1 completes');
|
|
611
608
|
} finally {
|
|
@@ -617,13 +614,16 @@ function testStateLock() {
|
|
|
617
614
|
const lock = setup();
|
|
618
615
|
try {
|
|
619
616
|
const lockPath = path.join(tmpDir, 'test.lock');
|
|
620
|
-
|
|
617
|
+
|
|
621
618
|
// Create a stale lock file manually
|
|
622
|
-
fs.writeFileSync(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
619
|
+
fs.writeFileSync(
|
|
620
|
+
lockPath,
|
|
621
|
+
JSON.stringify({
|
|
622
|
+
pid: 99_999_999, // Non-existent PID
|
|
623
|
+
timestamp: Date.now() - 60_000, // 60 seconds ago
|
|
624
|
+
}),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
627
|
// Should be able to acquire lock despite stale file
|
|
628
628
|
const result = await lock.withLock(lockPath, async () => 'success');
|
|
629
629
|
assertEqual(result, 'success', 'Should acquire lock after stale detection');
|
|
@@ -639,19 +639,19 @@ function testStateLock() {
|
|
|
639
639
|
|
|
640
640
|
function testScopeContext() {
|
|
641
641
|
console.log(`\n${colors.blue}ScopeContext Tests${colors.reset}`);
|
|
642
|
-
|
|
642
|
+
|
|
643
643
|
const { ScopeContext } = require('../src/core/lib/scope/scope-context');
|
|
644
644
|
const { ScopeManager } = require('../src/core/lib/scope/scope-manager');
|
|
645
|
-
|
|
645
|
+
|
|
646
646
|
let tmpDir;
|
|
647
|
-
|
|
647
|
+
|
|
648
648
|
function setup() {
|
|
649
649
|
tmpDir = createTempDir();
|
|
650
650
|
fs.mkdirSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true });
|
|
651
651
|
fs.mkdirSync(path.join(tmpDir, '_bmad-output', '_shared'), { recursive: true });
|
|
652
652
|
return new ScopeContext({ projectRoot: tmpDir });
|
|
653
653
|
}
|
|
654
|
-
|
|
654
|
+
|
|
655
655
|
function teardown() {
|
|
656
656
|
if (tmpDir) {
|
|
657
657
|
cleanupTempDir(tmpDir);
|
|
@@ -665,9 +665,9 @@ function testScopeContext() {
|
|
|
665
665
|
const manager = new ScopeManager({ projectRoot: tmpDir });
|
|
666
666
|
await manager.initialize();
|
|
667
667
|
await manager.createScope('auth', { name: 'Auth' });
|
|
668
|
-
|
|
668
|
+
|
|
669
669
|
await context.setScope('auth');
|
|
670
|
-
|
|
670
|
+
|
|
671
671
|
const scopeFile = path.join(tmpDir, '.bmad-scope');
|
|
672
672
|
assertTrue(fs.existsSync(scopeFile), '.bmad-scope file should be created');
|
|
673
673
|
} finally {
|
|
@@ -682,10 +682,10 @@ function testScopeContext() {
|
|
|
682
682
|
const manager = new ScopeManager({ projectRoot: tmpDir });
|
|
683
683
|
await manager.initialize();
|
|
684
684
|
await manager.createScope('auth', { name: 'Auth' });
|
|
685
|
-
|
|
685
|
+
|
|
686
686
|
await context.setScope('auth');
|
|
687
687
|
const current = await context.getCurrentScope();
|
|
688
|
-
|
|
688
|
+
|
|
689
689
|
assertEqual(current, 'auth', 'Should return session scope');
|
|
690
690
|
} finally {
|
|
691
691
|
teardown();
|
|
@@ -698,10 +698,10 @@ function testScopeContext() {
|
|
|
698
698
|
const manager = new ScopeManager({ projectRoot: tmpDir });
|
|
699
699
|
await manager.initialize();
|
|
700
700
|
await manager.createScope('auth', { name: 'Auth' });
|
|
701
|
-
|
|
701
|
+
|
|
702
702
|
await context.setScope('auth');
|
|
703
703
|
await context.clearScope();
|
|
704
|
-
|
|
704
|
+
|
|
705
705
|
const current = await context.getCurrentScope();
|
|
706
706
|
assertEqual(current, null, 'Should return null after clearing');
|
|
707
707
|
} finally {
|
|
@@ -715,22 +715,16 @@ function testScopeContext() {
|
|
|
715
715
|
const manager = new ScopeManager({ projectRoot: tmpDir });
|
|
716
716
|
await manager.initialize();
|
|
717
717
|
await manager.createScope('auth', { name: 'Auth' });
|
|
718
|
-
|
|
718
|
+
|
|
719
719
|
// Create global context
|
|
720
|
-
fs.writeFileSync(
|
|
721
|
-
|
|
722
|
-
'# Global Project\n\nGlobal content here.'
|
|
723
|
-
);
|
|
724
|
-
|
|
720
|
+
fs.writeFileSync(path.join(tmpDir, '_bmad-output', '_shared', 'project-context.md'), '# Global Project\n\nGlobal content here.');
|
|
721
|
+
|
|
725
722
|
// Create scope-specific context
|
|
726
723
|
fs.mkdirSync(path.join(tmpDir, '_bmad-output', 'auth'), { recursive: true });
|
|
727
|
-
fs.writeFileSync(
|
|
728
|
-
|
|
729
|
-
'# Auth Scope\n\nScope-specific content.'
|
|
730
|
-
);
|
|
731
|
-
|
|
724
|
+
fs.writeFileSync(path.join(tmpDir, '_bmad-output', 'auth', 'project-context.md'), '# Auth Scope\n\nScope-specific content.');
|
|
725
|
+
|
|
732
726
|
const merged = await context.loadProjectContext('auth');
|
|
733
|
-
|
|
727
|
+
|
|
734
728
|
assertTrue(merged.includes('Global content'), 'Should include global content');
|
|
735
729
|
assertTrue(merged.includes('Scope-specific content'), 'Should include scope content');
|
|
736
730
|
} finally {
|