@synth-coder/memhub 0.1.3 → 0.1.5

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.
@@ -1,26 +1,33 @@
1
1
  /**
2
2
  * MCP Server Tests
3
- * Tests for the McpServer class
3
+ * Tests for the MCP Server using @modelcontextprotocol/sdk
4
4
  */
5
5
 
6
6
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
- import { McpServer } from '../../src/server/mcp-server.js';
7
+ import { mkdtempSync, rmSync } from 'fs';
8
+ import { tmpdir } from 'os';
9
+ import { join } from 'path';
10
+ import { createMcpServer } from '../../src/server/mcp-server.js';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
12
  import {
9
13
  TOOL_DEFINITIONS,
10
14
  SERVER_INFO,
11
15
  ERROR_CODES,
12
16
  MCP_PROTOCOL_VERSION,
13
17
  } from '../../src/contracts/mcp.js';
14
- import { mkdtempSync, rmSync } from 'fs';
15
- import { tmpdir } from 'os';
16
- import { join } from 'path';
18
+ import { MemoryService } from '../../src/services/memory-service.js';
19
+ import { MemoryLoadInputSchema, MemoryUpdateInputV2Schema } from '../../src/contracts/schemas.js';
17
20
 
18
- describe('McpServer', () => {
21
+ describe('McpServer (SDK)', () => {
19
22
  let tempDir: string;
23
+ let server: Server;
24
+ let memoryService: MemoryService;
20
25
 
21
26
  beforeEach(() => {
22
27
  tempDir = mkdtempSync(join(tmpdir(), 'memhub-server-test-'));
23
28
  process.env.MEMHUB_STORAGE_PATH = tempDir;
29
+ server = createMcpServer();
30
+ memoryService = new MemoryService({ storagePath: tempDir });
24
31
  });
25
32
 
26
33
  afterEach(() => {
@@ -28,10 +35,10 @@ describe('McpServer', () => {
28
35
  delete process.env.MEMHUB_STORAGE_PATH;
29
36
  });
30
37
 
31
- describe('constructor', () => {
38
+ describe('createMcpServer', () => {
32
39
  it('should create server instance', () => {
33
- const server = new McpServer();
34
40
  expect(server).toBeDefined();
41
+ expect(server).toBeInstanceOf(Server);
35
42
  });
36
43
  });
37
44
 
@@ -94,4 +101,67 @@ describe('McpServer', () => {
94
101
  expect(MCP_PROTOCOL_VERSION).toBe('2024-11-05');
95
102
  });
96
103
  });
104
+
105
+ describe('Tool Integration Tests', () => {
106
+ it('should handle memory_update via MemoryService', async () => {
107
+ const input = MemoryUpdateInputV2Schema.parse({
108
+ sessionId: '550e8400-e29b-41d4-a716-446655440000',
109
+ entryType: 'decision',
110
+ title: 'Test decision',
111
+ content: 'This is a test decision',
112
+ tags: ['test'],
113
+ category: 'general',
114
+ });
115
+
116
+ const result = await memoryService.memoryUpdate(input);
117
+
118
+ expect(result).toHaveProperty('id');
119
+ expect(result).toHaveProperty('sessionId');
120
+ expect(result.sessionId).toBe('550e8400-e29b-41d4-a716-446655440000');
121
+ expect(result.created).toBe(true);
122
+ });
123
+
124
+ it('should handle memory_load via MemoryService', async () => {
125
+ // First create a memory
126
+ const updateInput = MemoryUpdateInputV2Schema.parse({
127
+ sessionId: '550e8400-e29b-41d4-a716-446655440001',
128
+ entryType: 'preference',
129
+ title: 'Test preference',
130
+ content: 'I prefer chocolate ice cream',
131
+ tags: ['food', 'preference'],
132
+ category: 'personal',
133
+ });
134
+
135
+ const updateResult = await memoryService.memoryUpdate(updateInput);
136
+
137
+ // Then load it
138
+ const loadInput = MemoryLoadInputSchema.parse({
139
+ id: updateResult.id,
140
+ });
141
+
142
+ const loadResult = await memoryService.memoryLoad(loadInput);
143
+
144
+ expect(loadResult).toHaveProperty('items');
145
+ expect(loadResult.items.length).toBeGreaterThan(0);
146
+ expect(loadResult.items[0].title).toBe('Test preference');
147
+ });
148
+
149
+ it('should return error for invalid tool arguments', () => {
150
+ expect(() => {
151
+ MemoryUpdateInputV2Schema.parse({ title: '' }); // content is required
152
+ }).toThrow();
153
+ });
154
+
155
+ it('should validate memory_load input schema', () => {
156
+ const validInput = MemoryLoadInputSchema.parse({
157
+ sessionId: '550e8400-e29b-41d4-a716-446655440002',
158
+ limit: 10,
159
+ scope: 'stm',
160
+ });
161
+
162
+ expect(validInput.sessionId).toBe('550e8400-e29b-41d4-a716-446655440002');
163
+ expect(validInput.limit).toBe(10);
164
+ expect(validInput.scope).toBe('stm');
165
+ });
166
+ });
97
167
  });
@@ -1,257 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { mkdtempSync, rmSync } from 'fs';
3
- import { join } from 'path';
4
- import { tmpdir } from 'os';
5
- import { McpServer } from '../../src/server/mcp-server.js';
6
- import { MCP_METHODS, ERROR_CODES, MCP_PROTOCOL_VERSION } from '../../src/contracts/mcp.js';
7
- import { ErrorCode } from '../../src/contracts/types.js';
8
- import { ServiceError } from '../../src/services/memory-service.js';
9
-
10
- interface ServerPrivate {
11
- handleMethod: (method: string, params: unknown) => Promise<unknown>;
12
- handleMessage: (message: string) => Promise<void>;
13
- handleError: (id: string | number | null, error: unknown) => void;
14
- sendResponse: (id: string | number, result: unknown) => void;
15
- sendError: (id: string | number | null, code: number, message: string, data?: Record<string, unknown>) => void;
16
- log: (level: 'debug' | 'info' | 'warn' | 'error', message: string) => void;
17
- }
18
-
19
- describe('McpServer internals', () => {
20
- let tempDir: string;
21
- let server: McpServer;
22
- let serverPrivate: ServerPrivate;
23
-
24
- beforeEach(() => {
25
- tempDir = mkdtempSync(join(tmpdir(), 'memhub-server-int-test-'));
26
- process.env.MEMHUB_STORAGE_PATH = tempDir;
27
- server = new McpServer();
28
- serverPrivate = server as unknown as ServerPrivate;
29
- });
30
-
31
- afterEach(() => {
32
- rmSync(tempDir, { recursive: true, force: true });
33
- delete process.env.MEMHUB_STORAGE_PATH;
34
- delete process.env.MEMHUB_LOG_LEVEL;
35
- vi.restoreAllMocks();
36
- });
37
-
38
- it('handles initialize method', async () => {
39
- const result = (await serverPrivate.handleMethod(MCP_METHODS.INITIALIZE, {
40
- protocolVersion: MCP_PROTOCOL_VERSION,
41
- capabilities: {},
42
- clientInfo: { name: 'tester', version: '1.0.0' },
43
- })) as { protocolVersion: string; serverInfo: { name: string; version: string } };
44
-
45
- expect(result.protocolVersion).toBe(MCP_PROTOCOL_VERSION);
46
- expect(result.serverInfo.name).toBe('memhub');
47
- expect(result.serverInfo.version.length).toBeGreaterThan(0);
48
- });
49
-
50
- it('handles tools/list method', async () => {
51
- const result = (await serverPrivate.handleMethod(MCP_METHODS.TOOLS_LIST, {})) as {
52
- tools: Array<{ name: string }>;
53
- };
54
-
55
- expect(result.tools.length).toBe(2);
56
- expect(result.tools.some(t => t.name === 'memory_load')).toBe(true);
57
- });
58
-
59
- it('handles tools/call for STM load + update flow', async () => {
60
- const updateResult = (await serverPrivate.handleMethod(MCP_METHODS.TOOLS_CALL, {
61
- name: 'memory_update',
62
- arguments: {
63
- sessionId: '550e8400-e29b-41d4-a716-446655440111',
64
- entryType: 'decision',
65
- title: 'TDD note',
66
- content: 'Write tests first',
67
- tags: ['tdd'],
68
- category: 'engineering',
69
- },
70
- })) as { content: Array<{ text: string }> };
71
-
72
- const updatePayload = JSON.parse(updateResult.content[0].text) as { id: string; sessionId: string };
73
- expect(updatePayload.id).toBeTruthy();
74
- expect(updatePayload.sessionId).toBe('550e8400-e29b-41d4-a716-446655440111');
75
-
76
- const loadById = (await serverPrivate.handleMethod(MCP_METHODS.TOOLS_CALL, {
77
- name: 'memory_load',
78
- arguments: { id: updatePayload.id },
79
- })) as { content: Array<{ text: string }> };
80
- expect(loadById.content[0].text).toContain('TDD note');
81
-
82
- const loadBySession = (await serverPrivate.handleMethod(MCP_METHODS.TOOLS_CALL, {
83
- name: 'memory_load',
84
- arguments: { sessionId: '550e8400-e29b-41d4-a716-446655440111', limit: 10 },
85
- })) as { content: Array<{ text: string }> };
86
- expect(loadBySession.content[0].text).toContain('items');
87
- });
88
-
89
- it('returns tool error payload for unknown tool', async () => {
90
- const result = (await serverPrivate.handleMethod(MCP_METHODS.TOOLS_CALL, {
91
- name: 'unknown_tool',
92
- arguments: {},
93
- })) as { isError?: boolean; content: Array<{ text: string }> };
94
-
95
- expect(result.isError).toBe(true);
96
- expect(result.content[0].text).toContain('Unknown tool');
97
- });
98
-
99
- it('handles lifecycle methods', async () => {
100
- await expect(serverPrivate.handleMethod(MCP_METHODS.INITIALIZED, {})).resolves.toBeNull();
101
- await expect(serverPrivate.handleMethod(MCP_METHODS.SHUTDOWN, {})).resolves.toBeNull();
102
-
103
- const exitSpy = vi
104
- .spyOn(process, 'exit')
105
- .mockImplementation(() => undefined as never);
106
-
107
- await expect(serverPrivate.handleMethod(MCP_METHODS.EXIT, {})).resolves.toBeNull();
108
- expect(exitSpy).toHaveBeenCalledWith(0);
109
- });
110
-
111
- it('throws ServiceError for unknown method', async () => {
112
- await expect(serverPrivate.handleMethod('unknown/method', {})).rejects.toMatchObject({
113
- code: ErrorCode.METHOD_NOT_FOUND,
114
- });
115
- });
116
-
117
- it('routes parse errors from handleMessage', async () => {
118
- const sendErrorSpy = vi
119
- .spyOn(serverPrivate, 'sendError')
120
- .mockImplementation(() => undefined);
121
-
122
- await serverPrivate.handleMessage('{bad-json');
123
-
124
- expect(sendErrorSpy).toHaveBeenCalledWith(
125
- null,
126
- ERROR_CODES.PARSE_ERROR,
127
- 'Parse error: Invalid JSON'
128
- );
129
- });
130
-
131
- it('routes invalid request errors from handleMessage', async () => {
132
- const sendErrorSpy = vi
133
- .spyOn(serverPrivate, 'sendError')
134
- .mockImplementation(() => undefined);
135
-
136
- await serverPrivate.handleMessage(JSON.stringify({ jsonrpc: '2.0' }));
137
-
138
- expect(sendErrorSpy).toHaveBeenCalledWith(
139
- null,
140
- ERROR_CODES.INVALID_REQUEST,
141
- 'Invalid Request'
142
- );
143
- });
144
-
145
- it('does not send response for notifications without id', async () => {
146
- const sendResponseSpy = vi
147
- .spyOn(serverPrivate, 'sendResponse')
148
- .mockImplementation(() => undefined);
149
-
150
- await serverPrivate.handleMessage(
151
- JSON.stringify({
152
- jsonrpc: '2.0',
153
- method: MCP_METHODS.INITIALIZED,
154
- params: {},
155
- })
156
- );
157
-
158
- expect(sendResponseSpy).not.toHaveBeenCalled();
159
- });
160
-
161
- it('returns INVALID_PARAMS on schema validation errors', async () => {
162
- const sendErrorSpy = vi
163
- .spyOn(serverPrivate, 'sendError')
164
- .mockImplementation(() => undefined);
165
-
166
- await serverPrivate.handleMessage(
167
- JSON.stringify({
168
- jsonrpc: '2.0',
169
- id: 123,
170
- method: MCP_METHODS.TOOLS_CALL,
171
- params: {
172
- name: 'memory_update',
173
- arguments: { title: '' },
174
- },
175
- })
176
- );
177
-
178
- expect(sendErrorSpy).toHaveBeenCalledWith(
179
- 123,
180
- ERROR_CODES.INVALID_PARAMS,
181
- expect.stringContaining('Invalid parameters')
182
- );
183
- });
184
-
185
- it('handleError maps ServiceError, validation error, and generic error', () => {
186
- const sendErrorSpy = vi
187
- .spyOn(serverPrivate, 'sendError')
188
- .mockImplementation(() => undefined);
189
-
190
- serverPrivate.handleError(
191
- 1,
192
- new ServiceError('boom', ErrorCode.STORAGE_ERROR, { foo: 'bar' })
193
- );
194
- const zodLikeError = new Error('invalid');
195
- zodLikeError.name = 'ZodError';
196
- serverPrivate.handleError(2, zodLikeError);
197
- serverPrivate.handleError(3, new Error('oops'));
198
-
199
- expect(sendErrorSpy).toHaveBeenNthCalledWith(
200
- 1,
201
- 1,
202
- ErrorCode.STORAGE_ERROR,
203
- 'boom',
204
- { foo: 'bar' }
205
- );
206
- expect(sendErrorSpy).toHaveBeenNthCalledWith(
207
- 2,
208
- 2,
209
- ERROR_CODES.INVALID_PARAMS,
210
- 'Invalid parameters: invalid'
211
- );
212
- expect(sendErrorSpy).toHaveBeenNthCalledWith(
213
- 3,
214
- 3,
215
- ERROR_CODES.INTERNAL_ERROR,
216
- 'Internal error: oops'
217
- );
218
- });
219
-
220
- it('sendResponse writes valid JSON-RPC response', () => {
221
- const writeSpy = vi
222
- .spyOn(process.stdout, 'write')
223
- .mockReturnValue(true);
224
-
225
- serverPrivate.sendResponse(7, { ok: true });
226
-
227
- const output = String(writeSpy.mock.calls[0][0]).trim();
228
- const parsed = JSON.parse(output) as { id: number; result: { ok: boolean } };
229
- expect(parsed.id).toBe(7);
230
- expect(parsed.result.ok).toBe(true);
231
- });
232
-
233
- it('sendError writes valid JSON-RPC error response', () => {
234
- const writeSpy = vi
235
- .spyOn(process.stdout, 'write')
236
- .mockReturnValue(true);
237
-
238
- serverPrivate.sendError(null, ERROR_CODES.INTERNAL_ERROR, 'failure');
239
-
240
- const output = String(writeSpy.mock.calls[0][0]).trim();
241
- const parsed = JSON.parse(output) as { id: null; error: { code: number; message: string } };
242
- expect(parsed.id).toBeNull();
243
- expect(parsed.error.code).toBe(ERROR_CODES.INTERNAL_ERROR);
244
- expect(parsed.error.message).toBe('failure');
245
- });
246
-
247
- it('log respects configured log level', () => {
248
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
249
-
250
- process.env.MEMHUB_LOG_LEVEL = 'error';
251
- serverPrivate.log('info', 'ignore');
252
- serverPrivate.log('error', 'report');
253
-
254
- expect(errorSpy).toHaveBeenCalledTimes(1);
255
- expect(errorSpy.mock.calls[0][0]).toContain('[ERROR] report');
256
- });
257
- });