@mndrk/memx 0.3.2 → 0.3.4

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.
Files changed (39) hide show
  1. package/README.md +98 -77
  2. package/coverage/clover.xml +1160 -0
  3. package/coverage/coverage-final.json +3 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/favicon.png +0 -0
  7. package/coverage/lcov-report/index.html +131 -0
  8. package/coverage/lcov-report/index.js.html +7255 -0
  9. package/coverage/lcov-report/mcp.js.html +1009 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov.info +2017 -0
  15. package/index.js +651 -243
  16. package/package.json +24 -2
  17. package/test/additional.test.js +373 -0
  18. package/test/branches.test.js +247 -0
  19. package/test/commands.test.js +663 -0
  20. package/test/context.test.js +185 -0
  21. package/test/coverage.test.js +366 -0
  22. package/test/dispatch.test.js +220 -0
  23. package/test/edge-coverage.test.js +250 -0
  24. package/test/edge.test.js +434 -0
  25. package/test/final-coverage.test.js +316 -0
  26. package/test/final-edges.test.js +199 -0
  27. package/test/init-local.test.js +316 -0
  28. package/test/init.test.js +122 -0
  29. package/test/interactive.test.js +229 -0
  30. package/test/main-dispatch.test.js +164 -0
  31. package/test/main-full.test.js +590 -0
  32. package/test/main.test.js +197 -0
  33. package/test/mcp-server.test.js +320 -0
  34. package/test/mcp.test.js +288 -0
  35. package/test/more.test.js +312 -0
  36. package/test/new.test.js +175 -0
  37. package/test/skill.test.js +247 -0
  38. package/test/tasks-interactive.test.js +243 -0
  39. package/test/utils.test.js +367 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Tests for main function and help
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ // Mock child_process before requiring index.js
10
+ jest.mock('child_process', () => ({
11
+ execSync: jest.fn(),
12
+ spawnSync: jest.fn(() => ({ status: 0, stdout: '', stderr: '' })),
13
+ spawn: jest.fn()
14
+ }));
15
+
16
+ // Mock readline
17
+ jest.mock('readline', () => ({
18
+ createInterface: jest.fn(() => ({
19
+ question: jest.fn((q, cb) => cb('')),
20
+ close: jest.fn(),
21
+ on: jest.fn()
22
+ }))
23
+ }));
24
+
25
+ const {
26
+ showHelp,
27
+ showMCPConfig,
28
+ startMCPServer
29
+ } = require('../index.js');
30
+
31
+ const { spawn } = require('child_process');
32
+
33
+ describe('showHelp', () => {
34
+ let consoleSpy;
35
+
36
+ beforeEach(() => {
37
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
38
+ });
39
+
40
+ afterEach(() => {
41
+ consoleSpy.mockRestore();
42
+ });
43
+
44
+ test('displays help message', () => {
45
+ showHelp();
46
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
47
+ expect(output).toContain('mem');
48
+ expect(output).toContain('Persistent memory');
49
+ });
50
+
51
+ test('shows LIFECYCLE section', () => {
52
+ showHelp();
53
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
54
+ expect(output).toContain('LIFECYCLE');
55
+ expect(output).toContain('init');
56
+ expect(output).toContain('status');
57
+ expect(output).toContain('done');
58
+ });
59
+
60
+ test('shows PROGRESS section', () => {
61
+ showHelp();
62
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
63
+ expect(output).toContain('PROGRESS');
64
+ expect(output).toContain('goal');
65
+ expect(output).toContain('next');
66
+ expect(output).toContain('checkpoint');
67
+ expect(output).toContain('stuck');
68
+ });
69
+
70
+ test('shows LEARNING section', () => {
71
+ showHelp();
72
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
73
+ expect(output).toContain('LEARNING');
74
+ expect(output).toContain('learn');
75
+ expect(output).toContain('playbook');
76
+ expect(output).toContain('promote');
77
+ });
78
+
79
+ test('shows QUERY section', () => {
80
+ showHelp();
81
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
82
+ expect(output).toContain('QUERY');
83
+ expect(output).toContain('context');
84
+ expect(output).toContain('history');
85
+ expect(output).toContain('query');
86
+ });
87
+
88
+ test('shows TASKS section', () => {
89
+ showHelp();
90
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
91
+ expect(output).toContain('TASKS');
92
+ expect(output).toContain('tasks');
93
+ expect(output).toContain('switch');
94
+ });
95
+
96
+ test('shows PRIMITIVES section', () => {
97
+ showHelp();
98
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
99
+ expect(output).toContain('SYNC');
100
+ expect(output).toContain('sync');
101
+ });
102
+
103
+ test('shows INTEGRATION section', () => {
104
+ showHelp();
105
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
106
+ expect(output).toContain('INTEGRATION');
107
+ expect(output).toContain('skill');
108
+ expect(output).toContain('mcp');
109
+ });
110
+
111
+ test('shows EXAMPLES section', () => {
112
+ showHelp();
113
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
114
+ expect(output).toContain('EXAMPLES');
115
+ });
116
+ });
117
+
118
+ describe('showMCPConfig', () => {
119
+ let consoleSpy;
120
+
121
+ beforeEach(() => {
122
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
123
+ });
124
+
125
+ afterEach(() => {
126
+ consoleSpy.mockRestore();
127
+ });
128
+
129
+ test('shows MCP configuration', () => {
130
+ showMCPConfig();
131
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
132
+ expect(output).toContain('MCP Server Configuration');
133
+ expect(output).toContain('mcpServers');
134
+ expect(output).toContain('mem');
135
+ });
136
+
137
+ test('shows Claude Desktop config path', () => {
138
+ showMCPConfig();
139
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
140
+ expect(output).toContain('Claude Desktop');
141
+ expect(output).toContain('claude_desktop_config.json');
142
+ });
143
+
144
+ test('shows project-specific config example', () => {
145
+ showMCPConfig();
146
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
147
+ expect(output).toContain('--dir');
148
+ expect(output).toContain('/path/to/your/project');
149
+ });
150
+ });
151
+
152
+ describe('startMCPServer', () => {
153
+ let mockChild;
154
+
155
+ beforeEach(() => {
156
+ mockChild = {
157
+ on: jest.fn()
158
+ };
159
+ spawn.mockReturnValue(mockChild);
160
+ });
161
+
162
+ afterEach(() => {
163
+ spawn.mockClear();
164
+ });
165
+
166
+ test('spawns MCP server process', () => {
167
+ startMCPServer(['mcp']);
168
+
169
+ expect(spawn).toHaveBeenCalledWith(
170
+ 'node',
171
+ expect.arrayContaining([expect.stringContaining('mcp.js')]),
172
+ expect.objectContaining({ stdio: 'inherit' })
173
+ );
174
+ });
175
+
176
+ test('passes through arguments', () => {
177
+ startMCPServer(['mcp', '--dir', '/test/path']);
178
+
179
+ expect(spawn).toHaveBeenCalledWith(
180
+ 'node',
181
+ expect.arrayContaining(['--dir', '/test/path']),
182
+ expect.any(Object)
183
+ );
184
+ });
185
+
186
+ test('sets up error handler', () => {
187
+ startMCPServer(['mcp']);
188
+
189
+ expect(mockChild.on).toHaveBeenCalledWith('error', expect.any(Function));
190
+ });
191
+
192
+ test('sets up exit handler', () => {
193
+ startMCPServer(['mcp']);
194
+
195
+ expect(mockChild.on).toHaveBeenCalledWith('exit', expect.any(Function));
196
+ });
197
+ });
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Tests for MCP server start() and related functions
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const EventEmitter = require('events');
9
+
10
+ // Create mock readline interface
11
+ class MockReadlineInterface extends EventEmitter {
12
+ constructor() {
13
+ super();
14
+ this.closed = false;
15
+ }
16
+ close() {
17
+ this.closed = true;
18
+ }
19
+ }
20
+
21
+ let mockRlInstance = null;
22
+
23
+ // Mock child_process
24
+ jest.mock('child_process', () => ({
25
+ execSync: jest.fn(),
26
+ spawnSync: jest.fn(() => ({ status: 0, stdout: '', stderr: '' })),
27
+ spawn: jest.fn()
28
+ }));
29
+
30
+ // Mock readline with controllable interface
31
+ jest.mock('readline', () => ({
32
+ createInterface: jest.fn(() => {
33
+ mockRlInstance = new MockReadlineInterface();
34
+ return mockRlInstance;
35
+ })
36
+ }));
37
+
38
+ const { MCPServer } = require('../mcp.js');
39
+ const { spawnSync, spawn } = require('child_process');
40
+
41
+ const testDir = path.join(os.tmpdir(), 'memx-test-mcp-server-' + Date.now());
42
+
43
+ beforeEach(() => {
44
+ fs.mkdirSync(path.join(testDir, '.mem', '.git'), { recursive: true });
45
+ spawnSync.mockReset();
46
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
47
+ spawn.mockReset();
48
+ mockRlInstance = null;
49
+ jest.spyOn(console, 'log').mockImplementation();
50
+ jest.spyOn(console, 'error').mockImplementation();
51
+ });
52
+
53
+ afterEach(() => {
54
+ if (fs.existsSync(testDir)) {
55
+ fs.rmSync(testDir, { recursive: true });
56
+ }
57
+ jest.restoreAllMocks();
58
+ });
59
+
60
+ describe('MCPServer start()', () => {
61
+ test('starts server and handles valid JSON-RPC message', () => {
62
+ const server = new MCPServer(testDir);
63
+ server.start();
64
+
65
+ // Simulate receiving a valid message
66
+ const message = JSON.stringify({
67
+ jsonrpc: '2.0',
68
+ id: 1,
69
+ method: 'initialize',
70
+ params: {}
71
+ });
72
+
73
+ mockRlInstance.emit('line', message);
74
+
75
+ expect(console.log).toHaveBeenCalled();
76
+ });
77
+
78
+ test('handles parse error for invalid JSON', () => {
79
+ const server = new MCPServer(testDir);
80
+ server.start();
81
+
82
+ // Simulate receiving invalid JSON
83
+ mockRlInstance.emit('line', 'not valid json');
84
+
85
+ // Should log parse error response
86
+ const calls = console.log.mock.calls;
87
+ const lastCall = calls[calls.length - 1][0];
88
+ const response = JSON.parse(lastCall);
89
+ expect(response.error.code).toBe(-32700);
90
+ expect(response.error.message).toBe('Parse error');
91
+ });
92
+
93
+ test('handles tools/list request', () => {
94
+ const server = new MCPServer(testDir);
95
+ server.start();
96
+
97
+ const message = JSON.stringify({
98
+ jsonrpc: '2.0',
99
+ id: 2,
100
+ method: 'tools/list',
101
+ params: {}
102
+ });
103
+
104
+ mockRlInstance.emit('line', message);
105
+
106
+ const calls = console.log.mock.calls;
107
+ const lastCall = calls[calls.length - 1][0];
108
+ const response = JSON.parse(lastCall);
109
+ expect(response.result.tools).toBeDefined();
110
+ expect(Array.isArray(response.result.tools)).toBe(true);
111
+ });
112
+
113
+ test('handles tools/call for mem_status', () => {
114
+ spawnSync.mockReturnValue({ status: 0, stdout: 'task/test', stderr: '' });
115
+
116
+ // Create necessary files
117
+ fs.writeFileSync(
118
+ path.join(testDir, '.mem', 'goal.md'),
119
+ '---\ntask: test\n---\n\n# Goal\n\nTest goal'
120
+ );
121
+ fs.writeFileSync(
122
+ path.join(testDir, '.mem', 'state.md'),
123
+ '---\nstatus: active\n---\n\n# State'
124
+ );
125
+
126
+ const server = new MCPServer(testDir);
127
+ server.start();
128
+
129
+ const message = JSON.stringify({
130
+ jsonrpc: '2.0',
131
+ id: 3,
132
+ method: 'tools/call',
133
+ params: {
134
+ name: 'mem_status',
135
+ arguments: {}
136
+ }
137
+ });
138
+
139
+ mockRlInstance.emit('line', message);
140
+
141
+ const calls = console.log.mock.calls;
142
+ expect(calls.length).toBeGreaterThan(0);
143
+ });
144
+
145
+ test('handles tools/call for mem_context', () => {
146
+ spawnSync.mockReturnValue({ status: 0, stdout: 'task/test', stderr: '' });
147
+
148
+ fs.writeFileSync(
149
+ path.join(testDir, '.mem', 'goal.md'),
150
+ '# Goal\n\nTest'
151
+ );
152
+ fs.writeFileSync(
153
+ path.join(testDir, '.mem', 'state.md'),
154
+ '---\nstatus: active\n---\n\n'
155
+ );
156
+
157
+ const server = new MCPServer(testDir);
158
+ server.start();
159
+
160
+ const message = JSON.stringify({
161
+ jsonrpc: '2.0',
162
+ id: 4,
163
+ method: 'tools/call',
164
+ params: {
165
+ name: 'mem_context',
166
+ arguments: {}
167
+ }
168
+ });
169
+
170
+ mockRlInstance.emit('line', message);
171
+
172
+ expect(console.log).toHaveBeenCalled();
173
+ });
174
+
175
+ test('handles tools/call for mem_checkpoint', () => {
176
+ fs.writeFileSync(
177
+ path.join(testDir, '.mem', 'state.md'),
178
+ '---\nstatus: active\n---\n\n## Checkpoints\n\n- [ ] Started'
179
+ );
180
+
181
+ const server = new MCPServer(testDir);
182
+ server.start();
183
+
184
+ const message = JSON.stringify({
185
+ jsonrpc: '2.0',
186
+ id: 5,
187
+ method: 'tools/call',
188
+ params: {
189
+ name: 'mem_checkpoint',
190
+ arguments: { text: 'New checkpoint' }
191
+ }
192
+ });
193
+
194
+ mockRlInstance.emit('line', message);
195
+
196
+ expect(console.log).toHaveBeenCalled();
197
+ });
198
+
199
+ test('handles tools/call for mem_learn', () => {
200
+ fs.writeFileSync(
201
+ path.join(testDir, '.mem', 'memory.md'),
202
+ '# Learnings\n\n'
203
+ );
204
+
205
+ const server = new MCPServer(testDir);
206
+ server.start();
207
+
208
+ const message = JSON.stringify({
209
+ jsonrpc: '2.0',
210
+ id: 6,
211
+ method: 'tools/call',
212
+ params: {
213
+ name: 'mem_learn',
214
+ arguments: { insight: 'New insight' }
215
+ }
216
+ });
217
+
218
+ mockRlInstance.emit('line', message);
219
+
220
+ expect(console.log).toHaveBeenCalled();
221
+ });
222
+
223
+ test('handles tools/call for mem_next', () => {
224
+ fs.writeFileSync(
225
+ path.join(testDir, '.mem', 'state.md'),
226
+ '---\nstatus: active\n---\n\n## Next Step\n\nOld step'
227
+ );
228
+
229
+ const server = new MCPServer(testDir);
230
+ server.start();
231
+
232
+ const message = JSON.stringify({
233
+ jsonrpc: '2.0',
234
+ id: 7,
235
+ method: 'tools/call',
236
+ params: {
237
+ name: 'mem_next',
238
+ arguments: { step: 'New step' }
239
+ }
240
+ });
241
+
242
+ mockRlInstance.emit('line', message);
243
+
244
+ expect(console.log).toHaveBeenCalled();
245
+ });
246
+
247
+ test('handles tools/call for unknown tool', () => {
248
+ const server = new MCPServer(testDir);
249
+ server.start();
250
+
251
+ const message = JSON.stringify({
252
+ jsonrpc: '2.0',
253
+ id: 8,
254
+ method: 'tools/call',
255
+ params: {
256
+ name: 'unknown_tool',
257
+ arguments: {}
258
+ }
259
+ });
260
+
261
+ mockRlInstance.emit('line', message);
262
+
263
+ const calls = console.log.mock.calls;
264
+ expect(calls.length).toBeGreaterThan(0);
265
+ const lastCall = calls[calls.length - 1][0];
266
+ const response = JSON.parse(lastCall);
267
+ // Unknown tool returns a result with error message in content, not a JSON-RPC error
268
+ expect(response.result || response.error).toBeDefined();
269
+ });
270
+
271
+ test('handles unknown method', () => {
272
+ const server = new MCPServer(testDir);
273
+ server.start();
274
+
275
+ const message = JSON.stringify({
276
+ jsonrpc: '2.0',
277
+ id: 9,
278
+ method: 'unknown/method',
279
+ params: {}
280
+ });
281
+
282
+ mockRlInstance.emit('line', message);
283
+
284
+ const calls = console.log.mock.calls;
285
+ const lastCall = calls[calls.length - 1][0];
286
+ const response = JSON.parse(lastCall);
287
+ expect(response.error.code).toBe(-32601);
288
+ });
289
+
290
+ test('handles close event', () => {
291
+ const originalExit = process.exit;
292
+ process.exit = jest.fn();
293
+
294
+ const server = new MCPServer(testDir);
295
+ server.start();
296
+
297
+ mockRlInstance.emit('close');
298
+
299
+ expect(process.exit).toHaveBeenCalledWith(0);
300
+
301
+ process.exit = originalExit;
302
+ });
303
+
304
+ test('handles notification (no id)', () => {
305
+ const server = new MCPServer(testDir);
306
+ server.start();
307
+
308
+ const message = JSON.stringify({
309
+ jsonrpc: '2.0',
310
+ method: 'notifications/initialized',
311
+ params: {}
312
+ });
313
+
314
+ mockRlInstance.emit('line', message);
315
+
316
+ // Notifications don't produce responses
317
+ // Just verify no error was thrown
318
+ expect(mockRlInstance.closed).toBe(false);
319
+ });
320
+ });