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