@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,190 @@
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
+ });
package/src/index.ts CHANGED
@@ -33,7 +33,17 @@ function getStateFilePath(sessionId: string): string {
33
33
 
34
34
  export function isAutoStoreEnabled(sessionId: string | undefined): boolean {
35
35
  if (!sessionId) return true;
36
- return autoStoreSessions.get(sessionId) ?? true;
36
+ const cached = autoStoreSessions.get(sessionId);
37
+ if (cached !== undefined) return cached;
38
+ // Fallback: read from persisted file (survives restart)
39
+ try {
40
+ const data = JSON.parse(readFileSync(getStateFilePath(sessionId), "utf-8"));
41
+ const enabled = data.enabled ?? true;
42
+ autoStoreSessions.set(sessionId, enabled); // cache for next time
43
+ return enabled;
44
+ } catch {
45
+ return true; // file doesn't exist → default ON
46
+ }
37
47
  }
38
48
 
39
49
  export function setAutoStoreEnabled(sessionId: string, enabled: boolean): void {
@@ -204,7 +214,7 @@ const OmemPlugin: Plugin = async (input) => {
204
214
  },
205
215
  "experimental.session.compacting": compactingHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
206
216
  "experimental.compaction.autocontinue": autocontinueHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
207
- tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId, getProjectPath: () => directory }),
217
+ tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId, getProjectPath: () => directory, config }),
208
218
  event: sessionIdleHook(cerebroClient, containerTags, tui, client, config.ingest.ingestMode, config.ingest.autoCaptureThreshold, () => mainSessionId, isAutoStoreEnabled, agentId, config, (name: string) => { cachedAgentName = name; }, directory),
209
219
  "shell.env": async (_input: any, output: any) => {
210
220
  if (directory) {
@@ -0,0 +1,283 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ detectSaveKeyword,
4
+ detectRecallKeyword,
5
+ KEYWORD_NUDGE,
6
+ RECALL_NUDGE,
7
+ } from "./keywords.js";
8
+
9
+ // ── detectSaveKeyword ──────────────────────────────────────────────
10
+
11
+ describe("detectSaveKeyword", () => {
12
+ // English keywords
13
+ it("detects 'remember'", () => {
14
+ expect(detectSaveKeyword("please remember this")).toBe(true);
15
+ });
16
+
17
+ it("detects 'save this'", () => {
18
+ expect(detectSaveKeyword("can you save this for me")).toBe(true);
19
+ });
20
+
21
+ it("detects \"don't forget\"", () => {
22
+ expect(detectSaveKeyword("don't forget to buy milk")).toBe(true);
23
+ });
24
+
25
+ // Chinese keywords
26
+ it("detects '记住'", () => {
27
+ expect(detectSaveKeyword("请记住这个")).toBe(true);
28
+ });
29
+
30
+ it("detects '记一下'", () => {
31
+ expect(detectSaveKeyword("帮我记一下")).toBe(true);
32
+ });
33
+
34
+ it("detects '保存'", () => {
35
+ expect(detectSaveKeyword("保存这个设置")).toBe(true);
36
+ });
37
+
38
+ it("detects '记下来'", () => {
39
+ expect(detectSaveKeyword("把这个记下来")).toBe(true);
40
+ });
41
+
42
+ it("detects '别忘了'", () => {
43
+ expect(detectSaveKeyword("别忘了明天的会议")).toBe(true);
44
+ });
45
+
46
+ // Tool reference
47
+ it("detects 'memory_store'", () => {
48
+ expect(detectSaveKeyword("use memory_store to save")).toBe(true);
49
+ });
50
+
51
+ // Case-insensitive
52
+ it("is case-insensitive", () => {
53
+ expect(detectSaveKeyword("REMEMBER this")).toBe(true);
54
+ expect(detectSaveKeyword("Remember This")).toBe(true);
55
+ expect(detectSaveKeyword("SAVE THIS please")).toBe(true);
56
+ expect(detectSaveKeyword("MEMORY_STORE")).toBe(true);
57
+ });
58
+
59
+ // Negative cases
60
+ it("returns false when no keyword present", () => {
61
+ expect(detectSaveKeyword("hello world")).toBe(false);
62
+ expect(detectSaveKeyword("what's the weather?")).toBe(false);
63
+ });
64
+
65
+ it("returns false for partial word that is not a keyword", () => {
66
+ // "member" is not "remember"
67
+ expect(detectSaveKeyword("team member")).toBe(false);
68
+ });
69
+
70
+ // Edge: keyword embedded in longer word (still matches because includes)
71
+ it("matches keyword as substring", () => {
72
+ // "remember" is in "disremember"
73
+ expect(detectSaveKeyword("I disremember that")).toBe(true);
74
+ });
75
+
76
+ it("handles empty string", () => {
77
+ expect(detectSaveKeyword("")).toBe(false);
78
+ });
79
+
80
+ it("handles string with only whitespace", () => {
81
+ expect(detectSaveKeyword(" ")).toBe(false);
82
+ });
83
+
84
+ it("detects mixed language text", () => {
85
+ expect(detectSaveKeyword("请 remember 这个设置")).toBe(true);
86
+ });
87
+ });
88
+
89
+ // ── detectRecallKeyword ────────────────────────────────────────────
90
+
91
+ describe("detectRecallKeyword", () => {
92
+ // English keywords
93
+ it("detects 'i remember'", () => {
94
+ expect(detectRecallKeyword("I remember we talked about this")).toBe(true);
95
+ });
96
+
97
+ it("detects 'i recall'", () => {
98
+ expect(detectRecallKeyword("I recall that discussion")).toBe(true);
99
+ });
100
+
101
+ it("detects 'we discussed'", () => {
102
+ expect(detectRecallKeyword("We discussed this before")).toBe(true);
103
+ });
104
+
105
+ it("detects 'we talked about'", () => {
106
+ expect(detectRecallKeyword("We talked about this last week")).toBe(true);
107
+ });
108
+
109
+ it("detects 'last time'", () => {
110
+ expect(detectRecallKeyword("Last time we met")).toBe(true);
111
+ });
112
+
113
+ it("detects 'look up'", () => {
114
+ expect(detectRecallKeyword("Can you look up that info")).toBe(true);
115
+ });
116
+
117
+ it("detects 'what did we'", () => {
118
+ expect(detectRecallKeyword("What did we decide last time")).toBe(true);
119
+ });
120
+
121
+ it("detects 'do you remember'", () => {
122
+ expect(detectRecallKeyword("Do you remember that project")).toBe(true);
123
+ });
124
+
125
+ it("detects 'as discussed'", () => {
126
+ expect(detectRecallKeyword("As discussed previously")).toBe(true);
127
+ });
128
+
129
+ // Chinese keywords
130
+ it("detects '我记得'", () => {
131
+ expect(detectRecallKeyword("我记得之前说过")).toBe(true);
132
+ });
133
+
134
+ it("detects '之前说过'", () => {
135
+ expect(detectRecallKeyword("你之前说过这个")).toBe(true);
136
+ });
137
+
138
+ it("detects '之前聊过'", () => {
139
+ expect(detectRecallKeyword("我们之前聊过")).toBe(true);
140
+ });
141
+
142
+ it("detects '查一下'", () => {
143
+ expect(detectRecallKeyword("帮我查一下")).toBe(true);
144
+ });
145
+
146
+ it("detects '搜一下'", () => {
147
+ expect(detectRecallKeyword("帮我搜一下")).toBe(true);
148
+ });
149
+
150
+ it("detects '记得吗'", () => {
151
+ expect(detectRecallKeyword("你还记得吗")).toBe(true);
152
+ });
153
+
154
+ it("detects '你还记得'", () => {
155
+ expect(detectRecallKeyword("你还记得那个bug吗")).toBe(true);
156
+ });
157
+
158
+ it("detects '上次那个'", () => {
159
+ expect(detectRecallKeyword("上次那个方案")).toBe(true);
160
+ });
161
+
162
+ // Tool references
163
+ it("detects 'memory_search'", () => {
164
+ expect(detectRecallKeyword("use memory_search")).toBe(true);
165
+ });
166
+
167
+ it("detects 'memory_get'", () => {
168
+ expect(detectRecallKeyword("use memory_get")).toBe(true);
169
+ });
170
+
171
+ // Case-insensitive
172
+ it("is case-insensitive", () => {
173
+ expect(detectRecallKeyword("I REMEMBER that")).toBe(true);
174
+ expect(detectRecallKeyword("WE DISCUSSED")).toBe(true);
175
+ expect(detectRecallKeyword("MEMORY_SEARCH")).toBe(true);
176
+ });
177
+
178
+ // Negative cases
179
+ it("returns false when no keyword present", () => {
180
+ expect(detectRecallKeyword("hello world")).toBe(false);
181
+ expect(detectRecallKeyword("the weather is nice")).toBe(false);
182
+ });
183
+
184
+ it("handles empty string", () => {
185
+ expect(detectRecallKeyword("")).toBe(false);
186
+ });
187
+
188
+ it("handles string with only whitespace", () => {
189
+ expect(detectRecallKeyword(" \n\t")).toBe(false);
190
+ });
191
+
192
+ it("detects 'previously'", () => {
193
+ expect(detectRecallKeyword("Previously mentioned")).toBe(true);
194
+ });
195
+
196
+ it("detects 'before we'", () => {
197
+ expect(detectRecallKeyword("before we proceed")).toBe(true);
198
+ });
199
+
200
+ it("detects 'search for'", () => {
201
+ expect(detectRecallKeyword("search for that topic")).toBe(true);
202
+ });
203
+
204
+ it("detects 'earlier we'", () => {
205
+ expect(detectRecallKeyword("earlier we decided")).toBe(true);
206
+ });
207
+
208
+ it("detects 'from our previous'", () => {
209
+ expect(detectRecallKeyword("from our previous meeting")).toBe(true);
210
+ });
211
+
212
+ it("detects '之前那个'", () => {
213
+ expect(detectRecallKeyword("之前那个问题")).toBe(true);
214
+ });
215
+
216
+ it("detects '上次讨论'", () => {
217
+ expect(detectRecallKeyword("上次讨论的方案")).toBe(true);
218
+ });
219
+
220
+ it("detects '上次做的'", () => {
221
+ expect(detectRecallKeyword("上次做的决定")).toBe(true);
222
+ });
223
+
224
+ it("detects '之前记录'", () => {
225
+ expect(detectRecallKeyword("之前记录的内容")).toBe(true);
226
+ });
227
+
228
+ it("detects '之前保存'", () => {
229
+ expect(detectRecallKeyword("之前保存的文件")).toBe(true);
230
+ });
231
+
232
+ it("detects '上次决定'", () => {
233
+ expect(detectRecallKeyword("上次决定的事情")).toBe(true);
234
+ });
235
+
236
+ it("detects '之前约定'", () => {
237
+ expect(detectRecallKeyword("之前约定的方案")).toBe(true);
238
+ });
239
+
240
+ it("detects '回忆一下'", () => {
241
+ expect(detectRecallKeyword("回忆一下当时的场景")).toBe(true);
242
+ });
243
+
244
+ it("detects 'find that'", () => {
245
+ expect(detectRecallKeyword("find that document")).toBe(true);
246
+ });
247
+
248
+ it("detects 'check what'", () => {
249
+ expect(detectRecallKeyword("check what we decided")).toBe(true);
250
+ });
251
+ });
252
+
253
+ // ── Nudge constants ────────────────────────────────────────────────
254
+
255
+ describe("KEYWORD_NUDGE", () => {
256
+ it("is a non-empty string", () => {
257
+ expect(KEYWORD_NUDGE).toBeTruthy();
258
+ expect(typeof KEYWORD_NUDGE).toBe("string");
259
+ });
260
+
261
+ it("contains 'memory_store'", () => {
262
+ expect(KEYWORD_NUDGE).toContain("memory_store");
263
+ });
264
+
265
+ it("contains 'cerebro' prefix", () => {
266
+ expect(KEYWORD_NUDGE).toContain("[cerebro]");
267
+ });
268
+ });
269
+
270
+ describe("RECALL_NUDGE", () => {
271
+ it("is a non-empty string", () => {
272
+ expect(RECALL_NUDGE).toBeTruthy();
273
+ expect(typeof RECALL_NUDGE).toBe("string");
274
+ });
275
+
276
+ it("contains 'memory_search'", () => {
277
+ expect(RECALL_NUDGE).toContain("memory_search");
278
+ });
279
+
280
+ it("contains 'cerebro' prefix", () => {
281
+ expect(RECALL_NUDGE).toContain("[cerebro]");
282
+ });
283
+ });