bmad-fh 6.0.0-alpha.23.66f19588 → 6.0.0-alpha.23.6874ced1
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/.husky/post-checkout +12 -0
- package/.husky/pre-commit +17 -2
- package/.husky/pre-push +10 -0
- package/README.md +13 -7
- package/docs/migration-guide.md +27 -7
- package/docs/multi-scope-guide.md +69 -33
- package/docs/plans/multi-scope-parallel-artifacts-plan.md +112 -91
- package/package.json +3 -2
- package/src/core/lib/scope/scope-context.js +13 -2
- package/src/core/lib/scope/scope-manager.js +1 -1
- package/src/core/lib/scope/scope-validator.js +6 -4
- package/test/test-cli-arguments.js +686 -0
- package/test/test-scope-cli.js +171 -2
- package/tools/bmad-npx-wrapper.js +12 -2
- package/tools/build-docs.js +1 -0
- package/tools/cli/commands/scope.js +37 -2
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Argument Handling Test Suite
|
|
3
|
+
*
|
|
4
|
+
* Tests for proper handling of CLI arguments, especially:
|
|
5
|
+
* - Arguments containing spaces
|
|
6
|
+
* - Arguments with special characters
|
|
7
|
+
* - The npx wrapper's argument preservation
|
|
8
|
+
* - Various quoting scenarios
|
|
9
|
+
*
|
|
10
|
+
* This test suite was created to prevent regression of the bug where
|
|
11
|
+
* the npx wrapper used args.join(' ') which broke arguments containing spaces.
|
|
12
|
+
*
|
|
13
|
+
* Usage: node test/test-cli-arguments.js
|
|
14
|
+
* Exit codes: 0 = all tests pass, 1 = test failures
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs-extra');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const os = require('node:os');
|
|
20
|
+
const { spawnSync } = require('node:child_process');
|
|
21
|
+
|
|
22
|
+
// ANSI color codes
|
|
23
|
+
const colors = {
|
|
24
|
+
reset: '\u001B[0m',
|
|
25
|
+
green: '\u001B[32m',
|
|
26
|
+
red: '\u001B[31m',
|
|
27
|
+
yellow: '\u001B[33m',
|
|
28
|
+
blue: '\u001B[34m',
|
|
29
|
+
cyan: '\u001B[36m',
|
|
30
|
+
dim: '\u001B[2m',
|
|
31
|
+
bold: '\u001B[1m',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Test utilities
|
|
35
|
+
let testCount = 0;
|
|
36
|
+
let passCount = 0;
|
|
37
|
+
let failCount = 0;
|
|
38
|
+
let skipCount = 0;
|
|
39
|
+
const failures = [];
|
|
40
|
+
|
|
41
|
+
function test(name, fn) {
|
|
42
|
+
testCount++;
|
|
43
|
+
try {
|
|
44
|
+
fn();
|
|
45
|
+
passCount++;
|
|
46
|
+
console.log(` ${colors.green}✓${colors.reset} ${name}`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
failCount++;
|
|
49
|
+
console.log(` ${colors.red}✗${colors.reset} ${name}`);
|
|
50
|
+
console.log(` ${colors.red}${error.message}${colors.reset}`);
|
|
51
|
+
failures.push({ name, error: error.message });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function testAsync(name, fn) {
|
|
56
|
+
testCount++;
|
|
57
|
+
try {
|
|
58
|
+
await fn();
|
|
59
|
+
passCount++;
|
|
60
|
+
console.log(` ${colors.green}✓${colors.reset} ${name}`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
failCount++;
|
|
63
|
+
console.log(` ${colors.red}✗${colors.reset} ${name}`);
|
|
64
|
+
console.log(` ${colors.red}${error.message}${colors.reset}`);
|
|
65
|
+
failures.push({ name, error: error.message });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function skip(name, reason = '') {
|
|
70
|
+
skipCount++;
|
|
71
|
+
console.log(` ${colors.yellow}○${colors.reset} ${name} ${colors.dim}(skipped${reason ? ': ' + reason : ''})${colors.reset}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertEqual(actual, expected, message = '') {
|
|
75
|
+
if (actual !== expected) {
|
|
76
|
+
throw new Error(`${message}\n Expected: ${JSON.stringify(expected)}\n Actual: ${JSON.stringify(actual)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertTrue(value, message = 'Expected true') {
|
|
81
|
+
if (!value) {
|
|
82
|
+
throw new Error(message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertFalse(value, message = 'Expected false') {
|
|
87
|
+
if (value) {
|
|
88
|
+
throw new Error(message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertContains(str, substring, message = '') {
|
|
93
|
+
if (!str.includes(substring)) {
|
|
94
|
+
throw new Error(`${message}\n Expected to contain: "${substring}"\n Actual: "${str.slice(0, 500)}..."`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertNotContains(str, substring, message = '') {
|
|
99
|
+
if (str.includes(substring)) {
|
|
100
|
+
throw new Error(`${message}\n Expected NOT to contain: "${substring}"`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertExists(filePath, message = '') {
|
|
105
|
+
if (!fs.existsSync(filePath)) {
|
|
106
|
+
throw new Error(`${message || 'File does not exist'}: ${filePath}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create temporary test directory with BMAD structure
|
|
111
|
+
function createTestProject() {
|
|
112
|
+
const tmpDir = path.join(os.tmpdir(), `bmad-cli-args-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
113
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
// Create minimal BMAD structure
|
|
116
|
+
fs.mkdirSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true });
|
|
117
|
+
fs.mkdirSync(path.join(tmpDir, '_bmad-output'), { recursive: true });
|
|
118
|
+
|
|
119
|
+
return tmpDir;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cleanupTestProject(tmpDir) {
|
|
123
|
+
try {
|
|
124
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
125
|
+
} catch {
|
|
126
|
+
// Ignore cleanup errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Paths to CLI entry points
|
|
131
|
+
const CLI_PATH = path.join(__dirname, '..', 'tools', 'cli', 'bmad-cli.js');
|
|
132
|
+
const NPX_WRAPPER_PATH = path.join(__dirname, '..', 'tools', 'bmad-npx-wrapper.js');
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute CLI command using spawnSync with an array of arguments.
|
|
136
|
+
* This properly preserves argument boundaries, just like the shell does.
|
|
137
|
+
*
|
|
138
|
+
* @param {string[]} args - Array of arguments (NOT a joined string)
|
|
139
|
+
* @param {string} cwd - Working directory
|
|
140
|
+
* @param {Object} options - Additional options
|
|
141
|
+
* @returns {Object} Result with success, output, stderr, exitCode
|
|
142
|
+
*/
|
|
143
|
+
function runCliArray(args, cwd, options = {}) {
|
|
144
|
+
const result = spawnSync('node', [CLI_PATH, ...args], {
|
|
145
|
+
cwd,
|
|
146
|
+
encoding: 'utf8',
|
|
147
|
+
timeout: options.timeout || 30_000,
|
|
148
|
+
env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
success: result.status === 0,
|
|
153
|
+
output: result.stdout || '',
|
|
154
|
+
stderr: result.stderr || '',
|
|
155
|
+
exitCode: result.status || 0,
|
|
156
|
+
error: result.error ? result.error.message : null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Execute CLI command via the npx wrapper using spawnSync.
|
|
162
|
+
* This tests the actual npx execution path.
|
|
163
|
+
*
|
|
164
|
+
* @param {string[]} args - Array of arguments
|
|
165
|
+
* @param {string} cwd - Working directory
|
|
166
|
+
* @param {Object} options - Additional options
|
|
167
|
+
* @returns {Object} Result with success, output, stderr, exitCode
|
|
168
|
+
*/
|
|
169
|
+
function runNpxWrapper(args, cwd, options = {}) {
|
|
170
|
+
const result = spawnSync('node', [NPX_WRAPPER_PATH, ...args], {
|
|
171
|
+
cwd,
|
|
172
|
+
encoding: 'utf8',
|
|
173
|
+
timeout: options.timeout || 30_000,
|
|
174
|
+
env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
success: result.status === 0,
|
|
179
|
+
output: result.stdout || '',
|
|
180
|
+
stderr: result.stderr || '',
|
|
181
|
+
exitCode: result.status || 0,
|
|
182
|
+
error: result.error ? result.error.message : null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Arguments with Spaces Tests
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
function testArgumentsWithSpaces() {
|
|
191
|
+
console.log(`\n${colors.blue}${colors.bold}Arguments with Spaces Tests${colors.reset}`);
|
|
192
|
+
|
|
193
|
+
test('scope create with description containing spaces (direct CLI)', () => {
|
|
194
|
+
const tmpDir = createTestProject();
|
|
195
|
+
try {
|
|
196
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
197
|
+
const result = runCliArray(
|
|
198
|
+
['scope', 'create', 'test-scope', '--name', 'Test Scope', '--description', 'This is a description with multiple words'],
|
|
199
|
+
tmpDir,
|
|
200
|
+
);
|
|
201
|
+
assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
|
|
202
|
+
assertContains(result.output, "Scope 'test-scope' created successfully");
|
|
203
|
+
|
|
204
|
+
// Verify the description was saved correctly
|
|
205
|
+
const infoResult = runCliArray(['scope', 'info', 'test-scope'], tmpDir);
|
|
206
|
+
assertContains(infoResult.output, 'This is a description with multiple words');
|
|
207
|
+
} finally {
|
|
208
|
+
cleanupTestProject(tmpDir);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('scope create with description containing spaces (via npx wrapper)', () => {
|
|
213
|
+
const tmpDir = createTestProject();
|
|
214
|
+
try {
|
|
215
|
+
runNpxWrapper(['scope', 'init'], tmpDir);
|
|
216
|
+
const result = runNpxWrapper(
|
|
217
|
+
['scope', 'create', 'test-scope', '--name', 'Test Scope', '--description', 'This is a description with multiple words'],
|
|
218
|
+
tmpDir,
|
|
219
|
+
);
|
|
220
|
+
assertTrue(result.success, `Create should succeed via wrapper: ${result.stderr || result.error}`);
|
|
221
|
+
assertContains(result.output, "Scope 'test-scope' created successfully");
|
|
222
|
+
|
|
223
|
+
// Verify the description was saved correctly
|
|
224
|
+
const infoResult = runNpxWrapper(['scope', 'info', 'test-scope'], tmpDir);
|
|
225
|
+
assertContains(infoResult.output, 'This is a description with multiple words');
|
|
226
|
+
} finally {
|
|
227
|
+
cleanupTestProject(tmpDir);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('scope create with long description (many spaces)', () => {
|
|
232
|
+
const tmpDir = createTestProject();
|
|
233
|
+
try {
|
|
234
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
235
|
+
const longDesc = 'PRD Auto queue for not inbound yet products with special handling for edge cases';
|
|
236
|
+
const result = runCliArray(['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', longDesc], tmpDir);
|
|
237
|
+
assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
|
|
238
|
+
|
|
239
|
+
const infoResult = runCliArray(['scope', 'info', 'auto-queue'], tmpDir);
|
|
240
|
+
assertContains(infoResult.output, 'PRD Auto queue for not inbound yet products');
|
|
241
|
+
} finally {
|
|
242
|
+
cleanupTestProject(tmpDir);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('scope create with name containing spaces', () => {
|
|
247
|
+
const tmpDir = createTestProject();
|
|
248
|
+
try {
|
|
249
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
250
|
+
const result = runCliArray(
|
|
251
|
+
['scope', 'create', 'auth', '--name', 'User Authentication Service', '--description', 'Handles user auth'],
|
|
252
|
+
tmpDir,
|
|
253
|
+
);
|
|
254
|
+
assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
|
|
255
|
+
|
|
256
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
257
|
+
assertContains(infoResult.output, 'User Authentication Service');
|
|
258
|
+
} finally {
|
|
259
|
+
cleanupTestProject(tmpDir);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Special Characters Tests
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
function testSpecialCharacters() {
|
|
269
|
+
console.log(`\n${colors.blue}${colors.bold}Special Characters Tests${colors.reset}`);
|
|
270
|
+
|
|
271
|
+
test('scope create with name containing ampersand', () => {
|
|
272
|
+
const tmpDir = createTestProject();
|
|
273
|
+
try {
|
|
274
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
275
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth & Users', '--description', ''], tmpDir);
|
|
276
|
+
assertTrue(result.success, 'Should handle ampersand');
|
|
277
|
+
|
|
278
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
279
|
+
assertContains(infoResult.output, 'Auth & Users');
|
|
280
|
+
} finally {
|
|
281
|
+
cleanupTestProject(tmpDir);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('scope create with name containing parentheses', () => {
|
|
286
|
+
const tmpDir = createTestProject();
|
|
287
|
+
try {
|
|
288
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
289
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth Service (v2)', '--description', ''], tmpDir);
|
|
290
|
+
assertTrue(result.success, 'Should handle parentheses');
|
|
291
|
+
|
|
292
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
293
|
+
assertContains(infoResult.output, 'Auth Service (v2)');
|
|
294
|
+
} finally {
|
|
295
|
+
cleanupTestProject(tmpDir);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('scope create with description containing quotes', () => {
|
|
300
|
+
const tmpDir = createTestProject();
|
|
301
|
+
try {
|
|
302
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
303
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Handle "special" cases'], tmpDir);
|
|
304
|
+
assertTrue(result.success, 'Should handle quotes in description');
|
|
305
|
+
|
|
306
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
307
|
+
assertContains(infoResult.output, 'Handle "special" cases');
|
|
308
|
+
} finally {
|
|
309
|
+
cleanupTestProject(tmpDir);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('scope create with description containing single quotes', () => {
|
|
314
|
+
const tmpDir = createTestProject();
|
|
315
|
+
try {
|
|
316
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
317
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', "Handle user's authentication"], tmpDir);
|
|
318
|
+
assertTrue(result.success, 'Should handle single quotes');
|
|
319
|
+
|
|
320
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
321
|
+
assertContains(infoResult.output, "user's");
|
|
322
|
+
} finally {
|
|
323
|
+
cleanupTestProject(tmpDir);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('scope create with description containing colons', () => {
|
|
328
|
+
const tmpDir = createTestProject();
|
|
329
|
+
try {
|
|
330
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
331
|
+
const result = runCliArray(
|
|
332
|
+
['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Features: login, logout, sessions'],
|
|
333
|
+
tmpDir,
|
|
334
|
+
);
|
|
335
|
+
assertTrue(result.success, 'Should handle colons');
|
|
336
|
+
|
|
337
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
338
|
+
assertContains(infoResult.output, 'Features: login, logout, sessions');
|
|
339
|
+
} finally {
|
|
340
|
+
cleanupTestProject(tmpDir);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('scope create with description containing hyphens and dashes', () => {
|
|
345
|
+
const tmpDir = createTestProject();
|
|
346
|
+
try {
|
|
347
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
348
|
+
const result = runCliArray(
|
|
349
|
+
['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Multi-factor auth - two-step verification'],
|
|
350
|
+
tmpDir,
|
|
351
|
+
);
|
|
352
|
+
assertTrue(result.success, 'Should handle hyphens and dashes');
|
|
353
|
+
|
|
354
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
355
|
+
assertContains(infoResult.output, 'Multi-factor auth - two-step verification');
|
|
356
|
+
} finally {
|
|
357
|
+
cleanupTestProject(tmpDir);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('scope create with description containing slashes', () => {
|
|
362
|
+
const tmpDir = createTestProject();
|
|
363
|
+
try {
|
|
364
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
365
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Handles /api/auth/* endpoints'], tmpDir);
|
|
366
|
+
assertTrue(result.success, 'Should handle slashes');
|
|
367
|
+
|
|
368
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
369
|
+
assertContains(infoResult.output, '/api/auth/*');
|
|
370
|
+
} finally {
|
|
371
|
+
cleanupTestProject(tmpDir);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ============================================================================
|
|
377
|
+
// NPX Wrapper Specific Tests
|
|
378
|
+
// ============================================================================
|
|
379
|
+
|
|
380
|
+
function testNpxWrapperBehavior() {
|
|
381
|
+
console.log(`\n${colors.blue}${colors.bold}NPX Wrapper Behavior Tests${colors.reset}`);
|
|
382
|
+
|
|
383
|
+
test('npx wrapper preserves argument boundaries', () => {
|
|
384
|
+
const tmpDir = createTestProject();
|
|
385
|
+
try {
|
|
386
|
+
runNpxWrapper(['scope', 'init'], tmpDir);
|
|
387
|
+
|
|
388
|
+
// This was the exact failing case: description with multiple words
|
|
389
|
+
const result = runNpxWrapper(
|
|
390
|
+
['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', 'PRD Auto queue for not inbound yet products'],
|
|
391
|
+
tmpDir,
|
|
392
|
+
);
|
|
393
|
+
assertTrue(result.success, `NPX wrapper should preserve spaces: ${result.stderr || result.output}`);
|
|
394
|
+
|
|
395
|
+
// Verify full description was saved
|
|
396
|
+
const infoResult = runNpxWrapper(['scope', 'info', 'auto-queue'], tmpDir);
|
|
397
|
+
assertContains(infoResult.output, 'PRD Auto queue for not inbound yet products');
|
|
398
|
+
} finally {
|
|
399
|
+
cleanupTestProject(tmpDir);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('npx wrapper handles multiple space-containing arguments', () => {
|
|
404
|
+
const tmpDir = createTestProject();
|
|
405
|
+
try {
|
|
406
|
+
runNpxWrapper(['scope', 'init'], tmpDir);
|
|
407
|
+
|
|
408
|
+
const result = runNpxWrapper(
|
|
409
|
+
['scope', 'create', 'test-scope', '--name', 'My Test Scope Name', '--description', 'A long description with many words and spaces'],
|
|
410
|
+
tmpDir,
|
|
411
|
+
);
|
|
412
|
+
assertTrue(result.success, 'Should handle multiple space-containing args');
|
|
413
|
+
|
|
414
|
+
const infoResult = runNpxWrapper(['scope', 'info', 'test-scope'], tmpDir);
|
|
415
|
+
assertContains(infoResult.output, 'My Test Scope Name');
|
|
416
|
+
assertContains(infoResult.output, 'A long description with many words and spaces');
|
|
417
|
+
} finally {
|
|
418
|
+
cleanupTestProject(tmpDir);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('npx wrapper handles help commands', () => {
|
|
423
|
+
const tmpDir = createTestProject();
|
|
424
|
+
try {
|
|
425
|
+
const result = runNpxWrapper(['scope', 'help'], tmpDir);
|
|
426
|
+
assertTrue(result.success, 'Help should work via wrapper');
|
|
427
|
+
assertContains(result.output, 'BMAD Scope Management');
|
|
428
|
+
} finally {
|
|
429
|
+
cleanupTestProject(tmpDir);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('npx wrapper handles subcommand help', () => {
|
|
434
|
+
const tmpDir = createTestProject();
|
|
435
|
+
try {
|
|
436
|
+
const result = runNpxWrapper(['scope', 'help', 'create'], tmpDir);
|
|
437
|
+
assertTrue(result.success, 'Subcommand help should work via wrapper');
|
|
438
|
+
assertContains(result.output, 'bmad scope create');
|
|
439
|
+
} finally {
|
|
440
|
+
cleanupTestProject(tmpDir);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('npx wrapper preserves exit codes on failure', () => {
|
|
445
|
+
const tmpDir = createTestProject();
|
|
446
|
+
try {
|
|
447
|
+
runNpxWrapper(['scope', 'init'], tmpDir);
|
|
448
|
+
const result = runNpxWrapper(['scope', 'info', 'nonexistent'], tmpDir);
|
|
449
|
+
assertFalse(result.success, 'Should fail for non-existent scope');
|
|
450
|
+
assertTrue(result.exitCode !== 0, 'Exit code should be non-zero');
|
|
451
|
+
} finally {
|
|
452
|
+
cleanupTestProject(tmpDir);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// Edge Cases Tests
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
function testEdgeCases() {
|
|
462
|
+
console.log(`\n${colors.blue}${colors.bold}Edge Cases Tests${colors.reset}`);
|
|
463
|
+
|
|
464
|
+
test('empty description argument', () => {
|
|
465
|
+
const tmpDir = createTestProject();
|
|
466
|
+
try {
|
|
467
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
468
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', ''], tmpDir);
|
|
469
|
+
assertTrue(result.success, 'Should handle empty description');
|
|
470
|
+
} finally {
|
|
471
|
+
cleanupTestProject(tmpDir);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('description with only spaces', () => {
|
|
476
|
+
const tmpDir = createTestProject();
|
|
477
|
+
try {
|
|
478
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
479
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', ' '], tmpDir);
|
|
480
|
+
assertTrue(result.success, 'Should handle whitespace-only description');
|
|
481
|
+
} finally {
|
|
482
|
+
cleanupTestProject(tmpDir);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('name with leading and trailing spaces', () => {
|
|
487
|
+
const tmpDir = createTestProject();
|
|
488
|
+
try {
|
|
489
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
490
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', ' Spaced Name ', '--description', ''], tmpDir);
|
|
491
|
+
assertTrue(result.success, 'Should handle leading/trailing spaces in name');
|
|
492
|
+
} finally {
|
|
493
|
+
cleanupTestProject(tmpDir);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('mixed flags and positional arguments', () => {
|
|
498
|
+
const tmpDir = createTestProject();
|
|
499
|
+
try {
|
|
500
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
501
|
+
// Some CLI parsers are sensitive to flag ordering
|
|
502
|
+
const result = runCliArray(['scope', 'create', '--name', 'Auth Service', 'auth', '--description', 'User authentication'], tmpDir);
|
|
503
|
+
// Depending on Commander.js behavior, this might fail or succeed
|
|
504
|
+
// The important thing is it doesn't crash unexpectedly
|
|
505
|
+
// Note: Commander.js is strict about positional arg ordering, so this may fail
|
|
506
|
+
} finally {
|
|
507
|
+
cleanupTestProject(tmpDir);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('very long description', () => {
|
|
512
|
+
const tmpDir = createTestProject();
|
|
513
|
+
try {
|
|
514
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
515
|
+
const longDesc = 'A '.repeat(100) + 'very long description';
|
|
516
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', longDesc], tmpDir);
|
|
517
|
+
assertTrue(result.success, 'Should handle very long description');
|
|
518
|
+
|
|
519
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
520
|
+
assertContains(infoResult.output, 'very long description');
|
|
521
|
+
} finally {
|
|
522
|
+
cleanupTestProject(tmpDir);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('description with newline-like content', () => {
|
|
527
|
+
const tmpDir = createTestProject();
|
|
528
|
+
try {
|
|
529
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
530
|
+
// Note: actual newlines would be handled by the shell, this tests the literal string
|
|
531
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', String.raw`Line1\nLine2`], tmpDir);
|
|
532
|
+
assertTrue(result.success, 'Should handle backslash-n in description');
|
|
533
|
+
} finally {
|
|
534
|
+
cleanupTestProject(tmpDir);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('description with unicode characters', () => {
|
|
539
|
+
const tmpDir = createTestProject();
|
|
540
|
+
try {
|
|
541
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
542
|
+
const result = runCliArray(['scope', 'create', 'auth', '--name', 'Auth', '--description', 'Handles authentication 认证 🔐'], tmpDir);
|
|
543
|
+
assertTrue(result.success, 'Should handle unicode in description');
|
|
544
|
+
|
|
545
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
546
|
+
assertContains(infoResult.output, '认证');
|
|
547
|
+
} finally {
|
|
548
|
+
cleanupTestProject(tmpDir);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ============================================================================
|
|
554
|
+
// Argument Count Tests (Regression tests for "too many arguments" error)
|
|
555
|
+
// ============================================================================
|
|
556
|
+
|
|
557
|
+
function testArgumentCounts() {
|
|
558
|
+
console.log(`\n${colors.blue}${colors.bold}Argument Count Tests (Regression)${colors.reset}`);
|
|
559
|
+
|
|
560
|
+
test('9-word description does not cause "too many arguments" error', () => {
|
|
561
|
+
const tmpDir = createTestProject();
|
|
562
|
+
try {
|
|
563
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
564
|
+
// This was the exact case that failed: 9 words became 9 separate arguments
|
|
565
|
+
const result = runCliArray(
|
|
566
|
+
['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', 'PRD Auto queue for not inbound yet products'],
|
|
567
|
+
tmpDir,
|
|
568
|
+
);
|
|
569
|
+
assertTrue(result.success, `Should not fail with "too many arguments": ${result.stderr}`);
|
|
570
|
+
assertNotContains(result.stderr || '', 'too many arguments');
|
|
571
|
+
} finally {
|
|
572
|
+
cleanupTestProject(tmpDir);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('20-word description works correctly', () => {
|
|
577
|
+
const tmpDir = createTestProject();
|
|
578
|
+
try {
|
|
579
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
580
|
+
const desc =
|
|
581
|
+
'This is a very long description with exactly twenty words to test that argument parsing works correctly for descriptions';
|
|
582
|
+
const result = runCliArray(['scope', 'create', 'test', '--name', 'Test', '--description', desc], tmpDir);
|
|
583
|
+
assertTrue(result.success, 'Should handle 20-word description');
|
|
584
|
+
} finally {
|
|
585
|
+
cleanupTestProject(tmpDir);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('multiple flag values with spaces all preserved', () => {
|
|
590
|
+
const tmpDir = createTestProject();
|
|
591
|
+
try {
|
|
592
|
+
runNpxWrapper(['scope', 'init'], tmpDir);
|
|
593
|
+
const result = runNpxWrapper(
|
|
594
|
+
['scope', 'create', 'my-scope', '--name', 'My Scope Name Here', '--description', 'This is a description with many spaces'],
|
|
595
|
+
tmpDir,
|
|
596
|
+
);
|
|
597
|
+
assertTrue(result.success, 'All spaced arguments should be preserved');
|
|
598
|
+
|
|
599
|
+
const infoResult = runNpxWrapper(['scope', 'info', 'my-scope'], tmpDir);
|
|
600
|
+
assertContains(infoResult.output, 'My Scope Name Here');
|
|
601
|
+
assertContains(infoResult.output, 'This is a description with many spaces');
|
|
602
|
+
} finally {
|
|
603
|
+
cleanupTestProject(tmpDir);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ============================================================================
|
|
609
|
+
// Install Command Tests (for completeness)
|
|
610
|
+
// ============================================================================
|
|
611
|
+
|
|
612
|
+
function testInstallCommand() {
|
|
613
|
+
console.log(`\n${colors.blue}${colors.bold}Install Command Tests${colors.reset}`);
|
|
614
|
+
|
|
615
|
+
test('install --help works via npx wrapper', () => {
|
|
616
|
+
const tmpDir = createTestProject();
|
|
617
|
+
try {
|
|
618
|
+
const result = runNpxWrapper(['install', '--help'], tmpDir);
|
|
619
|
+
assertTrue(result.success || result.output.includes('Install'), 'Install help should work');
|
|
620
|
+
} finally {
|
|
621
|
+
cleanupTestProject(tmpDir);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('install --debug flag works', () => {
|
|
626
|
+
const tmpDir = createTestProject();
|
|
627
|
+
try {
|
|
628
|
+
// Just verify the flag is recognized, don't actually run full install
|
|
629
|
+
const result = runNpxWrapper(['install', '--help'], tmpDir);
|
|
630
|
+
// If we got here without crashing, the CLI is working
|
|
631
|
+
assertTrue(true, 'Install command accepts flags');
|
|
632
|
+
} finally {
|
|
633
|
+
cleanupTestProject(tmpDir);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ============================================================================
|
|
639
|
+
// Main Test Runner
|
|
640
|
+
// ============================================================================
|
|
641
|
+
|
|
642
|
+
function main() {
|
|
643
|
+
console.log(`\n${colors.bold}BMAD CLI Argument Handling Test Suite${colors.reset}`);
|
|
644
|
+
console.log(colors.dim + '═'.repeat(70) + colors.reset);
|
|
645
|
+
console.log(colors.cyan + 'Testing proper preservation of argument boundaries,' + colors.reset);
|
|
646
|
+
console.log(colors.cyan + 'especially for arguments containing spaces.' + colors.reset);
|
|
647
|
+
|
|
648
|
+
const startTime = Date.now();
|
|
649
|
+
|
|
650
|
+
// Run all test suites
|
|
651
|
+
testArgumentsWithSpaces();
|
|
652
|
+
testSpecialCharacters();
|
|
653
|
+
testNpxWrapperBehavior();
|
|
654
|
+
testEdgeCases();
|
|
655
|
+
testArgumentCounts();
|
|
656
|
+
testInstallCommand();
|
|
657
|
+
|
|
658
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
659
|
+
|
|
660
|
+
// Summary
|
|
661
|
+
console.log(`\n${colors.dim}${'─'.repeat(70)}${colors.reset}`);
|
|
662
|
+
console.log(`\n${colors.bold}Test Results${colors.reset}`);
|
|
663
|
+
console.log(` Total: ${testCount}`);
|
|
664
|
+
console.log(` ${colors.green}Passed: ${passCount}${colors.reset}`);
|
|
665
|
+
if (failCount > 0) {
|
|
666
|
+
console.log(` ${colors.red}Failed: ${failCount}${colors.reset}`);
|
|
667
|
+
}
|
|
668
|
+
if (skipCount > 0) {
|
|
669
|
+
console.log(` ${colors.yellow}Skipped: ${skipCount}${colors.reset}`);
|
|
670
|
+
}
|
|
671
|
+
console.log(` Time: ${duration}s`);
|
|
672
|
+
|
|
673
|
+
if (failures.length > 0) {
|
|
674
|
+
console.log(`\n${colors.red}${colors.bold}Failures:${colors.reset}`);
|
|
675
|
+
for (const { name, error } of failures) {
|
|
676
|
+
console.log(`\n ${colors.red}✗${colors.reset} ${name}`);
|
|
677
|
+
console.log(` ${colors.dim}${error}${colors.reset}`);
|
|
678
|
+
}
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
console.log(`\n${colors.green}${colors.bold}All tests passed!${colors.reset}\n`);
|
|
683
|
+
process.exit(0);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
main();
|