@aitytech/agentkits-memory 1.0.0

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 (116) hide show
  1. package/README.md +250 -0
  2. package/dist/cache-manager.d.ts +134 -0
  3. package/dist/cache-manager.d.ts.map +1 -0
  4. package/dist/cache-manager.js +407 -0
  5. package/dist/cache-manager.js.map +1 -0
  6. package/dist/cli/save.d.ts +20 -0
  7. package/dist/cli/save.d.ts.map +1 -0
  8. package/dist/cli/save.js +94 -0
  9. package/dist/cli/save.js.map +1 -0
  10. package/dist/cli/setup.d.ts +18 -0
  11. package/dist/cli/setup.d.ts.map +1 -0
  12. package/dist/cli/setup.js +163 -0
  13. package/dist/cli/setup.js.map +1 -0
  14. package/dist/cli/viewer.d.ts +21 -0
  15. package/dist/cli/viewer.d.ts.map +1 -0
  16. package/dist/cli/viewer.js +182 -0
  17. package/dist/cli/viewer.js.map +1 -0
  18. package/dist/hnsw-index.d.ts +111 -0
  19. package/dist/hnsw-index.d.ts.map +1 -0
  20. package/dist/hnsw-index.js +781 -0
  21. package/dist/hnsw-index.js.map +1 -0
  22. package/dist/hooks/cli.d.ts +20 -0
  23. package/dist/hooks/cli.d.ts.map +1 -0
  24. package/dist/hooks/cli.js +102 -0
  25. package/dist/hooks/cli.js.map +1 -0
  26. package/dist/hooks/context.d.ts +31 -0
  27. package/dist/hooks/context.d.ts.map +1 -0
  28. package/dist/hooks/context.js +64 -0
  29. package/dist/hooks/context.js.map +1 -0
  30. package/dist/hooks/index.d.ts +16 -0
  31. package/dist/hooks/index.d.ts.map +1 -0
  32. package/dist/hooks/index.js +20 -0
  33. package/dist/hooks/index.js.map +1 -0
  34. package/dist/hooks/observation.d.ts +30 -0
  35. package/dist/hooks/observation.d.ts.map +1 -0
  36. package/dist/hooks/observation.js +79 -0
  37. package/dist/hooks/observation.js.map +1 -0
  38. package/dist/hooks/service.d.ts +102 -0
  39. package/dist/hooks/service.d.ts.map +1 -0
  40. package/dist/hooks/service.js +454 -0
  41. package/dist/hooks/service.js.map +1 -0
  42. package/dist/hooks/session-init.d.ts +30 -0
  43. package/dist/hooks/session-init.d.ts.map +1 -0
  44. package/dist/hooks/session-init.js +54 -0
  45. package/dist/hooks/session-init.js.map +1 -0
  46. package/dist/hooks/summarize.d.ts +30 -0
  47. package/dist/hooks/summarize.d.ts.map +1 -0
  48. package/dist/hooks/summarize.js +74 -0
  49. package/dist/hooks/summarize.js.map +1 -0
  50. package/dist/hooks/types.d.ts +193 -0
  51. package/dist/hooks/types.d.ts.map +1 -0
  52. package/dist/hooks/types.js +137 -0
  53. package/dist/hooks/types.js.map +1 -0
  54. package/dist/index.d.ts +173 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +564 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/mcp/index.d.ts +9 -0
  59. package/dist/mcp/index.d.ts.map +1 -0
  60. package/dist/mcp/index.js +9 -0
  61. package/dist/mcp/index.js.map +1 -0
  62. package/dist/mcp/server.d.ts +22 -0
  63. package/dist/mcp/server.d.ts.map +1 -0
  64. package/dist/mcp/server.js +368 -0
  65. package/dist/mcp/server.js.map +1 -0
  66. package/dist/mcp/tools.d.ts +14 -0
  67. package/dist/mcp/tools.d.ts.map +1 -0
  68. package/dist/mcp/tools.js +110 -0
  69. package/dist/mcp/tools.js.map +1 -0
  70. package/dist/mcp/types.d.ts +100 -0
  71. package/dist/mcp/types.d.ts.map +1 -0
  72. package/dist/mcp/types.js +9 -0
  73. package/dist/mcp/types.js.map +1 -0
  74. package/dist/migration.d.ts +77 -0
  75. package/dist/migration.d.ts.map +1 -0
  76. package/dist/migration.js +457 -0
  77. package/dist/migration.js.map +1 -0
  78. package/dist/sqljs-backend.d.ts +128 -0
  79. package/dist/sqljs-backend.d.ts.map +1 -0
  80. package/dist/sqljs-backend.js +623 -0
  81. package/dist/sqljs-backend.js.map +1 -0
  82. package/dist/types.d.ts +481 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +73 -0
  85. package/dist/types.js.map +1 -0
  86. package/hooks.json +46 -0
  87. package/package.json +67 -0
  88. package/src/__tests__/index.test.ts +407 -0
  89. package/src/__tests__/sqljs-backend.test.ts +410 -0
  90. package/src/cache-manager.ts +515 -0
  91. package/src/cli/save.ts +109 -0
  92. package/src/cli/setup.ts +203 -0
  93. package/src/cli/viewer.ts +218 -0
  94. package/src/hnsw-index.ts +1013 -0
  95. package/src/hooks/__tests__/handlers.test.ts +298 -0
  96. package/src/hooks/__tests__/integration.test.ts +431 -0
  97. package/src/hooks/__tests__/service.test.ts +487 -0
  98. package/src/hooks/__tests__/types.test.ts +341 -0
  99. package/src/hooks/cli.ts +121 -0
  100. package/src/hooks/context.ts +77 -0
  101. package/src/hooks/index.ts +23 -0
  102. package/src/hooks/observation.ts +102 -0
  103. package/src/hooks/service.ts +582 -0
  104. package/src/hooks/session-init.ts +70 -0
  105. package/src/hooks/summarize.ts +89 -0
  106. package/src/hooks/types.ts +365 -0
  107. package/src/index.ts +755 -0
  108. package/src/mcp/__tests__/server.test.ts +181 -0
  109. package/src/mcp/index.ts +9 -0
  110. package/src/mcp/server.ts +441 -0
  111. package/src/mcp/tools.ts +113 -0
  112. package/src/mcp/types.ts +109 -0
  113. package/src/migration.ts +574 -0
  114. package/src/sql.js.d.ts +70 -0
  115. package/src/sqljs-backend.ts +789 -0
  116. package/src/types.ts +715 -0
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Unit Tests for MemoryHookService
3
+ *
4
+ * @module @agentkits/memory/hooks/__tests__/service
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
+ import { existsSync, rmSync, mkdirSync } from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { MemoryHookService, createHookService } from '../service.js';
11
+
12
+ const TEST_DIR = path.join(process.cwd(), '.test-memory-hooks');
13
+
14
+ describe('MemoryHookService', () => {
15
+ let service: MemoryHookService;
16
+
17
+ beforeEach(() => {
18
+ // Clean up test directory
19
+ if (existsSync(TEST_DIR)) {
20
+ rmSync(TEST_DIR, { recursive: true });
21
+ }
22
+ mkdirSync(TEST_DIR, { recursive: true });
23
+
24
+ service = new MemoryHookService(TEST_DIR);
25
+ });
26
+
27
+ afterEach(async () => {
28
+ // Shutdown service
29
+ try {
30
+ await service.shutdown();
31
+ } catch {
32
+ // Ignore shutdown errors
33
+ }
34
+
35
+ // Clean up test directory
36
+ if (existsSync(TEST_DIR)) {
37
+ rmSync(TEST_DIR, { recursive: true });
38
+ }
39
+ });
40
+
41
+ describe('initialization', () => {
42
+ it('should initialize successfully', async () => {
43
+ await service.initialize();
44
+
45
+ // Database file is created after persist() call
46
+ // Initialize just creates the in-memory database
47
+ // Let's verify by adding some data and persisting
48
+ await service.initSession('test', 'test-project');
49
+
50
+ const dbPath = path.join(TEST_DIR, '.claude/memory', 'hooks.db');
51
+ expect(existsSync(dbPath)).toBe(true);
52
+ });
53
+
54
+ it('should be idempotent', async () => {
55
+ await service.initialize();
56
+ await service.initialize(); // Should not throw
57
+ });
58
+
59
+ it('should create memory directory if not exists', async () => {
60
+ const memDir = path.join(TEST_DIR, '.claude/memory');
61
+ expect(existsSync(memDir)).toBe(false);
62
+
63
+ await service.initialize();
64
+
65
+ expect(existsSync(memDir)).toBe(true);
66
+ });
67
+ });
68
+
69
+ describe('session management', () => {
70
+ it('should initialize a new session', async () => {
71
+ const session = await service.initSession('session-1', 'test-project', 'Hello Claude');
72
+
73
+ expect(session.sessionId).toBe('session-1');
74
+ expect(session.project).toBe('test-project');
75
+ expect(session.prompt).toBe('Hello Claude');
76
+ expect(session.status).toBe('active');
77
+ expect(session.observationCount).toBe(0);
78
+ });
79
+
80
+ it('should return existing session on re-init', async () => {
81
+ const session1 = await service.initSession('session-1', 'test-project', 'First prompt');
82
+ const session2 = await service.initSession('session-1', 'test-project', 'Second prompt');
83
+
84
+ expect(session1.sessionId).toBe(session2.sessionId);
85
+ expect(session1.prompt).toBe('First prompt'); // Original prompt preserved
86
+ });
87
+
88
+ it('should get session by ID', async () => {
89
+ await service.initSession('session-1', 'test-project', 'Test prompt');
90
+
91
+ const session = service.getSession('session-1');
92
+
93
+ expect(session).not.toBeNull();
94
+ expect(session?.sessionId).toBe('session-1');
95
+ expect(session?.project).toBe('test-project');
96
+ });
97
+
98
+ it('should return null for non-existent session', async () => {
99
+ await service.initialize();
100
+
101
+ const session = service.getSession('non-existent');
102
+
103
+ expect(session).toBeNull();
104
+ });
105
+
106
+ it('should complete a session with summary', async () => {
107
+ await service.initSession('session-1', 'test-project');
108
+ await service.completeSession('session-1', 'Task completed successfully');
109
+
110
+ const session = service.getSession('session-1');
111
+
112
+ expect(session?.status).toBe('completed');
113
+ expect(session?.summary).toBe('Task completed successfully');
114
+ expect(session?.endedAt).toBeGreaterThan(0);
115
+ });
116
+
117
+ it('should get recent sessions', async () => {
118
+ await service.initSession('session-1', 'test-project', 'First');
119
+ // Small delay to ensure different timestamps
120
+ await new Promise(resolve => setTimeout(resolve, 10));
121
+ await service.initSession('session-2', 'test-project', 'Second');
122
+ await service.initSession('session-3', 'other-project', 'Third');
123
+
124
+ const sessions = await service.getRecentSessions('test-project', 10);
125
+
126
+ expect(sessions.length).toBe(2);
127
+ expect(sessions[0].sessionId).toBe('session-2'); // Most recent first
128
+ expect(sessions[1].sessionId).toBe('session-1');
129
+ });
130
+
131
+ it('should limit recent sessions', async () => {
132
+ await service.initSession('session-1', 'test-project');
133
+ await service.initSession('session-2', 'test-project');
134
+ await service.initSession('session-3', 'test-project');
135
+
136
+ const sessions = await service.getRecentSessions('test-project', 2);
137
+
138
+ expect(sessions.length).toBe(2);
139
+ });
140
+ });
141
+
142
+ describe('observation management', () => {
143
+ it('should store an observation', async () => {
144
+ await service.initSession('session-1', 'test-project');
145
+
146
+ const observation = await service.storeObservation(
147
+ 'session-1',
148
+ 'test-project',
149
+ 'Read',
150
+ { file_path: '/path/to/file.ts' },
151
+ { content: 'file contents' },
152
+ TEST_DIR
153
+ );
154
+
155
+ expect(observation.id).toMatch(/^obs_/);
156
+ expect(observation.sessionId).toBe('session-1');
157
+ expect(observation.project).toBe('test-project');
158
+ expect(observation.toolName).toBe('Read');
159
+ expect(observation.type).toBe('read');
160
+ expect(observation.title).toBe('Read /path/to/file.ts');
161
+ });
162
+
163
+ it('should increment session observation count', async () => {
164
+ await service.initSession('session-1', 'test-project');
165
+
166
+ await service.storeObservation('session-1', 'test-project', 'Read', {}, {}, TEST_DIR);
167
+ await service.storeObservation('session-1', 'test-project', 'Write', {}, {}, TEST_DIR);
168
+
169
+ const session = service.getSession('session-1');
170
+
171
+ expect(session?.observationCount).toBe(2);
172
+ });
173
+
174
+ it('should get session observations', async () => {
175
+ await service.initSession('session-1', 'test-project');
176
+ await service.storeObservation('session-1', 'test-project', 'Read', { file_path: 'a.ts' }, {}, TEST_DIR);
177
+ // Small delay to ensure different timestamps
178
+ await new Promise(resolve => setTimeout(resolve, 10));
179
+ await service.storeObservation('session-1', 'test-project', 'Write', { file_path: 'b.ts' }, {}, TEST_DIR);
180
+
181
+ const observations = await service.getSessionObservations('session-1');
182
+
183
+ expect(observations.length).toBe(2);
184
+ // Most recent first
185
+ expect(observations[0].toolName).toBe('Write');
186
+ expect(observations[1].toolName).toBe('Read');
187
+ });
188
+
189
+ it('should get recent observations for project', async () => {
190
+ await service.initSession('session-1', 'test-project');
191
+ await service.initSession('session-2', 'test-project');
192
+
193
+ await service.storeObservation('session-1', 'test-project', 'Read', {}, {}, TEST_DIR);
194
+ await service.storeObservation('session-2', 'test-project', 'Write', {}, {}, TEST_DIR);
195
+
196
+ const observations = await service.getRecentObservations('test-project', 10);
197
+
198
+ expect(observations.length).toBe(2);
199
+ });
200
+
201
+ it('should truncate large responses', async () => {
202
+ await service.initSession('session-1', 'test-project');
203
+
204
+ const largeResponse = { content: 'A'.repeat(10000) };
205
+ const observation = await service.storeObservation(
206
+ 'session-1',
207
+ 'test-project',
208
+ 'Read',
209
+ {},
210
+ largeResponse,
211
+ TEST_DIR
212
+ );
213
+
214
+ expect(observation.toolResponse.length).toBeLessThan(10000);
215
+ expect(observation.toolResponse).toContain('[truncated]');
216
+ });
217
+
218
+ it('should handle null/undefined tool input and response', async () => {
219
+ await service.initSession('session-1', 'test-project');
220
+
221
+ // Pass null values - should use empty object fallback
222
+ const observation = await service.storeObservation(
223
+ 'session-1',
224
+ 'test-project',
225
+ 'Read',
226
+ null,
227
+ undefined,
228
+ TEST_DIR
229
+ );
230
+
231
+ expect(observation.toolInput).toBe('{}');
232
+ expect(observation.toolResponse).toBe('{}');
233
+ });
234
+ });
235
+
236
+ describe('context generation', () => {
237
+ it('should get context for project', async () => {
238
+ await service.initSession('session-1', 'test-project', 'First task');
239
+ await service.storeObservation('session-1', 'test-project', 'Read', { file_path: 'file.ts' }, {}, TEST_DIR);
240
+ await service.completeSession('session-1', 'Completed first task');
241
+
242
+ const context = await service.getContext('test-project');
243
+
244
+ expect(context.recentObservations.length).toBe(1);
245
+ expect(context.previousSessions.length).toBe(1);
246
+ expect(context.markdown).toContain('# Memory Context');
247
+ expect(context.markdown).toContain('test-project');
248
+ });
249
+
250
+ it('should include all observation type icons in context', async () => {
251
+ await service.initSession('session-1', 'test-project');
252
+
253
+ // Store observations of different types to test icon coverage
254
+ await service.storeObservation('session-1', 'test-project', 'Read', {}, {}, TEST_DIR); // read icon
255
+ await service.storeObservation('session-1', 'test-project', 'Write', {}, {}, TEST_DIR); // write icon
256
+ await service.storeObservation('session-1', 'test-project', 'Bash', {}, {}, TEST_DIR); // execute icon
257
+ await service.storeObservation('session-1', 'test-project', 'WebSearch', {}, {}, TEST_DIR); // search icon
258
+ await service.storeObservation('session-1', 'test-project', 'Unknown', {}, {}, TEST_DIR); // default icon
259
+
260
+ const context = await service.getContext('test-project');
261
+
262
+ // Verify icons are in the markdown
263
+ expect(context.markdown).toContain('📖'); // read
264
+ expect(context.markdown).toContain('✏️'); // write
265
+ expect(context.markdown).toContain('⚡'); // execute
266
+ expect(context.markdown).toContain('🔍'); // search
267
+ expect(context.markdown).toContain('•'); // default/other
268
+ });
269
+
270
+ it('should return empty context for new project', async () => {
271
+ await service.initialize();
272
+
273
+ const context = await service.getContext('new-project');
274
+
275
+ expect(context.recentObservations.length).toBe(0);
276
+ expect(context.previousSessions.length).toBe(0);
277
+ expect(context.markdown).toContain('No previous session context');
278
+ });
279
+
280
+ it('should format relative times correctly in context', async () => {
281
+ const baseTime = Date.now();
282
+
283
+ // Create session with observations at different times
284
+ await service.initSession('session-time', 'test-project');
285
+
286
+ // Store an observation
287
+ await service.storeObservation('session-time', 'test-project', 'Read', {}, {}, TEST_DIR);
288
+
289
+ // Mock Date.now to simulate time passing
290
+ const originalDateNow = Date.now;
291
+
292
+ // Test "just now" (less than 1 minute)
293
+ vi.spyOn(Date, 'now').mockReturnValue(baseTime + 30000); // 30 seconds later
294
+ let context = await service.getContext('test-project');
295
+ expect(context.markdown).toContain('just now');
296
+
297
+ // Test "Xm ago" (minutes)
298
+ vi.spyOn(Date, 'now').mockReturnValue(baseTime + 5 * 60000); // 5 minutes later
299
+ context = await service.getContext('test-project');
300
+ expect(context.markdown).toMatch(/\dm ago/);
301
+
302
+ // Test "Xh ago" (hours)
303
+ vi.spyOn(Date, 'now').mockReturnValue(baseTime + 3 * 3600000); // 3 hours later
304
+ context = await service.getContext('test-project');
305
+ expect(context.markdown).toMatch(/\dh ago/);
306
+
307
+ // Test "Xd ago" (days)
308
+ vi.spyOn(Date, 'now').mockReturnValue(baseTime + 3 * 86400000); // 3 days later
309
+ context = await service.getContext('test-project');
310
+ expect(context.markdown).toMatch(/\dd ago/);
311
+
312
+ // Test date format (more than 7 days)
313
+ vi.spyOn(Date, 'now').mockReturnValue(baseTime + 10 * 86400000); // 10 days later
314
+ context = await service.getContext('test-project');
315
+ // Should contain a date format like "1/20/2026" or similar
316
+ expect(context.markdown).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/);
317
+
318
+ // Restore
319
+ vi.restoreAllMocks();
320
+ });
321
+
322
+ it('should format context as markdown', async () => {
323
+ await service.initSession('session-1', 'test-project', 'Test prompt');
324
+ await service.storeObservation('session-1', 'test-project', 'Read', { file_path: 'file.ts' }, {}, TEST_DIR);
325
+ await service.completeSession('session-1', 'Done');
326
+
327
+ const context = await service.getContext('test-project');
328
+
329
+ expect(context.markdown).toContain('## Recent Activity');
330
+ expect(context.markdown).toContain('## Previous Sessions');
331
+ expect(context.markdown).toContain('Read');
332
+ });
333
+
334
+ it('should truncate long prompts in session context', async () => {
335
+ const longPrompt = 'A'.repeat(150); // More than 100 characters
336
+ await service.initSession('session-1', 'test-project', longPrompt);
337
+ await service.completeSession('session-1', 'Done');
338
+
339
+ const context = await service.getContext('test-project');
340
+
341
+ // Should contain truncated prompt with ellipsis
342
+ expect(context.markdown).toContain('A'.repeat(100));
343
+ expect(context.markdown).toContain('...');
344
+ });
345
+
346
+ it('should show active session status', async () => {
347
+ // Create an active session (not completed)
348
+ await service.initSession('session-active', 'test-project', 'Active task');
349
+
350
+ const context = await service.getContext('test-project');
351
+
352
+ // Active sessions should show → instead of ✓
353
+ expect(context.markdown).toContain('→');
354
+ });
355
+
356
+ it('should handle observations without title', async () => {
357
+ await service.initSession('session-1', 'test-project');
358
+
359
+ // Store an observation - the service will generate a title
360
+ await service.storeObservation('session-1', 'test-project', 'CustomTool', {}, {}, TEST_DIR);
361
+
362
+ const context = await service.getContext('test-project');
363
+
364
+ // Should not error and should include the tool name
365
+ expect(context.markdown).toContain('CustomTool');
366
+ });
367
+ });
368
+
369
+ describe('summary generation', () => {
370
+ it('should generate summary from observations', async () => {
371
+ await service.initSession('session-1', 'test-project');
372
+ await service.storeObservation('session-1', 'test-project', 'Read', { file_path: 'a.ts' }, {}, TEST_DIR);
373
+ await service.storeObservation('session-1', 'test-project', 'Read', { file_path: 'b.ts' }, {}, TEST_DIR);
374
+ await service.storeObservation('session-1', 'test-project', 'Write', { file_path: 'c.ts' }, {}, TEST_DIR);
375
+ await service.storeObservation('session-1', 'test-project', 'Bash', { command: 'npm test' }, {}, TEST_DIR);
376
+
377
+ const summary = await service.generateSummary('session-1');
378
+
379
+ expect(summary).toContain('file(s) modified');
380
+ expect(summary).toContain('file(s) read');
381
+ expect(summary).toContain('command(s) executed');
382
+ });
383
+
384
+ it('should return default summary for empty session', async () => {
385
+ await service.initSession('session-1', 'test-project');
386
+
387
+ const summary = await service.generateSummary('session-1');
388
+
389
+ expect(summary).toBe('No activity recorded in this session.');
390
+ });
391
+
392
+ it('should list files in summary', async () => {
393
+ await service.initSession('session-1', 'test-project');
394
+ await service.storeObservation('session-1', 'test-project', 'Write', { file_path: 'src/index.ts' }, {}, TEST_DIR);
395
+ await service.storeObservation('session-1', 'test-project', 'Write', { file_path: 'src/utils.ts' }, {}, TEST_DIR);
396
+
397
+ const summary = await service.generateSummary('session-1');
398
+
399
+ expect(summary).toContain('src/index.ts');
400
+ expect(summary).toContain('src/utils.ts');
401
+ });
402
+
403
+ it('should include search count in summary', async () => {
404
+ await service.initSession('session-1', 'test-project');
405
+ await service.storeObservation('session-1', 'test-project', 'WebSearch', { query: 'test' }, {}, TEST_DIR);
406
+ await service.storeObservation('session-1', 'test-project', 'WebFetch', { url: 'http://test.com' }, {}, TEST_DIR);
407
+
408
+ const summary = await service.generateSummary('session-1');
409
+
410
+ expect(summary).toContain('search(es)');
411
+ });
412
+
413
+ it('should show file count when more than 5 files touched', async () => {
414
+ await service.initSession('session-1', 'test-project');
415
+
416
+ // Touch more than 5 files
417
+ for (let i = 0; i < 7; i++) {
418
+ await service.storeObservation('session-1', 'test-project', 'Write', { file_path: `file${i}.ts` }, {}, TEST_DIR);
419
+ }
420
+
421
+ const summary = await service.generateSummary('session-1');
422
+
423
+ expect(summary).toContain('7 files touched');
424
+ });
425
+ });
426
+
427
+ describe('persistence', () => {
428
+ it('should auto-recreate database if deleted', async () => {
429
+ // Create and populate first instance
430
+ await service.initSession('session-1', 'test-project', 'Test prompt');
431
+ await service.storeObservation('session-1', 'test-project', 'Read', { file_path: 'file.ts' }, {}, TEST_DIR);
432
+ await service.shutdown();
433
+
434
+ // Delete the database file
435
+ const dbPath = path.join(TEST_DIR, '.claude/memory', 'hooks.db');
436
+ expect(existsSync(dbPath)).toBe(true);
437
+ rmSync(dbPath);
438
+ expect(existsSync(dbPath)).toBe(false);
439
+
440
+ // Create new instance - should auto-create new database
441
+ const service2 = new MemoryHookService(TEST_DIR);
442
+ await service2.initialize();
443
+
444
+ // Old data should be gone
445
+ const session = service2.getSession('session-1');
446
+ expect(session).toBeNull();
447
+
448
+ // But we can create new data
449
+ await service2.initSession('session-2', 'test-project', 'New prompt');
450
+ const newSession = service2.getSession('session-2');
451
+ expect(newSession).not.toBeNull();
452
+ expect(newSession?.prompt).toBe('New prompt');
453
+
454
+ // Database file should exist again
455
+ await service2.shutdown();
456
+ expect(existsSync(dbPath)).toBe(true);
457
+ });
458
+
459
+ it('should persist data across service restarts', async () => {
460
+ // Create and populate first instance
461
+ await service.initSession('session-1', 'test-project', 'Test prompt');
462
+ await service.storeObservation('session-1', 'test-project', 'Read', { file_path: 'file.ts' }, {}, TEST_DIR);
463
+ await service.shutdown();
464
+
465
+ // Create second instance
466
+ const service2 = new MemoryHookService(TEST_DIR);
467
+ await service2.initialize();
468
+
469
+ const session = service2.getSession('session-1');
470
+ const observations = await service2.getSessionObservations('session-1');
471
+
472
+ expect(session).not.toBeNull();
473
+ expect(session?.prompt).toBe('Test prompt');
474
+ expect(observations.length).toBe(1);
475
+
476
+ await service2.shutdown();
477
+ });
478
+ });
479
+
480
+ describe('createHookService factory', () => {
481
+ it('should create service with default config', () => {
482
+ const svc = createHookService(TEST_DIR);
483
+
484
+ expect(svc).toBeInstanceOf(MemoryHookService);
485
+ });
486
+ });
487
+ });