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
JavaScript
/**
* 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 };