BMAD-METHOD/test/test-custom-content-parent-...

211 lines
7.8 KiB
JavaScript

/**
* Custom Content Parent Directory Tests
*
* Tests that --custom-content works with parent directories containing
* multiple module subdirectories (1-level recursive scan).
*
* Related: https://github.com/bmad-code-org/BMAD-METHOD/issues/2040
*
* Usage: node test/test-custom-content-parent-dir.js
*/
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const { UI } = require('../tools/cli/lib/ui');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
yellow: '\u001B[33m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
let passed = 0;
let failed = 0;
function assert(condition, testName, errorMessage = '') {
if (condition) {
console.log(`${colors.green}${colors.reset} ${testName}`);
passed++;
} else {
console.log(`${colors.red}${colors.reset} ${testName}`);
if (errorMessage) {
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
}
failed++;
}
}
/**
* Create a temp directory simulating a parent dir with multiple custom modules
*
* Structure:
* parentDir/
* module-a/
* module.yaml (code: "module-a")
* module-b/
* module.yaml (code: "module-b")
* not-a-module/
* readme.md
*/
async function createParentDirFixture() {
const parentDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-parent-'));
// Module A
await fs.ensureDir(path.join(parentDir, 'module-a'));
await fs.writeFile(path.join(parentDir, 'module-a', 'module.yaml'), 'code: module-a\nname: Module A\n');
// Module B
await fs.ensureDir(path.join(parentDir, 'module-b'));
await fs.writeFile(path.join(parentDir, 'module-b', 'module.yaml'), 'code: module-b\nname: Module B\n');
// Not a module (no module.yaml)
await fs.ensureDir(path.join(parentDir, 'not-a-module'));
await fs.writeFile(path.join(parentDir, 'not-a-module', 'readme.md'), '# Not a module\n');
return parentDir;
}
/**
* Create a temp directory simulating a direct module path
*/
async function createDirectModuleFixture() {
const moduleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-module-'));
await fs.writeFile(path.join(moduleDir, 'module.yaml'), 'code: direct-module\nname: Direct Module\n');
return moduleDir;
}
/**
* Create a temp directory with no modules at any level
*/
async function createEmptyDirFixture() {
const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-empty-'));
await fs.writeFile(path.join(emptyDir, 'readme.md'), '# Empty\n');
return emptyDir;
}
async function runTests() {
const ui = new UI();
// ============================================================
// Test Suite 1: validateCustomContentPathSync with parent dirs
// ============================================================
console.log(`\n${colors.yellow}Test Suite 1: validateCustomContentPathSync${colors.reset}\n`);
let parentDir;
let directModuleDir;
let emptyDir;
try {
parentDir = await createParentDirFixture();
directModuleDir = await createDirectModuleFixture();
emptyDir = await createEmptyDirFixture();
// Direct module path should pass (existing behavior)
const directResult = ui.validateCustomContentPathSync(directModuleDir);
assert(directResult === undefined, 'Direct module path validates successfully');
// Parent dir with module subdirs should pass (new behavior)
const parentResult = ui.validateCustomContentPathSync(parentDir);
assert(parentResult === undefined, 'Parent directory with module subdirectories validates successfully', `Got error: ${parentResult}`);
// Empty dir (no modules at any level) should fail
const emptyResult = ui.validateCustomContentPathSync(emptyDir);
assert(
typeof emptyResult === 'string' && emptyResult.length > 0,
'Directory with no modules at any level fails validation',
`Expected error string, got: ${emptyResult}`,
);
// Non-existent path should fail
const nonExistResult = ui.validateCustomContentPathSync('/tmp/does-not-exist-bmad-xyz');
assert(typeof nonExistResult === 'string', 'Non-existent path fails validation');
// Empty input should pass (allows cancel)
const emptyInputResult = ui.validateCustomContentPathSync('');
assert(emptyInputResult === undefined, 'Empty input passes (allows cancel)');
} finally {
if (parentDir) await fs.remove(parentDir).catch(() => {});
if (directModuleDir) await fs.remove(directModuleDir).catch(() => {});
if (emptyDir) await fs.remove(emptyDir).catch(() => {});
}
// ============================================================
// Test Suite 2: resolveCustomContentPaths
// ============================================================
console.log(`\n${colors.yellow}Test Suite 2: resolveCustomContentPaths${colors.reset}\n`);
try {
parentDir = await createParentDirFixture();
directModuleDir = await createDirectModuleFixture();
emptyDir = await createEmptyDirFixture();
// Direct module path returns itself
const directPaths = ui.resolveCustomContentPaths(directModuleDir);
assert(Array.isArray(directPaths), 'resolveCustomContentPaths returns an array for direct module');
assert(directPaths.length === 1, 'Direct module path resolves to exactly 1 path');
assert(
directPaths[0] === directModuleDir,
'Direct module path resolves to itself',
`Expected ${directModuleDir}, got ${directPaths[0]}`,
);
// Parent dir expands to individual module subdirs
const parentPaths = ui.resolveCustomContentPaths(parentDir);
assert(Array.isArray(parentPaths), 'resolveCustomContentPaths returns an array for parent dir');
assert(
parentPaths.length === 2,
'Parent dir resolves to 2 module paths (skips not-a-module)',
`Expected 2, got ${parentPaths.length}: ${JSON.stringify(parentPaths)}`,
);
const resolvedNames = parentPaths.map((p) => path.basename(p)).sort();
assert(
resolvedNames[0] === 'module-a' && resolvedNames[1] === 'module-b',
'Parent dir resolves to module-a and module-b',
`Got: ${JSON.stringify(resolvedNames)}`,
);
// Empty dir returns empty array
const emptyPaths = ui.resolveCustomContentPaths(emptyDir);
assert(Array.isArray(emptyPaths), 'resolveCustomContentPaths returns an array for empty dir');
assert(emptyPaths.length === 0, 'Empty dir resolves to 0 paths', `Expected 0, got ${emptyPaths.length}`);
// Non-existent path returns empty array
const nonExistPaths = ui.resolveCustomContentPaths('/tmp/does-not-exist-bmad-xyz');
assert(Array.isArray(nonExistPaths), 'resolveCustomContentPaths returns array for non-existent path');
assert(nonExistPaths.length === 0, 'Non-existent path resolves to 0 paths');
} finally {
if (parentDir) await fs.remove(parentDir).catch(() => {});
if (directModuleDir) await fs.remove(directModuleDir).catch(() => {});
if (emptyDir) await fs.remove(emptyDir).catch(() => {});
}
// ============================================================
// Summary
// ============================================================
console.log(`\n${colors.cyan}========================================`);
console.log('Test Results:');
console.log(` Passed: ${colors.green}${passed}${colors.reset}`);
console.log(` Failed: ${colors.red}${failed}${colors.reset}`);
console.log(`========================================${colors.reset}\n`);
if (failed === 0) {
console.log(`${colors.green}✨ All custom content parent dir tests passed!${colors.reset}\n`);
process.exit(0);
} else {
console.log(`${colors.red}❌ Some custom content parent dir tests failed${colors.reset}\n`);
process.exit(1);
}
}
runTests().catch((error) => {
console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message);
console.error(error.stack);
process.exit(1);
});