@mingxy/cerebro 1.20.4 → 1.20.6

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.
@@ -0,0 +1,275 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { buildMemoryInjection, chatMessageRecallHook } from "./hooks.js";
3
+ import type { CerebroClient, SearchResult, MemoryDto } from "./client.js";
4
+
5
+ // ── Helpers ──────────────────────────────────────────────────────────
6
+
7
+ function makeMemoryDto(overrides: Partial<MemoryDto> = {}): MemoryDto {
8
+ return {
9
+ id: `mem-${Math.random().toString(36).slice(2, 8)}`,
10
+ content: "test memory content",
11
+ category: "cases",
12
+ memory_type: "WORK",
13
+ state: "active",
14
+ tags: [],
15
+ tenant_id: "test-tenant",
16
+ importance: 0.5,
17
+ created_at: new Date().toISOString(),
18
+ updated_at: new Date().toISOString(),
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ function makeSearchResult(overrides: Partial<SearchResult> = {}): SearchResult {
24
+ return {
25
+ memory: makeMemoryDto(),
26
+ score: 0.8,
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ function mockClient(overrides: Partial<CerebroClient> = {} as any): CerebroClient {
32
+ return {
33
+ getInjection: vi.fn().mockResolvedValue(null),
34
+ listRecent: vi.fn().mockResolvedValue([]),
35
+ searchMemories: vi.fn().mockResolvedValue([]),
36
+ createRecallEvent: vi.fn().mockResolvedValue(undefined),
37
+ ...overrides,
38
+ } as unknown as CerebroClient;
39
+ }
40
+
41
+ // ── buildMemoryInjection ─────────────────────────────────────────────
42
+
43
+ describe("buildMemoryInjection", () => {
44
+ let client: CerebroClient;
45
+
46
+ beforeEach(() => {
47
+ client = mockClient();
48
+ });
49
+
50
+ it("returns injection with [CEREBRO-MEMORY] wrapper even when empty", async () => {
51
+ const result = await buildMemoryInjection(client, undefined, "test query", {});
52
+ expect(result.text).toContain("[CEREBRO-MEMORY]");
53
+ expect(result.text).toContain("[/CEREBRO-MEMORY]");
54
+ expect(result.profileCount).toBe(0);
55
+ expect(result.memoryCount).toBe(0);
56
+ expect(result.projectMemoryCount).toBe(0);
57
+ });
58
+
59
+ it("includes profile content when getInjection returns data", async () => {
60
+ client = mockClient({
61
+ getInjection: vi.fn().mockResolvedValue({
62
+ content: "User prefers TypeScript over JavaScript",
63
+ preference_count: 3,
64
+ estimated_tokens: 50,
65
+ }),
66
+ } as any);
67
+
68
+ const result = await buildMemoryInjection(client, undefined, "test query", {});
69
+ expect(result.text).toContain("User prefers TypeScript over JavaScript");
70
+ expect(result.profileCount).toBe(3);
71
+ });
72
+
73
+ it("includes project memories from listRecent", async () => {
74
+ const memories = [
75
+ makeMemoryDto({ id: "pm-1", content: "Fixed auth bug in login.ts" }),
76
+ makeMemoryDto({ id: "pm-2", content: "Added rate limiting middleware" }),
77
+ ];
78
+ client = mockClient({
79
+ listRecent: vi.fn().mockResolvedValue(memories),
80
+ } as any);
81
+
82
+ const result = await buildMemoryInjection(client, "/project", "query", {});
83
+ expect(result.text).toContain("## Recent Project Activity");
84
+ expect(result.text).toContain("Fixed auth bug in login.ts");
85
+ expect(result.text).toContain("Added rate limiting middleware");
86
+ expect(result.projectMemoryCount).toBe(2);
87
+ });
88
+
89
+ it("includes relevant search results deduped against project memories", async () => {
90
+ const projectMemory = makeMemoryDto({ id: "dup-1", content: "project memory" });
91
+ const searchHit: SearchResult = makeSearchResult({
92
+ memory: makeMemoryDto({ id: "dup-1", content: "project memory" }),
93
+ score: 0.9,
94
+ });
95
+ const searchHit2: SearchResult = makeSearchResult({
96
+ memory: makeMemoryDto({ id: "unique-1", content: "relevant search result" }),
97
+ score: 0.75,
98
+ });
99
+
100
+ client = mockClient({
101
+ listRecent: vi.fn().mockResolvedValue([projectMemory]),
102
+ searchMemories: vi.fn().mockResolvedValue([searchHit, searchHit2]),
103
+ } as any);
104
+
105
+ const result = await buildMemoryInjection(client, undefined, "test query", {});
106
+ // dup-1 should be deduped, only unique-1 in Relevant Memories
107
+ expect(result.text).toContain("## Relevant Memories");
108
+ expect(result.text).toContain("relevant search result");
109
+ // "project memory" appears in Recent Project Activity, not in Relevant Memories
110
+ const relevantIdx = result.text.indexOf("## Relevant Memories");
111
+ const projectIdx = result.text.indexOf("## Recent Project Activity");
112
+ expect(projectIdx).toBeLessThan(relevantIdx);
113
+ expect(result.memoryCount).toBe(1); // deduped from 2 to 1
114
+ });
115
+
116
+ it("respects maxContentChars config to truncate output", async () => {
117
+ // Return a very large memory to force truncation
118
+ const longContent = "A".repeat(5000);
119
+ client = mockClient({
120
+ listRecent: vi.fn().mockResolvedValue([
121
+ makeMemoryDto({ id: "long-1", content: longContent }),
122
+ ]),
123
+ } as any);
124
+
125
+ const result = await buildMemoryInjection(
126
+ client,
127
+ undefined,
128
+ "query",
129
+ { content: { maxContentChars: 200 } } as any,
130
+ );
131
+ expect(result.text.length).toBeLessThan(5000);
132
+ expect(result.text).toContain("[/CEREBRO-MEMORY]");
133
+ });
134
+
135
+ it("computes maxScore and confidence from search results", async () => {
136
+ const results: SearchResult[] = [
137
+ makeSearchResult({ score: 0.6 }),
138
+ makeSearchResult({ score: 0.9 }),
139
+ makeSearchResult({ score: 0.3 }),
140
+ ];
141
+ client = mockClient({
142
+ searchMemories: vi.fn().mockResolvedValue(results),
143
+ } as any);
144
+
145
+ const result = await buildMemoryInjection(client, undefined, "test", {});
146
+ expect(result.maxScore).toBe(0.9);
147
+ expect(result.confidence).toBe(0.9);
148
+ });
149
+
150
+ it("handles all client calls returning null/empty gracefully", async () => {
151
+ client = mockClient({
152
+ getInjection: vi.fn().mockRejectedValue(new Error("timeout")),
153
+ listRecent: vi.fn().mockRejectedValue(new Error("timeout")),
154
+ searchMemories: vi.fn().mockRejectedValue(new Error("timeout")),
155
+ } as any);
156
+
157
+ const result = await buildMemoryInjection(client, undefined, "test query", {});
158
+ expect(result.text).toContain("[CEREBRO-MEMORY]");
159
+ expect(result.profileCount).toBe(0);
160
+ expect(result.memoryCount).toBe(0);
161
+ expect(result.projectMemoryCount).toBe(0);
162
+ expect(result.maxScore).toBe(0);
163
+ });
164
+
165
+ it("skips search when query is empty", async () => {
166
+ const searchSpy = vi.fn().mockResolvedValue([]);
167
+ client = mockClient({ searchMemories: searchSpy } as any);
168
+
169
+ await buildMemoryInjection(client, undefined, "", {});
170
+ expect(searchSpy).not.toHaveBeenCalled();
171
+ });
172
+ });
173
+
174
+ // ── chatMessageRecallHook ────────────────────────────────────────────
175
+
176
+ describe("chatMessageRecallHook", () => {
177
+ let client: CerebroClient;
178
+
179
+ beforeEach(() => {
180
+ client = mockClient();
181
+ });
182
+
183
+ it("returns a function", () => {
184
+ const hook = chatMessageRecallHook(client, [], null, {});
185
+ expect(typeof hook).toBe("function");
186
+ });
187
+
188
+ it("returned hook skips when sessionID is empty", async () => {
189
+ const hook = chatMessageRecallHook(client, [], null, {});
190
+ const input = { sessionID: "" };
191
+ const output = {
192
+ message: { id: "msg-1", content: "hello" } as any,
193
+ parts: [{ type: "text", text: "hello" } as any],
194
+ };
195
+ await hook(input, output);
196
+ expect(client.searchMemories).not.toHaveBeenCalled();
197
+ });
198
+
199
+ it("returned hook skips trivial messages", async () => {
200
+ const hook = chatMessageRecallHook(client, [], null, {});
201
+ const input = { sessionID: "sess-1" };
202
+ const output = {
203
+ message: { id: "msg-1", content: "hi" } as any,
204
+ parts: [{ type: "text", text: "hi" } as any],
205
+ };
206
+ await hook(input, output);
207
+ expect(client.searchMemories).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("returned hook injects memory for substantive messages", async () => {
211
+ const injectionClient = mockClient({
212
+ getInjection: vi.fn().mockResolvedValue({
213
+ content: "User profile info",
214
+ preference_count: 2,
215
+ estimated_tokens: 30,
216
+ }),
217
+ listRecent: vi.fn().mockResolvedValue([
218
+ makeMemoryDto({ id: "rm-1", content: "project note" }),
219
+ ]),
220
+ searchMemories: vi.fn().mockResolvedValue([
221
+ makeSearchResult({
222
+ memory: makeMemoryDto({ id: "sr-1", content: "relevant result" }),
223
+ score: 0.85,
224
+ }),
225
+ ]),
226
+ } as any);
227
+
228
+ const hook = chatMessageRecallHook(injectionClient, [], null, {});
229
+ const input = { sessionID: "sess-2" };
230
+ const output = {
231
+ message: { id: "msg-2", content: "how do I authenticate users?" } as any,
232
+ parts: [{ type: "text", text: "how do I authenticate users?" } as any],
233
+ };
234
+ await hook(input, output);
235
+
236
+ // Should have unshifted an injection part
237
+ expect(output.parts.length).toBe(2); // original + injected
238
+ expect(output.parts[0].type).toBe("text");
239
+ expect((output.parts[0] as any).text).toContain("[CEREBRO-MEMORY]");
240
+ expect((output.parts[0] as any).synthetic).toBe(true);
241
+ });
242
+
243
+ it("returned hook skips already-injected sessions", async () => {
244
+ const hook = chatMessageRecallHook(client, [], null, {});
245
+ const input = { sessionID: "sess-dup" };
246
+ const output = {
247
+ message: { id: "msg-3", content: "how do I authenticate?" } as any,
248
+ parts: [{ type: "text", text: "how do I authenticate?" } as any],
249
+ };
250
+
251
+ // First call may or may not inject depending on client mock,
252
+ // but after first injection, second call should be skipped
253
+ // We need a client that actually injects to add to injectedSessions
254
+ const richClient = mockClient({
255
+ getInjection: vi.fn().mockResolvedValue({
256
+ content: "profile",
257
+ preference_count: 1,
258
+ estimated_tokens: 10,
259
+ }),
260
+ listRecent: vi.fn().mockResolvedValue([makeMemoryDto({ id: "x", content: "m" })]),
261
+ searchMemories: vi.fn().mockResolvedValue([]),
262
+ } as any);
263
+
264
+ const hook2 = chatMessageRecallHook(richClient, [], null, {});
265
+ await hook2(input, output);
266
+ // Second call on same session should short-circuit
267
+ const output2 = {
268
+ message: { id: "msg-4", content: "another question" } as any,
269
+ parts: [{ type: "text", text: "another question" } as any],
270
+ };
271
+ await hook2(input, output2);
272
+ // getInjection should have been called only once (first call)
273
+ expect((richClient.getInjection as any).mock.calls.length).toBe(1);
274
+ });
275
+ });