@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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 +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
package/src/tools.test.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Value } from "@sinclair/typebox/value";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
6
|
import { registerOpenclawClawlingTools } from "./tools.ts";
|
|
7
|
+
import * as toolSchemas from "./tools-schema.ts";
|
|
8
|
+
|
|
9
|
+
const schemas = toolSchemas as Record<string, unknown>;
|
|
4
10
|
|
|
5
11
|
interface RegisteredTool {
|
|
6
12
|
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
parameters?: unknown;
|
|
7
15
|
execute: (callId: string, params: unknown) => Promise<unknown>;
|
|
8
16
|
}
|
|
9
17
|
|
|
@@ -13,9 +21,16 @@ const ALWAYS_VISIBLE_TOOL_NAMES = [
|
|
|
13
21
|
"clawchat_delete_moment",
|
|
14
22
|
"clawchat_delete_moment_comment",
|
|
15
23
|
"clawchat_get_account_profile",
|
|
24
|
+
"clawchat_get_conversation",
|
|
16
25
|
"clawchat_get_user_profile",
|
|
17
26
|
"clawchat_list_account_friends",
|
|
27
|
+
"clawchat_list_conversations",
|
|
18
28
|
"clawchat_list_moments",
|
|
29
|
+
"clawchat_memory_edit",
|
|
30
|
+
"clawchat_memory_read",
|
|
31
|
+
"clawchat_memory_write",
|
|
32
|
+
"clawchat_metadata_sync",
|
|
33
|
+
"clawchat_metadata_update",
|
|
19
34
|
"clawchat_reply_moment_comment",
|
|
20
35
|
"clawchat_search_users",
|
|
21
36
|
"clawchat_toggle_moment_reaction",
|
|
@@ -24,12 +39,26 @@ const ALWAYS_VISIBLE_TOOL_NAMES = [
|
|
|
24
39
|
"clawchat_upload_media_file",
|
|
25
40
|
];
|
|
26
41
|
|
|
42
|
+
const tempRoots: string[] = [];
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
for (const dir of tempRoots.splice(0)) {
|
|
46
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
27
50
|
function buildApi(opts: {
|
|
28
51
|
configChannel?: Record<string, unknown> | null;
|
|
29
52
|
configTools?: Record<string, unknown>;
|
|
53
|
+
workspaceDir?: string;
|
|
30
54
|
registerTool?: (tool: { name: string }, options?: { name: string }) => void;
|
|
31
55
|
}) {
|
|
32
56
|
const registered: RegisteredTool[] = [];
|
|
57
|
+
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-tools-"));
|
|
58
|
+
const workspaceDir =
|
|
59
|
+
opts.workspaceDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-workspace-"));
|
|
60
|
+
tempRoots.push(stateDir);
|
|
61
|
+
tempRoots.push(workspaceDir);
|
|
33
62
|
const api = {
|
|
34
63
|
config:
|
|
35
64
|
opts.configChannel === null
|
|
@@ -45,12 +74,32 @@ function buildApi(opts: {
|
|
|
45
74
|
error: vi.fn(),
|
|
46
75
|
},
|
|
47
76
|
runtime: {
|
|
77
|
+
state: {
|
|
78
|
+
resolveStateDir: () => stateDir,
|
|
79
|
+
},
|
|
48
80
|
config: {
|
|
49
81
|
mutateConfigFile: vi.fn(),
|
|
50
82
|
},
|
|
51
83
|
},
|
|
52
|
-
registerTool: (tool: RegisteredTool, _options?: { name: string }) => {
|
|
53
|
-
|
|
84
|
+
registerTool: (tool: RegisteredTool | ((ctx: unknown) => RegisteredTool | RegisteredTool[] | null | undefined), _options?: { name: string }) => {
|
|
85
|
+
const resolved =
|
|
86
|
+
typeof tool === "function"
|
|
87
|
+
? tool({
|
|
88
|
+
workspaceDir,
|
|
89
|
+
agentId: "agent-default",
|
|
90
|
+
runtimeConfig: {
|
|
91
|
+
channels: { "openclaw-clawchat": opts.configChannel ?? {} },
|
|
92
|
+
},
|
|
93
|
+
getRuntimeConfig: () => ({
|
|
94
|
+
channels: { "openclaw-clawchat": opts.configChannel ?? {} },
|
|
95
|
+
}),
|
|
96
|
+
})
|
|
97
|
+
: tool;
|
|
98
|
+
if (Array.isArray(resolved)) {
|
|
99
|
+
registered.push(...resolved);
|
|
100
|
+
} else if (resolved) {
|
|
101
|
+
registered.push(resolved);
|
|
102
|
+
}
|
|
54
103
|
},
|
|
55
104
|
} as unknown as Parameters<typeof registerOpenclawClawlingTools>[0];
|
|
56
105
|
return { api, registered };
|
|
@@ -60,6 +109,7 @@ function configuredChannel(extra: Record<string, unknown> = {}) {
|
|
|
60
109
|
return {
|
|
61
110
|
websocketUrl: "wss://w",
|
|
62
111
|
token: "tk",
|
|
112
|
+
agentId: "agt-1",
|
|
63
113
|
userId: "u",
|
|
64
114
|
...extra,
|
|
65
115
|
};
|
|
@@ -105,6 +155,25 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
105
155
|
expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
|
|
106
156
|
});
|
|
107
157
|
|
|
158
|
+
it("registers only read-only conversation tools", () => {
|
|
159
|
+
const { api, registered } = buildApi({
|
|
160
|
+
configChannel: configuredChannel(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
registerOpenclawClawlingTools(api);
|
|
164
|
+
|
|
165
|
+
const names = registered.map((t) => t.name);
|
|
166
|
+
expect(names).toContain("clawchat_list_conversations");
|
|
167
|
+
expect(names).toContain("clawchat_get_conversation");
|
|
168
|
+
expect(names).not.toContain("clawchat_create_group_conversation");
|
|
169
|
+
expect(names).not.toContain("clawchat_update_conversation");
|
|
170
|
+
expect(names).not.toContain("clawchat_leave_conversation");
|
|
171
|
+
expect(names).not.toContain("clawchat_dissolve_conversation");
|
|
172
|
+
expect(names).not.toContain("clawchat_add_conversation_member");
|
|
173
|
+
expect(names).not.toContain("clawchat_remove_conversation_member");
|
|
174
|
+
expect(names).not.toContain("clawchat_list_conversation_users");
|
|
175
|
+
});
|
|
176
|
+
|
|
108
177
|
it("skips registration when api.config is undefined", () => {
|
|
109
178
|
const { api, registered } = buildApi({ configChannel: null });
|
|
110
179
|
registerOpenclawClawlingTools(api);
|
|
@@ -133,10 +202,143 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
133
202
|
|
|
134
203
|
expect(logger.info).not.toHaveBeenCalled();
|
|
135
204
|
expect(logger.debug).toHaveBeenCalledWith(
|
|
136
|
-
"openclaw-clawchat: registered
|
|
205
|
+
"openclaw-clawchat: registered 21 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, search_users, list_conversations, get_conversation, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, upload_media_file, memory_read, memory_write, memory_edit, metadata_sync, metadata_update)",
|
|
137
206
|
);
|
|
138
207
|
});
|
|
139
208
|
|
|
209
|
+
it("registers exactly the five ClawChat memory and metadata tools without old-prefix aliases", () => {
|
|
210
|
+
const { api, registered } = buildApi({
|
|
211
|
+
configChannel: configuredChannel({ ownerUserId: "owner" }),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
registerOpenclawClawlingTools(api);
|
|
215
|
+
|
|
216
|
+
const names = registered.map((t) => t.name).sort();
|
|
217
|
+
expect(names.filter((name) => /memory|metadata/.test(name))).toEqual([
|
|
218
|
+
"clawchat_memory_edit",
|
|
219
|
+
"clawchat_memory_read",
|
|
220
|
+
"clawchat_memory_write",
|
|
221
|
+
"clawchat_metadata_sync",
|
|
222
|
+
"clawchat_metadata_update",
|
|
223
|
+
]);
|
|
224
|
+
expect(names).not.toContain("cc_memory_read");
|
|
225
|
+
expect(names).not.toContain("cc_metadata_sync");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("keeps old ClawChat tools registered and guarded against direct HTTP or shell fallbacks", () => {
|
|
229
|
+
const { api, registered } = buildApi({
|
|
230
|
+
configChannel: configuredChannel({ ownerUserId: "owner" }),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
registerOpenclawClawlingTools(api);
|
|
234
|
+
|
|
235
|
+
const oldTools = registered.filter((tool) => !/memory|metadata/.test(tool.name));
|
|
236
|
+
expect(oldTools.map((tool) => tool.name).sort()).toEqual(
|
|
237
|
+
ALWAYS_VISIBLE_TOOL_NAMES.filter((name) => !/memory|metadata/.test(name)),
|
|
238
|
+
);
|
|
239
|
+
for (const tool of oldTools) {
|
|
240
|
+
expect(tool.description).toMatch(/Do not use execute/);
|
|
241
|
+
expect(tool.description).toMatch(/shell commands/);
|
|
242
|
+
expect(tool.description).toMatch(/curl/);
|
|
243
|
+
expect(tool.description).toMatch(/direct ClawChat HTTP calls/);
|
|
244
|
+
expect(tool.description).toMatch(/generic fallback tools/);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("memory and metadata schemas require explicit targets and bounded modes", () => {
|
|
249
|
+
const readSchema = schemas.ClawchatMemoryReadSchema;
|
|
250
|
+
const writeSchema = schemas.ClawchatMemoryWriteSchema;
|
|
251
|
+
const editSchema = schemas.ClawchatMemoryEditSchema;
|
|
252
|
+
const syncSchema = schemas.ClawchatMetadataSyncSchema;
|
|
253
|
+
const updateSchema = schemas.ClawchatMetadataUpdateSchema;
|
|
254
|
+
expect(readSchema).toBeDefined();
|
|
255
|
+
expect(writeSchema).toBeDefined();
|
|
256
|
+
expect(editSchema).toBeDefined();
|
|
257
|
+
expect(syncSchema).toBeDefined();
|
|
258
|
+
expect(updateSchema).toBeDefined();
|
|
259
|
+
|
|
260
|
+
for (const schema of [readSchema, writeSchema, editSchema, syncSchema, updateSchema]) {
|
|
261
|
+
expect((schema as { type?: unknown }).type).toBe("object");
|
|
262
|
+
expect(schema).not.toHaveProperty("anyOf");
|
|
263
|
+
expect(schema).not.toHaveProperty("oneOf");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const targetType of ["owner", "user", "group"] as const) {
|
|
267
|
+
expect(Value.Check(readSchema, { targetType, targetId: targetType === "owner" ? "owner" : "id_1" })).toBe(true);
|
|
268
|
+
}
|
|
269
|
+
expect(Value.Check(readSchema, { targetType: "profile", targetId: "id_1" })).toBe(false);
|
|
270
|
+
expect(Value.Check(readSchema, { targetType: "user" })).toBe(false);
|
|
271
|
+
expect(Value.Check(readSchema, { targetType: "user", targetId: "id_1", filePath: "users/id_1.md" })).toBe(false);
|
|
272
|
+
|
|
273
|
+
expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "append", content: "note" })).toBe(true);
|
|
274
|
+
expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "replace", content: "" })).toBe(true);
|
|
275
|
+
expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "merge", content: "note" })).toBe(false);
|
|
276
|
+
|
|
277
|
+
expect(Value.Check(syncSchema, { targetType: "owner", targetId: "owner", direction: "pull" })).toBe(true);
|
|
278
|
+
expect(Value.Check(syncSchema, { targetType: "group", targetId: "grp_1", direction: "push" })).toBe(true);
|
|
279
|
+
expect(Value.Check(syncSchema, { targetType: "group", targetId: "grp_1", direction: "refresh" })).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("metadata update schema requires a non-empty whitelisted patch", () => {
|
|
283
|
+
const schema = schemas.ClawchatMetadataUpdateSchema;
|
|
284
|
+
expect(schema).toBeDefined();
|
|
285
|
+
|
|
286
|
+
expect(Value.Check(schema, {
|
|
287
|
+
targetType: "owner",
|
|
288
|
+
targetId: "owner",
|
|
289
|
+
patch: { nickname: "Bot" },
|
|
290
|
+
})).toBe(true);
|
|
291
|
+
expect(Value.Check(schema, {
|
|
292
|
+
targetType: "user",
|
|
293
|
+
targetId: "usr_1",
|
|
294
|
+
patch: { avatar_url: "https://cdn/avatar.png", bio: "" },
|
|
295
|
+
})).toBe(true);
|
|
296
|
+
expect(Value.Check(schema, {
|
|
297
|
+
targetType: "group",
|
|
298
|
+
targetId: "grp_1",
|
|
299
|
+
patch: { title: "Team", description: "Planning" },
|
|
300
|
+
})).toBe(true);
|
|
301
|
+
expect(Value.Check(schema, { targetType: "owner", targetId: "owner" })).toBe(false);
|
|
302
|
+
expect(Value.Check(schema, { targetType: "owner", targetId: "owner", patch: {} })).toBe(false);
|
|
303
|
+
for (const key of ["visibility", "status", "platform", "raw", "kind"]) {
|
|
304
|
+
expect(Value.Check(schema, {
|
|
305
|
+
targetType: "owner",
|
|
306
|
+
targetId: "owner",
|
|
307
|
+
patch: { [key]: "not allowed" },
|
|
308
|
+
})).toBe(false);
|
|
309
|
+
expect(Value.Check(schema, {
|
|
310
|
+
targetType: "group",
|
|
311
|
+
targetId: "grp_1",
|
|
312
|
+
patch: { [key]: "not allowed" },
|
|
313
|
+
})).toBe(false);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("memory tool safety errors do not expose workspace paths", async () => {
|
|
318
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-workspace-"));
|
|
319
|
+
tempRoots.push(workspaceDir);
|
|
320
|
+
fs.symlinkSync(os.tmpdir(), path.join(workspaceDir, "users"));
|
|
321
|
+
const { api, registered } = buildApi({
|
|
322
|
+
configChannel: configuredChannel({ ownerUserId: "owner" }),
|
|
323
|
+
workspaceDir,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
registerOpenclawClawlingTools(api);
|
|
327
|
+
const tool = registered.find((t) => t.name === "clawchat_memory_read")!;
|
|
328
|
+
const result = await tool.execute("call-1", { targetType: "user", targetId: "usr_1" });
|
|
329
|
+
|
|
330
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
331
|
+
error?: string;
|
|
332
|
+
message?: string;
|
|
333
|
+
};
|
|
334
|
+
expect(parsed).toMatchObject({
|
|
335
|
+
error: "validation",
|
|
336
|
+
message: "openclaw-clawchat: unsafe ClawChat memory target",
|
|
337
|
+
});
|
|
338
|
+
expect(parsed.message).not.toContain(workspaceDir);
|
|
339
|
+
expect(parsed.message).not.toContain(os.tmpdir());
|
|
340
|
+
});
|
|
341
|
+
|
|
140
342
|
it("account tools return a config error before activation instead of disappearing", async () => {
|
|
141
343
|
const { api, registered } = buildApi({
|
|
142
344
|
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
@@ -152,6 +354,285 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
152
354
|
expect(parsed.message).toMatch(/token is required/i);
|
|
153
355
|
});
|
|
154
356
|
|
|
357
|
+
it("records successful clawchat tool calls without changing the returned result", async () => {
|
|
358
|
+
const store = { recordToolCall: vi.fn() };
|
|
359
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
360
|
+
new Response(
|
|
361
|
+
JSON.stringify({
|
|
362
|
+
code: 0,
|
|
363
|
+
msg: "ok",
|
|
364
|
+
data: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
365
|
+
}),
|
|
366
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
try {
|
|
370
|
+
const { api, registered } = buildApi({
|
|
371
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
372
|
+
});
|
|
373
|
+
registerOpenclawClawlingTools(api, { store });
|
|
374
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
375
|
+
|
|
376
|
+
const result = await tool.execute("call-1", {});
|
|
377
|
+
|
|
378
|
+
expect((result as { details: unknown }).details).toEqual({
|
|
379
|
+
user_id: "u",
|
|
380
|
+
nickname: "Bot",
|
|
381
|
+
avatar_url: "",
|
|
382
|
+
bio: "",
|
|
383
|
+
});
|
|
384
|
+
expect(store.recordToolCall).toHaveBeenCalledTimes(1);
|
|
385
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(
|
|
386
|
+
expect.objectContaining({
|
|
387
|
+
platform: "openclaw",
|
|
388
|
+
accountId: "default",
|
|
389
|
+
toolName: "clawchat_get_account_profile",
|
|
390
|
+
args: {},
|
|
391
|
+
result: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
392
|
+
error: null,
|
|
393
|
+
startedAt: expect.any(Number),
|
|
394
|
+
endedAt: expect.any(Number),
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
} finally {
|
|
398
|
+
fetchMock.mockRestore();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("lists conversations, upserts returned summaries, and does not fetch or delete extra cache entries", async () => {
|
|
403
|
+
const store = {
|
|
404
|
+
recordToolCall: vi.fn(),
|
|
405
|
+
upsertConversationSummary: vi.fn(),
|
|
406
|
+
upsertConversationDetails: vi.fn(),
|
|
407
|
+
deleteConversationCache: vi.fn(),
|
|
408
|
+
};
|
|
409
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
410
|
+
new Response(
|
|
411
|
+
JSON.stringify({
|
|
412
|
+
code: 0,
|
|
413
|
+
msg: "ok",
|
|
414
|
+
data: {
|
|
415
|
+
conversations: [
|
|
416
|
+
{
|
|
417
|
+
id: "cnv_1",
|
|
418
|
+
type: "direct",
|
|
419
|
+
title: "Alice",
|
|
420
|
+
created_at: "2026-05-01T00:00:00Z",
|
|
421
|
+
updated_at: "2026-05-02T00:00:00Z",
|
|
422
|
+
peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
next_before: "cursor-2",
|
|
426
|
+
},
|
|
427
|
+
}),
|
|
428
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
try {
|
|
432
|
+
const { api, registered } = buildApi({
|
|
433
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
434
|
+
});
|
|
435
|
+
registerOpenclawClawlingTools(api, { store });
|
|
436
|
+
const tool = registered.find((t) => t.name === "clawchat_list_conversations")!;
|
|
437
|
+
|
|
438
|
+
const result = await tool.execute("call-1", { before: "cursor-1", limit: 10 });
|
|
439
|
+
|
|
440
|
+
expect((result as { details: unknown }).details).toEqual({
|
|
441
|
+
conversations: [
|
|
442
|
+
{
|
|
443
|
+
id: "cnv_1",
|
|
444
|
+
type: "direct",
|
|
445
|
+
title: "Alice",
|
|
446
|
+
created_at: "2026-05-01T00:00:00Z",
|
|
447
|
+
updated_at: "2026-05-02T00:00:00Z",
|
|
448
|
+
peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
next_before: "cursor-2",
|
|
452
|
+
});
|
|
453
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
454
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations?before=cursor-1&limit=10");
|
|
455
|
+
expect(store.upsertConversationSummary).toHaveBeenCalledWith(
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
platform: "openclaw",
|
|
458
|
+
accountId: "default",
|
|
459
|
+
conversationId: "cnv_1",
|
|
460
|
+
conversationType: "direct",
|
|
461
|
+
raw: expect.objectContaining({ id: "cnv_1" }),
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
expect(store.upsertConversationDetails).not.toHaveBeenCalled();
|
|
465
|
+
expect(store.deleteConversationCache).not.toHaveBeenCalled();
|
|
466
|
+
} finally {
|
|
467
|
+
fetchMock.mockRestore();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("gets a conversation and upserts full details", async () => {
|
|
472
|
+
const store = {
|
|
473
|
+
recordToolCall: vi.fn(),
|
|
474
|
+
upsertConversationSummary: vi.fn(),
|
|
475
|
+
upsertConversationDetails: vi.fn(),
|
|
476
|
+
deleteConversationCache: vi.fn(),
|
|
477
|
+
};
|
|
478
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
479
|
+
new Response(
|
|
480
|
+
JSON.stringify({
|
|
481
|
+
code: 0,
|
|
482
|
+
msg: "ok",
|
|
483
|
+
data: {
|
|
484
|
+
conversation: {
|
|
485
|
+
id: "group_1",
|
|
486
|
+
type: "group",
|
|
487
|
+
title: "Project Group",
|
|
488
|
+
creator_id: "owner",
|
|
489
|
+
created_at: "2026-05-01T00:00:00Z",
|
|
490
|
+
updated_at: "2026-05-02T00:00:00Z",
|
|
491
|
+
participants: [
|
|
492
|
+
{ conversation_id: "group_1", user_id: "owner", role: "owner", joined_at: "2026-05-01T00:00:00Z" },
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
}),
|
|
497
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
try {
|
|
501
|
+
const { api, registered } = buildApi({
|
|
502
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
503
|
+
});
|
|
504
|
+
registerOpenclawClawlingTools(api, { store });
|
|
505
|
+
const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
|
|
506
|
+
|
|
507
|
+
await tool.execute("call-1", { conversationId: "group_1" });
|
|
508
|
+
|
|
509
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
510
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations/group_1");
|
|
511
|
+
expect(store.upsertConversationDetails).toHaveBeenCalledWith(
|
|
512
|
+
expect.objectContaining({
|
|
513
|
+
platform: "openclaw",
|
|
514
|
+
accountId: "default",
|
|
515
|
+
conversationId: "group_1",
|
|
516
|
+
conversationType: "group",
|
|
517
|
+
raw: expect.objectContaining({ id: "group_1" }),
|
|
518
|
+
members: [expect.objectContaining({ userId: "owner", role: "owner" })],
|
|
519
|
+
membersComplete: true,
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
522
|
+
const details = store.upsertConversationDetails.mock.calls[0]?.[0] as Record<string, unknown>;
|
|
523
|
+
expect(Object.prototype.hasOwnProperty.call(details, "groupProfile")).toBe(false);
|
|
524
|
+
expect(Object.prototype.hasOwnProperty.call(details, "userProfiles")).toBe(false);
|
|
525
|
+
} finally {
|
|
526
|
+
fetchMock.mockRestore();
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("deletes conversation-scoped cache and returns the standard error result when get conversation is not found", async () => {
|
|
531
|
+
const store = {
|
|
532
|
+
recordToolCall: vi.fn(),
|
|
533
|
+
upsertConversationSummary: vi.fn(),
|
|
534
|
+
upsertConversationDetails: vi.fn(),
|
|
535
|
+
deleteConversationCache: vi.fn(),
|
|
536
|
+
};
|
|
537
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
538
|
+
new Response(JSON.stringify({ code: 40401, msg: "conversation not found", data: null }), {
|
|
539
|
+
status: 200,
|
|
540
|
+
headers: { "content-type": "application/json" },
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
try {
|
|
544
|
+
const { api, registered } = buildApi({
|
|
545
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
546
|
+
});
|
|
547
|
+
registerOpenclawClawlingTools(api, { store });
|
|
548
|
+
const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
|
|
549
|
+
|
|
550
|
+
const result = await tool.execute("call-1", { conversationId: "missing" });
|
|
551
|
+
|
|
552
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
553
|
+
error?: string;
|
|
554
|
+
message?: string;
|
|
555
|
+
meta?: { code?: number };
|
|
556
|
+
};
|
|
557
|
+
expect(parsed).toMatchObject({
|
|
558
|
+
error: "api",
|
|
559
|
+
message: "conversation not found",
|
|
560
|
+
meta: { code: 40401 },
|
|
561
|
+
});
|
|
562
|
+
expect(store.deleteConversationCache).toHaveBeenCalledWith({
|
|
563
|
+
platform: "openclaw",
|
|
564
|
+
accountId: "default",
|
|
565
|
+
conversationId: "missing",
|
|
566
|
+
});
|
|
567
|
+
expect(store.upsertConversationSummary).not.toHaveBeenCalled();
|
|
568
|
+
expect(store.upsertConversationDetails).not.toHaveBeenCalled();
|
|
569
|
+
} finally {
|
|
570
|
+
fetchMock.mockRestore();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("does not write conversation cache from account, user, friend, search, avatar, or media tools", async () => {
|
|
575
|
+
const store = {
|
|
576
|
+
recordToolCall: vi.fn(),
|
|
577
|
+
upsertConversationSummary: vi.fn(),
|
|
578
|
+
upsertConversationDetails: vi.fn(),
|
|
579
|
+
deleteConversationCache: vi.fn(),
|
|
580
|
+
};
|
|
581
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
582
|
+
new Response(
|
|
583
|
+
JSON.stringify({
|
|
584
|
+
code: 0,
|
|
585
|
+
msg: "ok",
|
|
586
|
+
data: { id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
587
|
+
}),
|
|
588
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
589
|
+
),
|
|
590
|
+
);
|
|
591
|
+
try {
|
|
592
|
+
const { api, registered } = buildApi({
|
|
593
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
594
|
+
});
|
|
595
|
+
registerOpenclawClawlingTools(api, { store });
|
|
596
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
597
|
+
|
|
598
|
+
await tool.execute("call-1", {});
|
|
599
|
+
|
|
600
|
+
expect(store.upsertConversationSummary).not.toHaveBeenCalled();
|
|
601
|
+
expect(store.upsertConversationDetails).not.toHaveBeenCalled();
|
|
602
|
+
expect(store.deleteConversationCache).not.toHaveBeenCalled();
|
|
603
|
+
} finally {
|
|
604
|
+
fetchMock.mockRestore();
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("records clawchat tool failures while preserving the returned error result", async () => {
|
|
609
|
+
const store = { recordToolCall: vi.fn() };
|
|
610
|
+
const { api, registered } = buildApi({
|
|
611
|
+
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
612
|
+
});
|
|
613
|
+
registerOpenclawClawlingTools(api, { store });
|
|
614
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
615
|
+
|
|
616
|
+
const result = await tool.execute("call-1", {});
|
|
617
|
+
|
|
618
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
619
|
+
error?: string;
|
|
620
|
+
message?: string;
|
|
621
|
+
};
|
|
622
|
+
expect(parsed).toMatchObject({ error: "config" });
|
|
623
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(
|
|
624
|
+
expect.objectContaining({
|
|
625
|
+
platform: "openclaw",
|
|
626
|
+
accountId: "default",
|
|
627
|
+
toolName: "clawchat_get_account_profile",
|
|
628
|
+
args: {},
|
|
629
|
+
result: parsed,
|
|
630
|
+
error: expect.stringContaining("config"),
|
|
631
|
+
}),
|
|
632
|
+
);
|
|
633
|
+
expect(registered.every((tool) => tool.name.startsWith("clawchat_"))).toBe(true);
|
|
634
|
+
});
|
|
635
|
+
|
|
155
636
|
it("clawchat_update_account_profile description names account profile triggers (EN + ZH)", () => {
|
|
156
637
|
const { api } = buildApi({ configChannel: configuredChannel() });
|
|
157
638
|
const fullTools: Array<{ name: string; description?: string }> = [];
|
|
@@ -175,15 +656,13 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
175
656
|
});
|
|
176
657
|
|
|
177
658
|
it("account query and upload tool descriptions include precise trigger semantics", () => {
|
|
178
|
-
const { api } = buildApi({ configChannel: configuredChannel() });
|
|
179
|
-
const fullTools: Array<{ name: string; description?: string }> = [];
|
|
180
|
-
api.registerTool = (tool: { name: string; description?: string }) => {
|
|
181
|
-
fullTools.push(tool);
|
|
182
|
-
};
|
|
659
|
+
const { api, registered: fullTools } = buildApi({ configChannel: configuredChannel() });
|
|
183
660
|
registerOpenclawClawlingTools(api);
|
|
184
661
|
|
|
185
662
|
const accountProfile = fullTools.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
186
663
|
const userProfile = fullTools.find((t) => t.name === "clawchat_get_user_profile")!;
|
|
664
|
+
const memoryWrite = fullTools.find((t) => t.name === "clawchat_memory_write")!;
|
|
665
|
+
const metadataSync = fullTools.find((t) => t.name === "clawchat_metadata_sync")!;
|
|
187
666
|
const friends = fullTools.find((t) => t.name === "clawchat_list_account_friends")!;
|
|
188
667
|
const avatar = fullTools.find((t) => t.name === "clawchat_upload_avatar_image")!;
|
|
189
668
|
const media = fullTools.find((t) => t.name === "clawchat_upload_media_file")!;
|
|
@@ -195,10 +674,22 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
195
674
|
expect(userProfile.description).toMatch(/userId/);
|
|
196
675
|
expect(userProfile.description).toMatch(/public profile/i);
|
|
197
676
|
expect(userProfile.description).toMatch(/do not guess|do not infer/i);
|
|
677
|
+
expect(userProfile.description).toMatch(/read-only lookup/i);
|
|
678
|
+
expect(userProfile.description).toMatch(/clawchat_metadata_sync.*direction=pull/i);
|
|
679
|
+
|
|
680
|
+
expect(memoryWrite.description).toMatch(/agent-authored body/i);
|
|
681
|
+
expect(memoryWrite.description).toMatch(/never modifies the metadata block/i);
|
|
682
|
+
expect(memoryWrite.description).toMatch(/Do not use.*nickname.*avatar_url.*bio.*profile_type/i);
|
|
683
|
+
expect(memoryWrite.description).toMatch(/refresh.*local ClawChat.*profile.*clawchat_metadata_sync.*direction=pull/i);
|
|
684
|
+
|
|
685
|
+
expect(metadataSync.description).toMatch(/refresh, sync, or update local ClawChat profile information/i);
|
|
686
|
+
expect(metadataSync.description).toMatch(/rewrites only the metadata block/i);
|
|
687
|
+
expect(metadataSync.description).toMatch(/Do not combine clawchat_get_user_profile with clawchat_memory_write/i);
|
|
198
688
|
|
|
199
689
|
expect(friends.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
|
|
200
690
|
expect(friends.description).toMatch(/friends|contacts/i);
|
|
201
|
-
expect(friends.description).toMatch(/page=1|pageSize=20/);
|
|
691
|
+
expect(friends.description).not.toMatch(/paginated|page=1|pageSize=20/i);
|
|
692
|
+
expect(friends.parameters?.properties ?? {}).toEqual({});
|
|
202
693
|
|
|
203
694
|
expect(avatar.description).toMatch(/local image/i);
|
|
204
695
|
expect(avatar.description).toMatch(/avatar URL|hosted avatar URL|public URL/i);
|