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
package/test/test-scope-e2e.js
CHANGED
|
@@ -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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
216
|
+
|
|
223
217
|
// Payments scope can read auth scope
|
|
224
218
|
assertTrue(
|
|
225
219
|
resolver.canRead(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md')),
|
|
226
|
-
'Should allow cross-scope read'
|
|
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,110 @@ async function testParallelScopeWorkflow() {
|
|
|
234
228
|
projectRoot: tmpDir,
|
|
235
229
|
isolationMode: 'strict',
|
|
236
230
|
});
|
|
237
|
-
|
|
231
|
+
|
|
238
232
|
// Payments scope cannot write to auth scope
|
|
239
233
|
assertFalse(
|
|
240
234
|
resolver.canWrite(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'new.md')),
|
|
241
|
-
'Should block cross-scope write'
|
|
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
|
-
|
|
264
|
+
|
|
271
265
|
// Create a promotable artifact (architecture.md)
|
|
272
266
|
const archPath = path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'architecture.md');
|
|
273
267
|
fs.mkdirSync(path.dirname(archPath), { recursive: true });
|
|
274
268
|
fs.writeFileSync(archPath, '# Auth Architecture\n\nShared auth patterns...');
|
|
275
|
-
|
|
269
|
+
|
|
276
270
|
// Create architecture directory in shared
|
|
277
271
|
fs.mkdirSync(path.join(tmpDir, '_bmad-output', '_shared', 'auth'), { recursive: true });
|
|
278
|
-
|
|
272
|
+
|
|
279
273
|
await sync.syncUp('auth');
|
|
280
|
-
|
|
274
|
+
|
|
281
275
|
// Check artifact was promoted
|
|
282
276
|
assertFileExists(
|
|
283
277
|
path.join(tmpDir, '_bmad-output', '_shared', 'auth', 'architecture.md'),
|
|
284
|
-
'Architecture should be promoted to shared'
|
|
278
|
+
'Architecture should be promoted to shared',
|
|
285
279
|
);
|
|
286
280
|
});
|
|
287
|
-
|
|
281
|
+
|
|
288
282
|
// ========================================
|
|
289
283
|
// Step 8: Test event logging
|
|
290
284
|
// ========================================
|
|
291
285
|
await asyncTest('Events are logged', async () => {
|
|
292
286
|
const eventLogger = new EventLogger({ projectRoot: tmpDir });
|
|
293
|
-
|
|
287
|
+
|
|
294
288
|
await eventLogger.log({
|
|
295
289
|
type: 'artifact_created',
|
|
296
290
|
scope: 'auth',
|
|
297
291
|
artifact: 'prd.md',
|
|
298
292
|
});
|
|
299
|
-
|
|
293
|
+
|
|
300
294
|
const events = await eventLogger.getEvents({ scope: 'auth' });
|
|
301
295
|
assertTrue(events.length > 0, 'Should have logged events');
|
|
302
296
|
assertEqual(events[0].type, 'artifact_created');
|
|
303
297
|
});
|
|
304
|
-
|
|
298
|
+
|
|
305
299
|
// ========================================
|
|
306
300
|
// Step 9: Test dependency tracking
|
|
307
301
|
// ========================================
|
|
308
302
|
await asyncTest('Dependent scopes can be found', async () => {
|
|
309
303
|
const dependents = await manager.findDependentScopes('auth');
|
|
310
|
-
|
|
304
|
+
|
|
311
305
|
assertTrue(dependents.includes('payments'), 'payments should depend on auth');
|
|
312
306
|
});
|
|
313
|
-
|
|
307
|
+
|
|
314
308
|
// ========================================
|
|
315
309
|
// Step 10: Test scope archival
|
|
316
310
|
// ========================================
|
|
317
311
|
await asyncTest('Scope can be archived', async () => {
|
|
318
312
|
await manager.archiveScope('auth');
|
|
319
|
-
|
|
313
|
+
|
|
320
314
|
const scope = await manager.getScope('auth');
|
|
321
315
|
assertEqual(scope.status, 'archived', 'Scope should be archived');
|
|
322
|
-
|
|
316
|
+
|
|
323
317
|
// Re-activate for cleanup
|
|
324
318
|
await manager.activateScope('auth');
|
|
325
319
|
});
|
|
326
|
-
|
|
320
|
+
|
|
327
321
|
// ========================================
|
|
328
322
|
// Step 11: Verify final state
|
|
329
323
|
// ========================================
|
|
330
324
|
await asyncTest('Final state verification', async () => {
|
|
331
325
|
const scopes = await manager.listScopes();
|
|
332
326
|
assertEqual(scopes.length, 2, 'Should have 2 scopes');
|
|
333
|
-
|
|
327
|
+
|
|
334
328
|
// Both scopes should have their artifacts
|
|
335
329
|
assertFileExists(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts', 'prd.md'));
|
|
336
330
|
assertFileExists(path.join(tmpDir, '_bmad-output', 'payments', 'planning-artifacts', 'prd.md'));
|
|
337
|
-
|
|
331
|
+
|
|
338
332
|
// Shared layer should have promoted artifacts
|
|
339
333
|
assertFileExists(path.join(tmpDir, '_bmad-output', '_shared'));
|
|
340
334
|
});
|
|
341
|
-
|
|
342
335
|
} finally {
|
|
343
336
|
if (tmpDir) {
|
|
344
337
|
cleanupTestProject(tmpDir);
|
|
@@ -352,52 +345,48 @@ async function testParallelScopeWorkflow() {
|
|
|
352
345
|
|
|
353
346
|
async function testConcurrentLockSimulation() {
|
|
354
347
|
console.log(`\n${colors.blue}E2E: Concurrent Lock Simulation${colors.reset}`);
|
|
355
|
-
|
|
348
|
+
|
|
356
349
|
const { StateLock } = require('../src/core/lib/scope/state-lock');
|
|
357
|
-
|
|
350
|
+
|
|
358
351
|
let tmpDir;
|
|
359
|
-
|
|
352
|
+
|
|
360
353
|
try {
|
|
361
354
|
tmpDir = createTestProject();
|
|
362
355
|
const lock = new StateLock();
|
|
363
356
|
const lockPath = path.join(tmpDir, 'state.lock');
|
|
364
|
-
|
|
357
|
+
|
|
365
358
|
// ========================================
|
|
366
359
|
// Simulate concurrent access from two "terminals"
|
|
367
360
|
// ========================================
|
|
368
361
|
await asyncTest('Concurrent operations are serialized', async () => {
|
|
369
362
|
const results = [];
|
|
370
363
|
const startTime = Date.now();
|
|
371
|
-
|
|
364
|
+
|
|
372
365
|
// Simulate Terminal 1 (auth scope)
|
|
373
366
|
const terminal1 = lock.withLock(lockPath, async () => {
|
|
374
367
|
results.push({ terminal: 1, action: 'start', time: Date.now() - startTime });
|
|
375
|
-
await new Promise(r => setTimeout(r, 50)); // Simulate work
|
|
368
|
+
await new Promise((r) => setTimeout(r, 50)); // Simulate work
|
|
376
369
|
results.push({ terminal: 1, action: 'end', time: Date.now() - startTime });
|
|
377
370
|
return 'terminal1';
|
|
378
371
|
});
|
|
379
|
-
|
|
372
|
+
|
|
380
373
|
// Simulate Terminal 2 (payments scope) - starts slightly after
|
|
381
|
-
await new Promise(r => setTimeout(r, 10));
|
|
374
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
382
375
|
const terminal2 = lock.withLock(lockPath, async () => {
|
|
383
376
|
results.push({ terminal: 2, action: 'start', time: Date.now() - startTime });
|
|
384
|
-
await new Promise(r => setTimeout(r, 50)); // Simulate work
|
|
377
|
+
await new Promise((r) => setTimeout(r, 50)); // Simulate work
|
|
385
378
|
results.push({ terminal: 2, action: 'end', time: Date.now() - startTime });
|
|
386
379
|
return 'terminal2';
|
|
387
380
|
});
|
|
388
|
-
|
|
381
|
+
|
|
389
382
|
await Promise.all([terminal1, terminal2]);
|
|
390
|
-
|
|
383
|
+
|
|
391
384
|
// 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
|
-
);
|
|
385
|
+
const t1End = results.find((r) => r.terminal === 1 && r.action === 'end');
|
|
386
|
+
const t2Start = results.find((r) => r.terminal === 2 && r.action === 'start');
|
|
387
|
+
|
|
388
|
+
assertTrue(t2Start.time >= t1End.time, `Terminal 2 should start (${t2Start.time}ms) after Terminal 1 ends (${t1End.time}ms)`);
|
|
399
389
|
});
|
|
400
|
-
|
|
401
390
|
} finally {
|
|
402
391
|
if (tmpDir) {
|
|
403
392
|
cleanupTestProject(tmpDir);
|