@mingxy/cerebro 2.3.3 → 2.3.5

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/src/tools.test.ts DELETED
@@ -1,508 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- // ─── Hoisted mock functions ────────────────────────────────────
4
-
5
- const { mockResolve, mockIsEnabled, mockSetEnabled } = vi.hoisted(() => ({
6
- mockResolve: vi.fn<() => string>().mockReturnValue("readwrite"),
7
- mockIsEnabled: vi.fn<() => boolean>().mockReturnValue(true),
8
- mockSetEnabled: vi.fn(),
9
- }));
10
-
11
- // ─── Module mocks ──────────────────────────────────────────────
12
-
13
- vi.mock("@opencode-ai/plugin", () => {
14
- function sb() {
15
- const b = { describe() { return b; }, optional() { return b; } };
16
- return b;
17
- }
18
- return {
19
- tool: Object.assign((def: any) => def, {
20
- schema: { string: sb, number: sb, array: sb, enum: sb, object: sb },
21
- }),
22
- };
23
- });
24
-
25
- vi.mock("./config.js", () => ({ resolveAgentPolicy: mockResolve }));
26
- vi.mock("./index.js", () => ({
27
- isAutoStoreEnabled: mockIsEnabled,
28
- setAutoStoreEnabled: mockSetEnabled,
29
- }));
30
-
31
- // ─── Imports ───────────────────────────────────────────────────
32
-
33
- import { buildTools, type ToolContext } from "./tools.js";
34
-
35
- // ─── Constants ─────────────────────────────────────────────────
36
-
37
- const ALL_TOOLS = [
38
- "memory_store", "memory_search", "memory_get", "memory_update",
39
- "memory_profile", "memory_profile_stats", "memory_list", "memory_ingest",
40
- "memory_stats", "memory_delete", "space_create", "space_list",
41
- "space_add_member", "memory_share", "memory_pull", "memory_reshare",
42
- "memory_toggle",
43
- ] as const;
44
-
45
- type TN = typeof ALL_TOOLS[number];
46
-
47
- const READ_TOOLS: readonly TN[] = [
48
- "memory_search", "memory_get", "memory_profile", "memory_profile_stats",
49
- "memory_list", "memory_stats", "space_list",
50
- ];
51
-
52
- const WRITE_TOOLS: readonly TN[] = [
53
- "memory_store", "memory_update", "memory_ingest", "memory_delete",
54
- "space_create", "space_add_member", "memory_share", "memory_pull",
55
- "memory_reshare", "memory_toggle",
56
- ];
57
-
58
- const A: Record<TN, any> = {
59
- memory_store: { content: "c", source: "s", tags: ["t1"] },
60
- memory_search: { query: "q" },
61
- memory_get: { id: "m1" },
62
- memory_update: { id: "m1", content: "up" },
63
- memory_profile: {},
64
- memory_profile_stats: {},
65
- memory_list: {},
66
- memory_ingest: { messages: [{ role: "user", content: "hi" }] },
67
- memory_stats: {},
68
- memory_delete: { id: "m1" },
69
- space_create: { name: "sp", space_type: "team" },
70
- space_list: {},
71
- space_add_member: { space_id: "s1", user_id: "u1", role: "admin" },
72
- memory_share: { memory_id: "m1", target_space: "s1" },
73
- memory_pull: { memory_id: "m1", source_space: "s1" },
74
- memory_reshare: { memory_id: "m1" },
75
- memory_toggle: { state: "on" },
76
- };
77
-
78
- // ─── Helpers ───────────────────────────────────────────────────
79
-
80
- function mkClient() {
81
- return {
82
- createMemory: vi.fn().mockResolvedValue({ id: "m-new", tags: ["t1"] }),
83
- searchMemories: vi.fn().mockResolvedValue([{ memory: { id: "m1", content: "hello world" }, score: 0.9 }]),
84
- getMemory: vi.fn().mockResolvedValue({ id: "m1", content: "full content", tags: ["t"] }),
85
- updateMemory: vi.fn().mockResolvedValue({ id: "m1" }),
86
- getProfile: vi.fn().mockResolvedValue([{ slot: "lang", value: "ts" }]),
87
- getProfileStats: vi.fn().mockResolvedValue({ total: 5 }),
88
- listRecent: vi.fn().mockResolvedValue([{ id: "m1", content: "recent", category: "cases", tags: ["t"] }]),
89
- ingestMessages: vi.fn().mockResolvedValue({ created: 1 }),
90
- getStats: vi.fn().mockResolvedValue({ total: 100 }),
91
- deleteMemory: vi.fn().mockResolvedValue(undefined),
92
- createSpace: vi.fn().mockResolvedValue({ id: "s-new", name: "sp" }),
93
- listSpaces: vi.fn().mockResolvedValue([{ id: "s1" }]),
94
- addSpaceMember: vi.fn().mockResolvedValue({ ok: true }),
95
- shareMemory: vi.fn().mockResolvedValue({ ok: true }),
96
- pullMemory: vi.fn().mockResolvedValue({ ok: true }),
97
- reshareMemory: vi.fn().mockResolvedValue({ ok: true }),
98
- };
99
- }
100
-
101
- function mkCtx(p?: Partial<ToolContext>): ToolContext {
102
- return {
103
- agentId: "test-agent",
104
- getSessionId: () => "sess-1",
105
- getAgentName: () => "test-agent",
106
- getProjectPath: () => "/test",
107
- config: {},
108
- ...p,
109
- };
110
- }
111
-
112
- /** Parse the JSON string returned by execute() */
113
- async function exec(tools: ReturnType<typeof buildTools>, name: TN, args?: any) {
114
- return JSON.parse(await tools[name].execute(args ?? A[name]));
115
- }
116
-
117
- // ─── Tests ─────────────────────────────────────────────────────
118
-
119
- describe("buildTools", () => {
120
- let client: ReturnType<typeof mkClient>;
121
- let tools: ReturnType<typeof buildTools>;
122
-
123
- beforeEach(() => {
124
- vi.clearAllMocks();
125
- mockResolve.mockReturnValue("readwrite");
126
- client = mkClient();
127
- tools = buildTools(client, ["user:x", "project:y"], mkCtx());
128
- });
129
-
130
- // ────────────────── Definitions ──────────────────
131
-
132
- describe("tool definitions", () => {
133
- it("returns all defined tools with description + execute", () => {
134
- expect(Object.keys(tools)).toHaveLength(ALL_TOOLS.length);
135
- for (const name of ALL_TOOLS) {
136
- expect(typeof tools[name].description).toBe("string");
137
- expect(typeof tools[name].execute).toBe("function");
138
- }
139
- });
140
- });
141
-
142
- // ────────────────── Backward compat ──────────────────
143
-
144
- describe("backward compat: no agentId / no config → allow all", () => {
145
- it("allows WRITE when agentId is undefined", async () => {
146
- tools = buildTools(client, [], mkCtx({ agentId: undefined, getAgentName: () => undefined }));
147
- expect((await exec(tools, "memory_store")).ok).toBe(true);
148
- });
149
-
150
- it("allows READ when config is undefined", async () => {
151
- tools = buildTools(client, [], mkCtx({ config: undefined }));
152
- expect((await exec(tools, "memory_search")).ok).toBe(true);
153
- });
154
- });
155
-
156
- // ────────────────── Permission: none ──────────────────
157
-
158
- describe("policy 'none' → deny all 16 tools", () => {
159
- beforeEach(() => { mockResolve.mockReturnValue("none"); });
160
-
161
- it.each([...READ_TOOLS, ...WRITE_TOOLS] as unknown as string[])(
162
- "denies %s", async (name: string) => {
163
- const r = await exec(tools, name as TN);
164
- expect(r).toMatchObject({ ok: false });
165
- expect(r.error).toContain("Permission denied");
166
- expect(r.error).toContain("'none'");
167
- },
168
- );
169
- });
170
-
171
- // ────────────────── Permission: readonly ──────────────────
172
-
173
- describe("policy 'readonly'", () => {
174
- beforeEach(() => { mockResolve.mockReturnValue("readonly"); });
175
-
176
- it.each(READ_TOOLS as unknown as string[])("allows READ %s", async (name) => {
177
- expect((await exec(tools, name as TN)).ok).toBe(true);
178
- });
179
-
180
- it.each(WRITE_TOOLS as unknown as string[])("denies WRITE %s", async (name) => {
181
- const r = await exec(tools, name as TN);
182
- expect(r).toMatchObject({ ok: false });
183
- expect(r.error).toContain("Permission denied");
184
- expect(r.error).toContain("'readonly'");
185
- expect(r.error).toContain("'write'");
186
- });
187
- });
188
-
189
- // ────────────────── Permission: readwrite ──────────────────
190
-
191
- describe("policy 'readwrite' → allow all", () => {
192
- it.each(ALL_TOOLS as unknown as string[])("allows %s", async (name) => {
193
- expect((await exec(tools, name as TN)).ok).toBe(true);
194
- });
195
- });
196
-
197
- // ────────────────── memory_store ──────────────────
198
-
199
- describe("memory_store", () => {
200
- it("success: returns id and merged tags", async () => {
201
- const r = await exec(tools, "memory_store");
202
- expect(r).toMatchObject({ ok: true, id: "m-new" });
203
- expect(client.createMemory).toHaveBeenCalledWith(
204
- "c", ["user:x", "project:y", "t1"], "s", "project",
205
- "test-agent", "sess-1", undefined, undefined, "/test",
206
- );
207
- });
208
-
209
- it("failure: returns error when client returns null", async () => {
210
- client.createMemory.mockResolvedValue(null);
211
- const r = await exec(tools, "memory_store");
212
- expect(r).toMatchObject({ ok: false });
213
- expect(r.error).toContain("unavailable");
214
- });
215
-
216
- it("passes scope, visibility, category when provided", async () => {
217
- await tools.memory_store.execute({
218
- content: "c", source: "s", tags: [], scope: "global",
219
- visibility: "private", category: "preferences",
220
- });
221
- expect(client.createMemory).toHaveBeenCalledWith(
222
- "c", ["user:x", "project:y"], "s", "global",
223
- "test-agent", "sess-1", "private", "preferences", "/test",
224
- );
225
- });
226
- });
227
-
228
- // ────────────────── memory_search ──────────────────
229
-
230
- describe("memory_search", () => {
231
- it("success: returns results with truncated content", async () => {
232
- const r = await exec(tools, "memory_search");
233
- expect(r).toMatchObject({ ok: true, count: 1 });
234
- expect(r.results[0]).toMatchObject({ id: "m1", score: 0.9 });
235
- });
236
-
237
- it("empty: returns ok with count 0", async () => {
238
- client.searchMemories.mockResolvedValue([]);
239
- const r = await exec(tools, "memory_search");
240
- expect(r).toMatchObject({ ok: true, count: 0, results: [] });
241
- });
242
-
243
- it("passes limit and scope to client", async () => {
244
- await tools.memory_search.execute({ query: "q", limit: 5, scope: "global" });
245
- expect(client.searchMemories).toHaveBeenCalledWith("q", 5, "global", ["user:x", "project:y"], "/test");
246
- });
247
- });
248
-
249
- // ────────────────── memory_get ──────────────────
250
-
251
- describe("memory_get", () => {
252
- it("success: returns full memory object", async () => {
253
- const r = await exec(tools, "memory_get");
254
- expect(r).toMatchObject({ ok: true, memory: { id: "m1" } });
255
- });
256
-
257
- it("not found: returns error when null", async () => {
258
- client.getMemory.mockResolvedValue(null);
259
- const r = await exec(tools, "memory_get");
260
- expect(r).toMatchObject({ ok: false, error: "not found" });
261
- });
262
- });
263
-
264
- // ────────────────── memory_update ──────────────────
265
-
266
- describe("memory_update", () => {
267
- it("success: returns ok with id", async () => {
268
- const r = await exec(tools, "memory_update");
269
- expect(r).toMatchObject({ ok: true, id: "m1" });
270
- });
271
-
272
- it("failure: returns error when client returns null", async () => {
273
- client.updateMemory.mockResolvedValue(null);
274
- const r = await exec(tools, "memory_update");
275
- expect(r).toMatchObject({ ok: false });
276
- expect(r.error).toContain("Failed to update");
277
- });
278
- });
279
-
280
- // ────────────────── memory_profile ──────────────────
281
-
282
- describe("memory_profile", () => {
283
- it("success: returns preferences", async () => {
284
- const r = await exec(tools, "memory_profile");
285
- expect(r).toMatchObject({ ok: true, count: 1 });
286
- });
287
-
288
- it("empty: returns ok with empty array", async () => {
289
- client.getProfile.mockResolvedValue([]);
290
- const r = await exec(tools, "memory_profile");
291
- expect(r).toMatchObject({ ok: true, count: 0, preferences: [] });
292
- });
293
- });
294
-
295
- // ────────────────── memory_profile_stats ──────────────────
296
-
297
- describe("memory_profile_stats", () => {
298
- it("success: returns stats object", async () => {
299
- const r = await exec(tools, "memory_profile_stats");
300
- expect(r).toMatchObject({ ok: true, stats: { total: 5 } });
301
- });
302
- });
303
-
304
- // ────────────────── memory_list ──────────────────
305
-
306
- describe("memory_list", () => {
307
- it("success: returns memories with truncated content", async () => {
308
- const r = await exec(tools, "memory_list");
309
- expect(r).toMatchObject({ ok: true, count: 1 });
310
- expect(r.memories[0].id).toBe("m1");
311
- });
312
-
313
- it("empty: returns ok with empty array", async () => {
314
- client.listRecent.mockResolvedValue([]);
315
- const r = await exec(tools, "memory_list");
316
- expect(r).toMatchObject({ ok: true, count: 0, memories: [] });
317
- });
318
-
319
- it("passes limit to client", async () => {
320
- await tools.memory_list.execute({ limit: 5 });
321
- expect(client.listRecent).toHaveBeenCalledWith(5);
322
- });
323
- });
324
-
325
- // ────────────────── memory_ingest ──────────────────
326
-
327
- describe("memory_ingest", () => {
328
- it("success: returns ingestion result", async () => {
329
- const r = await exec(tools, "memory_ingest");
330
- expect(r).toMatchObject({ ok: true, result: { created: 1 } });
331
- });
332
-
333
- it("failure: returns error when client returns null", async () => {
334
- client.ingestMessages.mockResolvedValue(null);
335
- const r = await exec(tools, "memory_ingest");
336
- expect(r).toMatchObject({ ok: false, error: "Ingestion failed" });
337
- });
338
- });
339
-
340
- // ────────────────── memory_stats ──────────────────
341
-
342
- describe("memory_stats", () => {
343
- it("success: returns stats", async () => {
344
- const r = await exec(tools, "memory_stats");
345
- expect(r).toMatchObject({ ok: true, stats: { total: 100 } });
346
- });
347
-
348
- it("failure: returns error when client returns null", async () => {
349
- client.getStats.mockResolvedValue(null);
350
- const r = await exec(tools, "memory_stats");
351
- expect(r).toMatchObject({ ok: false });
352
- expect(r.error).toContain("Failed to get stats");
353
- });
354
- });
355
-
356
- // ────────────────── memory_delete ──────────────────
357
-
358
- describe("memory_delete", () => {
359
- it("success: returns ok with id", async () => {
360
- const r = await exec(tools, "memory_delete");
361
- expect(r).toMatchObject({ ok: true, id: "m1" });
362
- });
363
-
364
- it("failure: catches error and returns failure", async () => {
365
- client.deleteMemory.mockRejectedValue(new Error("boom"));
366
- const r = await exec(tools, "memory_delete");
367
- expect(r).toMatchObject({ ok: false });
368
- expect(r.error).toContain("Failed to delete");
369
- });
370
- });
371
-
372
- // ────────────────── space_create ──────────────────
373
-
374
- describe("space_create", () => {
375
- it("success: returns space object", async () => {
376
- const r = await exec(tools, "space_create");
377
- expect(r).toMatchObject({ ok: true, space: { id: "s-new" } });
378
- });
379
-
380
- it("failure: returns error when null", async () => {
381
- client.createSpace.mockResolvedValue(null);
382
- const r = await exec(tools, "space_create");
383
- expect(r).toMatchObject({ ok: false, error: "Failed to create space" });
384
- });
385
- });
386
-
387
- // ────────────────── space_list ──────────────────
388
-
389
- describe("space_list", () => {
390
- it("success: returns spaces array", async () => {
391
- const r = await exec(tools, "space_list");
392
- expect(r).toMatchObject({ ok: true, spaces: [{ id: "s1" }] });
393
- });
394
- });
395
-
396
- // ────────────────── space_add_member ──────────────────
397
-
398
- describe("space_add_member", () => {
399
- it("success: returns result", async () => {
400
- const r = await exec(tools, "space_add_member");
401
- expect(r).toMatchObject({ ok: true, result: { ok: true } });
402
- });
403
-
404
- it("failure: returns error when null", async () => {
405
- client.addSpaceMember.mockResolvedValue(null);
406
- const r = await exec(tools, "space_add_member");
407
- expect(r).toMatchObject({ ok: false, error: "Failed to add member" });
408
- });
409
- });
410
-
411
- // ────────────────── memory_share ──────────────────
412
-
413
- describe("memory_share", () => {
414
- it("success: returns result", async () => {
415
- const r = await exec(tools, "memory_share");
416
- expect(r).toMatchObject({ ok: true, result: { ok: true } });
417
- });
418
-
419
- it("failure: returns error when null", async () => {
420
- client.shareMemory.mockResolvedValue(null);
421
- const r = await exec(tools, "memory_share");
422
- expect(r).toMatchObject({ ok: false, error: "Failed to share memory" });
423
- });
424
- });
425
-
426
- // ────────────────── memory_pull ──────────────────
427
-
428
- describe("memory_pull", () => {
429
- it("success: returns result", async () => {
430
- const r = await exec(tools, "memory_pull");
431
- expect(r).toMatchObject({ ok: true, result: { ok: true } });
432
- });
433
-
434
- it("failure: returns error when null", async () => {
435
- client.pullMemory.mockResolvedValue(null);
436
- const r = await exec(tools, "memory_pull");
437
- expect(r).toMatchObject({ ok: false, error: "Failed to pull memory" });
438
- });
439
- });
440
-
441
- // ────────────────── memory_reshare ──────────────────
442
-
443
- describe("memory_reshare", () => {
444
- it("success: returns result", async () => {
445
- const r = await exec(tools, "memory_reshare");
446
- expect(r).toMatchObject({ ok: true, result: { ok: true } });
447
- });
448
-
449
- it("failure: returns error when null", async () => {
450
- client.reshareMemory.mockResolvedValue(null);
451
- const r = await exec(tools, "memory_reshare");
452
- expect(r).toMatchObject({ ok: false, error: "Failed to reshare memory" });
453
- });
454
- });
455
-
456
- // ────────────────── memory_toggle ──────────────────
457
-
458
- describe("memory_toggle", () => {
459
- it("state='on': sets auto-store ON", async () => {
460
- const r = await exec(tools, "memory_toggle", { state: "on" });
461
- expect(r).toMatchObject({ ok: true, auto_store: true });
462
- expect(mockSetEnabled).toHaveBeenCalledWith("sess-1", true);
463
- });
464
-
465
- it("state='off': sets auto-store OFF", async () => {
466
- const r = await exec(tools, "memory_toggle", { state: "off" });
467
- expect(r).toMatchObject({ ok: true, auto_store: false });
468
- expect(mockSetEnabled).toHaveBeenCalledWith("sess-1", false);
469
- });
470
-
471
- it("state='ON' (uppercase): still recognized as on", async () => {
472
- const r = await exec(tools, "memory_toggle", { state: "ON" });
473
- expect(r).toMatchObject({ ok: true, auto_store: true });
474
- });
475
-
476
- it("no state: queries current status (true)", async () => {
477
- mockIsEnabled.mockReturnValue(true);
478
- const r = await exec(tools, "memory_toggle", {});
479
- expect(r).toMatchObject({ ok: true, auto_store: true });
480
- expect(mockIsEnabled).toHaveBeenCalledWith("sess-1");
481
- });
482
-
483
- it("no state: queries current status (false)", async () => {
484
- mockIsEnabled.mockReturnValue(false);
485
- const r = await exec(tools, "memory_toggle", {});
486
- expect(r).toMatchObject({ ok: true, auto_store: false });
487
- });
488
-
489
- it("returns error when no session", async () => {
490
- tools = buildTools(client, [], mkCtx({ getSessionId: () => undefined }));
491
- const r = await exec(tools, "memory_toggle", { state: "on" });
492
- expect(r).toMatchObject({ ok: false });
493
- expect(r.error).toContain("No active session");
494
- });
495
- });
496
-
497
- // ────────────────── JSON output format ──────────────────
498
-
499
- describe("JSON output format", () => {
500
- it("all tools return valid JSON with ok boolean", async () => {
501
- for (const name of ALL_TOOLS) {
502
- const raw = await tools[name].execute(A[name]);
503
- const parsed = JSON.parse(raw);
504
- expect(typeof parsed.ok).toBe("boolean");
505
- }
506
- });
507
- });
508
- });