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.
@@ -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
-
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);