@mingxy/cerebro 2.3.4 → 2.3.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.
- package/package.json +2 -2
- package/src/client.ts +412 -403
- package/src/config.ts +277 -275
- package/src/hooks.ts +1040 -1019
- package/src/index.ts +229 -229
- package/src/tools.ts +455 -455
- package/web/assets/{index-CTdp-yyn.js → index-BITUpTtU.js} +67 -67
- package/web/icons.svg +24 -24
- package/web/index.html +14 -14
- package/src/client.test.ts +0 -373
- package/src/config.test.ts +0 -405
- package/src/hooks-tier1.test.ts +0 -220
- package/src/hooks-tier2.test.ts +0 -275
- package/src/hooks-tier3.test.ts +0 -461
- package/src/index.test.ts +0 -190
- package/src/keywords.test.ts +0 -283
- package/src/logger.test.ts +0 -640
- package/src/privacy.test.ts +0 -128
- package/src/tags.test.ts +0 -86
- package/src/tools.test.ts +0 -508
- package/src/updater.test.ts +0 -380
- package/src/web-server.test.ts +0 -740
package/src/hooks-tier3.test.ts
DELETED
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
// --- Mocks ---
|
|
4
|
-
vi.mock("./logger.js", () => ({
|
|
5
|
-
logInfo: vi.fn(),
|
|
6
|
-
logDebug: vi.fn(),
|
|
7
|
-
logError: vi.fn(),
|
|
8
|
-
setOpencodeClient: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock("./config.js", () => ({
|
|
12
|
-
DEFAULTS: {
|
|
13
|
-
ui: { toastDelayMs: 7000 },
|
|
14
|
-
logging: { logEnabled: false },
|
|
15
|
-
content: { maxContentChars: 6000 },
|
|
16
|
-
},
|
|
17
|
-
resolveAgentPolicy: vi.fn(() => "readwrite"),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
vi.mock("node:fs/promises", () => ({
|
|
21
|
-
readFile: vi.fn().mockRejectedValue("not found"),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
import { compactingHook, autocontinueHook, sessionIdleHook } from "./hooks.js";
|
|
25
|
-
import { resolveAgentPolicy } from "./config.js";
|
|
26
|
-
|
|
27
|
-
const mockCerebroClient = {
|
|
28
|
-
searchMemories: vi.fn().mockResolvedValue([]),
|
|
29
|
-
ingestMessages: vi.fn().mockResolvedValue({ id: "mem1" }),
|
|
30
|
-
sessionIngest: vi.fn().mockResolvedValue(undefined),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const mockTui = { showToast: vi.fn() };
|
|
34
|
-
const containerTags = ["test-project"];
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
vi.clearAllMocks();
|
|
38
|
-
mockCerebroClient.searchMemories.mockResolvedValue([]);
|
|
39
|
-
mockCerebroClient.ingestMessages.mockResolvedValue({ id: "mem1" });
|
|
40
|
-
mockCerebroClient.sessionIngest.mockResolvedValue(undefined);
|
|
41
|
-
(resolveAgentPolicy as ReturnType<typeof vi.fn>).mockReturnValue("readwrite");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ========================================
|
|
45
|
-
// compactingHook
|
|
46
|
-
// ========================================
|
|
47
|
-
describe("compactingHook", () => {
|
|
48
|
-
it("returns an async function", () => {
|
|
49
|
-
const hook = compactingHook(mockCerebroClient as any, containerTags, mockTui);
|
|
50
|
-
expect(typeof hook).toBe("function");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("searches memories and injects compaction prompt into output", async () => {
|
|
54
|
-
const hook = compactingHook(mockCerebroClient as any, containerTags, mockTui);
|
|
55
|
-
const input = { sessionID: "sess1" };
|
|
56
|
-
const output = { context: ["existing"], prompt: "old" };
|
|
57
|
-
await hook(input, output);
|
|
58
|
-
expect(mockCerebroClient.searchMemories).toHaveBeenCalledWith("*", 20, undefined, containerTags);
|
|
59
|
-
// output.prompt should be replaced with compaction prompt
|
|
60
|
-
expect(output.prompt).toContain("[Cerebro Compaction Context]");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("appends to context when output.prompt is undefined", async () => {
|
|
64
|
-
const hook = compactingHook(mockCerebroClient as any, containerTags, mockTui);
|
|
65
|
-
const input = { sessionID: "sess1" };
|
|
66
|
-
const output = { context: ["existing"] };
|
|
67
|
-
await hook(input, output);
|
|
68
|
-
expect(output.context.some((c: string) => c.includes("[Cerebro Compaction Context]"))).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("skips ingest when sessionMessages is empty for session", async () => {
|
|
72
|
-
const hook = compactingHook(mockCerebroClient as any, containerTags, mockTui);
|
|
73
|
-
await hook({ sessionID: "no-messages-session" }, { context: [] });
|
|
74
|
-
expect(mockCerebroClient.searchMemories).toHaveBeenCalled();
|
|
75
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("skips write when policy is readonly", async () => {
|
|
79
|
-
(resolveAgentPolicy as ReturnType<typeof vi.fn>).mockReturnValue("readonly");
|
|
80
|
-
const hook = compactingHook(
|
|
81
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
82
|
-
undefined, undefined, undefined, {},
|
|
83
|
-
);
|
|
84
|
-
await hook({ sessionID: "readonly-sess" }, { context: [] });
|
|
85
|
-
expect(mockCerebroClient.searchMemories).toHaveBeenCalled();
|
|
86
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("skips non-main session when getMainSessionId mismatches", async () => {
|
|
90
|
-
const getMainSessionId = vi.fn(() => "main-sess");
|
|
91
|
-
const hook = compactingHook(
|
|
92
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
93
|
-
undefined, getMainSessionId,
|
|
94
|
-
);
|
|
95
|
-
await hook({ sessionID: "sub-sess" }, { context: [] });
|
|
96
|
-
// Should return early after search phase
|
|
97
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("gracefully handles searchMemories failure", async () => {
|
|
101
|
-
mockCerebroClient.searchMemories.mockRejectedValue(new Error("network"));
|
|
102
|
-
const hook = compactingHook(mockCerebroClient as any, containerTags, mockTui);
|
|
103
|
-
const output = { context: [] };
|
|
104
|
-
await expect(hook({ sessionID: "s1" }, output)).resolves.toBeUndefined();
|
|
105
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// ========================================
|
|
110
|
-
// autocontinueHook
|
|
111
|
-
// ========================================
|
|
112
|
-
describe("autocontinueHook", () => {
|
|
113
|
-
const makeInput = (overrides = {}) => ({
|
|
114
|
-
sessionID: "sess1",
|
|
115
|
-
agent: "opencode",
|
|
116
|
-
model: { id: "test" } as any,
|
|
117
|
-
message: { id: "msg1" } as any,
|
|
118
|
-
overflow: false,
|
|
119
|
-
...overrides,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("returns an async function", () => {
|
|
123
|
-
const hook = autocontinueHook(mockCerebroClient as any, containerTags, mockTui);
|
|
124
|
-
expect(typeof hook).toBe("function");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("stores summary when sdkClient returns messages with matching id (level 1 fallback)", async () => {
|
|
128
|
-
const sdkClient = {
|
|
129
|
-
session: {
|
|
130
|
-
messages: vi.fn().mockResolvedValue({
|
|
131
|
-
data: [{
|
|
132
|
-
info: { id: "msg1", role: "assistant" },
|
|
133
|
-
parts: [{ type: "text", text: "This is a summary of the session." }],
|
|
134
|
-
}],
|
|
135
|
-
}),
|
|
136
|
-
get: vi.fn().mockResolvedValue({ data: { directory: "/tmp/project" } }),
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
const hook = autocontinueHook(
|
|
140
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
141
|
-
undefined, undefined, sdkClient,
|
|
142
|
-
);
|
|
143
|
-
await hook(makeInput(), { enabled: true });
|
|
144
|
-
expect(mockCerebroClient.ingestMessages).toHaveBeenCalledTimes(1);
|
|
145
|
-
expect(mockCerebroClient.ingestMessages.mock.calls[0][0][0].content).toBe("This is a summary of the session.");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("falls back to summary-flagged message (level 2)", async () => {
|
|
149
|
-
const sdkClient = {
|
|
150
|
-
session: {
|
|
151
|
-
messages: vi.fn().mockResolvedValue({
|
|
152
|
-
data: [
|
|
153
|
-
{ info: { id: "msg-other", role: "user" } },
|
|
154
|
-
{
|
|
155
|
-
info: { id: "msg2", role: "assistant", summary: true },
|
|
156
|
-
parts: [{ type: "text", text: "Summary from flag that is long enough to pass." }],
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
}),
|
|
160
|
-
get: vi.fn().mockResolvedValue({ data: { directory: "/tmp" } }),
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
const hook = autocontinueHook(
|
|
164
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
165
|
-
undefined, undefined, sdkClient,
|
|
166
|
-
);
|
|
167
|
-
await hook(makeInput({ message: { id: "not-found" } as any }), { enabled: true });
|
|
168
|
-
expect(mockCerebroClient.ingestMessages).toHaveBeenCalledTimes(1);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("falls back to last assistant message (level 3)", async () => {
|
|
172
|
-
const sdkClient = {
|
|
173
|
-
session: {
|
|
174
|
-
messages: vi.fn().mockResolvedValue({
|
|
175
|
-
data: [
|
|
176
|
-
{
|
|
177
|
-
info: { id: "a1", role: "assistant" },
|
|
178
|
-
parts: [{ type: "text", text: "Last assistant response here with enough content." }],
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
}),
|
|
182
|
-
get: vi.fn().mockResolvedValue({ data: {} }),
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
const hook = autocontinueHook(
|
|
186
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
187
|
-
undefined, undefined, sdkClient,
|
|
188
|
-
);
|
|
189
|
-
await hook(makeInput({ message: { id: "none" } as any }), { enabled: true });
|
|
190
|
-
expect(mockCerebroClient.ingestMessages).toHaveBeenCalledTimes(1);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("skips when no sdkClient", async () => {
|
|
194
|
-
const hook = autocontinueHook(mockCerebroClient as any, containerTags, mockTui);
|
|
195
|
-
await hook(makeInput(), { enabled: true });
|
|
196
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("skips when policy is readonly", async () => {
|
|
200
|
-
(resolveAgentPolicy as ReturnType<typeof vi.fn>).mockReturnValue("readonly");
|
|
201
|
-
const sdkClient = {
|
|
202
|
-
session: { messages: vi.fn(), get: vi.fn() },
|
|
203
|
-
};
|
|
204
|
-
const hook = autocontinueHook(
|
|
205
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
206
|
-
undefined, undefined, sdkClient,
|
|
207
|
-
);
|
|
208
|
-
await hook(makeInput(), { enabled: true });
|
|
209
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("skips when autoStore is disabled", async () => {
|
|
213
|
-
const isAutoStoreEnabled = vi.fn(() => false);
|
|
214
|
-
const hook = autocontinueHook(
|
|
215
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
216
|
-
isAutoStoreEnabled,
|
|
217
|
-
);
|
|
218
|
-
await hook(makeInput(), { enabled: true });
|
|
219
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("skips when summaryText is empty", async () => {
|
|
223
|
-
const sdkClient = {
|
|
224
|
-
session: {
|
|
225
|
-
messages: vi.fn().mockResolvedValue({ data: [] }),
|
|
226
|
-
get: vi.fn(),
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
const hook = autocontinueHook(
|
|
230
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
231
|
-
undefined, undefined, sdkClient,
|
|
232
|
-
);
|
|
233
|
-
await hook(makeInput(), { enabled: true });
|
|
234
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("shows error toast when ingestMessages throws", async () => {
|
|
238
|
-
mockCerebroClient.ingestMessages.mockRejectedValue(new Error("fail"));
|
|
239
|
-
const sdkClient = {
|
|
240
|
-
session: {
|
|
241
|
-
messages: vi.fn().mockResolvedValue({
|
|
242
|
-
data: [{
|
|
243
|
-
info: { id: "msg1", role: "assistant" },
|
|
244
|
-
parts: [{ type: "text", text: "Summary text here that is definitely long enough to pass." }],
|
|
245
|
-
}],
|
|
246
|
-
}),
|
|
247
|
-
get: vi.fn().mockResolvedValue({ data: {} }),
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
const hook = autocontinueHook(
|
|
251
|
-
mockCerebroClient as any, containerTags, mockTui, "smart",
|
|
252
|
-
undefined, undefined, sdkClient,
|
|
253
|
-
);
|
|
254
|
-
await hook(makeInput(), { enabled: true });
|
|
255
|
-
// Should not throw
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// ========================================
|
|
260
|
-
// sessionIdleHook
|
|
261
|
-
// ========================================
|
|
262
|
-
describe("sessionIdleHook", () => {
|
|
263
|
-
const makeEvent = (type: string, properties: any = {}) => ({
|
|
264
|
-
event: { type, properties },
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("returns an async function", () => {
|
|
268
|
-
const hook = sessionIdleHook(mockCerebroClient as any, containerTags, mockTui, {});
|
|
269
|
-
expect(typeof hook).toBe("function");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("handles message.updated event", async () => {
|
|
273
|
-
const sdkClient = {
|
|
274
|
-
session: {
|
|
275
|
-
messages: vi.fn().mockResolvedValue({
|
|
276
|
-
data: [{
|
|
277
|
-
info: { role: "assistant", summary: true },
|
|
278
|
-
parts: [{ type: "text", text: "This is a decent summary of the session content." }],
|
|
279
|
-
}],
|
|
280
|
-
}),
|
|
281
|
-
get: vi.fn().mockResolvedValue({ data: { directory: "/tmp/project" } }),
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
const hook = sessionIdleHook(
|
|
285
|
-
mockCerebroClient as any, containerTags, mockTui, sdkClient,
|
|
286
|
-
);
|
|
287
|
-
await hook(makeEvent("message.updated", {
|
|
288
|
-
info: { role: "assistant", sessionID: "s1", finish: true },
|
|
289
|
-
}));
|
|
290
|
-
// Should trigger handleSummaryCapture → ingestMessages
|
|
291
|
-
expect(mockCerebroClient.ingestMessages).toHaveBeenCalledTimes(1);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("skips message.updated for non-assistant role", async () => {
|
|
295
|
-
const hook = sessionIdleHook(mockCerebroClient as any, containerTags, mockTui, {});
|
|
296
|
-
await hook(makeEvent("message.updated", {
|
|
297
|
-
info: { role: "user", sessionID: "s1" },
|
|
298
|
-
}));
|
|
299
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("skips message.updated when finish is false", async () => {
|
|
303
|
-
const hook = sessionIdleHook(mockCerebroClient as any, containerTags, mockTui, {});
|
|
304
|
-
await hook(makeEvent("message.updated", {
|
|
305
|
-
info: { role: "assistant", sessionID: "s1", finish: false },
|
|
306
|
-
}));
|
|
307
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("handles session.deleted event (cleanup)", async () => {
|
|
311
|
-
const hook = sessionIdleHook(mockCerebroClient as any, containerTags, mockTui, {});
|
|
312
|
-
// Should not throw
|
|
313
|
-
await hook(makeEvent("session.deleted", {
|
|
314
|
-
info: { id: "s1" },
|
|
315
|
-
}));
|
|
316
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("ignores unknown event types", async () => {
|
|
320
|
-
const hook = sessionIdleHook(mockCerebroClient as any, containerTags, mockTui, {});
|
|
321
|
-
await hook(makeEvent("unknown.event", {}));
|
|
322
|
-
expect(mockCerebroClient.ingestMessages).not.toHaveBeenCalled();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("handles session.idle event with setTimeout", async () => {
|
|
326
|
-
vi.useFakeTimers();
|
|
327
|
-
const sdkClient = {
|
|
328
|
-
session: {
|
|
329
|
-
messages: vi.fn().mockResolvedValue({
|
|
330
|
-
data: [{
|
|
331
|
-
info: { id: "m1", role: "user", createdAt: new Date().toISOString() },
|
|
332
|
-
parts: [{ type: "text", text: "hello world" }],
|
|
333
|
-
}],
|
|
334
|
-
}),
|
|
335
|
-
get: vi.fn().mockResolvedValue({ data: { directory: "/tmp", agent: "opencode" } }),
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
const onAgentResolved = vi.fn();
|
|
339
|
-
const hook = sessionIdleHook(
|
|
340
|
-
mockCerebroClient as any, containerTags, mockTui, sdkClient,
|
|
341
|
-
"smart", 0, undefined, undefined, "opencode", {}, onAgentResolved,
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
await hook(makeEvent("session.idle", { sessionID: "s1" }));
|
|
345
|
-
|
|
346
|
-
// Not yet called (10s timeout)
|
|
347
|
-
expect(mockCerebroClient.sessionIngest).not.toHaveBeenCalled();
|
|
348
|
-
|
|
349
|
-
// Advance past 10s
|
|
350
|
-
await vi.advanceTimersByTimeAsync(11000);
|
|
351
|
-
|
|
352
|
-
expect(mockCerebroClient.sessionIngest).toHaveBeenCalledTimes(1);
|
|
353
|
-
expect(onAgentResolved).toHaveBeenCalledWith("opencode");
|
|
354
|
-
|
|
355
|
-
vi.useRealTimers();
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it("skips session.idle when autoStore is disabled", async () => {
|
|
359
|
-
vi.useFakeTimers();
|
|
360
|
-
const isAutoStoreEnabled = vi.fn(() => false);
|
|
361
|
-
const hook = sessionIdleHook(
|
|
362
|
-
mockCerebroClient as any, containerTags, mockTui, {},
|
|
363
|
-
"smart", 0, undefined, isAutoStoreEnabled,
|
|
364
|
-
);
|
|
365
|
-
await hook(makeEvent("session.idle", { sessionID: "s1" }));
|
|
366
|
-
await vi.advanceTimersByTimeAsync(11000);
|
|
367
|
-
expect(mockCerebroClient.sessionIngest).not.toHaveBeenCalled();
|
|
368
|
-
vi.useRealTimers();
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("skips session.idle for non-main session", async () => {
|
|
372
|
-
vi.useFakeTimers();
|
|
373
|
-
const getMainSessionId = vi.fn(() => "main-sess");
|
|
374
|
-
const hook = sessionIdleHook(
|
|
375
|
-
mockCerebroClient as any, containerTags, mockTui, {},
|
|
376
|
-
"smart", 0, getMainSessionId,
|
|
377
|
-
);
|
|
378
|
-
await hook(makeEvent("session.idle", { sessionID: "sub-sess" }));
|
|
379
|
-
await vi.advanceTimersByTimeAsync(11000);
|
|
380
|
-
expect(mockCerebroClient.sessionIngest).not.toHaveBeenCalled();
|
|
381
|
-
vi.useRealTimers();
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("skips session.idle when policy is readonly", async () => {
|
|
385
|
-
vi.useFakeTimers();
|
|
386
|
-
(resolveAgentPolicy as ReturnType<typeof vi.fn>).mockReturnValue("readonly");
|
|
387
|
-
const sdkClient = {
|
|
388
|
-
session: {
|
|
389
|
-
messages: vi.fn().mockResolvedValue({
|
|
390
|
-
data: [{
|
|
391
|
-
info: { id: "policy-m1", role: "user", createdAt: new Date().toISOString() },
|
|
392
|
-
parts: [{ type: "text", text: "hello" }],
|
|
393
|
-
}],
|
|
394
|
-
}),
|
|
395
|
-
get: vi.fn().mockResolvedValue({ data: { directory: "/tmp" } }),
|
|
396
|
-
},
|
|
397
|
-
};
|
|
398
|
-
const hook = sessionIdleHook(
|
|
399
|
-
mockCerebroClient as any, containerTags, mockTui, sdkClient,
|
|
400
|
-
"smart", 0, undefined, undefined, "opencode", {},
|
|
401
|
-
);
|
|
402
|
-
await hook(makeEvent("session.idle", { sessionID: "policy-sess" }));
|
|
403
|
-
await vi.advanceTimersByTimeAsync(11000);
|
|
404
|
-
expect(mockCerebroClient.sessionIngest).not.toHaveBeenCalled();
|
|
405
|
-
vi.useRealTimers();
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it("skips session.idle when no sessionID", async () => {
|
|
409
|
-
const hook = sessionIdleHook(mockCerebroClient as any, containerTags, mockTui, {});
|
|
410
|
-
await hook(makeEvent("session.idle", {}));
|
|
411
|
-
expect(mockCerebroClient.sessionIngest).not.toHaveBeenCalled();
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("skips messages below threshold", async () => {
|
|
415
|
-
vi.useFakeTimers();
|
|
416
|
-
const sdkClient = {
|
|
417
|
-
session: {
|
|
418
|
-
messages: vi.fn().mockResolvedValue({
|
|
419
|
-
data: [{
|
|
420
|
-
info: { id: "thresh-m1", role: "user", createdAt: new Date().toISOString() },
|
|
421
|
-
parts: [{ type: "text", text: "just one msg" }],
|
|
422
|
-
}],
|
|
423
|
-
}),
|
|
424
|
-
get: vi.fn().mockResolvedValue({ data: { directory: "/tmp", agent: "opencode" } }),
|
|
425
|
-
},
|
|
426
|
-
};
|
|
427
|
-
const hook = sessionIdleHook(
|
|
428
|
-
mockCerebroClient as any, containerTags, mockTui, sdkClient,
|
|
429
|
-
"smart", 5,
|
|
430
|
-
undefined, undefined, "opencode", {},
|
|
431
|
-
);
|
|
432
|
-
await hook(makeEvent("session.idle", { sessionID: "thresh-sess" }));
|
|
433
|
-
await vi.advanceTimersByTimeAsync(11000);
|
|
434
|
-
expect(mockCerebroClient.sessionIngest).not.toHaveBeenCalled();
|
|
435
|
-
vi.useRealTimers();
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it("shows error toast on sessionIngest failure", async () => {
|
|
439
|
-
vi.useFakeTimers();
|
|
440
|
-
mockCerebroClient.sessionIngest.mockRejectedValue(new Error("server error"));
|
|
441
|
-
const sdkClient = {
|
|
442
|
-
session: {
|
|
443
|
-
messages: vi.fn().mockResolvedValue({
|
|
444
|
-
data: [{
|
|
445
|
-
info: { id: "err-m1", role: "user", createdAt: new Date().toISOString() },
|
|
446
|
-
parts: [{ type: "text", text: "hello world test" }],
|
|
447
|
-
}],
|
|
448
|
-
}),
|
|
449
|
-
get: vi.fn().mockResolvedValue({ data: { directory: "/tmp", agent: "opencode" } }),
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
const hook = sessionIdleHook(
|
|
453
|
-
mockCerebroClient as any, containerTags, mockTui, sdkClient,
|
|
454
|
-
"smart", 0, undefined, undefined, "opencode", {},
|
|
455
|
-
);
|
|
456
|
-
await hook(makeEvent("session.idle", { sessionID: "err-sess" }));
|
|
457
|
-
await vi.advanceTimersByTimeAsync(11000);
|
|
458
|
-
expect(mockCerebroClient.sessionIngest).toHaveBeenCalled();
|
|
459
|
-
vi.useRealTimers();
|
|
460
|
-
});
|
|
461
|
-
});
|
package/src/index.test.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
// ─── Hoisted mocks ─────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
const { mockReadFileSync, mockWriteFileSync } = vi.hoisted(() => ({
|
|
6
|
-
mockReadFileSync: vi.fn(),
|
|
7
|
-
mockWriteFileSync: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
// ─── Module mocks ──────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
vi.mock("node:fs", () => ({
|
|
13
|
-
readFileSync: mockReadFileSync,
|
|
14
|
-
writeFileSync: mockWriteFileSync,
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock("node:os", () => ({ tmpdir: () => "/test-tmp" }));
|
|
18
|
-
|
|
19
|
-
vi.mock("node:path", () => ({
|
|
20
|
-
join: (...args: string[]) => args.join("/"),
|
|
21
|
-
dirname: (p: string) => p.split("/").slice(0, -1).join("/") || ".",
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
vi.mock("node:url", () => ({
|
|
25
|
-
fileURLToPath: (url: string) => url.replace("file://", "/"),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
vi.mock("@opencode-ai/plugin", () => ({}));
|
|
29
|
-
vi.mock("./client.js", () => ({ CerebroClient: vi.fn() }));
|
|
30
|
-
vi.mock("./hooks.js", () => ({
|
|
31
|
-
chatMessageRecallHook: vi.fn(),
|
|
32
|
-
autocontinueHook: vi.fn(),
|
|
33
|
-
compactingHook: vi.fn(),
|
|
34
|
-
sessionIdleHook: vi.fn(),
|
|
35
|
-
sessionMessages: new Map(),
|
|
36
|
-
firstMessages: new Map(),
|
|
37
|
-
showToast: vi.fn(),
|
|
38
|
-
}));
|
|
39
|
-
vi.mock("./keywords.js", () => ({
|
|
40
|
-
detectSaveKeyword: vi.fn(),
|
|
41
|
-
detectRecallKeyword: vi.fn(),
|
|
42
|
-
KEYWORD_NUDGE: "",
|
|
43
|
-
RECALL_NUDGE: "",
|
|
44
|
-
}));
|
|
45
|
-
vi.mock("./tags.js", () => ({
|
|
46
|
-
getUserTag: vi.fn(() => "user:t"),
|
|
47
|
-
getProjectTag: vi.fn(() => "project:t"),
|
|
48
|
-
}));
|
|
49
|
-
vi.mock("./tools.js", () => ({ buildTools: vi.fn(() => ({})) }));
|
|
50
|
-
vi.mock("./logger.js", () => ({
|
|
51
|
-
logInfo: vi.fn(),
|
|
52
|
-
logDebug: vi.fn(),
|
|
53
|
-
logError: vi.fn(),
|
|
54
|
-
setOpencodeClient: vi.fn(),
|
|
55
|
-
}));
|
|
56
|
-
vi.mock("./config.js", () => ({
|
|
57
|
-
loadPluginConfig: vi.fn(() => ({})),
|
|
58
|
-
resolveAgentPolicy: vi.fn(),
|
|
59
|
-
}));
|
|
60
|
-
vi.mock("./updater.js", () => ({ checkAndUpdate: vi.fn() }));
|
|
61
|
-
vi.mock("./web-server.js", () => ({
|
|
62
|
-
startWebServer: vi.fn(),
|
|
63
|
-
stopWebServer: vi.fn(),
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
// ─── Imports ───────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
import { isAutoStoreEnabled, setAutoStoreEnabled } from "./index.js";
|
|
69
|
-
|
|
70
|
-
// ─── Helpers ───────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
function getMap(): Map<string, boolean> {
|
|
73
|
-
return (globalThis as any).__cerebro_autoStoreMap;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Make readFileSync return JSON for a specific sessionId file, throw ENOENT otherwise */
|
|
77
|
-
function stubFileForSession(sessionId: string, data: Record<string, unknown>) {
|
|
78
|
-
mockReadFileSync.mockImplementation((path: string) => {
|
|
79
|
-
if (path.includes(`cerebro_autostore_${sessionId}.json`)) {
|
|
80
|
-
return JSON.stringify(data);
|
|
81
|
-
}
|
|
82
|
-
throw new Error("ENOENT");
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ─── Tests ─────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
describe("isAutoStoreEnabled", () => {
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
getMap().clear();
|
|
91
|
-
mockReadFileSync.mockImplementation(() => { throw new Error("ENOENT"); });
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("returns true when sessionId is undefined", () => {
|
|
95
|
-
expect(isAutoStoreEnabled(undefined)).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("returns cached value from Map (true)", () => {
|
|
99
|
-
getMap().set("sess-a", true);
|
|
100
|
-
expect(isAutoStoreEnabled("sess-a")).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("returns cached value from Map (false)", () => {
|
|
104
|
-
getMap().set("sess-b", false);
|
|
105
|
-
expect(isAutoStoreEnabled("sess-b")).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("reads from file when Map has no entry (Bug1 fix: restart recovery)", () => {
|
|
109
|
-
stubFileForSession("sess-file", { enabled: false });
|
|
110
|
-
expect(isAutoStoreEnabled("sess-file")).toBe(false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("caches file value back to Map after reading", () => {
|
|
114
|
-
stubFileForSession("sess-cache", { enabled: false });
|
|
115
|
-
isAutoStoreEnabled("sess-cache");
|
|
116
|
-
// Map should now have the value
|
|
117
|
-
expect(getMap().get("sess-cache")).toBe(false);
|
|
118
|
-
// Second call uses Map, not file
|
|
119
|
-
mockReadFileSync.mockImplementation(() => { throw new Error("should not read file again"); });
|
|
120
|
-
expect(isAutoStoreEnabled("sess-cache")).toBe(false);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("returns true (default) when no Map entry and file does not exist", () => {
|
|
124
|
-
expect(isAutoStoreEnabled("sess-noexist")).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("handles malformed JSON in file gracefully → returns true", () => {
|
|
128
|
-
mockReadFileSync.mockImplementation((path: string) => {
|
|
129
|
-
if (path.includes("cerebro_autostore_sess-bad.json")) return "{invalid";
|
|
130
|
-
throw new Error("ENOENT");
|
|
131
|
-
});
|
|
132
|
-
expect(isAutoStoreEnabled("sess-bad")).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("defaults to true when file has no 'enabled' field", () => {
|
|
136
|
-
stubFileForSession("sess-nofield", { something: "else" });
|
|
137
|
-
expect(isAutoStoreEnabled("sess-nofield")).toBe(true);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("reads enabled:true from file correctly", () => {
|
|
141
|
-
stubFileForSession("sess-on", { enabled: true });
|
|
142
|
-
expect(isAutoStoreEnabled("sess-on")).toBe(true);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("setAutoStoreEnabled", () => {
|
|
147
|
-
beforeEach(() => {
|
|
148
|
-
getMap().clear();
|
|
149
|
-
mockWriteFileSync.mockImplementation(() => {});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("sets Map entry to true", () => {
|
|
153
|
-
setAutoStoreEnabled("sess-1", true);
|
|
154
|
-
expect(getMap().get("sess-1")).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("sets Map entry to false", () => {
|
|
158
|
-
setAutoStoreEnabled("sess-2", false);
|
|
159
|
-
expect(getMap().get("sess-2")).toBe(false);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("writes correct JSON { enabled: false } to file", () => {
|
|
163
|
-
setAutoStoreEnabled("sess-1", false);
|
|
164
|
-
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
165
|
-
expect.stringContaining("cerebro_autostore_sess-1.json"),
|
|
166
|
-
JSON.stringify({ enabled: false }),
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("writes correct JSON { enabled: true } to file", () => {
|
|
171
|
-
setAutoStoreEnabled("sess-2", true);
|
|
172
|
-
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
173
|
-
expect.stringContaining("cerebro_autostore_sess-2.json"),
|
|
174
|
-
JSON.stringify({ enabled: true }),
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("handles write error gracefully — Map still updated", () => {
|
|
179
|
-
mockWriteFileSync.mockImplementation(() => { throw new Error("disk full"); });
|
|
180
|
-
expect(() => setAutoStoreEnabled("sess-err", true)).not.toThrow();
|
|
181
|
-
expect(getMap().get("sess-err")).toBe(true);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("round-trip: set then get returns consistent value", () => {
|
|
185
|
-
setAutoStoreEnabled("sess-rt", false);
|
|
186
|
-
expect(isAutoStoreEnabled("sess-rt")).toBe(false);
|
|
187
|
-
setAutoStoreEnabled("sess-rt", true);
|
|
188
|
-
expect(isAutoStoreEnabled("sess-rt")).toBe(true);
|
|
189
|
-
});
|
|
190
|
-
});
|