local-memory-mcp 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +256 -0
- package/bin/local-memory +15 -0
- package/bin/local-memory-macos-arm +0 -0
- package/bin/local-memory-macos-intel +0 -0
- package/bin/local-memory-windows.exe +0 -0
- package/index.js +120 -0
- package/package.json +79 -0
- package/scripts/install.js +300 -0
- package/scripts/test.js +193 -0
- package/scripts/uninstall.js +106 -0
- package/test/test-integration.js +423 -0
- package/test/test-npm-package.js +408 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local Memory Server NPM Package - Comprehensive Test Suite
|
|
5
|
+
*
|
|
6
|
+
* This test suite validates the npm package installation, binary downloads,
|
|
7
|
+
* platform detection, and basic functionality across different scenarios.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn, exec } = require('child_process');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const https = require('https');
|
|
15
|
+
|
|
16
|
+
// Test configuration
|
|
17
|
+
const TEST_CONFIG = {
|
|
18
|
+
timeout: 30000,
|
|
19
|
+
verbose: process.argv.includes('--verbose') || process.env.VERBOSE === 'true',
|
|
20
|
+
cleanup: !process.argv.includes('--no-cleanup'),
|
|
21
|
+
testDir: path.join(os.tmpdir(), `local-memory-npm-test-${Date.now()}`)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Test utilities
|
|
26
|
+
*/
|
|
27
|
+
class TestRunner {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.tests = [];
|
|
30
|
+
this.passed = 0;
|
|
31
|
+
this.failed = 0;
|
|
32
|
+
this.skipped = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
log(message, level = 'info') {
|
|
36
|
+
const timestamp = new Date().toISOString();
|
|
37
|
+
const prefix = {
|
|
38
|
+
info: '๐',
|
|
39
|
+
success: 'โ
',
|
|
40
|
+
error: 'โ',
|
|
41
|
+
warning: 'โ ๏ธ',
|
|
42
|
+
debug: '๐'
|
|
43
|
+
}[level] || '๐';
|
|
44
|
+
|
|
45
|
+
console.log(`${prefix} ${message}`);
|
|
46
|
+
|
|
47
|
+
if (level === 'debug' && !TEST_CONFIG.verbose) {
|
|
48
|
+
return; // Skip debug messages unless verbose
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async runCommand(command, args = [], options = {}) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const proc = spawn(command, args, {
|
|
55
|
+
stdio: TEST_CONFIG.verbose ? 'inherit' : 'pipe',
|
|
56
|
+
timeout: TEST_CONFIG.timeout,
|
|
57
|
+
...options
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let stdout = '';
|
|
61
|
+
let stderr = '';
|
|
62
|
+
|
|
63
|
+
if (!TEST_CONFIG.verbose) {
|
|
64
|
+
proc.stdout?.on('data', data => stdout += data.toString());
|
|
65
|
+
proc.stderr?.on('data', data => stderr += data.toString());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
proc.on('close', code => resolve({ code, stdout, stderr }));
|
|
69
|
+
proc.on('error', reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async test(name, testFn, options = {}) {
|
|
74
|
+
this.log(`Running: ${name}`);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await testFn();
|
|
78
|
+
this.passed++;
|
|
79
|
+
this.log(`PASS: ${name}`, 'success');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.failed++;
|
|
82
|
+
this.log(`FAIL: ${name} - ${error.message}`, 'error');
|
|
83
|
+
if (TEST_CONFIG.verbose) {
|
|
84
|
+
console.error(error.stack);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (options.critical) {
|
|
88
|
+
throw new Error(`Critical test failed: ${name}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
summary() {
|
|
94
|
+
const total = this.passed + this.failed + this.skipped;
|
|
95
|
+
this.log(`\nTest Summary: ${this.passed}/${total} passed, ${this.failed} failed, ${this.skipped} skipped`);
|
|
96
|
+
|
|
97
|
+
if (this.failed > 0) {
|
|
98
|
+
this.log('Some tests failed!', 'error');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
} else {
|
|
101
|
+
this.log('All tests passed!', 'success');
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Test functions
|
|
109
|
+
*/
|
|
110
|
+
async function testPackageStructure(runner) {
|
|
111
|
+
await runner.test('Package structure validation', async () => {
|
|
112
|
+
const packageDir = path.join(__dirname, '..');
|
|
113
|
+
|
|
114
|
+
// Check required files exist
|
|
115
|
+
const requiredFiles = [
|
|
116
|
+
'package.json',
|
|
117
|
+
'README.md',
|
|
118
|
+
'CHANGELOG.md',
|
|
119
|
+
'index.js',
|
|
120
|
+
'bin/local-memory',
|
|
121
|
+
'scripts/install.js',
|
|
122
|
+
'scripts/test.js',
|
|
123
|
+
'scripts/uninstall.js'
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
for (const file of requiredFiles) {
|
|
127
|
+
const filePath = path.join(packageDir, file);
|
|
128
|
+
if (!fs.existsSync(filePath)) {
|
|
129
|
+
throw new Error(`Missing required file: ${file}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Validate package.json
|
|
134
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
135
|
+
|
|
136
|
+
if (packageJson.name !== '@local-memory/server') {
|
|
137
|
+
throw new Error('Invalid package name');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!packageJson.version || !packageJson.version.match(/^\d+\.\d+\.\d+/)) {
|
|
141
|
+
throw new Error('Invalid package version');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!packageJson.bin || !packageJson.bin['local-memory']) {
|
|
145
|
+
throw new Error('Missing binary configuration');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
runner.log('Package structure is valid', 'debug');
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function testPlatformDetection(runner) {
|
|
153
|
+
await runner.test('Platform detection', async () => {
|
|
154
|
+
const { getPlatformInfo } = require('../scripts/install.js');
|
|
155
|
+
|
|
156
|
+
const platformInfo = getPlatformInfo();
|
|
157
|
+
|
|
158
|
+
if (!platformInfo.binaryName) {
|
|
159
|
+
throw new Error('Platform detection failed - no binary name');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!platformInfo.platformPath) {
|
|
163
|
+
throw new Error('Platform detection failed - no platform path');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Verify binary name follows expected pattern
|
|
167
|
+
const validPatterns = [
|
|
168
|
+
/^local-memory-linux$/,
|
|
169
|
+
/^local-memory-macos-(arm|intel)$/,
|
|
170
|
+
/^local-memory-windows\.exe$/
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const isValid = validPatterns.some(pattern => pattern.test(platformInfo.binaryName));
|
|
174
|
+
if (!isValid) {
|
|
175
|
+
throw new Error(`Invalid binary name for platform: ${platformInfo.binaryName}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
runner.log(`Detected platform: ${platformInfo.platform}-${platformInfo.arch} -> ${platformInfo.binaryName}`, 'debug');
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function testInstallationSimulation(runner) {
|
|
183
|
+
await runner.test('Installation simulation', async () => {
|
|
184
|
+
// Create temporary test environment
|
|
185
|
+
fs.mkdirSync(TEST_CONFIG.testDir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
// Copy package files to test directory
|
|
188
|
+
const packageDir = path.join(__dirname, '..');
|
|
189
|
+
const testPackageDir = path.join(TEST_CONFIG.testDir, 'package');
|
|
190
|
+
|
|
191
|
+
// Copy package structure (without binaries for simulation)
|
|
192
|
+
await runner.runCommand('cp', ['-r', packageDir, testPackageDir]);
|
|
193
|
+
|
|
194
|
+
// Remove any existing binaries to simulate fresh install
|
|
195
|
+
const binDir = path.join(testPackageDir, 'bin');
|
|
196
|
+
if (fs.existsSync(binDir)) {
|
|
197
|
+
fs.rmSync(binDir, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Test that installation script can detect platform
|
|
201
|
+
const originalCwd = process.cwd();
|
|
202
|
+
process.chdir(testPackageDir);
|
|
203
|
+
|
|
204
|
+
const installScriptPath = path.join(testPackageDir, 'scripts', 'install.js');
|
|
205
|
+
delete require.cache[installScriptPath];
|
|
206
|
+
const { getPlatformInfo } = require(installScriptPath);
|
|
207
|
+
const platformInfo = getPlatformInfo();
|
|
208
|
+
|
|
209
|
+
if (!platformInfo || !platformInfo.binaryName) {
|
|
210
|
+
throw new Error('Installation simulation failed - platform detection');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
runner.log('Installation simulation passed', 'debug');
|
|
214
|
+
|
|
215
|
+
// Cleanup
|
|
216
|
+
process.chdir(originalCwd);
|
|
217
|
+
if (TEST_CONFIG.cleanup) {
|
|
218
|
+
fs.rmSync(TEST_CONFIG.testDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function testMainEntryPoint(runner) {
|
|
224
|
+
await runner.test('Main entry point functionality', async () => {
|
|
225
|
+
const indexPath = path.join(__dirname, '..', 'index.js');
|
|
226
|
+
|
|
227
|
+
// Test that index.js can be required without errors
|
|
228
|
+
delete require.cache[require.resolve(indexPath)];
|
|
229
|
+
const indexModule = require(indexPath);
|
|
230
|
+
|
|
231
|
+
if (typeof indexModule.getBinaryName !== 'function') {
|
|
232
|
+
throw new Error('Missing getBinaryName function');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (typeof indexModule.getBinaryPath !== 'function') {
|
|
236
|
+
throw new Error('Missing getBinaryPath function');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (typeof indexModule.main !== 'function') {
|
|
240
|
+
throw new Error('Missing main function');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Test binary name detection
|
|
244
|
+
const binaryName = indexModule.getBinaryName();
|
|
245
|
+
if (!binaryName || typeof binaryName !== 'string') {
|
|
246
|
+
throw new Error('Invalid binary name detection');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
runner.log(`Entry point functions work correctly, binary name: ${binaryName}`, 'debug');
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function testScriptFunctionality(runner) {
|
|
254
|
+
await runner.test('Script functionality', async () => {
|
|
255
|
+
const scriptsDir = path.join(__dirname, '..', 'scripts');
|
|
256
|
+
|
|
257
|
+
// Test that all scripts can be required
|
|
258
|
+
const scripts = ['install.js', 'test.js', 'uninstall.js'];
|
|
259
|
+
|
|
260
|
+
for (const script of scripts) {
|
|
261
|
+
const scriptPath = path.join(scriptsDir, script);
|
|
262
|
+
|
|
263
|
+
if (!fs.existsSync(scriptPath)) {
|
|
264
|
+
throw new Error(`Missing script: ${script}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check that script is executable
|
|
268
|
+
const stats = fs.statSync(scriptPath);
|
|
269
|
+
if (!(stats.mode & 0o111)) {
|
|
270
|
+
// Not executable - this is okay for scripts, they're run with node
|
|
271
|
+
runner.log(`Script ${script} is not executable (but will work with node)`, 'debug');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Test that script can be required (basic syntax check)
|
|
275
|
+
try {
|
|
276
|
+
delete require.cache[require.resolve(scriptPath)];
|
|
277
|
+
require(scriptPath);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw new Error(`Script ${script} has syntax errors: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
runner.log('All scripts have valid syntax', 'debug');
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function testDownloadURLGeneration(runner) {
|
|
288
|
+
await runner.test('Download URL generation', async () => {
|
|
289
|
+
const { getPlatformInfo } = require('../scripts/install.js');
|
|
290
|
+
|
|
291
|
+
const platformInfo = getPlatformInfo();
|
|
292
|
+
|
|
293
|
+
// Test GitHub releases URL generation
|
|
294
|
+
const githubUrl = `https://github.com/danieleugenewilliams/local-memory-golang/releases/latest/download/${platformInfo.binaryName}`;
|
|
295
|
+
|
|
296
|
+
// Test that URL is properly formatted
|
|
297
|
+
if (!githubUrl.includes(platformInfo.binaryName)) {
|
|
298
|
+
throw new Error('GitHub URL does not include binary name');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!githubUrl.startsWith('https://')) {
|
|
302
|
+
throw new Error('GitHub URL is not HTTPS');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
runner.log(`Generated GitHub fallback URL: ${githubUrl}`, 'debug');
|
|
306
|
+
|
|
307
|
+
// We don't test the actual download here to avoid network dependencies
|
|
308
|
+
// That will be tested in the integration test phase
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function testPackageJsonValidation(runner) {
|
|
313
|
+
await runner.test('package.json validation', async () => {
|
|
314
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
315
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
316
|
+
|
|
317
|
+
// Validate required fields
|
|
318
|
+
const requiredFields = [
|
|
319
|
+
'name', 'version', 'description', 'keywords', 'author', 'license',
|
|
320
|
+
'homepage', 'repository', 'bugs', 'engines', 'main', 'bin', 'scripts', 'files'
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (const field of requiredFields) {
|
|
324
|
+
if (!packageJson[field]) {
|
|
325
|
+
throw new Error(`Missing required field: ${field}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Validate specific values
|
|
330
|
+
if (!packageJson.keywords.includes('mcp')) {
|
|
331
|
+
throw new Error('Package should include "mcp" keyword');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!packageJson.engines.node) {
|
|
335
|
+
throw new Error('Missing Node.js engine requirement');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!packageJson.scripts.postinstall) {
|
|
339
|
+
throw new Error('Missing postinstall script');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!packageJson.mcp || !packageJson.mcp.server) {
|
|
343
|
+
throw new Error('Missing MCP server metadata');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
runner.log('package.json validation passed', 'debug');
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Main test runner
|
|
352
|
+
*/
|
|
353
|
+
async function runTests() {
|
|
354
|
+
const runner = new TestRunner();
|
|
355
|
+
|
|
356
|
+
console.log('๐งช Local Memory NPM Package - Comprehensive Test Suite');
|
|
357
|
+
console.log('====================================================');
|
|
358
|
+
console.log(`Test directory: ${TEST_CONFIG.testDir}`);
|
|
359
|
+
console.log(`Verbose: ${TEST_CONFIG.verbose}`);
|
|
360
|
+
console.log(`Cleanup: ${TEST_CONFIG.cleanup}`);
|
|
361
|
+
console.log('');
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Structure and validation tests
|
|
365
|
+
await testPackageStructure(runner);
|
|
366
|
+
await testPackageJsonValidation(runner);
|
|
367
|
+
|
|
368
|
+
// Platform and installation tests
|
|
369
|
+
await testPlatformDetection(runner);
|
|
370
|
+
await testMainEntryPoint(runner);
|
|
371
|
+
await testScriptFunctionality(runner);
|
|
372
|
+
|
|
373
|
+
// URL and download tests (without network)
|
|
374
|
+
await testDownloadURLGeneration(runner);
|
|
375
|
+
|
|
376
|
+
// Installation simulation
|
|
377
|
+
await testInstallationSimulation(runner);
|
|
378
|
+
|
|
379
|
+
} catch (error) {
|
|
380
|
+
runner.log(`Critical error: ${error.message}`, 'error');
|
|
381
|
+
if (TEST_CONFIG.verbose) {
|
|
382
|
+
console.error(error.stack);
|
|
383
|
+
}
|
|
384
|
+
process.exit(1);
|
|
385
|
+
} finally {
|
|
386
|
+
// Cleanup
|
|
387
|
+
if (TEST_CONFIG.cleanup && fs.existsSync(TEST_CONFIG.testDir)) {
|
|
388
|
+
try {
|
|
389
|
+
fs.rmSync(TEST_CONFIG.testDir, { recursive: true, force: true });
|
|
390
|
+
runner.log('Cleaned up test directory', 'debug');
|
|
391
|
+
} catch (error) {
|
|
392
|
+
runner.log(`Cleanup warning: ${error.message}`, 'warning');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
runner.summary();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Run tests if executed directly
|
|
401
|
+
if (require.main === module) {
|
|
402
|
+
runTests().catch(error => {
|
|
403
|
+
console.error('Test runner crashed:', error);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = { runTests, TestRunner };
|