UNPKG

local-memory-mcp

Version:

Local Memory MCP Server - AI-powered persistent memory system for Claude Desktop and other MCP-compatible tools

408 lines (329 loc) โ€ข 12.4 kB
#!/usr/bin/env node /** * Local Memory Server NPM Package - Comprehensive Test Suite * * This test suite validates the npm package installation, binary downloads, * platform detection, and basic functionality across different scenarios. */ const { spawn, exec } = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); const https = require('https'); // Test configuration const TEST_CONFIG = { timeout: 30000, verbose: process.argv.includes('--verbose') || process.env.VERBOSE === 'true', cleanup: !process.argv.includes('--no-cleanup'), testDir: path.join(os.tmpdir(), `local-memory-npm-test-${Date.now()}`) }; /** * Test utilities */ class TestRunner { constructor() { this.tests = []; this.passed = 0; this.failed = 0; this.skipped = 0; } log(message, level = 'info') { const timestamp = new Date().toISOString(); const prefix = { info: '๐Ÿ“‹', success: 'โœ…', error: 'โŒ', warning: 'โš ๏ธ', debug: '๐Ÿ”' }[level] || '๐Ÿ“‹'; console.log(`${prefix} ${message}`); if (level === 'debug' && !TEST_CONFIG.verbose) { return; // Skip debug messages unless verbose } } async runCommand(command, args = [], options = {}) { return new Promise((resolve, reject) => { const proc = spawn(command, args, { stdio: TEST_CONFIG.verbose ? 'inherit' : 'pipe', timeout: TEST_CONFIG.timeout, ...options }); let stdout = ''; let stderr = ''; if (!TEST_CONFIG.verbose) { proc.stdout?.on('data', data => stdout += data.toString()); proc.stderr?.on('data', data => stderr += data.toString()); } proc.on('close', code => resolve({ code, stdout, stderr })); proc.on('error', reject); }); } async test(name, testFn, options = {}) { this.log(`Running: ${name}`); try { await testFn(); this.passed++; this.log(`PASS: ${name}`, 'success'); } catch (error) { this.failed++; this.log(`FAIL: ${name} - ${error.message}`, 'error'); if (TEST_CONFIG.verbose) { console.error(error.stack); } if (options.critical) { throw new Error(`Critical test failed: ${name}`); } } } summary() { const total = this.passed + this.failed + this.skipped; this.log(`\nTest Summary: ${this.passed}/${total} passed, ${this.failed} failed, ${this.skipped} skipped`); if (this.failed > 0) { this.log('Some tests failed!', 'error'); process.exit(1); } else { this.log('All tests passed!', 'success'); process.exit(0); } } } /** * Test functions */ async function testPackageStructure(runner) { await runner.test('Package structure validation', async () => { const packageDir = path.join(__dirname, '..'); // Check required files exist const requiredFiles = [ 'package.json', 'README.md', 'CHANGELOG.md', 'index.js', 'bin/local-memory', 'scripts/install.js', 'scripts/test.js', 'scripts/uninstall.js' ]; for (const file of requiredFiles) { const filePath = path.join(packageDir, file); if (!fs.existsSync(filePath)) { throw new Error(`Missing required file: ${file}`); } } // Validate package.json const packageJson = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')); if (packageJson.name !== '@local-memory/server') { throw new Error('Invalid package name'); } if (!packageJson.version || !packageJson.version.match(/^\d+\.\d+\.\d+/)) { throw new Error('Invalid package version'); } if (!packageJson.bin || !packageJson.bin['local-memory']) { throw new Error('Missing binary configuration'); } runner.log('Package structure is valid', 'debug'); }); } async function testPlatformDetection(runner) { await runner.test('Platform detection', async () => { const { getPlatformInfo } = require('../scripts/install.js'); const platformInfo = getPlatformInfo(); if (!platformInfo.binaryName) { throw new Error('Platform detection failed - no binary name'); } if (!platformInfo.platformPath) { throw new Error('Platform detection failed - no platform path'); } // Verify binary name follows expected pattern const validPatterns = [ /^local-memory-linux$/, /^local-memory-macos-(arm|intel)$/, /^local-memory-windows\.exe$/ ]; const isValid = validPatterns.some(pattern => pattern.test(platformInfo.binaryName)); if (!isValid) { throw new Error(`Invalid binary name for platform: ${platformInfo.binaryName}`); } runner.log(`Detected platform: ${platformInfo.platform}-${platformInfo.arch} -> ${platformInfo.binaryName}`, 'debug'); }); } async function testInstallationSimulation(runner) { await runner.test('Installation simulation', async () => { // Create temporary test environment fs.mkdirSync(TEST_CONFIG.testDir, { recursive: true }); // Copy package files to test directory const packageDir = path.join(__dirname, '..'); const testPackageDir = path.join(TEST_CONFIG.testDir, 'package'); // Copy package structure (without binaries for simulation) await runner.runCommand('cp', ['-r', packageDir, testPackageDir]); // Remove any existing binaries to simulate fresh install const binDir = path.join(testPackageDir, 'bin'); if (fs.existsSync(binDir)) { fs.rmSync(binDir, { recursive: true, force: true }); } // Test that installation script can detect platform const originalCwd = process.cwd(); process.chdir(testPackageDir); const installScriptPath = path.join(testPackageDir, 'scripts', 'install.js'); delete require.cache[installScriptPath]; const { getPlatformInfo } = require(installScriptPath); const platformInfo = getPlatformInfo(); if (!platformInfo || !platformInfo.binaryName) { throw new Error('Installation simulation failed - platform detection'); } runner.log('Installation simulation passed', 'debug'); // Cleanup process.chdir(originalCwd); if (TEST_CONFIG.cleanup) { fs.rmSync(TEST_CONFIG.testDir, { recursive: true, force: true }); } }); } async function testMainEntryPoint(runner) { await runner.test('Main entry point functionality', async () => { const indexPath = path.join(__dirname, '..', 'index.js'); // Test that index.js can be required without errors delete require.cache[require.resolve(indexPath)]; const indexModule = require(indexPath); if (typeof indexModule.getBinaryName !== 'function') { throw new Error('Missing getBinaryName function'); } if (typeof indexModule.getBinaryPath !== 'function') { throw new Error('Missing getBinaryPath function'); } if (typeof indexModule.main !== 'function') { throw new Error('Missing main function'); } // Test binary name detection const binaryName = indexModule.getBinaryName(); if (!binaryName || typeof binaryName !== 'string') { throw new Error('Invalid binary name detection'); } runner.log(`Entry point functions work correctly, binary name: ${binaryName}`, 'debug'); }); } async function testScriptFunctionality(runner) { await runner.test('Script functionality', async () => { const scriptsDir = path.join(__dirname, '..', 'scripts'); // Test that all scripts can be required const scripts = ['install.js', 'test.js', 'uninstall.js']; for (const script of scripts) { const scriptPath = path.join(scriptsDir, script); if (!fs.existsSync(scriptPath)) { throw new Error(`Missing script: ${script}`); } // Check that script is executable const stats = fs.statSync(scriptPath); if (!(stats.mode & 0o111)) { // Not executable - this is okay for scripts, they're run with node runner.log(`Script ${script} is not executable (but will work with node)`, 'debug'); } // Test that script can be required (basic syntax check) try { delete require.cache[require.resolve(scriptPath)]; require(scriptPath); } catch (error) { throw new Error(`Script ${script} has syntax errors: ${error.message}`); } } runner.log('All scripts have valid syntax', 'debug'); }); } async function testDownloadURLGeneration(runner) { await runner.test('Download URL generation', async () => { const { getPlatformInfo } = require('../scripts/install.js'); const platformInfo = getPlatformInfo(); // Test GitHub releases URL generation const githubUrl = `https://github.com/danieleugenewilliams/local-memory-golang/releases/latest/download/${platformInfo.binaryName}`; // Test that URL is properly formatted if (!githubUrl.includes(platformInfo.binaryName)) { throw new Error('GitHub URL does not include binary name'); } if (!githubUrl.startsWith('https://')) { throw new Error('GitHub URL is not HTTPS'); } runner.log(`Generated GitHub fallback URL: ${githubUrl}`, 'debug'); // We don't test the actual download here to avoid network dependencies // That will be tested in the integration test phase }); } async function testPackageJsonValidation(runner) { await runner.test('package.json validation', async () => { const packageJsonPath = path.join(__dirname, '..', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); // Validate required fields const requiredFields = [ 'name', 'version', 'description', 'keywords', 'author', 'license', 'homepage', 'repository', 'bugs', 'engines', 'main', 'bin', 'scripts', 'files' ]; for (const field of requiredFields) { if (!packageJson[field]) { throw new Error(`Missing required field: ${field}`); } } // Validate specific values if (!packageJson.keywords.includes('mcp')) { throw new Error('Package should include "mcp" keyword'); } if (!packageJson.engines.node) { throw new Error('Missing Node.js engine requirement'); } if (!packageJson.scripts.postinstall) { throw new Error('Missing postinstall script'); } if (!packageJson.mcp || !packageJson.mcp.server) { throw new Error('Missing MCP server metadata'); } runner.log('package.json validation passed', 'debug'); }); } /** * Main test runner */ async function runTests() { const runner = new TestRunner(); console.log('๐Ÿงช Local Memory NPM Package - Comprehensive Test Suite'); console.log('===================================================='); console.log(`Test directory: ${TEST_CONFIG.testDir}`); console.log(`Verbose: ${TEST_CONFIG.verbose}`); console.log(`Cleanup: ${TEST_CONFIG.cleanup}`); console.log(''); try { // Structure and validation tests await testPackageStructure(runner); await testPackageJsonValidation(runner); // Platform and installation tests await testPlatformDetection(runner); await testMainEntryPoint(runner); await testScriptFunctionality(runner); // URL and download tests (without network) await testDownloadURLGeneration(runner); // Installation simulation await testInstallationSimulation(runner); } catch (error) { runner.log(`Critical error: ${error.message}`, 'error'); if (TEST_CONFIG.verbose) { console.error(error.stack); } process.exit(1); } finally { // Cleanup if (TEST_CONFIG.cleanup && fs.existsSync(TEST_CONFIG.testDir)) { try { fs.rmSync(TEST_CONFIG.testDir, { recursive: true, force: true }); runner.log('Cleaned up test directory', 'debug'); } catch (error) { runner.log(`Cleanup warning: ${error.message}`, 'warning'); } } } runner.summary(); } // Run tests if executed directly if (require.main === module) { runTests().catch(error => { console.error('Test runner crashed:', error); process.exit(1); }); } module.exports = { runTests, TestRunner };