@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.
- package/README.md +250 -0
- package/dist/cache-manager.d.ts +134 -0
- package/dist/cache-manager.d.ts.map +1 -0
- package/dist/cache-manager.js +407 -0
- package/dist/cache-manager.js.map +1 -0
- package/dist/cli/save.d.ts +20 -0
- package/dist/cli/save.d.ts.map +1 -0
- package/dist/cli/save.js +94 -0
- package/dist/cli/save.js.map +1 -0
- package/dist/cli/setup.d.ts +18 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +163 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/viewer.d.ts +21 -0
- package/dist/cli/viewer.d.ts.map +1 -0
- package/dist/cli/viewer.js +182 -0
- package/dist/cli/viewer.js.map +1 -0
- package/dist/hnsw-index.d.ts +111 -0
- package/dist/hnsw-index.d.ts.map +1 -0
- package/dist/hnsw-index.js +781 -0
- package/dist/hnsw-index.js.map +1 -0
- package/dist/hooks/cli.d.ts +20 -0
- package/dist/hooks/cli.d.ts.map +1 -0
- package/dist/hooks/cli.js +102 -0
- package/dist/hooks/cli.js.map +1 -0
- package/dist/hooks/context.d.ts +31 -0
- package/dist/hooks/context.d.ts.map +1 -0
- package/dist/hooks/context.js +64 -0
- package/dist/hooks/context.js.map +1 -0
- package/dist/hooks/index.d.ts +16 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +20 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/observation.d.ts +30 -0
- package/dist/hooks/observation.d.ts.map +1 -0
- package/dist/hooks/observation.js +79 -0
- package/dist/hooks/observation.js.map +1 -0
- package/dist/hooks/service.d.ts +102 -0
- package/dist/hooks/service.d.ts.map +1 -0
- package/dist/hooks/service.js +454 -0
- package/dist/hooks/service.js.map +1 -0
- package/dist/hooks/session-init.d.ts +30 -0
- package/dist/hooks/session-init.d.ts.map +1 -0
- package/dist/hooks/session-init.js +54 -0
- package/dist/hooks/session-init.js.map +1 -0
- package/dist/hooks/summarize.d.ts +30 -0
- package/dist/hooks/summarize.d.ts.map +1 -0
- package/dist/hooks/summarize.js +74 -0
- package/dist/hooks/summarize.js.map +1 -0
- package/dist/hooks/types.d.ts +193 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +137 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/index.d.ts +173 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +564 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +22 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +368 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +14 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +110 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +100 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +9 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/migration.d.ts +77 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +457 -0
- package/dist/migration.js.map +1 -0
- package/dist/sqljs-backend.d.ts +128 -0
- package/dist/sqljs-backend.d.ts.map +1 -0
- package/dist/sqljs-backend.js +623 -0
- package/dist/sqljs-backend.js.map +1 -0
- package/dist/types.d.ts +481 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +73 -0
- package/dist/types.js.map +1 -0
- package/hooks.json +46 -0
- package/package.json +67 -0
- package/src/__tests__/index.test.ts +407 -0
- package/src/__tests__/sqljs-backend.test.ts +410 -0
- package/src/cache-manager.ts +515 -0
- package/src/cli/save.ts +109 -0
- package/src/cli/setup.ts +203 -0
- package/src/cli/viewer.ts +218 -0
- package/src/hnsw-index.ts +1013 -0
- package/src/hooks/__tests__/handlers.test.ts +298 -0
- package/src/hooks/__tests__/integration.test.ts +431 -0
- package/src/hooks/__tests__/service.test.ts +487 -0
- package/src/hooks/__tests__/types.test.ts +341 -0
- package/src/hooks/cli.ts +121 -0
- package/src/hooks/context.ts +77 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/observation.ts +102 -0
- package/src/hooks/service.ts +582 -0
- package/src/hooks/session-init.ts +70 -0
- package/src/hooks/summarize.ts +89 -0
- package/src/hooks/types.ts +365 -0
- package/src/index.ts +755 -0
- package/src/mcp/__tests__/server.test.ts +181 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/server.ts +441 -0
- package/src/mcp/tools.ts +113 -0
- package/src/mcp/types.ts +109 -0
- package/src/migration.ts +574 -0
- package/src/sql.js.d.ts +70 -0
- package/src/sqljs-backend.ts +789 -0
- 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
|
+
});
|