BMAD-METHOD/test/verify-installer.js

215 lines
8.1 KiB
JavaScript

/**
* Verification Test for install-bmad-memtrace.sh
*
* Simulates the installation process in a clean temporary directory by:
* 1. Initializing a mock Git repository.
* 2. Committing some dummy "legacy clone" files.
* 3. Adding the install script and helper script fixtures.
* 4. Running the bash installer.
* 5. Verifying cleanup, restore, workspace anchor generation, and MCP config injection.
*
* Usage: node test/verify-installer.js
*/
const fs = require('node:fs/promises');
const path = require('node:path');
const os = require('node:os');
const { exec } = require('node:child_process');
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++;
}
}
function runCmd(command, cwd) {
return new Promise((resolve, reject) => {
exec(command, { cwd }, (error, stdout, stderr) => {
if (error) {
reject({ error, stdout, stderr });
} else {
resolve({ stdout, stderr });
}
});
});
}
// Helper to ensure parent directories exist
async function ensureDir(filePath) {
const dir = path.dirname(filePath);
try {
await fs.mkdir(dir, { recursive: true });
} catch (err) {}
}
async function runVerification() {
console.log(`${colors.cyan}========================================`);
console.log('Standalone Installer Verification');
console.log(`========================================${colors.reset}\n`);
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-installer-verify-'));
console.log(`Created temporary test environment at: ${tempDir}\n`);
try {
// 1. Initialize git repository
await runCmd('git init -b main', tempDir);
await runCmd('git config user.name "Test"', tempDir);
await runCmd('git config user.email "test@example.com"', tempDir);
// 2. Create and commit dummy cloned files (to simulate legacy clone cleanup)
const dummyClonedFile = path.join(tempDir, 'README.md');
await fs.writeFile(dummyClonedFile, '# Legacy README\n');
await runCmd('git add README.md', tempDir);
await runCmd('git commit -m "initial commit"', tempDir);
// 3. Create core BMad directory structure and helper script
const helperSourcePath = path.resolve(__dirname, '../_bmad/scripts/memtrace/inject-mcp-config.mjs');
const helperDestPath = path.join(tempDir, '_bmad/scripts/memtrace/inject-mcp-config.mjs');
await ensureDir(helperDestPath);
await fs.copyFile(helperSourcePath, helperDestPath);
// Also copy install script
const installerSourcePath = path.resolve(__dirname, '../install-bmad-memtrace.sh');
const installerDestPath = path.join(tempDir, 'install-bmad-memtrace.sh');
await fs.copyFile(installerSourcePath, installerDestPath);
// 4. Run installer script in bash
// We override TEST_CLAUDE_CONFIG_PATH and TEST_OPENCODE_CONFIG_PATH so it targets temp config files
const claudeTestConfig = path.join(tempDir, 'claude_desktop_config.json');
const opencodeTestConfig = path.join(tempDir, 'opencode.json');
console.log('Running install-bmad-memtrace.sh in temp directory...');
const envOverrides = {
...process.env,
TEST_CLAUDE_CONFIG_PATH: claudeTestConfig,
TEST_OPENCODE_CONFIG_PATH: opencodeTestConfig,
};
// Run bash on Windows (locate git-bash path to avoid WSL-relay failures)
let command = 'bash install-bmad-memtrace.sh';
if (process.platform === 'win32') {
try {
const { execSync } = require('node:child_process');
const gitPath = execSync('where git').toString().trim().split('\r\n')[0];
if (gitPath) {
const gitDir = path.dirname(gitPath); // C:\Program Files\Git\cmd
const gitParent = path.dirname(gitDir);
const possibleBash1 = path.join(gitParent, 'bin', 'bash.exe');
const possibleBash2 = path.join(gitParent, 'bin', 'sh.exe');
if (await fs.access(possibleBash1).then(() => true).catch(() => false)) {
command = `"${possibleBash1}" install-bmad-memtrace.sh`;
} else if (await fs.access(possibleBash2).then(() => true).catch(() => false)) {
command = `"${possibleBash2}" install-bmad-memtrace.sh`;
}
}
} catch (e) {
// Fallback
const paths = [
'C:\\Program Files\\Git\\bin\\bash.exe',
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
];
for (const p of paths) {
if (await fs.access(p).then(() => true).catch(() => false)) {
command = `"${p}" install-bmad-memtrace.sh`;
break;
}
}
}
}
console.log(`Running installer with command: ${command}`);
const result = await new Promise((resolve, reject) => {
exec(command, { cwd: tempDir, env: envOverrides }, (error, stdout, stderr) => {
if (error) reject({ error, stdout, stderr });
else resolve({ stdout, stderr });
});
});
console.log(`${colors.dim}${result.stdout}${colors.reset}\n`);
// 5. Assertions
// AC 1: .memtrace-workspace exists
const anchorExists = await fs.access(path.join(tempDir, '.memtrace-workspace')).then(() => true).catch(() => false);
assert(anchorExists, 'AC 1: .memtrace-workspace anchor file successfully created in project root');
// Legacy cleanup: README.md is deleted
const readmeExists = await fs.access(dummyClonedFile).then(() => true).catch(() => false);
assert(!readmeExists, 'Legacy cleanup: Tracked clone files (README.md) successfully deleted');
// Git removal: .git is deleted
const gitExists = await fs.access(path.join(tempDir, '.git')).then(() => true).catch(() => false);
assert(!gitExists, 'Security/Standalone: .git directory completely removed');
// Staging cleanup: bmad-install is deleted
const stagingExists = await fs.access(path.join(tempDir, 'bmad-install')).then(() => true).catch(() => false);
assert(!stagingExists, 'Runtime cleanup: bmad-install staging directory successfully removed');
// BMad preservation: _bmad directory remains
const bmadExists = await fs.access(path.join(tempDir, '_bmad')).then(() => true).catch(() => false);
assert(bmadExists, 'Core preservation: _bmad directory preserved post-cleanup');
// AC 2: Claude Desktop configuration successfully injected
const claudeContent = await fs.readFile(claudeTestConfig, 'utf8');
const claudeConfig = JSON.parse(claudeContent);
assert(
claudeConfig.mcpServers && claudeConfig.mcpServers.memtrace && claudeConfig.mcpServers.memtrace.command === 'memtrace',
'AC 2: Claude Desktop config correctly created and populated with memtrace MCP server'
);
// AC 3: OpenCode configuration successfully injected
const opencodeContent = await fs.readFile(opencodeTestConfig, 'utf8');
const opencodeConfig = JSON.parse(opencodeContent);
assert(
opencodeConfig.mcp && opencodeConfig.mcp.memtrace && opencodeConfig.mcp.memtrace.type === 'local',
'AC 3: OpenCode config correctly created and populated with memtrace local definition'
);
} catch (err) {
console.error('Verification failed with error:', err);
if (err.stdout) console.error('stdout:', err.stdout);
if (err.stderr) console.error('stderr:', err.stderr);
failed++;
}
// Clean up
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (err) {}
console.log(`\n${colors.cyan}========================================`);
console.log(`Verification Summary: Passed: ${passed}, Failed: ${failed}`);
console.log(`========================================${colors.reset}\n`);
if (failed > 0) {
process.exit(1);
} else {
process.exit(0);
}
}
runVerification().catch(err => {
console.error('Fatal verification error:', err);
process.exit(1);
});