1646 lines
71 KiB
JavaScript
1646 lines
71 KiB
JavaScript
/**
|
|
* Installation Component Tests
|
|
*
|
|
* Tests individual installation components in isolation:
|
|
* - Agent YAML → XML compilation
|
|
* - Manifest generation
|
|
* - Path resolution
|
|
* - Customization merging
|
|
*
|
|
* These are deterministic unit tests that don't require full installation.
|
|
* Usage: node test/test-installation-components.js
|
|
*/
|
|
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const fs = require('fs-extra');
|
|
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
|
|
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
|
|
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
|
|
const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes');
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* Test helper: Assert condition
|
|
*/
|
|
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++;
|
|
}
|
|
}
|
|
|
|
async function createTestBmadFixture() {
|
|
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
|
|
|
|
// Minimal workflow manifest (generators check for this)
|
|
await fs.ensureDir(path.join(fixtureDir, '_config'));
|
|
await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), '');
|
|
|
|
// Minimal compiled agent for core/agents (contains <agent tag and frontmatter)
|
|
const minimalAgent = [
|
|
'---',
|
|
'name: "test agent"',
|
|
'description: "Minimal test agent fixture"',
|
|
'---',
|
|
'',
|
|
'You are a test agent.',
|
|
'',
|
|
'<agent id="test-agent.agent.yaml" name="Test Agent" title="Test Agent">',
|
|
'<persona>Test persona</persona>',
|
|
'</agent>',
|
|
].join('\n');
|
|
|
|
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
|
|
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent);
|
|
// Skill manifest so the installer uses 'bmad-master' as the canonical skill name
|
|
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n');
|
|
|
|
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
|
|
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
|
|
await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent);
|
|
|
|
return fixtureDir;
|
|
}
|
|
|
|
async function createShardDocPrototypeFixture() {
|
|
const fixtureDir = await createTestBmadFixture();
|
|
|
|
await fs.ensureDir(path.join(fixtureDir, 'core', 'tasks'));
|
|
await fs.writeFile(
|
|
path.join(fixtureDir, 'core', 'tasks', 'shard-doc.xml'),
|
|
'<task id="_bmad/core/tasks/shard-doc" name="Shard Document" description="Test shard-doc task"><objective>Test objective</objective></task>\n',
|
|
);
|
|
|
|
await fs.writeFile(
|
|
path.join(fixtureDir, 'core', 'tasks', 'bmad-skill-manifest.yaml'),
|
|
[
|
|
'shard-doc.xml:',
|
|
' canonicalId: bmad-shard-doc',
|
|
' type: task',
|
|
' description: "Splits large markdown documents into smaller, organized files based on sections"',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
|
|
await fs.ensureDir(path.join(fixtureDir, 'core', 'tasks', 'bmad-shard-doc-skill-prototype'));
|
|
await fs.writeFile(
|
|
path.join(fixtureDir, 'core', 'tasks', 'bmad-shard-doc-skill-prototype', 'SKILL.md'),
|
|
[
|
|
'---',
|
|
'name: bmad-shard-doc-skill-prototype',
|
|
'description: Source-authored prototype skill',
|
|
'---',
|
|
'',
|
|
'# bmad-shard-doc-skill-prototype',
|
|
'',
|
|
'Prototype marker: source-authored-skill',
|
|
'',
|
|
'Read and execute from: {project-root}/_bmad/core/tasks/shard-doc.xml',
|
|
'',
|
|
'Follow all shard-doc task instructions exactly as written.',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
|
|
await fs.writeFile(
|
|
path.join(fixtureDir, '_config', 'task-manifest.csv'),
|
|
[
|
|
'name,displayName,description,module,path,standalone,canonicalId',
|
|
'shard-doc,Shard Document,Test shard-doc task,core,_bmad/core/tasks/shard-doc.xml,true,bmad-shard-doc',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
|
|
// Ensure tool manifest exists to avoid parser edge-cases in some environments.
|
|
await fs.writeFile(path.join(fixtureDir, '_config', 'tool-manifest.csv'), '');
|
|
|
|
return fixtureDir;
|
|
}
|
|
|
|
/**
|
|
* Test Suite
|
|
*/
|
|
async function runTests() {
|
|
console.log(`${colors.cyan}========================================`);
|
|
console.log('Installation Component Tests');
|
|
console.log(`========================================${colors.reset}\n`);
|
|
|
|
const projectRoot = path.join(__dirname, '..');
|
|
|
|
// ============================================================
|
|
// Test 1: YAML → XML Agent Compilation (In-Memory)
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 1: Agent Compilation${colors.reset}\n`);
|
|
|
|
try {
|
|
const builder = new YamlXmlBuilder();
|
|
const pmAgentPath = path.join(projectRoot, 'src/bmm/agents/pm.agent.yaml');
|
|
|
|
// Create temp output path
|
|
const tempOutput = path.join(__dirname, 'temp-pm-agent.md');
|
|
|
|
try {
|
|
const result = await builder.buildAgent(pmAgentPath, null, tempOutput, { includeMetadata: true });
|
|
|
|
assert(result && result.outputPath === tempOutput, 'Agent compilation returns result object with outputPath');
|
|
|
|
// Read the output
|
|
const compiled = await fs.readFile(tempOutput, 'utf8');
|
|
|
|
assert(compiled.includes('<agent'), 'Compiled agent contains <agent> tag');
|
|
|
|
assert(compiled.includes('<persona>'), 'Compiled agent contains <persona> tag');
|
|
|
|
assert(compiled.includes('<menu>'), 'Compiled agent contains <menu> tag');
|
|
|
|
assert(compiled.includes('Product Manager'), 'Compiled agent contains agent title');
|
|
|
|
// Cleanup
|
|
await fs.remove(tempOutput);
|
|
} catch (error) {
|
|
assert(false, 'Agent compilation succeeds', error.message);
|
|
}
|
|
} catch (error) {
|
|
assert(false, 'YamlXmlBuilder instantiates', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 2: Customization Merging
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 2: Customization Merging${colors.reset}\n`);
|
|
|
|
try {
|
|
const builder = new YamlXmlBuilder();
|
|
|
|
// Test deepMerge function
|
|
const base = {
|
|
agent: {
|
|
metadata: { name: 'John', title: 'PM' },
|
|
persona: { role: 'Product Manager', style: 'Analytical' },
|
|
},
|
|
};
|
|
|
|
const customize = {
|
|
agent: {
|
|
metadata: { name: 'Sarah' }, // Override name only
|
|
persona: { style: 'Concise' }, // Override style only
|
|
},
|
|
};
|
|
|
|
const merged = builder.deepMerge(base, customize);
|
|
|
|
assert(merged.agent.metadata.name === 'Sarah', 'Deep merge overrides customized name');
|
|
|
|
assert(merged.agent.metadata.title === 'PM', 'Deep merge preserves non-overridden title');
|
|
|
|
assert(merged.agent.persona.role === 'Product Manager', 'Deep merge preserves non-overridden role');
|
|
|
|
assert(merged.agent.persona.style === 'Concise', 'Deep merge overrides customized style');
|
|
} catch (error) {
|
|
assert(false, 'Customization merging works', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 3: Path Resolution
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 3: Path Variable Resolution${colors.reset}\n`);
|
|
|
|
try {
|
|
const builder = new YamlXmlBuilder();
|
|
|
|
// Test path resolution logic (if exposed)
|
|
// This would test {project-root}, {installed_path}, {config_source} resolution
|
|
|
|
const testPath = '{project-root}/bmad/bmm/config.yaml';
|
|
const expectedPattern = /\/bmad\/bmm\/config\.yaml$/;
|
|
|
|
assert(
|
|
true, // Placeholder - would test actual resolution
|
|
'Path variable resolution pattern matches expected format',
|
|
'Note: This test validates path resolution logic exists',
|
|
);
|
|
} catch (error) {
|
|
assert(false, 'Path resolution works', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 4: Windsurf Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 4: Windsurf Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes = await loadPlatformCodes();
|
|
const windsurfInstaller = platformCodes.platforms.windsurf?.installer;
|
|
|
|
assert(windsurfInstaller?.target_dir === '.windsurf/skills', 'Windsurf target_dir uses native skills path');
|
|
|
|
assert(windsurfInstaller?.skill_format === true, 'Windsurf installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(windsurfInstaller?.legacy_targets) && windsurfInstaller.legacy_targets.includes('.windsurf/workflows'),
|
|
'Windsurf installer cleans legacy workflow output',
|
|
);
|
|
|
|
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-'));
|
|
const installedBmadDir = await createTestBmadFixture();
|
|
const legacyDir = path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir);
|
|
await fs.writeFile(path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager = new IdeManager();
|
|
await ideManager.ensureInitialized();
|
|
const result = await ideManager.setup('windsurf', tempProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result.success === true, 'Windsurf setup succeeds against temp project');
|
|
|
|
const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
|
|
|
|
await fs.remove(tempProjectDir);
|
|
await fs.remove(installedBmadDir);
|
|
} catch (error) {
|
|
assert(false, 'Windsurf native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 5: Kiro Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 5: Kiro Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes = await loadPlatformCodes();
|
|
const kiroInstaller = platformCodes.platforms.kiro?.installer;
|
|
|
|
assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path');
|
|
|
|
assert(kiroInstaller?.skill_format === true, 'Kiro installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(kiroInstaller?.legacy_targets) && kiroInstaller.legacy_targets.includes('.kiro/steering'),
|
|
'Kiro installer cleans legacy steering output',
|
|
);
|
|
|
|
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-'));
|
|
const installedBmadDir = await createTestBmadFixture();
|
|
const legacyDir = path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir);
|
|
await fs.writeFile(path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager = new IdeManager();
|
|
await ideManager.ensureInitialized();
|
|
const result = await ideManager.setup('kiro', tempProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result.success === true, 'Kiro setup succeeds against temp project');
|
|
|
|
const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
|
|
|
|
await fs.remove(tempProjectDir);
|
|
await fs.remove(installedBmadDir);
|
|
} catch (error) {
|
|
assert(false, 'Kiro native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 6: Antigravity Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 6: Antigravity Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes = await loadPlatformCodes();
|
|
const antigravityInstaller = platformCodes.platforms.antigravity?.installer;
|
|
|
|
assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path');
|
|
|
|
assert(antigravityInstaller?.skill_format === true, 'Antigravity installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(antigravityInstaller?.legacy_targets) && antigravityInstaller.legacy_targets.includes('.agent/workflows'),
|
|
'Antigravity installer cleans legacy workflow output',
|
|
);
|
|
|
|
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-'));
|
|
const installedBmadDir = await createTestBmadFixture();
|
|
const legacyDir = path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir);
|
|
await fs.writeFile(path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager = new IdeManager();
|
|
await ideManager.ensureInitialized();
|
|
const result = await ideManager.setup('antigravity', tempProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result.success === true, 'Antigravity setup succeeds against temp project');
|
|
|
|
const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
|
|
|
|
await fs.remove(tempProjectDir);
|
|
await fs.remove(installedBmadDir);
|
|
} catch (error) {
|
|
assert(false, 'Antigravity native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 7: Auggie Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 7: Auggie Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes = await loadPlatformCodes();
|
|
const auggieInstaller = platformCodes.platforms.auggie?.installer;
|
|
|
|
assert(auggieInstaller?.target_dir === '.augment/skills', 'Auggie target_dir uses native skills path');
|
|
|
|
assert(auggieInstaller?.skill_format === true, 'Auggie installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(auggieInstaller?.legacy_targets) && auggieInstaller.legacy_targets.includes('.augment/commands'),
|
|
'Auggie installer cleans legacy command output',
|
|
);
|
|
|
|
assert(
|
|
auggieInstaller?.ancestor_conflict_check !== true,
|
|
'Auggie installer does not enable ancestor conflict checks without verified inheritance',
|
|
);
|
|
|
|
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-'));
|
|
const installedBmadDir = await createTestBmadFixture();
|
|
const legacyDir = path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir);
|
|
await fs.writeFile(path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager = new IdeManager();
|
|
await ideManager.ensureInitialized();
|
|
const result = await ideManager.setup('auggie', tempProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result.success === true, 'Auggie setup succeeds against temp project');
|
|
|
|
const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
|
|
|
|
await fs.remove(tempProjectDir);
|
|
await fs.remove(installedBmadDir);
|
|
} catch (error) {
|
|
assert(false, 'Auggie native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 8: OpenCode Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 8: OpenCode Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes = await loadPlatformCodes();
|
|
const opencodeInstaller = platformCodes.platforms.opencode?.installer;
|
|
|
|
assert(opencodeInstaller?.target_dir === '.opencode/skills', 'OpenCode target_dir uses native skills path');
|
|
|
|
assert(opencodeInstaller?.skill_format === true, 'OpenCode installer enables native skill output');
|
|
|
|
assert(opencodeInstaller?.ancestor_conflict_check === true, 'OpenCode installer enables ancestor conflict checks');
|
|
|
|
assert(
|
|
Array.isArray(opencodeInstaller?.legacy_targets) &&
|
|
['.opencode/agents', '.opencode/commands', '.opencode/agent', '.opencode/command'].every((legacyTarget) =>
|
|
opencodeInstaller.legacy_targets.includes(legacyTarget),
|
|
),
|
|
'OpenCode installer cleans split legacy agent and command output',
|
|
);
|
|
|
|
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-'));
|
|
const installedBmadDir = await createTestBmadFixture();
|
|
const legacyDirs = [
|
|
path.join(tempProjectDir, '.opencode', 'agents', 'bmad-legacy-agent'),
|
|
path.join(tempProjectDir, '.opencode', 'commands', 'bmad-legacy-command'),
|
|
path.join(tempProjectDir, '.opencode', 'agent', 'bmad-legacy-agent-singular'),
|
|
path.join(tempProjectDir, '.opencode', 'command', 'bmad-legacy-command-singular'),
|
|
];
|
|
|
|
for (const legacyDir of legacyDirs) {
|
|
await fs.ensureDir(legacyDir);
|
|
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(path.dirname(legacyDir), `${path.basename(legacyDir)}.md`), 'legacy\n');
|
|
}
|
|
|
|
const ideManager = new IdeManager();
|
|
await ideManager.ensureInitialized();
|
|
const result = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result.success === true, 'OpenCode setup succeeds against temp project');
|
|
|
|
const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
|
|
|
|
for (const legacyDir of ['agents', 'commands', 'agent', 'command']) {
|
|
assert(
|
|
!(await fs.pathExists(path.join(tempProjectDir, '.opencode', legacyDir))),
|
|
`OpenCode setup removes legacy .opencode/${legacyDir} dir`,
|
|
);
|
|
}
|
|
|
|
await fs.remove(tempProjectDir);
|
|
await fs.remove(installedBmadDir);
|
|
} catch (error) {
|
|
assert(false, 'OpenCode native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 9: Claude Code Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 9: Claude Code Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes9 = await loadPlatformCodes();
|
|
const claudeInstaller = platformCodes9.platforms['claude-code']?.installer;
|
|
|
|
assert(claudeInstaller?.target_dir === '.claude/skills', 'Claude Code target_dir uses native skills path');
|
|
|
|
assert(claudeInstaller?.skill_format === true, 'Claude Code installer enables native skill output');
|
|
|
|
assert(claudeInstaller?.ancestor_conflict_check === true, 'Claude Code installer enables ancestor conflict checks');
|
|
|
|
assert(
|
|
Array.isArray(claudeInstaller?.legacy_targets) && claudeInstaller.legacy_targets.includes('.claude/commands'),
|
|
'Claude Code installer cleans legacy command output',
|
|
);
|
|
|
|
const tempProjectDir9 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-test-'));
|
|
const installedBmadDir9 = await createTestBmadFixture();
|
|
const legacyDir9 = path.join(tempProjectDir9, '.claude', 'commands');
|
|
await fs.ensureDir(legacyDir9);
|
|
await fs.writeFile(path.join(legacyDir9, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
const ideManager9 = new IdeManager();
|
|
await ideManager9.ensureInitialized();
|
|
const result9 = await ideManager9.setup('claude-code', tempProjectDir9, installedBmadDir9, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result9.success === true, 'Claude Code setup succeeds against temp project');
|
|
|
|
const skillFile9 = path.join(tempProjectDir9, '.claude', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile9), 'Claude Code install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent9 = await fs.readFile(skillFile9, 'utf8');
|
|
const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
|
|
|
|
await fs.remove(tempProjectDir9);
|
|
await fs.remove(installedBmadDir9);
|
|
} catch (error) {
|
|
assert(false, 'Claude Code native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 10: Claude Code Ancestor Conflict
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 10: Claude Code Ancestor Conflict${colors.reset}\n`);
|
|
|
|
try {
|
|
const tempRoot10 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-ancestor-test-'));
|
|
const parentProjectDir10 = path.join(tempRoot10, 'parent');
|
|
const childProjectDir10 = path.join(parentProjectDir10, 'child');
|
|
const installedBmadDir10 = await createTestBmadFixture();
|
|
|
|
await fs.ensureDir(path.join(parentProjectDir10, '.git'));
|
|
await fs.ensureDir(path.join(parentProjectDir10, '.claude', 'skills', 'bmad-existing'));
|
|
await fs.ensureDir(childProjectDir10);
|
|
await fs.writeFile(path.join(parentProjectDir10, '.claude', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager10 = new IdeManager();
|
|
await ideManager10.ensureInitialized();
|
|
const result10 = await ideManager10.setup('claude-code', childProjectDir10, installedBmadDir10, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
const expectedConflictDir10 = await fs.realpath(path.join(parentProjectDir10, '.claude', 'skills'));
|
|
|
|
assert(result10.success === false, 'Claude Code setup refuses install when ancestor skills already exist');
|
|
assert(result10.handlerResult?.reason === 'ancestor-conflict', 'Claude Code ancestor rejection reports ancestor-conflict reason');
|
|
assert(
|
|
result10.handlerResult?.conflictDir === expectedConflictDir10,
|
|
'Claude Code ancestor rejection points at ancestor .claude/skills dir',
|
|
);
|
|
|
|
await fs.remove(tempRoot10);
|
|
await fs.remove(installedBmadDir10);
|
|
} catch (error) {
|
|
assert(false, 'Claude Code ancestor conflict protection test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 11: Codex Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 11: Codex Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes11 = await loadPlatformCodes();
|
|
const codexInstaller = platformCodes11.platforms.codex?.installer;
|
|
|
|
assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path');
|
|
|
|
assert(codexInstaller?.skill_format === true, 'Codex installer enables native skill output');
|
|
|
|
assert(codexInstaller?.ancestor_conflict_check === true, 'Codex installer enables ancestor conflict checks');
|
|
|
|
assert(
|
|
Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.codex/prompts'),
|
|
'Codex installer cleans legacy prompt output',
|
|
);
|
|
|
|
const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-'));
|
|
const installedBmadDir11 = await createTestBmadFixture();
|
|
const legacyDir11 = path.join(tempProjectDir11, '.codex', 'prompts');
|
|
await fs.ensureDir(legacyDir11);
|
|
await fs.writeFile(path.join(legacyDir11, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
const ideManager11 = new IdeManager();
|
|
await ideManager11.ensureInitialized();
|
|
const result11 = await ideManager11.setup('codex', tempProjectDir11, installedBmadDir11, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result11.success === true, 'Codex setup succeeds against temp project');
|
|
|
|
const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent11 = await fs.readFile(skillFile11, 'utf8');
|
|
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
|
|
|
|
await fs.remove(tempProjectDir11);
|
|
await fs.remove(installedBmadDir11);
|
|
} catch (error) {
|
|
assert(false, 'Codex native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 12: Codex Ancestor Conflict
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 12: Codex Ancestor Conflict${colors.reset}\n`);
|
|
|
|
try {
|
|
const tempRoot12 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-ancestor-test-'));
|
|
const parentProjectDir12 = path.join(tempRoot12, 'parent');
|
|
const childProjectDir12 = path.join(parentProjectDir12, 'child');
|
|
const installedBmadDir12 = await createTestBmadFixture();
|
|
|
|
await fs.ensureDir(path.join(parentProjectDir12, '.git'));
|
|
await fs.ensureDir(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing'));
|
|
await fs.ensureDir(childProjectDir12);
|
|
await fs.writeFile(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager12 = new IdeManager();
|
|
await ideManager12.ensureInitialized();
|
|
const result12 = await ideManager12.setup('codex', childProjectDir12, installedBmadDir12, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
const expectedConflictDir12 = await fs.realpath(path.join(parentProjectDir12, '.agents', 'skills'));
|
|
|
|
assert(result12.success === false, 'Codex setup refuses install when ancestor skills already exist');
|
|
assert(result12.handlerResult?.reason === 'ancestor-conflict', 'Codex ancestor rejection reports ancestor-conflict reason');
|
|
assert(result12.handlerResult?.conflictDir === expectedConflictDir12, 'Codex ancestor rejection points at ancestor .agents/skills dir');
|
|
|
|
await fs.remove(tempRoot12);
|
|
await fs.remove(installedBmadDir12);
|
|
} catch (error) {
|
|
assert(false, 'Codex ancestor conflict protection test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 13: Cursor Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 13: Cursor Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes13 = await loadPlatformCodes();
|
|
const cursorInstaller = platformCodes13.platforms.cursor?.installer;
|
|
|
|
assert(cursorInstaller?.target_dir === '.cursor/skills', 'Cursor target_dir uses native skills path');
|
|
|
|
assert(cursorInstaller?.skill_format === true, 'Cursor installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(cursorInstaller?.legacy_targets) && cursorInstaller.legacy_targets.includes('.cursor/commands'),
|
|
'Cursor installer cleans legacy command output',
|
|
);
|
|
|
|
assert(!cursorInstaller?.ancestor_conflict_check, 'Cursor installer does not enable ancestor conflict checks');
|
|
|
|
const tempProjectDir13c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cursor-test-'));
|
|
const installedBmadDir13c = await createTestBmadFixture();
|
|
const legacyDir13c = path.join(tempProjectDir13c, '.cursor', 'commands');
|
|
await fs.ensureDir(legacyDir13c);
|
|
await fs.writeFile(path.join(legacyDir13c, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
const ideManager13c = new IdeManager();
|
|
await ideManager13c.ensureInitialized();
|
|
const result13c = await ideManager13c.setup('cursor', tempProjectDir13c, installedBmadDir13c, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result13c.success === true, 'Cursor setup succeeds against temp project');
|
|
|
|
const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent13c = await fs.readFile(skillFile13c, 'utf8');
|
|
const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
|
|
|
|
await fs.remove(tempProjectDir13c);
|
|
await fs.remove(installedBmadDir13c);
|
|
} catch (error) {
|
|
assert(false, 'Cursor native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 14: Roo Code Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 14: Roo Code Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes13 = await loadPlatformCodes();
|
|
const rooInstaller = platformCodes13.platforms.roo?.installer;
|
|
|
|
assert(rooInstaller?.target_dir === '.roo/skills', 'Roo target_dir uses native skills path');
|
|
|
|
assert(rooInstaller?.skill_format === true, 'Roo installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(rooInstaller?.legacy_targets) && rooInstaller.legacy_targets.includes('.roo/commands'),
|
|
'Roo installer cleans legacy command output',
|
|
);
|
|
|
|
const tempProjectDir13 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-roo-test-'));
|
|
const installedBmadDir13 = await createTestBmadFixture();
|
|
const legacyDir13 = path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir13);
|
|
await fs.writeFile(path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir13, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager13 = new IdeManager();
|
|
await ideManager13.ensureInitialized();
|
|
const result13 = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result13.success === true, 'Roo setup succeeds against temp project');
|
|
|
|
const skillFile13 = path.join(tempProjectDir13, '.roo', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens)
|
|
const skillContent13 = await fs.readFile(skillFile13, 'utf8');
|
|
const nameMatch13 = skillContent13.match(/^name:\s*(.+)$/m);
|
|
assert(
|
|
nameMatch13 && nameMatch13[1].trim() === 'bmad-master',
|
|
'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)',
|
|
);
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir13, '.roo', 'commands'))), 'Roo setup removes legacy commands dir');
|
|
|
|
// Reinstall/upgrade: run setup again over existing skills output
|
|
const result13b = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result13b.success === true, 'Roo reinstall/upgrade succeeds over existing skills');
|
|
assert(await fs.pathExists(skillFile13), 'Roo reinstall preserves SKILL.md output');
|
|
|
|
await fs.remove(tempProjectDir13);
|
|
await fs.remove(installedBmadDir13);
|
|
} catch (error) {
|
|
assert(false, 'Roo native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 15: OpenCode Ancestor Conflict
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 15: OpenCode Ancestor Conflict${colors.reset}\n`);
|
|
|
|
try {
|
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-ancestor-test-'));
|
|
const parentProjectDir = path.join(tempRoot, 'parent');
|
|
const childProjectDir = path.join(parentProjectDir, 'child');
|
|
const installedBmadDir = await createTestBmadFixture();
|
|
|
|
await fs.ensureDir(path.join(parentProjectDir, '.git'));
|
|
await fs.ensureDir(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing'));
|
|
await fs.ensureDir(childProjectDir);
|
|
await fs.writeFile(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager = new IdeManager();
|
|
await ideManager.ensureInitialized();
|
|
const result = await ideManager.setup('opencode', childProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
const expectedConflictDir = await fs.realpath(path.join(parentProjectDir, '.opencode', 'skills'));
|
|
|
|
assert(result.success === false, 'OpenCode setup refuses install when ancestor skills already exist');
|
|
assert(result.handlerResult?.reason === 'ancestor-conflict', 'OpenCode ancestor rejection reports ancestor-conflict reason');
|
|
assert(
|
|
result.handlerResult?.conflictDir === expectedConflictDir,
|
|
'OpenCode ancestor rejection points at ancestor .opencode/skills dir',
|
|
);
|
|
|
|
await fs.remove(tempRoot);
|
|
await fs.remove(installedBmadDir);
|
|
} catch (error) {
|
|
assert(false, 'OpenCode ancestor conflict protection test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 16: QA Agent Compilation
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 16: QA Agent Compilation${colors.reset}\n`);
|
|
|
|
try {
|
|
const builder = new YamlXmlBuilder();
|
|
const qaAgentPath = path.join(projectRoot, 'src/bmm/agents/qa.agent.yaml');
|
|
const tempOutput = path.join(__dirname, 'temp-qa-agent.md');
|
|
|
|
try {
|
|
const result = await builder.buildAgent(qaAgentPath, null, tempOutput, { includeMetadata: true });
|
|
const compiled = await fs.readFile(tempOutput, 'utf8');
|
|
|
|
assert(compiled.includes('QA Engineer'), 'QA agent compilation includes agent title');
|
|
|
|
assert(compiled.includes('qa-generate-e2e-tests'), 'QA agent menu includes automate workflow');
|
|
|
|
// Cleanup
|
|
await fs.remove(tempOutput);
|
|
} catch (error) {
|
|
assert(false, 'QA agent compiles successfully', error.message);
|
|
}
|
|
} catch (error) {
|
|
assert(false, 'QA compilation test setup', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 11: Shard-doc Prototype Duplication (Skill-Format Only)
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 11: Shard-doc Prototype Duplication${colors.reset}\n`);
|
|
|
|
let tempCodexProjectDir;
|
|
let tempGeminiProjectDir;
|
|
let installedBmadDir;
|
|
try {
|
|
clearCache();
|
|
const platformCodes = await loadPlatformCodes();
|
|
const codexInstaller = platformCodes.platforms.codex?.installer;
|
|
const geminiInstaller = platformCodes.platforms.gemini?.installer;
|
|
|
|
assert(codexInstaller?.skill_format === true, 'Codex installer uses skill_format output');
|
|
assert(geminiInstaller?.skill_format === true, 'Gemini installer uses skill_format output');
|
|
|
|
tempCodexProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-prototype-test-'));
|
|
tempGeminiProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-prototype-test-'));
|
|
installedBmadDir = await createShardDocPrototypeFixture();
|
|
|
|
const ideManager = new IdeManager();
|
|
await ideManager.ensureInitialized();
|
|
|
|
const codexResult = await ideManager.setup('codex', tempCodexProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(codexResult.success === true, 'Codex setup succeeds for shard-doc prototype fixture');
|
|
|
|
const codexCanonicalSkill = path.join(tempCodexProjectDir, '.agents', 'skills', 'bmad-shard-doc', 'SKILL.md');
|
|
const codexPrototypeSkill = path.join(tempCodexProjectDir, '.agents', 'skills', 'bmad-shard-doc-skill-prototype', 'SKILL.md');
|
|
assert(await fs.pathExists(codexCanonicalSkill), 'Codex install writes canonical shard-doc skill');
|
|
assert(await fs.pathExists(codexPrototypeSkill), 'Codex install writes duplicated shard-doc prototype skill');
|
|
|
|
const codexCanonicalContent = await fs.readFile(codexCanonicalSkill, 'utf8');
|
|
const codexPrototypeContent = await fs.readFile(codexPrototypeSkill, 'utf8');
|
|
assert(codexCanonicalContent.includes('name: bmad-shard-doc'), 'Canonical shard-doc skill keeps canonical frontmatter name');
|
|
assert(
|
|
codexPrototypeContent.includes('name: bmad-shard-doc-skill-prototype'),
|
|
'Prototype shard-doc skill uses prototype frontmatter name',
|
|
);
|
|
assert(
|
|
codexPrototypeContent.includes('Prototype marker: source-authored-skill'),
|
|
'Prototype shard-doc skill is copied from source SKILL.md',
|
|
);
|
|
|
|
const geminiResult = await ideManager.setup('gemini', tempGeminiProjectDir, installedBmadDir, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(geminiResult.success === true, 'Gemini setup succeeds for shard-doc prototype fixture');
|
|
|
|
const geminiCanonicalSkill = path.join(tempGeminiProjectDir, '.gemini', 'skills', 'bmad-shard-doc', 'SKILL.md');
|
|
const geminiPrototypeSkill = path.join(tempGeminiProjectDir, '.gemini', 'skills', 'bmad-shard-doc-skill-prototype', 'SKILL.md');
|
|
assert(await fs.pathExists(geminiCanonicalSkill), 'Gemini install writes canonical shard-doc skill');
|
|
assert(await fs.pathExists(geminiPrototypeSkill), 'Gemini install writes duplicated shard-doc prototype skill');
|
|
} catch (error) {
|
|
assert(false, 'Shard-doc prototype duplication test succeeds', error.message);
|
|
} finally {
|
|
await Promise.allSettled([tempCodexProjectDir, tempGeminiProjectDir, installedBmadDir].filter(Boolean).map((dir) => fs.remove(dir)));
|
|
}
|
|
|
|
// Test 17: GitHub Copilot Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 17: GitHub Copilot Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes17 = await loadPlatformCodes();
|
|
const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer;
|
|
|
|
assert(copilotInstaller?.target_dir === '.github/skills', 'GitHub Copilot target_dir uses native skills path');
|
|
|
|
assert(copilotInstaller?.skill_format === true, 'GitHub Copilot installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/agents'),
|
|
'GitHub Copilot installer cleans legacy agents output',
|
|
);
|
|
|
|
assert(
|
|
Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/prompts'),
|
|
'GitHub Copilot installer cleans legacy prompts output',
|
|
);
|
|
|
|
const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
|
|
const installedBmadDir17 = await createTestBmadFixture();
|
|
|
|
// Create legacy .github/agents/ and .github/prompts/ files
|
|
const legacyAgentsDir17 = path.join(tempProjectDir17, '.github', 'agents');
|
|
const legacyPromptsDir17 = path.join(tempProjectDir17, '.github', 'prompts');
|
|
await fs.ensureDir(legacyAgentsDir17);
|
|
await fs.ensureDir(legacyPromptsDir17);
|
|
await fs.writeFile(path.join(legacyAgentsDir17, 'bmad-legacy.agent.md'), 'legacy agent\n');
|
|
await fs.writeFile(path.join(legacyPromptsDir17, 'bmad-legacy.prompt.md'), 'legacy prompt\n');
|
|
|
|
// Create legacy copilot-instructions.md with BMAD markers
|
|
const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
|
|
await fs.writeFile(
|
|
copilotInstructionsPath17,
|
|
'User content before\n<!-- BMAD:START -->\nBMAD generated content\n<!-- BMAD:END -->\nUser content after\n',
|
|
);
|
|
|
|
const ideManager17 = new IdeManager();
|
|
await ideManager17.ensureInitialized();
|
|
const result17 = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project');
|
|
|
|
const skillFile17 = path.join(tempProjectDir17, '.github', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent17 = await fs.readFile(skillFile17, 'utf8');
|
|
const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(legacyAgentsDir17)), 'GitHub Copilot setup removes legacy agents dir');
|
|
|
|
assert(!(await fs.pathExists(legacyPromptsDir17)), 'GitHub Copilot setup removes legacy prompts dir');
|
|
|
|
// Verify copilot-instructions.md BMAD markers were stripped but user content preserved
|
|
const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8');
|
|
assert(
|
|
!cleanedInstructions17.includes('BMAD:START') && !cleanedInstructions17.includes('BMAD generated content'),
|
|
'GitHub Copilot setup strips BMAD markers from copilot-instructions.md',
|
|
);
|
|
assert(
|
|
cleanedInstructions17.includes('User content before') && cleanedInstructions17.includes('User content after'),
|
|
'GitHub Copilot setup preserves user content in copilot-instructions.md',
|
|
);
|
|
|
|
await fs.remove(tempProjectDir17);
|
|
await fs.remove(installedBmadDir17);
|
|
} catch (error) {
|
|
assert(false, 'GitHub Copilot native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 18: Cline Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 18: Cline Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes18 = await loadPlatformCodes();
|
|
const clineInstaller = platformCodes18.platforms.cline?.installer;
|
|
|
|
assert(clineInstaller?.target_dir === '.cline/skills', 'Cline target_dir uses native skills path');
|
|
|
|
assert(clineInstaller?.skill_format === true, 'Cline installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(clineInstaller?.legacy_targets) && clineInstaller.legacy_targets.includes('.clinerules/workflows'),
|
|
'Cline installer cleans legacy workflow output',
|
|
);
|
|
|
|
const tempProjectDir18 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cline-test-'));
|
|
const installedBmadDir18 = await createTestBmadFixture();
|
|
const legacyDir18 = path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir18);
|
|
await fs.writeFile(path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir18, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager18 = new IdeManager();
|
|
await ideManager18.ensureInitialized();
|
|
const result18 = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result18.success === true, 'Cline setup succeeds against temp project');
|
|
|
|
const skillFile18 = path.join(tempProjectDir18, '.cline', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile18), 'Cline install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent18 = await fs.readFile(skillFile18, 'utf8');
|
|
const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir18, '.clinerules', 'workflows'))), 'Cline setup removes legacy workflows dir');
|
|
|
|
// Reinstall/upgrade: run setup again over existing skills output
|
|
const result18b = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result18b.success === true, 'Cline reinstall/upgrade succeeds over existing skills');
|
|
assert(await fs.pathExists(skillFile18), 'Cline reinstall preserves SKILL.md output');
|
|
|
|
await fs.remove(tempProjectDir18);
|
|
await fs.remove(installedBmadDir18);
|
|
} catch (error) {
|
|
assert(false, 'Cline native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 19: CodeBuddy Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 19: CodeBuddy Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes19 = await loadPlatformCodes();
|
|
const codebuddyInstaller = platformCodes19.platforms.codebuddy?.installer;
|
|
|
|
assert(codebuddyInstaller?.target_dir === '.codebuddy/skills', 'CodeBuddy target_dir uses native skills path');
|
|
|
|
assert(codebuddyInstaller?.skill_format === true, 'CodeBuddy installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(codebuddyInstaller?.legacy_targets) && codebuddyInstaller.legacy_targets.includes('.codebuddy/commands'),
|
|
'CodeBuddy installer cleans legacy command output',
|
|
);
|
|
|
|
const tempProjectDir19 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codebuddy-test-'));
|
|
const installedBmadDir19 = await createTestBmadFixture();
|
|
const legacyDir19 = path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir19);
|
|
await fs.writeFile(path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir19, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager19 = new IdeManager();
|
|
await ideManager19.ensureInitialized();
|
|
const result19 = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result19.success === true, 'CodeBuddy setup succeeds against temp project');
|
|
|
|
const skillFile19 = path.join(tempProjectDir19, '.codebuddy', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile19), 'CodeBuddy install writes SKILL.md directory output');
|
|
|
|
const skillContent19 = await fs.readFile(skillFile19, 'utf8');
|
|
const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir19, '.codebuddy', 'commands'))), 'CodeBuddy setup removes legacy commands dir');
|
|
|
|
const result19b = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result19b.success === true, 'CodeBuddy reinstall/upgrade succeeds over existing skills');
|
|
assert(await fs.pathExists(skillFile19), 'CodeBuddy reinstall preserves SKILL.md output');
|
|
|
|
await fs.remove(tempProjectDir19);
|
|
await fs.remove(installedBmadDir19);
|
|
} catch (error) {
|
|
assert(false, 'CodeBuddy native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 20: Crush Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 20: Crush Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes20 = await loadPlatformCodes();
|
|
const crushInstaller = platformCodes20.platforms.crush?.installer;
|
|
|
|
assert(crushInstaller?.target_dir === '.crush/skills', 'Crush target_dir uses native skills path');
|
|
|
|
assert(crushInstaller?.skill_format === true, 'Crush installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(crushInstaller?.legacy_targets) && crushInstaller.legacy_targets.includes('.crush/commands'),
|
|
'Crush installer cleans legacy command output',
|
|
);
|
|
|
|
const tempProjectDir20 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-crush-test-'));
|
|
const installedBmadDir20 = await createTestBmadFixture();
|
|
const legacyDir20 = path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy-dir');
|
|
await fs.ensureDir(legacyDir20);
|
|
await fs.writeFile(path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
|
await fs.writeFile(path.join(legacyDir20, 'SKILL.md'), 'legacy\n');
|
|
|
|
const ideManager20 = new IdeManager();
|
|
await ideManager20.ensureInitialized();
|
|
const result20 = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result20.success === true, 'Crush setup succeeds against temp project');
|
|
|
|
const skillFile20 = path.join(tempProjectDir20, '.crush', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output');
|
|
|
|
const skillContent20 = await fs.readFile(skillFile20, 'utf8');
|
|
const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir20, '.crush', 'commands'))), 'Crush setup removes legacy commands dir');
|
|
|
|
const result20b = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result20b.success === true, 'Crush reinstall/upgrade succeeds over existing skills');
|
|
assert(await fs.pathExists(skillFile20), 'Crush reinstall preserves SKILL.md output');
|
|
|
|
await fs.remove(tempProjectDir20);
|
|
await fs.remove(installedBmadDir20);
|
|
} catch (error) {
|
|
assert(false, 'Crush native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Test 21: Trae Native Skills Install
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 21: Trae Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes21 = await loadPlatformCodes();
|
|
const traeInstaller = platformCodes21.platforms.trae?.installer;
|
|
|
|
assert(traeInstaller?.target_dir === '.trae/skills', 'Trae target_dir uses native skills path');
|
|
|
|
assert(traeInstaller?.skill_format === true, 'Trae installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(traeInstaller?.legacy_targets) && traeInstaller.legacy_targets.includes('.trae/rules'),
|
|
'Trae installer cleans legacy rules output',
|
|
);
|
|
|
|
const tempProjectDir21 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-trae-test-'));
|
|
const installedBmadDir21 = await createTestBmadFixture();
|
|
const legacyDir21 = path.join(tempProjectDir21, '.trae', 'rules');
|
|
await fs.ensureDir(legacyDir21);
|
|
await fs.writeFile(path.join(legacyDir21, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
const ideManager21 = new IdeManager();
|
|
await ideManager21.ensureInitialized();
|
|
const result21 = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result21.success === true, 'Trae setup succeeds against temp project');
|
|
|
|
const skillFile21 = path.join(tempProjectDir21, '.trae', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile21), 'Trae install writes SKILL.md directory output');
|
|
|
|
const skillContent21 = await fs.readFile(skillFile21, 'utf8');
|
|
const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir21, '.trae', 'rules'))), 'Trae setup removes legacy rules dir');
|
|
|
|
const result21b = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result21b.success === true, 'Trae reinstall/upgrade succeeds over existing skills');
|
|
assert(await fs.pathExists(skillFile21), 'Trae reinstall preserves SKILL.md output');
|
|
|
|
await fs.remove(tempProjectDir21);
|
|
await fs.remove(installedBmadDir21);
|
|
} catch (error) {
|
|
assert(false, 'Trae native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Suite 22: KiloCoder Suspended
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 22: KiloCoder Suspended${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes22 = await loadPlatformCodes();
|
|
const kiloConfig22 = platformCodes22.platforms.kilo;
|
|
|
|
assert(typeof kiloConfig22?.suspended === 'string', 'KiloCoder has a suspended message in platform config');
|
|
|
|
assert(kiloConfig22?.installer?.target_dir === '.kilocode/skills', 'KiloCoder retains target_dir config for future use');
|
|
|
|
const ideManager22 = new IdeManager();
|
|
await ideManager22.ensureInitialized();
|
|
|
|
// Should not appear in available IDEs
|
|
const availableIdes22 = ideManager22.getAvailableIdes();
|
|
assert(!availableIdes22.some((ide) => ide.value === 'kilo'), 'KiloCoder is hidden from IDE selection');
|
|
|
|
// Setup should be blocked but legacy files should be cleaned up
|
|
const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
|
|
const installedBmadDir22 = await createTestBmadFixture();
|
|
|
|
// Pre-populate legacy Kilo artifacts that should be cleaned up
|
|
const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows');
|
|
await fs.ensureDir(legacyDir22);
|
|
await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result22.success === false, 'KiloCoder setup is blocked when suspended');
|
|
assert(result22.error === 'suspended', 'KiloCoder setup returns suspended error');
|
|
|
|
// Should not write new skill files
|
|
assert(
|
|
!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'skills'))),
|
|
'KiloCoder does not create skills directory when suspended',
|
|
);
|
|
|
|
// Legacy files should be cleaned up
|
|
assert(
|
|
!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))),
|
|
'KiloCoder legacy workflows are cleaned up even when suspended',
|
|
);
|
|
|
|
await fs.remove(tempProjectDir22);
|
|
await fs.remove(installedBmadDir22);
|
|
} catch (error) {
|
|
assert(false, 'KiloCoder suspended test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Suite 23: Gemini CLI Native Skills
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 23: Gemini CLI Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes23 = await loadPlatformCodes();
|
|
const geminiInstaller = platformCodes23.platforms.gemini?.installer;
|
|
|
|
assert(geminiInstaller?.target_dir === '.gemini/skills', 'Gemini target_dir uses native skills path');
|
|
|
|
assert(geminiInstaller?.skill_format === true, 'Gemini installer enables native skill output');
|
|
|
|
assert(
|
|
Array.isArray(geminiInstaller?.legacy_targets) && geminiInstaller.legacy_targets.includes('.gemini/commands'),
|
|
'Gemini installer cleans legacy commands output',
|
|
);
|
|
|
|
const tempProjectDir23 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-test-'));
|
|
const installedBmadDir23 = await createTestBmadFixture();
|
|
const legacyDir23 = path.join(tempProjectDir23, '.gemini', 'commands');
|
|
await fs.ensureDir(legacyDir23);
|
|
await fs.writeFile(path.join(legacyDir23, 'bmad-legacy.toml'), 'legacy\n');
|
|
|
|
const ideManager23 = new IdeManager();
|
|
await ideManager23.ensureInitialized();
|
|
const result23 = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result23.success === true, 'Gemini setup succeeds against temp project');
|
|
|
|
const skillFile23 = path.join(tempProjectDir23, '.gemini', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output');
|
|
|
|
const skillContent23 = await fs.readFile(skillFile23, 'utf8');
|
|
const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir23, '.gemini', 'commands'))), 'Gemini setup removes legacy commands dir');
|
|
|
|
const result23b = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result23b.success === true, 'Gemini reinstall/upgrade succeeds over existing skills');
|
|
assert(await fs.pathExists(skillFile23), 'Gemini reinstall preserves SKILL.md output');
|
|
|
|
await fs.remove(tempProjectDir23);
|
|
await fs.remove(installedBmadDir23);
|
|
} catch (error) {
|
|
assert(false, 'Gemini native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Suite 24: iFlow Native Skills
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 24: iFlow Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes24 = await loadPlatformCodes();
|
|
const iflowInstaller = platformCodes24.platforms.iflow?.installer;
|
|
|
|
assert(iflowInstaller?.target_dir === '.iflow/skills', 'iFlow target_dir uses native skills path');
|
|
assert(iflowInstaller?.skill_format === true, 'iFlow installer enables native skill output');
|
|
assert(
|
|
Array.isArray(iflowInstaller?.legacy_targets) && iflowInstaller.legacy_targets.includes('.iflow/commands'),
|
|
'iFlow installer cleans legacy commands output',
|
|
);
|
|
|
|
const tempProjectDir24 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-iflow-test-'));
|
|
const installedBmadDir24 = await createTestBmadFixture();
|
|
const legacyDir24 = path.join(tempProjectDir24, '.iflow', 'commands');
|
|
await fs.ensureDir(legacyDir24);
|
|
await fs.writeFile(path.join(legacyDir24, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
const ideManager24 = new IdeManager();
|
|
await ideManager24.ensureInitialized();
|
|
const result24 = await ideManager24.setup('iflow', tempProjectDir24, installedBmadDir24, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result24.success === true, 'iFlow setup succeeds against temp project');
|
|
|
|
const skillFile24 = path.join(tempProjectDir24, '.iflow', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile24), 'iFlow install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent24 = await fs.readFile(skillFile24, 'utf8');
|
|
const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
|
|
|
|
await fs.remove(tempProjectDir24);
|
|
await fs.remove(installedBmadDir24);
|
|
} catch (error) {
|
|
assert(false, 'iFlow native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Suite 25: QwenCoder Native Skills
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 25: QwenCoder Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes25 = await loadPlatformCodes();
|
|
const qwenInstaller = platformCodes25.platforms.qwen?.installer;
|
|
|
|
assert(qwenInstaller?.target_dir === '.qwen/skills', 'QwenCoder target_dir uses native skills path');
|
|
assert(qwenInstaller?.skill_format === true, 'QwenCoder installer enables native skill output');
|
|
assert(
|
|
Array.isArray(qwenInstaller?.legacy_targets) && qwenInstaller.legacy_targets.includes('.qwen/commands'),
|
|
'QwenCoder installer cleans legacy commands output',
|
|
);
|
|
|
|
const tempProjectDir25 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-qwen-test-'));
|
|
const installedBmadDir25 = await createTestBmadFixture();
|
|
const legacyDir25 = path.join(tempProjectDir25, '.qwen', 'commands');
|
|
await fs.ensureDir(legacyDir25);
|
|
await fs.writeFile(path.join(legacyDir25, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
const ideManager25 = new IdeManager();
|
|
await ideManager25.ensureInitialized();
|
|
const result25 = await ideManager25.setup('qwen', tempProjectDir25, installedBmadDir25, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result25.success === true, 'QwenCoder setup succeeds against temp project');
|
|
|
|
const skillFile25 = path.join(tempProjectDir25, '.qwen', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile25), 'QwenCoder install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent25 = await fs.readFile(skillFile25, 'utf8');
|
|
const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
|
|
|
|
await fs.remove(tempProjectDir25);
|
|
await fs.remove(installedBmadDir25);
|
|
} catch (error) {
|
|
assert(false, 'QwenCoder native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Suite 26: Rovo Dev Native Skills
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 26: Rovo Dev Native Skills${colors.reset}\n`);
|
|
|
|
try {
|
|
clearCache();
|
|
const platformCodes26 = await loadPlatformCodes();
|
|
const rovoInstaller = platformCodes26.platforms['rovo-dev']?.installer;
|
|
|
|
assert(rovoInstaller?.target_dir === '.rovodev/skills', 'Rovo Dev target_dir uses native skills path');
|
|
assert(rovoInstaller?.skill_format === true, 'Rovo Dev installer enables native skill output');
|
|
assert(
|
|
Array.isArray(rovoInstaller?.legacy_targets) && rovoInstaller.legacy_targets.includes('.rovodev/workflows'),
|
|
'Rovo Dev installer cleans legacy workflows output',
|
|
);
|
|
|
|
const tempProjectDir26 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-rovodev-test-'));
|
|
const installedBmadDir26 = await createTestBmadFixture();
|
|
const legacyDir26 = path.join(tempProjectDir26, '.rovodev', 'workflows');
|
|
await fs.ensureDir(legacyDir26);
|
|
await fs.writeFile(path.join(legacyDir26, 'bmad-legacy.md'), 'legacy\n');
|
|
|
|
// Create a prompts.yml with BMAD entries and a user entry
|
|
const yaml26 = require('yaml');
|
|
const promptsPath26 = path.join(tempProjectDir26, '.rovodev', 'prompts.yml');
|
|
const promptsContent26 = yaml26.stringify({
|
|
prompts: [
|
|
{ name: 'bmad-bmm-create-prd', description: 'BMAD workflow', content_file: 'workflows/bmad-bmm-create-prd.md' },
|
|
{ name: 'my-custom-prompt', description: 'User prompt', content_file: 'custom.md' },
|
|
],
|
|
});
|
|
await fs.writeFile(promptsPath26, promptsContent26);
|
|
|
|
const ideManager26 = new IdeManager();
|
|
await ideManager26.ensureInitialized();
|
|
const result26 = await ideManager26.setup('rovo-dev', tempProjectDir26, installedBmadDir26, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result26.success === true, 'Rovo Dev setup succeeds against temp project');
|
|
|
|
const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
|
|
|
|
// Verify name frontmatter matches directory name
|
|
const skillContent26 = await fs.readFile(skillFile26, 'utf8');
|
|
const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m);
|
|
assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly');
|
|
|
|
assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir');
|
|
|
|
// Verify prompts.yml cleanup: BMAD entries removed, user entry preserved
|
|
const cleanedPrompts26 = yaml26.parse(await fs.readFile(promptsPath26, 'utf8'));
|
|
assert(
|
|
Array.isArray(cleanedPrompts26.prompts) && cleanedPrompts26.prompts.length === 1,
|
|
'Rovo Dev cleanup removes BMAD entries from prompts.yml',
|
|
);
|
|
assert(cleanedPrompts26.prompts[0].name === 'my-custom-prompt', 'Rovo Dev cleanup preserves non-BMAD entries in prompts.yml');
|
|
|
|
await fs.remove(tempProjectDir26);
|
|
await fs.remove(installedBmadDir26);
|
|
} catch (error) {
|
|
assert(false, 'Rovo Dev native skills migration test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Suite 27: Cleanup preserves bmad-os-* skills
|
|
// ============================================================
|
|
console.log(`${colors.yellow}Test Suite 27: Cleanup preserves bmad-os-* skills${colors.reset}\n`);
|
|
|
|
try {
|
|
const tempProjectDir27 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-os-preserve-test-'));
|
|
const installedBmadDir27 = await createTestBmadFixture();
|
|
|
|
// Pre-populate .claude/skills with bmad-os-* skills (version-controlled repo skills)
|
|
const osSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-review-pr');
|
|
await fs.ensureDir(osSkillDir27);
|
|
await fs.writeFile(
|
|
path.join(osSkillDir27, 'SKILL.md'),
|
|
'---\nname: bmad-os-review-pr\ndescription: Review PRs\n---\nOS skill content\n',
|
|
);
|
|
|
|
const osSkillDir27b = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-release-module');
|
|
await fs.ensureDir(osSkillDir27b);
|
|
await fs.writeFile(
|
|
path.join(osSkillDir27b, 'SKILL.md'),
|
|
'---\nname: bmad-os-release-module\ndescription: Release module\n---\nOS skill content\n',
|
|
);
|
|
|
|
// Also add a regular bmad skill that SHOULD be cleaned up
|
|
const regularSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-architect');
|
|
await fs.ensureDir(regularSkillDir27);
|
|
await fs.writeFile(
|
|
path.join(regularSkillDir27, 'SKILL.md'),
|
|
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
|
|
);
|
|
|
|
// Run Claude Code setup (which triggers cleanup then install)
|
|
const ideManager27 = new IdeManager();
|
|
await ideManager27.ensureInitialized();
|
|
const result27 = await ideManager27.setup('claude-code', tempProjectDir27, installedBmadDir27, {
|
|
silent: true,
|
|
selectedModules: ['bmm'],
|
|
});
|
|
|
|
assert(result27.success === true, 'Claude Code setup succeeds with bmad-os-* skills present');
|
|
|
|
// bmad-os-* skills must survive
|
|
assert(await fs.pathExists(osSkillDir27), 'Cleanup preserves bmad-os-review-pr skill');
|
|
assert(await fs.pathExists(osSkillDir27b), 'Cleanup preserves bmad-os-release-module skill');
|
|
|
|
// bmad-os skill content must be untouched
|
|
const osContent27 = await fs.readFile(path.join(osSkillDir27, 'SKILL.md'), 'utf8');
|
|
assert(osContent27.includes('OS skill content'), 'bmad-os-review-pr skill content is unchanged');
|
|
|
|
// Regular bmad skill should have been replaced by fresh install
|
|
const newSkillFile27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-master', 'SKILL.md');
|
|
assert(await fs.pathExists(newSkillFile27), 'Fresh bmad skills are installed alongside preserved bmad-os-* skills');
|
|
|
|
// Stale non-bmad-os skill must have been removed by cleanup
|
|
assert(!(await fs.pathExists(regularSkillDir27)), 'Cleanup removes stale non-bmad-os skills');
|
|
|
|
await fs.remove(tempProjectDir27);
|
|
await fs.remove(installedBmadDir27);
|
|
} catch (error) {
|
|
assert(false, 'bmad-os-* skill preservation test succeeds', error.message);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// ============================================================
|
|
// Summary
|
|
// ============================================================
|
|
console.log(`${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 installation component tests passed!${colors.reset}\n`);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(`${colors.red}❌ Some installation component tests failed${colors.reset}\n`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run tests
|
|
runTests().catch((error) => {
|
|
console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message);
|
|
console.error(error.stack);
|
|
process.exit(1);
|
|
});
|