@synth-coder/memhub 0.2.1 → 0.2.3

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 (71) hide show
  1. package/.eslintrc.cjs +45 -45
  2. package/.factory/commands/opsx-apply.md +150 -0
  3. package/.factory/commands/opsx-archive.md +155 -0
  4. package/.factory/commands/opsx-explore.md +171 -0
  5. package/.factory/commands/opsx-propose.md +104 -0
  6. package/.factory/skills/openspec-apply-change/SKILL.md +156 -0
  7. package/.factory/skills/openspec-archive-change/SKILL.md +114 -0
  8. package/.factory/skills/openspec-explore/SKILL.md +288 -0
  9. package/.factory/skills/openspec-propose/SKILL.md +110 -0
  10. package/.github/workflows/ci.yml +74 -74
  11. package/.iflow/commands/opsx-apply.md +152 -152
  12. package/.iflow/commands/opsx-archive.md +157 -157
  13. package/.iflow/commands/opsx-explore.md +173 -173
  14. package/.iflow/commands/opsx-propose.md +106 -106
  15. package/.iflow/skills/openspec-apply-change/SKILL.md +156 -156
  16. package/.iflow/skills/openspec-archive-change/SKILL.md +114 -114
  17. package/.iflow/skills/openspec-explore/SKILL.md +288 -288
  18. package/.iflow/skills/openspec-propose/SKILL.md +110 -110
  19. package/.prettierrc +11 -11
  20. package/AGENTS.md +169 -26
  21. package/README.md +195 -195
  22. package/README.zh-CN.md +193 -193
  23. package/dist/src/contracts/mcp.js +34 -34
  24. package/dist/src/server/mcp-server.d.ts +8 -0
  25. package/dist/src/server/mcp-server.d.ts.map +1 -1
  26. package/dist/src/server/mcp-server.js +23 -2
  27. package/dist/src/server/mcp-server.js.map +1 -1
  28. package/dist/src/services/memory-service.d.ts +1 -0
  29. package/dist/src/services/memory-service.d.ts.map +1 -1
  30. package/dist/src/services/memory-service.js +125 -82
  31. package/dist/src/services/memory-service.js.map +1 -1
  32. package/docs/architecture-diagrams.md +368 -0
  33. package/docs/architecture.md +381 -349
  34. package/docs/contracts.md +190 -119
  35. package/docs/prompt-template.md +33 -79
  36. package/docs/proposals/mcp-typescript-sdk-refactor.md +568 -568
  37. package/docs/proposals/proposal-close-gates.md +58 -58
  38. package/docs/tool-calling-policy.md +101 -107
  39. package/docs/vector-search.md +306 -0
  40. package/package.json +59 -58
  41. package/src/contracts/index.ts +12 -12
  42. package/src/contracts/mcp.ts +222 -222
  43. package/src/contracts/schemas.ts +307 -307
  44. package/src/contracts/types.ts +410 -410
  45. package/src/index.ts +8 -8
  46. package/src/server/index.ts +5 -5
  47. package/src/server/mcp-server.ts +185 -161
  48. package/src/services/embedding-service.ts +114 -114
  49. package/src/services/index.ts +5 -5
  50. package/src/services/memory-service.ts +663 -621
  51. package/src/storage/frontmatter-parser.ts +243 -243
  52. package/src/storage/index.ts +6 -6
  53. package/src/storage/markdown-storage.ts +236 -236
  54. package/src/storage/vector-index.ts +160 -160
  55. package/src/utils/index.ts +5 -5
  56. package/src/utils/slugify.ts +63 -63
  57. package/test/contracts/schemas.test.ts +313 -313
  58. package/test/contracts/types.test.ts +21 -21
  59. package/test/frontmatter-parser-more.test.ts +94 -94
  60. package/test/server/mcp-server.test.ts +210 -169
  61. package/test/services/memory-service-edge.test.ts +248 -248
  62. package/test/services/memory-service.test.ts +278 -278
  63. package/test/storage/frontmatter-parser.test.ts +222 -222
  64. package/test/storage/markdown-storage.test.ts +216 -216
  65. package/test/storage/storage-edge.test.ts +238 -238
  66. package/test/storage/vector-index.test.ts +153 -153
  67. package/test/utils/slugify-edge.test.ts +94 -94
  68. package/test/utils/slugify.test.ts +68 -68
  69. package/tsconfig.json +25 -25
  70. package/tsconfig.test.json +8 -8
  71. package/vitest.config.ts +29 -29
@@ -1,94 +1,94 @@
1
- /**
2
- * Additional FrontMatter Parser Tests
3
- * For better branch coverage
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import {
8
- parseFrontMatter,
9
- stringifyFrontMatter,
10
- FrontMatterError,
11
- } from '../src/storage/frontmatter-parser.js';
12
- import type { MemoryFrontMatter } from '../src/contracts/types.js';
13
-
14
- describe('parseFrontMatter additional branches', () => {
15
- it('should throw for invalid front matter format', () => {
16
- // Missing closing ---
17
- const markdown = '---\nid: "test"\n# Title\n\nContent';
18
- expect(() => parseFrontMatter(markdown)).toThrow(FrontMatterError);
19
- });
20
-
21
- it('should parse with Windows line endings', () => {
22
- const markdown = `---\r\nid: "550e8400-e29b-41d4-a716-446655440000"\r\ncreated_at: "2024-03-15T10:30:00Z"\r\nupdated_at: "2024-03-15T10:30:00Z"\r\ntags: []\r\ncategory: "general"\r\nimportance: 3\r\n---\r\n\r\n# Title\r\n\r\nContent`;
23
-
24
- const result = parseFrontMatter(markdown);
25
- expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
26
- expect(result.title).toBe('Title');
27
- });
28
-
29
- it('should handle front matter with extra whitespace', () => {
30
- const markdown = `---
31
- id: "550e8400-e29b-41d4-a716-446655440000"
32
- created_at: "2024-03-15T10:30:00Z"
33
- updated_at: "2024-03-15T10:30:00Z"
34
- tags: []
35
- category: "general"
36
- importance: 3
37
- ---
38
-
39
- # Title
40
-
41
- Content`;
42
-
43
- const result = parseFrontMatter(markdown);
44
- expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
45
- });
46
- });
47
-
48
- describe('stringifyFrontMatter additional branches', () => {
49
- const baseFrontMatter: MemoryFrontMatter = {
50
- id: '550e8400-e29b-41d4-a716-446655440000',
51
- created_at: '2024-03-15T10:30:00Z',
52
- updated_at: '2024-03-15T10:30:00Z',
53
- tags: [],
54
- category: 'general',
55
- importance: 3,
56
- };
57
-
58
- it('should handle empty title', () => {
59
- const result = stringifyFrontMatter(baseFrontMatter, '', 'Content');
60
- expect(result).toContain('---');
61
- expect(result).toContain('Content');
62
- });
63
-
64
- it('should handle empty content', () => {
65
- const result = stringifyFrontMatter(baseFrontMatter, 'Title', '');
66
- expect(result).toContain('# Title');
67
- });
68
-
69
- it('should handle both empty title and content', () => {
70
- const result = stringifyFrontMatter(baseFrontMatter, '', '');
71
- expect(result).toContain('---');
72
- expect(result).toContain('id:');
73
- });
74
-
75
- it('should handle single tag', () => {
76
- const fm = { ...baseFrontMatter, tags: ['single'] };
77
- const result = stringifyFrontMatter(fm, 'Title', 'Content');
78
- expect(result).toContain('single');
79
- });
80
-
81
- it('should handle many tags', () => {
82
- const fm = { ...baseFrontMatter, tags: ['a', 'b', 'c', 'd', 'e'] };
83
- const result = stringifyFrontMatter(fm, 'Title', 'Content');
84
- expect(result).toContain('a');
85
- expect(result).toContain('e');
86
- });
87
-
88
- it('should end with single newline', () => {
89
- const result = stringifyFrontMatter(baseFrontMatter, 'Title', 'Content');
90
- const lines = result.split('\n');
91
- expect(lines[lines.length - 1]).toBe('');
92
- expect(lines[lines.length - 2]).not.toBe('');
93
- });
94
- });
1
+ /**
2
+ * Additional FrontMatter Parser Tests
3
+ * For better branch coverage
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ parseFrontMatter,
9
+ stringifyFrontMatter,
10
+ FrontMatterError,
11
+ } from '../src/storage/frontmatter-parser.js';
12
+ import type { MemoryFrontMatter } from '../src/contracts/types.js';
13
+
14
+ describe('parseFrontMatter additional branches', () => {
15
+ it('should throw for invalid front matter format', () => {
16
+ // Missing closing ---
17
+ const markdown = '---\nid: "test"\n# Title\n\nContent';
18
+ expect(() => parseFrontMatter(markdown)).toThrow(FrontMatterError);
19
+ });
20
+
21
+ it('should parse with Windows line endings', () => {
22
+ const markdown = `---\r\nid: "550e8400-e29b-41d4-a716-446655440000"\r\ncreated_at: "2024-03-15T10:30:00Z"\r\nupdated_at: "2024-03-15T10:30:00Z"\r\ntags: []\r\ncategory: "general"\r\nimportance: 3\r\n---\r\n\r\n# Title\r\n\r\nContent`;
23
+
24
+ const result = parseFrontMatter(markdown);
25
+ expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
26
+ expect(result.title).toBe('Title');
27
+ });
28
+
29
+ it('should handle front matter with extra whitespace', () => {
30
+ const markdown = `---
31
+ id: "550e8400-e29b-41d4-a716-446655440000"
32
+ created_at: "2024-03-15T10:30:00Z"
33
+ updated_at: "2024-03-15T10:30:00Z"
34
+ tags: []
35
+ category: "general"
36
+ importance: 3
37
+ ---
38
+
39
+ # Title
40
+
41
+ Content`;
42
+
43
+ const result = parseFrontMatter(markdown);
44
+ expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
45
+ });
46
+ });
47
+
48
+ describe('stringifyFrontMatter additional branches', () => {
49
+ const baseFrontMatter: MemoryFrontMatter = {
50
+ id: '550e8400-e29b-41d4-a716-446655440000',
51
+ created_at: '2024-03-15T10:30:00Z',
52
+ updated_at: '2024-03-15T10:30:00Z',
53
+ tags: [],
54
+ category: 'general',
55
+ importance: 3,
56
+ };
57
+
58
+ it('should handle empty title', () => {
59
+ const result = stringifyFrontMatter(baseFrontMatter, '', 'Content');
60
+ expect(result).toContain('---');
61
+ expect(result).toContain('Content');
62
+ });
63
+
64
+ it('should handle empty content', () => {
65
+ const result = stringifyFrontMatter(baseFrontMatter, 'Title', '');
66
+ expect(result).toContain('# Title');
67
+ });
68
+
69
+ it('should handle both empty title and content', () => {
70
+ const result = stringifyFrontMatter(baseFrontMatter, '', '');
71
+ expect(result).toContain('---');
72
+ expect(result).toContain('id:');
73
+ });
74
+
75
+ it('should handle single tag', () => {
76
+ const fm = { ...baseFrontMatter, tags: ['single'] };
77
+ const result = stringifyFrontMatter(fm, 'Title', 'Content');
78
+ expect(result).toContain('single');
79
+ });
80
+
81
+ it('should handle many tags', () => {
82
+ const fm = { ...baseFrontMatter, tags: ['a', 'b', 'c', 'd', 'e'] };
83
+ const result = stringifyFrontMatter(fm, 'Title', 'Content');
84
+ expect(result).toContain('a');
85
+ expect(result).toContain('e');
86
+ });
87
+
88
+ it('should end with single newline', () => {
89
+ const result = stringifyFrontMatter(baseFrontMatter, 'Title', 'Content');
90
+ const lines = result.split('\n');
91
+ expect(lines[lines.length - 1]).toBe('');
92
+ expect(lines[lines.length - 2]).not.toBe('');
93
+ });
94
+ });
@@ -1,169 +1,210 @@
1
- /**
2
- * MCP Server Tests
3
- * Tests for the MCP Server using @modelcontextprotocol/sdk
4
- */
5
-
6
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
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';
12
- import {
13
- TOOL_DEFINITIONS,
14
- SERVER_INFO,
15
- ERROR_CODES,
16
- MCP_PROTOCOL_VERSION,
17
- } from '../../src/contracts/mcp.js';
18
- import { MemoryService } from '../../src/services/memory-service.js';
19
- import { MemoryLoadInputSchema, MemoryUpdateInputV2Schema } from '../../src/contracts/schemas.js';
20
-
21
- describe('McpServer (SDK)', () => {
22
- let tempDir: string;
23
- let server: Server;
24
- let memoryService: MemoryService;
25
-
26
- beforeEach(() => {
27
- tempDir = mkdtempSync(join(tmpdir(), 'memhub-server-test-'));
28
- process.env.MEMHUB_STORAGE_PATH = tempDir;
29
- process.env.MEMHUB_VECTOR_SEARCH = 'false';
30
- server = createMcpServer();
31
- memoryService = new MemoryService({ storagePath: tempDir, vectorSearch: false });
32
- });
33
-
34
- afterEach(() => {
35
- rmSync(tempDir, { recursive: true, force: true });
36
- delete process.env.MEMHUB_STORAGE_PATH;
37
- delete process.env.MEMHUB_VECTOR_SEARCH;
38
- });
39
-
40
- describe('createMcpServer', () => {
41
- it('should create server instance', () => {
42
- expect(server).toBeDefined();
43
- expect(server).toBeInstanceOf(Server);
44
- });
45
- });
46
-
47
- describe('TOOL_DEFINITIONS', () => {
48
- it('should have 2 tools defined', () => {
49
- expect(TOOL_DEFINITIONS).toHaveLength(2);
50
- });
51
-
52
- it('should include STM-first tools', () => {
53
- const toolNames = TOOL_DEFINITIONS.map(t => t.name);
54
- expect(toolNames).toContain('memory_load');
55
- expect(toolNames).toContain('memory_update');
56
- });
57
-
58
- it('should have descriptions for all tools', () => {
59
- for (const tool of TOOL_DEFINITIONS) {
60
- expect(tool.description).toBeDefined();
61
- expect(tool.description.length).toBeGreaterThan(0);
62
- }
63
- });
64
-
65
- it('should have input schemas for all tools', () => {
66
- for (const tool of TOOL_DEFINITIONS) {
67
- expect(tool.inputSchema).toBeDefined();
68
- expect(tool.inputSchema.type).toBe('object');
69
- }
70
- });
71
- });
72
-
73
- describe('ServerInfo', () => {
74
- it('should have correct server name', () => {
75
- expect(SERVER_INFO.name).toBe('memhub');
76
- });
77
-
78
- it('should have version', () => {
79
- expect(SERVER_INFO.version).toBeDefined();
80
- expect(SERVER_INFO.version).toMatch(/^\d+\.\d+\.\d+/);
81
- });
82
- });
83
-
84
- describe('ErrorCodes', () => {
85
- it('should have standard MCP error codes', () => {
86
- expect(ERROR_CODES.PARSE_ERROR).toBe(-32700);
87
- expect(ERROR_CODES.INVALID_REQUEST).toBe(-32600);
88
- expect(ERROR_CODES.METHOD_NOT_FOUND).toBe(-32601);
89
- expect(ERROR_CODES.INVALID_PARAMS).toBe(-32602);
90
- expect(ERROR_CODES.INTERNAL_ERROR).toBe(-32603);
91
- });
92
-
93
- it('should have custom MemHub error codes', () => {
94
- expect(ERROR_CODES.NOT_FOUND).toBe(-32001);
95
- expect(ERROR_CODES.STORAGE_ERROR).toBe(-32002);
96
- expect(ERROR_CODES.VALIDATION_ERROR).toBe(-32003);
97
- expect(ERROR_CODES.DUPLICATE_ERROR).toBe(-32004);
98
- });
99
- });
100
-
101
- describe('Protocol', () => {
102
- it('should have correct protocol version', () => {
103
- expect(MCP_PROTOCOL_VERSION).toBe('2024-11-05');
104
- });
105
- });
106
-
107
- describe('Tool Integration Tests', () => {
108
- it('should handle memory_update via MemoryService', async () => {
109
- const input = MemoryUpdateInputV2Schema.parse({
110
- sessionId: '550e8400-e29b-41d4-a716-446655440000',
111
- entryType: 'decision',
112
- title: 'Test decision',
113
- content: 'This is a test decision',
114
- tags: ['test'],
115
- category: 'general',
116
- });
117
-
118
- const result = await memoryService.memoryUpdate(input);
119
-
120
- expect(result).toHaveProperty('id');
121
- expect(result).toHaveProperty('sessionId');
122
- expect(result.sessionId).toBe('550e8400-e29b-41d4-a716-446655440000');
123
- expect(result.created).toBe(true);
124
- });
125
-
126
- it('should handle memory_load via MemoryService', async () => {
127
- // First create a memory
128
- const updateInput = MemoryUpdateInputV2Schema.parse({
129
- sessionId: '550e8400-e29b-41d4-a716-446655440001',
130
- entryType: 'preference',
131
- title: 'Test preference',
132
- content: 'I prefer chocolate ice cream',
133
- tags: ['food', 'preference'],
134
- category: 'personal',
135
- });
136
-
137
- const updateResult = await memoryService.memoryUpdate(updateInput);
138
-
139
- // Then load it
140
- const loadInput = MemoryLoadInputSchema.parse({
141
- id: updateResult.id,
142
- });
143
-
144
- const loadResult = await memoryService.memoryLoad(loadInput);
145
-
146
- expect(loadResult).toHaveProperty('items');
147
- expect(loadResult.items.length).toBeGreaterThan(0);
148
- expect(loadResult.items[0].title).toBe('Test preference');
149
- });
150
-
151
- it('should return error for invalid tool arguments', () => {
152
- expect(() => {
153
- MemoryUpdateInputV2Schema.parse({ title: '' }); // content is required
154
- }).toThrow();
155
- });
156
-
157
- it('should validate memory_load input schema', () => {
158
- const validInput = MemoryLoadInputSchema.parse({
159
- query: 'test query',
160
- limit: 10,
161
- category: 'general',
162
- });
163
-
164
- expect(validInput.query).toBe('test query');
165
- expect(validInput.limit).toBe(10);
166
- expect(validInput.category).toBe('general');
167
- });
168
- });
169
- });
1
+ /**
2
+ * MCP Server Tests
3
+ * Tests for the MCP Server using @modelcontextprotocol/sdk
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { mkdtempSync, rmSync } from 'fs';
8
+ import { tmpdir, homedir } from 'os';
9
+ import { join, resolve } from 'path';
10
+ import { createMcpServer, resolveStoragePath } from '../../src/server/mcp-server.js';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import {
13
+ TOOL_DEFINITIONS,
14
+ SERVER_INFO,
15
+ ERROR_CODES,
16
+ MCP_PROTOCOL_VERSION,
17
+ } from '../../src/contracts/mcp.js';
18
+ import { MemoryService } from '../../src/services/memory-service.js';
19
+ import { MemoryLoadInputSchema, MemoryUpdateInputV2Schema } from '../../src/contracts/schemas.js';
20
+
21
+ describe('McpServer (SDK)', () => {
22
+ let tempDir: string;
23
+ let server: Server;
24
+ let memoryService: MemoryService;
25
+
26
+ beforeEach(() => {
27
+ tempDir = mkdtempSync(join(tmpdir(), 'memhub-server-test-'));
28
+ process.env.MEMHUB_STORAGE_PATH = tempDir;
29
+ process.env.MEMHUB_VECTOR_SEARCH = 'false';
30
+ server = createMcpServer();
31
+ memoryService = new MemoryService({ storagePath: tempDir, vectorSearch: false });
32
+ });
33
+
34
+ afterEach(() => {
35
+ rmSync(tempDir, { recursive: true, force: true });
36
+ delete process.env.MEMHUB_STORAGE_PATH;
37
+ delete process.env.MEMHUB_VECTOR_SEARCH;
38
+ });
39
+
40
+ describe('createMcpServer', () => {
41
+ it('should create server instance', () => {
42
+ expect(server).toBeDefined();
43
+ expect(server).toBeInstanceOf(Server);
44
+ });
45
+ });
46
+
47
+ describe('TOOL_DEFINITIONS', () => {
48
+ it('should have 2 tools defined', () => {
49
+ expect(TOOL_DEFINITIONS).toHaveLength(2);
50
+ });
51
+
52
+ it('should include STM-first tools', () => {
53
+ const toolNames = TOOL_DEFINITIONS.map(t => t.name);
54
+ expect(toolNames).toContain('memory_load');
55
+ expect(toolNames).toContain('memory_update');
56
+ });
57
+
58
+ it('should have descriptions for all tools', () => {
59
+ for (const tool of TOOL_DEFINITIONS) {
60
+ expect(tool.description).toBeDefined();
61
+ expect(tool.description.length).toBeGreaterThan(0);
62
+ }
63
+ });
64
+
65
+ it('should have input schemas for all tools', () => {
66
+ for (const tool of TOOL_DEFINITIONS) {
67
+ expect(tool.inputSchema).toBeDefined();
68
+ expect(tool.inputSchema.type).toBe('object');
69
+ }
70
+ });
71
+ });
72
+
73
+ describe('ServerInfo', () => {
74
+ it('should have correct server name', () => {
75
+ expect(SERVER_INFO.name).toBe('memhub');
76
+ });
77
+
78
+ it('should have version', () => {
79
+ expect(SERVER_INFO.version).toBeDefined();
80
+ expect(SERVER_INFO.version).toMatch(/^\d+\.\d+\.\d+/);
81
+ });
82
+ });
83
+
84
+ describe('ErrorCodes', () => {
85
+ it('should have standard MCP error codes', () => {
86
+ expect(ERROR_CODES.PARSE_ERROR).toBe(-32700);
87
+ expect(ERROR_CODES.INVALID_REQUEST).toBe(-32600);
88
+ expect(ERROR_CODES.METHOD_NOT_FOUND).toBe(-32601);
89
+ expect(ERROR_CODES.INVALID_PARAMS).toBe(-32602);
90
+ expect(ERROR_CODES.INTERNAL_ERROR).toBe(-32603);
91
+ });
92
+
93
+ it('should have custom MemHub error codes', () => {
94
+ expect(ERROR_CODES.NOT_FOUND).toBe(-32001);
95
+ expect(ERROR_CODES.STORAGE_ERROR).toBe(-32002);
96
+ expect(ERROR_CODES.VALIDATION_ERROR).toBe(-32003);
97
+ expect(ERROR_CODES.DUPLICATE_ERROR).toBe(-32004);
98
+ });
99
+ });
100
+
101
+ describe('Protocol', () => {
102
+ it('should have correct protocol version', () => {
103
+ expect(MCP_PROTOCOL_VERSION).toBe('2024-11-05');
104
+ });
105
+ });
106
+
107
+ describe('Tool Integration Tests', () => {
108
+ it('should handle memory_update via MemoryService', async () => {
109
+ const input = MemoryUpdateInputV2Schema.parse({
110
+ sessionId: '550e8400-e29b-41d4-a716-446655440000',
111
+ entryType: 'decision',
112
+ title: 'Test decision',
113
+ content: 'This is a test decision',
114
+ tags: ['test'],
115
+ category: 'general',
116
+ });
117
+
118
+ const result = await memoryService.memoryUpdate(input);
119
+
120
+ expect(result).toHaveProperty('id');
121
+ expect(result).toHaveProperty('sessionId');
122
+ expect(result.sessionId).toBe('550e8400-e29b-41d4-a716-446655440000');
123
+ expect(result.created).toBe(true);
124
+ });
125
+
126
+ it('should handle memory_load via MemoryService', async () => {
127
+ // First create a memory
128
+ const updateInput = MemoryUpdateInputV2Schema.parse({
129
+ sessionId: '550e8400-e29b-41d4-a716-446655440001',
130
+ entryType: 'preference',
131
+ title: 'Test preference',
132
+ content: 'I prefer chocolate ice cream',
133
+ tags: ['food', 'preference'],
134
+ category: 'personal',
135
+ });
136
+
137
+ const updateResult = await memoryService.memoryUpdate(updateInput);
138
+
139
+ // Then load it
140
+ const loadInput = MemoryLoadInputSchema.parse({
141
+ id: updateResult.id,
142
+ });
143
+
144
+ const loadResult = await memoryService.memoryLoad(loadInput);
145
+
146
+ expect(loadResult).toHaveProperty('items');
147
+ expect(loadResult.items.length).toBeGreaterThan(0);
148
+ expect(loadResult.items[0].title).toBe('Test preference');
149
+ });
150
+
151
+ it('should return error for invalid tool arguments', () => {
152
+ expect(() => {
153
+ MemoryUpdateInputV2Schema.parse({ title: '' }); // content is required
154
+ }).toThrow();
155
+ });
156
+
157
+ it('should validate memory_load input schema', () => {
158
+ const validInput = MemoryLoadInputSchema.parse({
159
+ query: 'test query',
160
+ limit: 10,
161
+ category: 'general',
162
+ });
163
+
164
+ expect(validInput.query).toBe('test query');
165
+ expect(validInput.limit).toBe(10);
166
+ expect(validInput.category).toBe('general');
167
+ });
168
+ });
169
+
170
+ describe('resolveStoragePath', () => {
171
+ const originalEnv = process.env.MEMHUB_STORAGE_PATH;
172
+ const originalCwd = process.cwd.bind(process);
173
+
174
+ afterEach(() => {
175
+ if (originalEnv === undefined) {
176
+ delete process.env.MEMHUB_STORAGE_PATH;
177
+ } else {
178
+ process.env.MEMHUB_STORAGE_PATH = originalEnv;
179
+ }
180
+ process.cwd = originalCwd;
181
+ });
182
+
183
+ it('should return ~/.memhub by default', () => {
184
+ delete process.env.MEMHUB_STORAGE_PATH;
185
+ const expectedPath = join(homedir(), '.memhub');
186
+ expect(resolveStoragePath()).toBe(expectedPath);
187
+ });
188
+
189
+ it('should use absolute path from MEMHUB_STORAGE_PATH', () => {
190
+ const absolutePath = process.platform === 'win32' ? 'C:\\custom\\path' : '/custom/path';
191
+ process.env.MEMHUB_STORAGE_PATH = absolutePath;
192
+ expect(resolveStoragePath()).toBe(absolutePath);
193
+ });
194
+
195
+ it('should resolve relative path from cwd', () => {
196
+ const cwd = process.platform === 'win32' ? 'C:\\project' : '/project';
197
+ process.cwd = () => cwd;
198
+ process.env.MEMHUB_STORAGE_PATH = '.memhub';
199
+ expect(resolveStoragePath()).toBe(resolve(cwd, '.memhub'));
200
+ });
201
+
202
+ it('should resolve .memhub relative path', () => {
203
+ const cwd = process.platform === 'win32' ? 'C:\\myproject' : '/myproject';
204
+ process.cwd = () => cwd;
205
+ process.env.MEMHUB_STORAGE_PATH = '.memhub';
206
+ const expected = process.platform === 'win32' ? 'C:\\myproject\\.memhub' : '/myproject/.memhub';
207
+ expect(resolveStoragePath()).toBe(expected);
208
+ });
209
+ });
210
+ });