@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13
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/INSTALL.md +64 -0
- package/README.md +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- 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 +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -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.test.ts +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
package/src/tools.test.ts
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
5
|
import { registerOpenclawClawlingTools } from "./tools.ts";
|
|
4
6
|
|
|
5
|
-
const loginRuntime = vi.hoisted(() => ({
|
|
6
|
-
runOpenclawClawlingLogin: vi.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
vi.mock("./login.runtime.ts", () => loginRuntime);
|
|
10
|
-
|
|
11
7
|
interface RegisteredTool {
|
|
12
8
|
name: string;
|
|
13
9
|
execute: (callId: string, params: unknown) => Promise<unknown>;
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
const ALWAYS_VISIBLE_TOOL_NAMES = [
|
|
17
|
-
"
|
|
13
|
+
"clawchat_create_moment",
|
|
14
|
+
"clawchat_create_moment_comment",
|
|
15
|
+
"clawchat_delete_moment",
|
|
16
|
+
"clawchat_delete_moment_comment",
|
|
18
17
|
"clawchat_get_account_profile",
|
|
18
|
+
"clawchat_get_conversation",
|
|
19
19
|
"clawchat_get_user_profile",
|
|
20
20
|
"clawchat_list_account_friends",
|
|
21
|
+
"clawchat_list_conversations",
|
|
22
|
+
"clawchat_list_moments",
|
|
23
|
+
"clawchat_reply_moment_comment",
|
|
24
|
+
"clawchat_search_users",
|
|
25
|
+
"clawchat_toggle_moment_reaction",
|
|
21
26
|
"clawchat_update_account_profile",
|
|
22
27
|
"clawchat_upload_avatar_image",
|
|
23
28
|
"clawchat_upload_media_file",
|
|
24
29
|
];
|
|
25
30
|
|
|
31
|
+
const tempRoots: string[] = [];
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
for (const dir of tempRoots.splice(0)) {
|
|
35
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
26
39
|
function buildApi(opts: {
|
|
27
40
|
configChannel?: Record<string, unknown> | null;
|
|
28
41
|
configTools?: Record<string, unknown>;
|
|
29
42
|
registerTool?: (tool: { name: string }, options?: { name: string }) => void;
|
|
30
43
|
}) {
|
|
31
44
|
const registered: RegisteredTool[] = [];
|
|
45
|
+
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-tools-"));
|
|
46
|
+
tempRoots.push(stateDir);
|
|
32
47
|
const api = {
|
|
33
48
|
config:
|
|
34
49
|
opts.configChannel === null
|
|
@@ -44,6 +59,9 @@ function buildApi(opts: {
|
|
|
44
59
|
error: vi.fn(),
|
|
45
60
|
},
|
|
46
61
|
runtime: {
|
|
62
|
+
state: {
|
|
63
|
+
resolveStateDir: () => stateDir,
|
|
64
|
+
},
|
|
47
65
|
config: {
|
|
48
66
|
mutateConfigFile: vi.fn(),
|
|
49
67
|
},
|
|
@@ -65,10 +83,6 @@ function configuredChannel(extra: Record<string, unknown> = {}) {
|
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
describe("registerOpenclawClawlingTools", () => {
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
vi.clearAllMocks();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
86
|
it("uses OpenClaw SDK tool result types instead of direct pi-agent-core imports", () => {
|
|
73
87
|
const source = fs.readFileSync(new URL("./tools.ts", import.meta.url), "utf8");
|
|
74
88
|
expect(source).not.toMatch(/@mariozechner\/pi-agent-core/);
|
|
@@ -98,44 +112,33 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
98
112
|
});
|
|
99
113
|
});
|
|
100
114
|
|
|
101
|
-
it("
|
|
115
|
+
it("does not register invite-code activation as an agent tool", () => {
|
|
102
116
|
const { api, registered } = buildApi({
|
|
103
117
|
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
104
118
|
});
|
|
105
119
|
|
|
106
120
|
registerOpenclawClawlingTools(api);
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
expect(tool).toBeDefined();
|
|
110
|
-
loginRuntime.runOpenclawClawlingLogin.mockResolvedValueOnce(undefined);
|
|
111
|
-
|
|
112
|
-
const result = await tool!.execute("call-1", { code: "A1B2C3" });
|
|
113
|
-
|
|
114
|
-
expect(loginRuntime.runOpenclawClawlingLogin).toHaveBeenCalledTimes(1);
|
|
115
|
-
const params = loginRuntime.runOpenclawClawlingLogin.mock.calls[0]?.[0];
|
|
116
|
-
expect(params.cfg).toBe(api.config);
|
|
117
|
-
expect(params.mutateConfigFile).toBe(api.runtime.config.mutateConfigFile);
|
|
118
|
-
await expect(params.readInviteCode()).resolves.toBe("A1B2C3");
|
|
119
|
-
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
120
|
-
const parsed = JSON.parse(text) as { ok?: boolean; message?: string };
|
|
121
|
-
expect(parsed.ok).toBe(true);
|
|
122
|
-
expect(parsed.message).toMatch(/activated successfully/i);
|
|
122
|
+
expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
it("
|
|
125
|
+
it("registers only read-only conversation tools", () => {
|
|
126
126
|
const { api, registered } = buildApi({
|
|
127
|
-
configChannel:
|
|
127
|
+
configChannel: configuredChannel(),
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
registerOpenclawClawlingTools(api);
|
|
131
|
-
const tool = registered.find((t) => t.name === "clawchat_activate")!;
|
|
132
|
-
const result = await tool.execute("call-1", { code: " " });
|
|
133
131
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
expect(
|
|
138
|
-
expect(
|
|
132
|
+
const names = registered.map((t) => t.name);
|
|
133
|
+
expect(names).toContain("clawchat_list_conversations");
|
|
134
|
+
expect(names).toContain("clawchat_get_conversation");
|
|
135
|
+
expect(names).not.toContain("clawchat_create_group_conversation");
|
|
136
|
+
expect(names).not.toContain("clawchat_update_conversation");
|
|
137
|
+
expect(names).not.toContain("clawchat_leave_conversation");
|
|
138
|
+
expect(names).not.toContain("clawchat_dissolve_conversation");
|
|
139
|
+
expect(names).not.toContain("clawchat_add_conversation_member");
|
|
140
|
+
expect(names).not.toContain("clawchat_remove_conversation_member");
|
|
141
|
+
expect(names).not.toContain("clawchat_list_conversation_users");
|
|
139
142
|
});
|
|
140
143
|
|
|
141
144
|
it("skips registration when api.config is undefined", () => {
|
|
@@ -144,7 +147,7 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
144
147
|
expect(registered).toHaveLength(0);
|
|
145
148
|
});
|
|
146
149
|
|
|
147
|
-
it("registers all
|
|
150
|
+
it("registers all account/media/search/moment ClawChat tools when configured (regardless of baseUrl)", () => {
|
|
148
151
|
const { api, registered } = buildApi({
|
|
149
152
|
configChannel: configuredChannel(/* no baseUrl */),
|
|
150
153
|
});
|
|
@@ -166,18 +169,10 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
166
169
|
|
|
167
170
|
expect(logger.info).not.toHaveBeenCalled();
|
|
168
171
|
expect(logger.debug).toHaveBeenCalledWith(
|
|
169
|
-
"openclaw-clawchat: registered
|
|
172
|
+
"openclaw-clawchat: registered 16 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)",
|
|
170
173
|
);
|
|
171
174
|
});
|
|
172
175
|
|
|
173
|
-
it("registers clawchat_activate when configured", () => {
|
|
174
|
-
const { api, registered } = buildApi({
|
|
175
|
-
configChannel: configuredChannel(),
|
|
176
|
-
});
|
|
177
|
-
registerOpenclawClawlingTools(api);
|
|
178
|
-
expect(registered.some((t) => t.name === "clawchat_activate")).toBe(true);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
176
|
it("account tools return a config error before activation instead of disappearing", async () => {
|
|
182
177
|
const { api, registered } = buildApi({
|
|
183
178
|
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
@@ -193,6 +188,283 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
193
188
|
expect(parsed.message).toMatch(/token is required/i);
|
|
194
189
|
});
|
|
195
190
|
|
|
191
|
+
it("records successful clawchat tool calls without changing the returned result", async () => {
|
|
192
|
+
const store = { recordToolCall: vi.fn() };
|
|
193
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
194
|
+
new Response(
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
code: 0,
|
|
197
|
+
msg: "ok",
|
|
198
|
+
data: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
199
|
+
}),
|
|
200
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
try {
|
|
204
|
+
const { api, registered } = buildApi({
|
|
205
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
206
|
+
});
|
|
207
|
+
registerOpenclawClawlingTools(api, { store });
|
|
208
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
209
|
+
|
|
210
|
+
const result = await tool.execute("call-1", {});
|
|
211
|
+
|
|
212
|
+
expect((result as { details: unknown }).details).toEqual({
|
|
213
|
+
user_id: "u",
|
|
214
|
+
nickname: "Bot",
|
|
215
|
+
avatar_url: "",
|
|
216
|
+
bio: "",
|
|
217
|
+
});
|
|
218
|
+
expect(store.recordToolCall).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({
|
|
221
|
+
platform: "openclaw",
|
|
222
|
+
accountId: "default",
|
|
223
|
+
toolName: "clawchat_get_account_profile",
|
|
224
|
+
args: {},
|
|
225
|
+
result: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
226
|
+
error: null,
|
|
227
|
+
startedAt: expect.any(Number),
|
|
228
|
+
endedAt: expect.any(Number),
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
} finally {
|
|
232
|
+
fetchMock.mockRestore();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("lists conversations, upserts returned summaries, and does not fetch or delete extra cache entries", async () => {
|
|
237
|
+
const store = {
|
|
238
|
+
recordToolCall: vi.fn(),
|
|
239
|
+
upsertConversationSummary: vi.fn(),
|
|
240
|
+
upsertConversationDetails: vi.fn(),
|
|
241
|
+
deleteConversationCache: vi.fn(),
|
|
242
|
+
};
|
|
243
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
244
|
+
new Response(
|
|
245
|
+
JSON.stringify({
|
|
246
|
+
code: 0,
|
|
247
|
+
msg: "ok",
|
|
248
|
+
data: {
|
|
249
|
+
conversations: [
|
|
250
|
+
{
|
|
251
|
+
id: "cnv_1",
|
|
252
|
+
type: "direct",
|
|
253
|
+
title: "Alice",
|
|
254
|
+
created_at: "2026-05-01T00:00:00Z",
|
|
255
|
+
updated_at: "2026-05-02T00:00:00Z",
|
|
256
|
+
peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
next_before: "cursor-2",
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
try {
|
|
266
|
+
const { api, registered } = buildApi({
|
|
267
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
268
|
+
});
|
|
269
|
+
registerOpenclawClawlingTools(api, { store });
|
|
270
|
+
const tool = registered.find((t) => t.name === "clawchat_list_conversations")!;
|
|
271
|
+
|
|
272
|
+
const result = await tool.execute("call-1", { before: "cursor-1", limit: 10 });
|
|
273
|
+
|
|
274
|
+
expect((result as { details: unknown }).details).toEqual({
|
|
275
|
+
conversations: [
|
|
276
|
+
{
|
|
277
|
+
id: "cnv_1",
|
|
278
|
+
type: "direct",
|
|
279
|
+
title: "Alice",
|
|
280
|
+
created_at: "2026-05-01T00:00:00Z",
|
|
281
|
+
updated_at: "2026-05-02T00:00:00Z",
|
|
282
|
+
peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
next_before: "cursor-2",
|
|
286
|
+
});
|
|
287
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
288
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations?before=cursor-1&limit=10");
|
|
289
|
+
expect(store.upsertConversationSummary).toHaveBeenCalledWith(
|
|
290
|
+
expect.objectContaining({
|
|
291
|
+
platform: "openclaw",
|
|
292
|
+
accountId: "default",
|
|
293
|
+
conversationId: "cnv_1",
|
|
294
|
+
conversationType: "direct",
|
|
295
|
+
raw: expect.objectContaining({ id: "cnv_1" }),
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
expect(store.upsertConversationDetails).not.toHaveBeenCalled();
|
|
299
|
+
expect(store.deleteConversationCache).not.toHaveBeenCalled();
|
|
300
|
+
} finally {
|
|
301
|
+
fetchMock.mockRestore();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("gets a conversation and upserts full details", async () => {
|
|
306
|
+
const store = {
|
|
307
|
+
recordToolCall: vi.fn(),
|
|
308
|
+
upsertConversationSummary: vi.fn(),
|
|
309
|
+
upsertConversationDetails: vi.fn(),
|
|
310
|
+
deleteConversationCache: vi.fn(),
|
|
311
|
+
};
|
|
312
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
313
|
+
new Response(
|
|
314
|
+
JSON.stringify({
|
|
315
|
+
code: 0,
|
|
316
|
+
msg: "ok",
|
|
317
|
+
data: {
|
|
318
|
+
conversation: {
|
|
319
|
+
id: "group_1",
|
|
320
|
+
type: "group",
|
|
321
|
+
title: "Project Group",
|
|
322
|
+
creator_id: "owner",
|
|
323
|
+
created_at: "2026-05-01T00:00:00Z",
|
|
324
|
+
updated_at: "2026-05-02T00:00:00Z",
|
|
325
|
+
participants: [
|
|
326
|
+
{ conversation_id: "group_1", user_id: "owner", role: "owner", joined_at: "2026-05-01T00:00:00Z" },
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
try {
|
|
335
|
+
const { api, registered } = buildApi({
|
|
336
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
337
|
+
});
|
|
338
|
+
registerOpenclawClawlingTools(api, { store });
|
|
339
|
+
const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
|
|
340
|
+
|
|
341
|
+
await tool.execute("call-1", { conversationId: "group_1" });
|
|
342
|
+
|
|
343
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
344
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations/group_1");
|
|
345
|
+
expect(store.upsertConversationDetails).toHaveBeenCalledWith(
|
|
346
|
+
expect.objectContaining({
|
|
347
|
+
platform: "openclaw",
|
|
348
|
+
accountId: "default",
|
|
349
|
+
conversationId: "group_1",
|
|
350
|
+
conversationType: "group",
|
|
351
|
+
raw: expect.objectContaining({ id: "group_1" }),
|
|
352
|
+
groupProfile: expect.objectContaining({ title: "Project Group" }),
|
|
353
|
+
members: [expect.objectContaining({ userId: "owner", role: "owner" })],
|
|
354
|
+
membersComplete: true,
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
} finally {
|
|
358
|
+
fetchMock.mockRestore();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("deletes conversation-scoped cache and returns the standard error result when get conversation is not found", async () => {
|
|
363
|
+
const store = {
|
|
364
|
+
recordToolCall: vi.fn(),
|
|
365
|
+
upsertConversationSummary: vi.fn(),
|
|
366
|
+
upsertConversationDetails: vi.fn(),
|
|
367
|
+
deleteConversationCache: vi.fn(),
|
|
368
|
+
};
|
|
369
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
370
|
+
new Response(JSON.stringify({ code: 40401, msg: "conversation not found", data: null }), {
|
|
371
|
+
status: 200,
|
|
372
|
+
headers: { "content-type": "application/json" },
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
try {
|
|
376
|
+
const { api, registered } = buildApi({
|
|
377
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
378
|
+
});
|
|
379
|
+
registerOpenclawClawlingTools(api, { store });
|
|
380
|
+
const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
|
|
381
|
+
|
|
382
|
+
const result = await tool.execute("call-1", { conversationId: "missing" });
|
|
383
|
+
|
|
384
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
385
|
+
error?: string;
|
|
386
|
+
message?: string;
|
|
387
|
+
meta?: { code?: number };
|
|
388
|
+
};
|
|
389
|
+
expect(parsed).toMatchObject({
|
|
390
|
+
error: "api",
|
|
391
|
+
message: "conversation not found",
|
|
392
|
+
meta: { code: 40401 },
|
|
393
|
+
});
|
|
394
|
+
expect(store.deleteConversationCache).toHaveBeenCalledWith({
|
|
395
|
+
platform: "openclaw",
|
|
396
|
+
accountId: "default",
|
|
397
|
+
conversationId: "missing",
|
|
398
|
+
});
|
|
399
|
+
expect(store.upsertConversationSummary).not.toHaveBeenCalled();
|
|
400
|
+
expect(store.upsertConversationDetails).not.toHaveBeenCalled();
|
|
401
|
+
} finally {
|
|
402
|
+
fetchMock.mockRestore();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("does not write conversation cache from account, user, friend, search, avatar, or media tools", async () => {
|
|
407
|
+
const store = {
|
|
408
|
+
recordToolCall: vi.fn(),
|
|
409
|
+
upsertConversationSummary: vi.fn(),
|
|
410
|
+
upsertConversationDetails: vi.fn(),
|
|
411
|
+
deleteConversationCache: vi.fn(),
|
|
412
|
+
};
|
|
413
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
414
|
+
new Response(
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
code: 0,
|
|
417
|
+
msg: "ok",
|
|
418
|
+
data: { id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
419
|
+
}),
|
|
420
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
421
|
+
),
|
|
422
|
+
);
|
|
423
|
+
try {
|
|
424
|
+
const { api, registered } = buildApi({
|
|
425
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
426
|
+
});
|
|
427
|
+
registerOpenclawClawlingTools(api, { store });
|
|
428
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
429
|
+
|
|
430
|
+
await tool.execute("call-1", {});
|
|
431
|
+
|
|
432
|
+
expect(store.upsertConversationSummary).not.toHaveBeenCalled();
|
|
433
|
+
expect(store.upsertConversationDetails).not.toHaveBeenCalled();
|
|
434
|
+
expect(store.deleteConversationCache).not.toHaveBeenCalled();
|
|
435
|
+
} finally {
|
|
436
|
+
fetchMock.mockRestore();
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("records clawchat tool failures while preserving the returned error result", async () => {
|
|
441
|
+
const store = { recordToolCall: vi.fn() };
|
|
442
|
+
const { api, registered } = buildApi({
|
|
443
|
+
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
444
|
+
});
|
|
445
|
+
registerOpenclawClawlingTools(api, { store });
|
|
446
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
447
|
+
|
|
448
|
+
const result = await tool.execute("call-1", {});
|
|
449
|
+
|
|
450
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
451
|
+
error?: string;
|
|
452
|
+
message?: string;
|
|
453
|
+
};
|
|
454
|
+
expect(parsed).toMatchObject({ error: "config" });
|
|
455
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
platform: "openclaw",
|
|
458
|
+
accountId: "default",
|
|
459
|
+
toolName: "clawchat_get_account_profile",
|
|
460
|
+
args: {},
|
|
461
|
+
result: parsed,
|
|
462
|
+
error: expect.stringContaining("config"),
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
expect(registered.every((tool) => tool.name.startsWith("clawchat_"))).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
|
|
196
468
|
it("clawchat_update_account_profile description names account profile triggers (EN + ZH)", () => {
|
|
197
469
|
const { api } = buildApi({ configChannel: configuredChannel() });
|
|
198
470
|
const fullTools: Array<{ name: string; description?: string }> = [];
|
|
@@ -217,8 +489,16 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
217
489
|
|
|
218
490
|
it("account query and upload tool descriptions include precise trigger semantics", () => {
|
|
219
491
|
const { api } = buildApi({ configChannel: configuredChannel() });
|
|
220
|
-
const fullTools: Array<{
|
|
221
|
-
|
|
492
|
+
const fullTools: Array<{
|
|
493
|
+
name: string;
|
|
494
|
+
description?: string;
|
|
495
|
+
parameters?: { properties?: Record<string, unknown> };
|
|
496
|
+
}> = [];
|
|
497
|
+
api.registerTool = (tool: {
|
|
498
|
+
name: string;
|
|
499
|
+
description?: string;
|
|
500
|
+
parameters?: { properties?: Record<string, unknown> };
|
|
501
|
+
}) => {
|
|
222
502
|
fullTools.push(tool);
|
|
223
503
|
};
|
|
224
504
|
registerOpenclawClawlingTools(api);
|
|
@@ -239,7 +519,8 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
239
519
|
|
|
240
520
|
expect(friends.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
|
|
241
521
|
expect(friends.description).toMatch(/friends|contacts/i);
|
|
242
|
-
expect(friends.description).toMatch(/page=1|pageSize=20/);
|
|
522
|
+
expect(friends.description).not.toMatch(/paginated|page=1|pageSize=20/i);
|
|
523
|
+
expect(friends.parameters?.properties ?? {}).toEqual({});
|
|
243
524
|
|
|
244
525
|
expect(avatar.description).toMatch(/local image/i);
|
|
245
526
|
expect(avatar.description).toMatch(/avatar URL|hosted avatar URL|public URL/i);
|
|
@@ -251,6 +532,52 @@ describe("registerOpenclawClawlingTools", () => {
|
|
|
251
532
|
expect(media.description).toMatch(/not.*avatar|do not use.*avatar/i);
|
|
252
533
|
});
|
|
253
534
|
|
|
535
|
+
it("search and moment tool descriptions match reviewed trigger semantics", () => {
|
|
536
|
+
const { api } = buildApi({ configChannel: configuredChannel() });
|
|
537
|
+
const fullTools: Array<{ name: string; description?: string }> = [];
|
|
538
|
+
api.registerTool = (tool: { name: string; description?: string }) => {
|
|
539
|
+
fullTools.push(tool);
|
|
540
|
+
};
|
|
541
|
+
registerOpenclawClawlingTools(api);
|
|
542
|
+
|
|
543
|
+
const search = fullTools.find((t) => t.name === "clawchat_search_users")!;
|
|
544
|
+
const listMoments = fullTools.find((t) => t.name === "clawchat_list_moments")!;
|
|
545
|
+
const createMoment = fullTools.find((t) => t.name === "clawchat_create_moment")!;
|
|
546
|
+
const deleteMoment = fullTools.find((t) => t.name === "clawchat_delete_moment")!;
|
|
547
|
+
const reaction = fullTools.find((t) => t.name === "clawchat_toggle_moment_reaction")!;
|
|
548
|
+
const comment = fullTools.find((t) => t.name === "clawchat_create_moment_comment")!;
|
|
549
|
+
const reply = fullTools.find((t) => t.name === "clawchat_reply_moment_comment")!;
|
|
550
|
+
const deleteComment = fullTools.find((t) => t.name === "clawchat_delete_moment_comment")!;
|
|
551
|
+
|
|
552
|
+
for (const tool of [
|
|
553
|
+
search,
|
|
554
|
+
listMoments,
|
|
555
|
+
createMoment,
|
|
556
|
+
deleteMoment,
|
|
557
|
+
reaction,
|
|
558
|
+
comment,
|
|
559
|
+
reply,
|
|
560
|
+
deleteComment,
|
|
561
|
+
]) {
|
|
562
|
+
expect(tool.description).toMatch(/Do not use execute/);
|
|
563
|
+
expect(tool.description).toMatch(/direct ClawChat HTTP calls/);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
expect(search.description).toMatch(/Search ClawChat users by username or nickname/);
|
|
567
|
+
expect(search.description).toMatch(/do not guess a userId/);
|
|
568
|
+
expect(listMoments.description).toMatch(/moments\/dynamics\/feed/);
|
|
569
|
+
expect(listMoments.description).toMatch(/friends-only feed endpoint/);
|
|
570
|
+
expect(createMoment.description).toMatch(/At least one of text or images/);
|
|
571
|
+
expect(createMoment.description).toMatch(/do not pass local file paths as images/);
|
|
572
|
+
expect(deleteMoment.description).toMatch(/Do not guess the id/);
|
|
573
|
+
expect(reaction.description).toMatch(/adds the reaction if missing and removes it/);
|
|
574
|
+
expect(comment.description).toMatch(/top-level comment/);
|
|
575
|
+
expect(comment.description).toMatch(/Use clawchat_reply_moment_comment/);
|
|
576
|
+
expect(reply.description).toMatch(/single-level reply/);
|
|
577
|
+
expect(reply.description).toMatch(/do not use this for top-level comments/);
|
|
578
|
+
expect(deleteComment.description).toMatch(/moment and comment ids/);
|
|
579
|
+
});
|
|
580
|
+
|
|
254
581
|
it("clawchat_upload_avatar_image rejects oversized files before upload", async () => {
|
|
255
582
|
const fs = await import("node:fs/promises");
|
|
256
583
|
const path = await import("node:path");
|