@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.
- package/.eslintrc.cjs +45 -45
- package/.factory/commands/opsx-apply.md +150 -0
- package/.factory/commands/opsx-archive.md +155 -0
- package/.factory/commands/opsx-explore.md +171 -0
- package/.factory/commands/opsx-propose.md +104 -0
- package/.factory/skills/openspec-apply-change/SKILL.md +156 -0
- package/.factory/skills/openspec-archive-change/SKILL.md +114 -0
- package/.factory/skills/openspec-explore/SKILL.md +288 -0
- package/.factory/skills/openspec-propose/SKILL.md +110 -0
- package/.github/workflows/ci.yml +74 -74
- package/.iflow/commands/opsx-apply.md +152 -152
- package/.iflow/commands/opsx-archive.md +157 -157
- package/.iflow/commands/opsx-explore.md +173 -173
- package/.iflow/commands/opsx-propose.md +106 -106
- package/.iflow/skills/openspec-apply-change/SKILL.md +156 -156
- package/.iflow/skills/openspec-archive-change/SKILL.md +114 -114
- package/.iflow/skills/openspec-explore/SKILL.md +288 -288
- package/.iflow/skills/openspec-propose/SKILL.md +110 -110
- package/.prettierrc +11 -11
- package/AGENTS.md +169 -26
- package/README.md +195 -195
- package/README.zh-CN.md +193 -193
- package/dist/src/contracts/mcp.js +34 -34
- package/dist/src/server/mcp-server.d.ts +8 -0
- package/dist/src/server/mcp-server.d.ts.map +1 -1
- package/dist/src/server/mcp-server.js +23 -2
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/services/memory-service.d.ts +1 -0
- package/dist/src/services/memory-service.d.ts.map +1 -1
- package/dist/src/services/memory-service.js +125 -82
- package/dist/src/services/memory-service.js.map +1 -1
- package/docs/architecture-diagrams.md +368 -0
- package/docs/architecture.md +381 -349
- package/docs/contracts.md +190 -119
- package/docs/prompt-template.md +33 -79
- package/docs/proposals/mcp-typescript-sdk-refactor.md +568 -568
- package/docs/proposals/proposal-close-gates.md +58 -58
- package/docs/tool-calling-policy.md +101 -107
- package/docs/vector-search.md +306 -0
- package/package.json +59 -58
- package/src/contracts/index.ts +12 -12
- package/src/contracts/mcp.ts +222 -222
- package/src/contracts/schemas.ts +307 -307
- package/src/contracts/types.ts +410 -410
- package/src/index.ts +8 -8
- package/src/server/index.ts +5 -5
- package/src/server/mcp-server.ts +185 -161
- package/src/services/embedding-service.ts +114 -114
- package/src/services/index.ts +5 -5
- package/src/services/memory-service.ts +663 -621
- package/src/storage/frontmatter-parser.ts +243 -243
- package/src/storage/index.ts +6 -6
- package/src/storage/markdown-storage.ts +236 -236
- package/src/storage/vector-index.ts +160 -160
- package/src/utils/index.ts +5 -5
- package/src/utils/slugify.ts +63 -63
- package/test/contracts/schemas.test.ts +313 -313
- package/test/contracts/types.test.ts +21 -21
- package/test/frontmatter-parser-more.test.ts +94 -94
- package/test/server/mcp-server.test.ts +210 -169
- package/test/services/memory-service-edge.test.ts +248 -248
- package/test/services/memory-service.test.ts +278 -278
- package/test/storage/frontmatter-parser.test.ts +222 -222
- package/test/storage/markdown-storage.test.ts +216 -216
- package/test/storage/storage-edge.test.ts +238 -238
- package/test/storage/vector-index.test.ts +153 -153
- package/test/utils/slugify-edge.test.ts +94 -94
- package/test/utils/slugify.test.ts +68 -68
- package/tsconfig.json +25 -25
- package/tsconfig.test.json +8 -8
- package/vitest.config.ts +29 -29
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory Service Tests
|
|
3
|
-
* Tests for the MemoryService class
|
|
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 { MemoryService, ServiceError } from '../../src/services/memory-service.js';
|
|
11
|
-
import type {
|
|
12
|
-
CreateMemoryInput,
|
|
13
|
-
UpdateMemoryInput,
|
|
14
|
-
ListMemoryInput,
|
|
15
|
-
SearchMemoryInput,
|
|
16
|
-
} from '../../src/contracts/types.js';
|
|
17
|
-
|
|
18
|
-
describe('MemoryService', () => {
|
|
19
|
-
let tempDir: string;
|
|
20
|
-
let memoryService: MemoryService;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
tempDir = mkdtempSync(join(tmpdir(), 'memhub-test-'));
|
|
24
|
-
memoryService = new MemoryService({ storagePath: tempDir, vectorSearch: false });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('create', () => {
|
|
32
|
-
it('should create a new memory with generated ID', async () => {
|
|
33
|
-
const input: CreateMemoryInput = {
|
|
34
|
-
title: 'Test Memory',
|
|
35
|
-
content: 'This is a test memory',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const result = await memoryService.create(input);
|
|
39
|
-
expect(result.id).toBeDefined();
|
|
40
|
-
expect(result.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should create memory file with correct format', async () => {
|
|
44
|
-
const input: CreateMemoryInput = {
|
|
45
|
-
title: 'Test Memory',
|
|
46
|
-
content: 'This is a test memory',
|
|
47
|
-
tags: ['test', 'memory'],
|
|
48
|
-
category: 'testing',
|
|
49
|
-
importance: 4,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const result = await memoryService.create(input);
|
|
53
|
-
const { readFileSync } = await import('fs');
|
|
54
|
-
const fileContent = readFileSync(result.filePath, 'utf-8');
|
|
55
|
-
expect(fileContent).toContain('---');
|
|
56
|
-
expect(fileContent).toContain('id:');
|
|
57
|
-
expect(fileContent).toContain('# Test Memory');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should set timestamps on creation', async () => {
|
|
61
|
-
const input: CreateMemoryInput = {
|
|
62
|
-
title: 'Test Memory',
|
|
63
|
-
content: 'Content',
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const before = new Date().toISOString();
|
|
67
|
-
const result = await memoryService.create(input);
|
|
68
|
-
const after = new Date().toISOString();
|
|
69
|
-
expect(result.memory.createdAt >= before).toBe(true);
|
|
70
|
-
expect(result.memory.createdAt <= after).toBe(true);
|
|
71
|
-
expect(result.memory.updatedAt).toBe(result.memory.createdAt);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should apply default values for optional fields', async () => {
|
|
75
|
-
const input: CreateMemoryInput = {
|
|
76
|
-
title: 'Test Memory',
|
|
77
|
-
content: 'Content',
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const result = await memoryService.create(input);
|
|
81
|
-
expect(result.memory.tags).toEqual([]);
|
|
82
|
-
expect(result.memory.category).toBe('general');
|
|
83
|
-
expect(result.memory.importance).toBe(3);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should generate URL-friendly filename from title', async () => {
|
|
87
|
-
const input: CreateMemoryInput = {
|
|
88
|
-
title: 'Hello World Test',
|
|
89
|
-
content: 'Content',
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const result = await memoryService.create(input);
|
|
93
|
-
expect(result.filePath).toContain('hello-world-test');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('read', () => {
|
|
98
|
-
it('should read existing memory by ID', async () => {
|
|
99
|
-
const created = await memoryService.create({ title: 'Test', content: 'Content' });
|
|
100
|
-
const read = await memoryService.read({ id: created.id });
|
|
101
|
-
expect(read.memory.id).toBe(created.id);
|
|
102
|
-
expect(read.memory.title).toBe('Test');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should throw NOT_FOUND error for non-existent ID', async () => {
|
|
106
|
-
await expect(
|
|
107
|
-
memoryService.read({ id: '550e8400-e29b-41d4-a716-446655440000' })
|
|
108
|
-
).rejects.toThrow(ServiceError);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('update', () => {
|
|
113
|
-
it('should update memory title', async () => {
|
|
114
|
-
const created = await memoryService.create({ title: 'Old Title', content: 'Content' });
|
|
115
|
-
const updated = await memoryService.update({ id: created.id, title: 'New Title' });
|
|
116
|
-
expect(updated.memory.title).toBe('New Title');
|
|
117
|
-
expect(updated.memory.content).toBe('Content');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should update memory content', async () => {
|
|
121
|
-
const created = await memoryService.create({ title: 'Title', content: 'Old Content' });
|
|
122
|
-
const updated = await memoryService.update({ id: created.id, content: 'New Content' });
|
|
123
|
-
expect(updated.memory.content).toBe('New Content');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should update updatedAt timestamp', async () => {
|
|
127
|
-
const created = await memoryService.create({ title: 'Title', content: 'Content' });
|
|
128
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
129
|
-
const updated = await memoryService.update({ id: created.id, title: 'New Title' });
|
|
130
|
-
expect(new Date(updated.memory.updatedAt).getTime()).toBeGreaterThan(
|
|
131
|
-
new Date(created.memory.updatedAt).getTime()
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should not change createdAt timestamp', async () => {
|
|
136
|
-
const created = await memoryService.create({ title: 'Title', content: 'Content' });
|
|
137
|
-
const updated = await memoryService.update({ id: created.id, title: 'New Title' });
|
|
138
|
-
expect(updated.memory.createdAt).toBe(created.memory.createdAt);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should throw NOT_FOUND error for non-existent ID', async () => {
|
|
142
|
-
await expect(
|
|
143
|
-
memoryService.update({ id: '550e8400-e29b-41d4-a716-446655440000', title: 'New' })
|
|
144
|
-
).rejects.toThrow(ServiceError);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('delete', () => {
|
|
149
|
-
it('should delete existing memory', async () => {
|
|
150
|
-
const created = await memoryService.create({ title: 'To Delete', content: 'Content' });
|
|
151
|
-
const result = await memoryService.delete({ id: created.id });
|
|
152
|
-
expect(result.success).toBe(true);
|
|
153
|
-
await expect(memoryService.read({ id: created.id })).rejects.toThrow(ServiceError);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should return file path of deleted memory', async () => {
|
|
157
|
-
const created = await memoryService.create({ title: 'To Delete', content: 'Content' });
|
|
158
|
-
const result = await memoryService.delete({ id: created.id });
|
|
159
|
-
expect(result.filePath).toBe(created.filePath);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should throw NOT_FOUND error for non-existent ID', async () => {
|
|
163
|
-
await expect(
|
|
164
|
-
memoryService.delete({ id: '550e8400-e29b-41d4-a716-446655440000' })
|
|
165
|
-
).rejects.toThrow(ServiceError);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('list', () => {
|
|
170
|
-
beforeEach(async () => {
|
|
171
|
-
await memoryService.create({ title: 'Work 1', content: 'Content', category: 'work', tags: ['project'] });
|
|
172
|
-
await memoryService.create({ title: 'Work 2', content: 'Content', category: 'work', tags: ['meeting'] });
|
|
173
|
-
await memoryService.create({ title: 'Personal', content: 'Content', category: 'personal' });
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should list all memories', async () => {
|
|
177
|
-
const result = await memoryService.list({});
|
|
178
|
-
expect(result.memories).toHaveLength(3);
|
|
179
|
-
expect(result.total).toBe(3);
|
|
180
|
-
expect(result.hasMore).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should filter by category', async () => {
|
|
184
|
-
const result = await memoryService.list({ category: 'work' });
|
|
185
|
-
expect(result.memories).toHaveLength(2);
|
|
186
|
-
expect(result.total).toBe(2);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('should filter by tags', async () => {
|
|
190
|
-
const result = await memoryService.list({ tags: ['project'] });
|
|
191
|
-
expect(result.memories).toHaveLength(1);
|
|
192
|
-
expect(result.memories[0].title).toBe('Work 1');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('should support pagination', async () => {
|
|
196
|
-
const result = await memoryService.list({ limit: 2, offset: 0 });
|
|
197
|
-
expect(result.memories).toHaveLength(2);
|
|
198
|
-
expect(result.hasMore).toBe(true);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('should sort by specified field', async () => {
|
|
202
|
-
const result = await memoryService.list({ sortBy: 'title', sortOrder: 'asc' });
|
|
203
|
-
expect(result.memories[0].title).toBe('Personal');
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('search', () => {
|
|
208
|
-
beforeEach(async () => {
|
|
209
|
-
await memoryService.create({
|
|
210
|
-
title: 'Project Planning',
|
|
211
|
-
content: 'We need to plan the project timeline and resources.',
|
|
212
|
-
tags: ['planning'],
|
|
213
|
-
});
|
|
214
|
-
await memoryService.create({
|
|
215
|
-
title: 'Meeting Notes',
|
|
216
|
-
content: 'Discussed project requirements and timeline.',
|
|
217
|
-
tags: ['meeting'],
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should search in title', async () => {
|
|
222
|
-
const result = await memoryService.search({ query: 'planning' });
|
|
223
|
-
expect(result.results.length).toBeGreaterThan(0);
|
|
224
|
-
expect(result.results[0].memory.title).toBe('Project Planning');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should search in content', async () => {
|
|
228
|
-
const result = await memoryService.search({ query: 'timeline' });
|
|
229
|
-
expect(result.results.length).toBeGreaterThan(0);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should search in tags', async () => {
|
|
233
|
-
const result = await memoryService.search({ query: 'meeting' });
|
|
234
|
-
expect(result.results.length).toBeGreaterThan(0);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('should return match snippets', async () => {
|
|
238
|
-
const result = await memoryService.search({ query: 'project' });
|
|
239
|
-
expect(result.results[0].matches.length).toBeGreaterThan(0);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should support multiple keywords', async () => {
|
|
243
|
-
const result = await memoryService.search({ query: 'project timeline' });
|
|
244
|
-
expect(result.results.length).toBeGreaterThan(0);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('getCategories', () => {
|
|
249
|
-
it('should return all unique categories', async () => {
|
|
250
|
-
await memoryService.create({ title: '1', content: 'C', category: 'work' });
|
|
251
|
-
await memoryService.create({ title: '2', content: 'C', category: 'personal' });
|
|
252
|
-
await memoryService.create({ title: '3', content: 'C', category: 'work' });
|
|
253
|
-
const result = await memoryService.getCategories();
|
|
254
|
-
expect(result.categories).toContain('personal');
|
|
255
|
-
expect(result.categories).toContain('work');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should return empty array when no memories exist', async () => {
|
|
259
|
-
const result = await memoryService.getCategories();
|
|
260
|
-
expect(result.categories).toEqual([]);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
describe('getTags', () => {
|
|
265
|
-
it('should return all unique tags', async () => {
|
|
266
|
-
await memoryService.create({ title: '1', content: 'C', tags: ['a', 'b'] });
|
|
267
|
-
await memoryService.create({ title: '2', content: 'C', tags: ['b', 'c'] });
|
|
268
|
-
const result = await memoryService.getTags();
|
|
269
|
-
expect(result.tags).toContain('a');
|
|
270
|
-
expect(result.tags).toContain('b');
|
|
271
|
-
expect(result.tags).toContain('c');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('should return empty array when no memories exist', async () => {
|
|
275
|
-
const result = await memoryService.getTags();
|
|
276
|
-
expect(result.tags).toEqual([]);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Memory Service Tests
|
|
3
|
+
* Tests for the MemoryService class
|
|
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 { MemoryService, ServiceError } from '../../src/services/memory-service.js';
|
|
11
|
+
import type {
|
|
12
|
+
CreateMemoryInput,
|
|
13
|
+
UpdateMemoryInput,
|
|
14
|
+
ListMemoryInput,
|
|
15
|
+
SearchMemoryInput,
|
|
16
|
+
} from '../../src/contracts/types.js';
|
|
17
|
+
|
|
18
|
+
describe('MemoryService', () => {
|
|
19
|
+
let tempDir: string;
|
|
20
|
+
let memoryService: MemoryService;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tempDir = mkdtempSync(join(tmpdir(), 'memhub-test-'));
|
|
24
|
+
memoryService = new MemoryService({ storagePath: tempDir, vectorSearch: false });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('create', () => {
|
|
32
|
+
it('should create a new memory with generated ID', async () => {
|
|
33
|
+
const input: CreateMemoryInput = {
|
|
34
|
+
title: 'Test Memory',
|
|
35
|
+
content: 'This is a test memory',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = await memoryService.create(input);
|
|
39
|
+
expect(result.id).toBeDefined();
|
|
40
|
+
expect(result.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should create memory file with correct format', async () => {
|
|
44
|
+
const input: CreateMemoryInput = {
|
|
45
|
+
title: 'Test Memory',
|
|
46
|
+
content: 'This is a test memory',
|
|
47
|
+
tags: ['test', 'memory'],
|
|
48
|
+
category: 'testing',
|
|
49
|
+
importance: 4,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = await memoryService.create(input);
|
|
53
|
+
const { readFileSync } = await import('fs');
|
|
54
|
+
const fileContent = readFileSync(result.filePath, 'utf-8');
|
|
55
|
+
expect(fileContent).toContain('---');
|
|
56
|
+
expect(fileContent).toContain('id:');
|
|
57
|
+
expect(fileContent).toContain('# Test Memory');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should set timestamps on creation', async () => {
|
|
61
|
+
const input: CreateMemoryInput = {
|
|
62
|
+
title: 'Test Memory',
|
|
63
|
+
content: 'Content',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const before = new Date().toISOString();
|
|
67
|
+
const result = await memoryService.create(input);
|
|
68
|
+
const after = new Date().toISOString();
|
|
69
|
+
expect(result.memory.createdAt >= before).toBe(true);
|
|
70
|
+
expect(result.memory.createdAt <= after).toBe(true);
|
|
71
|
+
expect(result.memory.updatedAt).toBe(result.memory.createdAt);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should apply default values for optional fields', async () => {
|
|
75
|
+
const input: CreateMemoryInput = {
|
|
76
|
+
title: 'Test Memory',
|
|
77
|
+
content: 'Content',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const result = await memoryService.create(input);
|
|
81
|
+
expect(result.memory.tags).toEqual([]);
|
|
82
|
+
expect(result.memory.category).toBe('general');
|
|
83
|
+
expect(result.memory.importance).toBe(3);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should generate URL-friendly filename from title', async () => {
|
|
87
|
+
const input: CreateMemoryInput = {
|
|
88
|
+
title: 'Hello World Test',
|
|
89
|
+
content: 'Content',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = await memoryService.create(input);
|
|
93
|
+
expect(result.filePath).toContain('hello-world-test');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('read', () => {
|
|
98
|
+
it('should read existing memory by ID', async () => {
|
|
99
|
+
const created = await memoryService.create({ title: 'Test', content: 'Content' });
|
|
100
|
+
const read = await memoryService.read({ id: created.id });
|
|
101
|
+
expect(read.memory.id).toBe(created.id);
|
|
102
|
+
expect(read.memory.title).toBe('Test');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw NOT_FOUND error for non-existent ID', async () => {
|
|
106
|
+
await expect(
|
|
107
|
+
memoryService.read({ id: '550e8400-e29b-41d4-a716-446655440000' })
|
|
108
|
+
).rejects.toThrow(ServiceError);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('update', () => {
|
|
113
|
+
it('should update memory title', async () => {
|
|
114
|
+
const created = await memoryService.create({ title: 'Old Title', content: 'Content' });
|
|
115
|
+
const updated = await memoryService.update({ id: created.id, title: 'New Title' });
|
|
116
|
+
expect(updated.memory.title).toBe('New Title');
|
|
117
|
+
expect(updated.memory.content).toBe('Content');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should update memory content', async () => {
|
|
121
|
+
const created = await memoryService.create({ title: 'Title', content: 'Old Content' });
|
|
122
|
+
const updated = await memoryService.update({ id: created.id, content: 'New Content' });
|
|
123
|
+
expect(updated.memory.content).toBe('New Content');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should update updatedAt timestamp', async () => {
|
|
127
|
+
const created = await memoryService.create({ title: 'Title', content: 'Content' });
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
129
|
+
const updated = await memoryService.update({ id: created.id, title: 'New Title' });
|
|
130
|
+
expect(new Date(updated.memory.updatedAt).getTime()).toBeGreaterThan(
|
|
131
|
+
new Date(created.memory.updatedAt).getTime()
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should not change createdAt timestamp', async () => {
|
|
136
|
+
const created = await memoryService.create({ title: 'Title', content: 'Content' });
|
|
137
|
+
const updated = await memoryService.update({ id: created.id, title: 'New Title' });
|
|
138
|
+
expect(updated.memory.createdAt).toBe(created.memory.createdAt);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should throw NOT_FOUND error for non-existent ID', async () => {
|
|
142
|
+
await expect(
|
|
143
|
+
memoryService.update({ id: '550e8400-e29b-41d4-a716-446655440000', title: 'New' })
|
|
144
|
+
).rejects.toThrow(ServiceError);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('delete', () => {
|
|
149
|
+
it('should delete existing memory', async () => {
|
|
150
|
+
const created = await memoryService.create({ title: 'To Delete', content: 'Content' });
|
|
151
|
+
const result = await memoryService.delete({ id: created.id });
|
|
152
|
+
expect(result.success).toBe(true);
|
|
153
|
+
await expect(memoryService.read({ id: created.id })).rejects.toThrow(ServiceError);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should return file path of deleted memory', async () => {
|
|
157
|
+
const created = await memoryService.create({ title: 'To Delete', content: 'Content' });
|
|
158
|
+
const result = await memoryService.delete({ id: created.id });
|
|
159
|
+
expect(result.filePath).toBe(created.filePath);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should throw NOT_FOUND error for non-existent ID', async () => {
|
|
163
|
+
await expect(
|
|
164
|
+
memoryService.delete({ id: '550e8400-e29b-41d4-a716-446655440000' })
|
|
165
|
+
).rejects.toThrow(ServiceError);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('list', () => {
|
|
170
|
+
beforeEach(async () => {
|
|
171
|
+
await memoryService.create({ title: 'Work 1', content: 'Content', category: 'work', tags: ['project'] });
|
|
172
|
+
await memoryService.create({ title: 'Work 2', content: 'Content', category: 'work', tags: ['meeting'] });
|
|
173
|
+
await memoryService.create({ title: 'Personal', content: 'Content', category: 'personal' });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should list all memories', async () => {
|
|
177
|
+
const result = await memoryService.list({});
|
|
178
|
+
expect(result.memories).toHaveLength(3);
|
|
179
|
+
expect(result.total).toBe(3);
|
|
180
|
+
expect(result.hasMore).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should filter by category', async () => {
|
|
184
|
+
const result = await memoryService.list({ category: 'work' });
|
|
185
|
+
expect(result.memories).toHaveLength(2);
|
|
186
|
+
expect(result.total).toBe(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should filter by tags', async () => {
|
|
190
|
+
const result = await memoryService.list({ tags: ['project'] });
|
|
191
|
+
expect(result.memories).toHaveLength(1);
|
|
192
|
+
expect(result.memories[0].title).toBe('Work 1');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should support pagination', async () => {
|
|
196
|
+
const result = await memoryService.list({ limit: 2, offset: 0 });
|
|
197
|
+
expect(result.memories).toHaveLength(2);
|
|
198
|
+
expect(result.hasMore).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should sort by specified field', async () => {
|
|
202
|
+
const result = await memoryService.list({ sortBy: 'title', sortOrder: 'asc' });
|
|
203
|
+
expect(result.memories[0].title).toBe('Personal');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('search', () => {
|
|
208
|
+
beforeEach(async () => {
|
|
209
|
+
await memoryService.create({
|
|
210
|
+
title: 'Project Planning',
|
|
211
|
+
content: 'We need to plan the project timeline and resources.',
|
|
212
|
+
tags: ['planning'],
|
|
213
|
+
});
|
|
214
|
+
await memoryService.create({
|
|
215
|
+
title: 'Meeting Notes',
|
|
216
|
+
content: 'Discussed project requirements and timeline.',
|
|
217
|
+
tags: ['meeting'],
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should search in title', async () => {
|
|
222
|
+
const result = await memoryService.search({ query: 'planning' });
|
|
223
|
+
expect(result.results.length).toBeGreaterThan(0);
|
|
224
|
+
expect(result.results[0].memory.title).toBe('Project Planning');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should search in content', async () => {
|
|
228
|
+
const result = await memoryService.search({ query: 'timeline' });
|
|
229
|
+
expect(result.results.length).toBeGreaterThan(0);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should search in tags', async () => {
|
|
233
|
+
const result = await memoryService.search({ query: 'meeting' });
|
|
234
|
+
expect(result.results.length).toBeGreaterThan(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should return match snippets', async () => {
|
|
238
|
+
const result = await memoryService.search({ query: 'project' });
|
|
239
|
+
expect(result.results[0].matches.length).toBeGreaterThan(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should support multiple keywords', async () => {
|
|
243
|
+
const result = await memoryService.search({ query: 'project timeline' });
|
|
244
|
+
expect(result.results.length).toBeGreaterThan(0);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('getCategories', () => {
|
|
249
|
+
it('should return all unique categories', async () => {
|
|
250
|
+
await memoryService.create({ title: '1', content: 'C', category: 'work' });
|
|
251
|
+
await memoryService.create({ title: '2', content: 'C', category: 'personal' });
|
|
252
|
+
await memoryService.create({ title: '3', content: 'C', category: 'work' });
|
|
253
|
+
const result = await memoryService.getCategories();
|
|
254
|
+
expect(result.categories).toContain('personal');
|
|
255
|
+
expect(result.categories).toContain('work');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return empty array when no memories exist', async () => {
|
|
259
|
+
const result = await memoryService.getCategories();
|
|
260
|
+
expect(result.categories).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('getTags', () => {
|
|
265
|
+
it('should return all unique tags', async () => {
|
|
266
|
+
await memoryService.create({ title: '1', content: 'C', tags: ['a', 'b'] });
|
|
267
|
+
await memoryService.create({ title: '2', content: 'C', tags: ['b', 'c'] });
|
|
268
|
+
const result = await memoryService.getTags();
|
|
269
|
+
expect(result.tags).toContain('a');
|
|
270
|
+
expect(result.tags).toContain('b');
|
|
271
|
+
expect(result.tags).toContain('c');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should return empty array when no memories exist', async () => {
|
|
275
|
+
const result = await memoryService.getTags();
|
|
276
|
+
expect(result.tags).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
279
|
});
|