@clawling/clawchat-plugin-openclaw 2026.5.12-28
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 +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -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 +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- 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 +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -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.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { Value } from "@sinclair/typebox/value";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { registerOpenclawClawlingTools } from "./tools.ts";
|
|
8
|
+
import * as toolSchemas from "./tools-schema.ts";
|
|
9
|
+
import * as runtime from "./runtime.ts";
|
|
10
|
+
import {
|
|
11
|
+
clearTerminalClawChatSendsForTest,
|
|
12
|
+
consumeTerminalClawChatSend,
|
|
13
|
+
runWithTerminalClawChatSendScope,
|
|
14
|
+
} from "./terminal-send.ts";
|
|
15
|
+
|
|
16
|
+
const schemas = toolSchemas as Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
interface RegisteredTool {
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
parameters?: unknown;
|
|
22
|
+
execute: (callId: string, params: unknown) => Promise<unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ALWAYS_VISIBLE_TOOL_NAMES = [
|
|
26
|
+
"clawchat_create_moment",
|
|
27
|
+
"clawchat_create_moment_comment",
|
|
28
|
+
"clawchat_delete_moment",
|
|
29
|
+
"clawchat_delete_moment_comment",
|
|
30
|
+
"clawchat_get_account_profile",
|
|
31
|
+
"clawchat_get_conversation",
|
|
32
|
+
"clawchat_get_user_profile",
|
|
33
|
+
"clawchat_list_account_friends",
|
|
34
|
+
"clawchat_list_moments",
|
|
35
|
+
"clawchat_memory_edit",
|
|
36
|
+
"clawchat_memory_read",
|
|
37
|
+
"clawchat_memory_search",
|
|
38
|
+
"clawchat_memory_write",
|
|
39
|
+
"clawchat_mention_message",
|
|
40
|
+
"clawchat_metadata_sync",
|
|
41
|
+
"clawchat_metadata_update",
|
|
42
|
+
"clawchat_reply_moment_comment",
|
|
43
|
+
"clawchat_search_users",
|
|
44
|
+
"clawchat_toggle_moment_reaction",
|
|
45
|
+
"clawchat_update_account_profile",
|
|
46
|
+
"clawchat_upload_avatar_image",
|
|
47
|
+
"clawchat_upload_media_file",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const tempRoots: string[] = [];
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
for (const dir of tempRoots.splice(0)) {
|
|
54
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
vi.restoreAllMocks();
|
|
57
|
+
clearTerminalClawChatSendsForTest();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function mockMentionClient() {
|
|
61
|
+
let trace = 0;
|
|
62
|
+
const sent: Array<{ event: string; trace_id: string; payload: { message_id?: string } }> = [];
|
|
63
|
+
const client = Object.assign(new EventEmitter(), {
|
|
64
|
+
sent,
|
|
65
|
+
state: "connected",
|
|
66
|
+
nextTraceId: vi.fn(() => `trace-${++trace}`),
|
|
67
|
+
sendWire: vi.fn((wire: string) => {
|
|
68
|
+
const env = JSON.parse(wire) as {
|
|
69
|
+
event: string;
|
|
70
|
+
trace_id: string;
|
|
71
|
+
payload: { message_id?: string };
|
|
72
|
+
};
|
|
73
|
+
sent.push(env);
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
client.emit("raw", {
|
|
76
|
+
version: "2",
|
|
77
|
+
event: "message.ack",
|
|
78
|
+
trace_id: env.trace_id,
|
|
79
|
+
emitted_at: Date.now(),
|
|
80
|
+
payload: {
|
|
81
|
+
message_id: env.payload.message_id ?? "msg-mention",
|
|
82
|
+
accepted_at: 1234,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}),
|
|
87
|
+
typing: vi.fn(),
|
|
88
|
+
emitRaw: vi.fn(),
|
|
89
|
+
sendRawEnvelope: vi.fn(),
|
|
90
|
+
});
|
|
91
|
+
Object.defineProperty(client, "transportState", { get: () => "open" });
|
|
92
|
+
return client;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildApi(opts: {
|
|
96
|
+
configChannel?: Record<string, unknown> | null;
|
|
97
|
+
configTools?: Record<string, unknown>;
|
|
98
|
+
workspaceDir?: string;
|
|
99
|
+
registerTool?: (tool: { name: string }, options?: { name: string }) => void;
|
|
100
|
+
}) {
|
|
101
|
+
const registered: RegisteredTool[] = [];
|
|
102
|
+
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawchat-plugin-openclaw-tools-"));
|
|
103
|
+
const workspaceDir =
|
|
104
|
+
opts.workspaceDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "clawchat-plugin-openclaw-workspace-"));
|
|
105
|
+
tempRoots.push(stateDir);
|
|
106
|
+
tempRoots.push(workspaceDir);
|
|
107
|
+
const api = {
|
|
108
|
+
config:
|
|
109
|
+
opts.configChannel === null
|
|
110
|
+
? undefined
|
|
111
|
+
: {
|
|
112
|
+
channels: { "clawchat-plugin-openclaw": opts.configChannel ?? {} },
|
|
113
|
+
...(opts.configTools ? { tools: opts.configTools } : {}),
|
|
114
|
+
},
|
|
115
|
+
logger: {
|
|
116
|
+
info: vi.fn(),
|
|
117
|
+
debug: vi.fn(),
|
|
118
|
+
warn: vi.fn(),
|
|
119
|
+
error: vi.fn(),
|
|
120
|
+
},
|
|
121
|
+
runtime: {
|
|
122
|
+
state: {
|
|
123
|
+
resolveStateDir: () => stateDir,
|
|
124
|
+
},
|
|
125
|
+
config: {
|
|
126
|
+
mutateConfigFile: vi.fn(),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
registerTool: (tool: RegisteredTool | ((ctx: unknown) => RegisteredTool | RegisteredTool[] | null | undefined), _options?: { name: string }) => {
|
|
130
|
+
const resolved =
|
|
131
|
+
typeof tool === "function"
|
|
132
|
+
? tool({
|
|
133
|
+
workspaceDir,
|
|
134
|
+
agentId: "agent-default",
|
|
135
|
+
runtimeConfig: {
|
|
136
|
+
channels: { "clawchat-plugin-openclaw": opts.configChannel ?? {} },
|
|
137
|
+
},
|
|
138
|
+
getRuntimeConfig: () => ({
|
|
139
|
+
channels: { "clawchat-plugin-openclaw": opts.configChannel ?? {} },
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
: tool;
|
|
143
|
+
if (Array.isArray(resolved)) {
|
|
144
|
+
registered.push(...resolved);
|
|
145
|
+
} else if (resolved) {
|
|
146
|
+
registered.push(resolved);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
} as unknown as Parameters<typeof registerOpenclawClawlingTools>[0];
|
|
150
|
+
return { api, registered };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function configuredChannel(extra: Record<string, unknown> = {}) {
|
|
154
|
+
return {
|
|
155
|
+
websocketUrl: "wss://w",
|
|
156
|
+
token: "tk",
|
|
157
|
+
agentId: "agt-1",
|
|
158
|
+
userId: "u",
|
|
159
|
+
ownerUserId: "owner-u",
|
|
160
|
+
...extra,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
describe("registerOpenclawClawlingTools", () => {
|
|
165
|
+
it("uses OpenClaw SDK tool result types instead of direct pi-agent-core imports", () => {
|
|
166
|
+
const source = fs.readFileSync(new URL("./tools.ts", import.meta.url), "utf8");
|
|
167
|
+
expect(source).not.toMatch(/@mariozechner\/pi-agent-core/);
|
|
168
|
+
expect(source).toMatch(/openclaw\/plugin-sdk\/agent-harness-runtime/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("registers all ClawChat tools even when account.configured is false", () => {
|
|
172
|
+
const { api, registered } = buildApi({
|
|
173
|
+
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
174
|
+
});
|
|
175
|
+
registerOpenclawClawlingTools(api);
|
|
176
|
+
expect(registered.map((t) => t.name).sort()).toEqual(ALWAYS_VISIBLE_TOOL_NAMES);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("does not mutate tool policy during registration before account activation", () => {
|
|
180
|
+
const { api, registered } = buildApi({
|
|
181
|
+
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
182
|
+
configTools: { profile: "coding", allow: [] },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
registerOpenclawClawlingTools(api);
|
|
186
|
+
|
|
187
|
+
expect(registered.map((t) => t.name).sort()).toEqual(ALWAYS_VISIBLE_TOOL_NAMES);
|
|
188
|
+
expect(api.config?.tools).toEqual({
|
|
189
|
+
profile: "coding",
|
|
190
|
+
allow: [],
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("does not register invite-code activation as an agent tool", () => {
|
|
195
|
+
const { api, registered } = buildApi({
|
|
196
|
+
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
registerOpenclawClawlingTools(api);
|
|
200
|
+
|
|
201
|
+
expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("registers only read-only conversation tools", () => {
|
|
205
|
+
const { api, registered } = buildApi({
|
|
206
|
+
configChannel: configuredChannel(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
registerOpenclawClawlingTools(api);
|
|
210
|
+
|
|
211
|
+
const names = registered.map((t) => t.name);
|
|
212
|
+
expect(names).toContain("clawchat_get_conversation");
|
|
213
|
+
expect(names).not.toContain("clawchat_create_group_conversation");
|
|
214
|
+
expect(names).not.toContain("clawchat_update_conversation");
|
|
215
|
+
expect(names).not.toContain("clawchat_leave_conversation");
|
|
216
|
+
expect(names).not.toContain("clawchat_dissolve_conversation");
|
|
217
|
+
expect(names).not.toContain("clawchat_add_conversation_member");
|
|
218
|
+
expect(names).not.toContain("clawchat_remove_conversation_member");
|
|
219
|
+
expect(names).not.toContain("clawchat_list_conversation_users");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("skips registration when api.config is undefined", () => {
|
|
223
|
+
const { api, registered } = buildApi({ configChannel: null });
|
|
224
|
+
registerOpenclawClawlingTools(api);
|
|
225
|
+
expect(registered).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("registers all account/media/search/moment ClawChat tools when configured (regardless of baseUrl)", () => {
|
|
229
|
+
const { api, registered } = buildApi({
|
|
230
|
+
configChannel: configuredChannel(/* no baseUrl */),
|
|
231
|
+
});
|
|
232
|
+
registerOpenclawClawlingTools(api);
|
|
233
|
+
const names = registered.map((t) => t.name).sort();
|
|
234
|
+
expect(names).toEqual(ALWAYS_VISIBLE_TOOL_NAMES);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("logs configured tool registration at debug level only", () => {
|
|
238
|
+
const { api } = buildApi({
|
|
239
|
+
configChannel: configuredChannel(),
|
|
240
|
+
});
|
|
241
|
+
const logger = api.logger as {
|
|
242
|
+
info: ReturnType<typeof vi.fn>;
|
|
243
|
+
debug: ReturnType<typeof vi.fn>;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
registerOpenclawClawlingTools(api);
|
|
247
|
+
|
|
248
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
249
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
250
|
+
"clawchat-plugin-openclaw: registered 22 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, search_users, get_conversation, mention_message, 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_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update)",
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("registers clawchat_mention_message with mention target schema", () => {
|
|
255
|
+
const { api, registered } = buildApi({
|
|
256
|
+
configChannel: configuredChannel(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
registerOpenclawClawlingTools(api);
|
|
260
|
+
|
|
261
|
+
const mention = registered.find((t) => t.name === "clawchat_mention_message");
|
|
262
|
+
expect(mention).toBeDefined();
|
|
263
|
+
expect(mention?.description).toMatch(/@|mention|notify|address/i);
|
|
264
|
+
expect(mention?.description).toContain("Use `mentioned_users` or `sender_id` from current `[message]` blocks as `mentions[].userId`");
|
|
265
|
+
expect(mention?.description).toContain("Do not copy the visible @mention into `text`; put only the message body in `text`");
|
|
266
|
+
const parameters = mention?.parameters as { properties?: Record<string, unknown> } | undefined;
|
|
267
|
+
const properties = parameters?.properties ?? {};
|
|
268
|
+
expect(properties).toHaveProperty("mentions");
|
|
269
|
+
expect(JSON.stringify(properties.mentions)).toMatch(/userId/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("registers exactly the six ClawChat memory and metadata tools without old-prefix aliases", () => {
|
|
273
|
+
const { api, registered } = buildApi({
|
|
274
|
+
configChannel: configuredChannel({ ownerUserId: "owner" }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
registerOpenclawClawlingTools(api);
|
|
278
|
+
|
|
279
|
+
const names = registered.map((t) => t.name).sort();
|
|
280
|
+
expect(names.filter((name) => /memory|metadata/.test(name))).toEqual([
|
|
281
|
+
"clawchat_memory_edit",
|
|
282
|
+
"clawchat_memory_read",
|
|
283
|
+
"clawchat_memory_search",
|
|
284
|
+
"clawchat_memory_write",
|
|
285
|
+
"clawchat_metadata_sync",
|
|
286
|
+
"clawchat_metadata_update",
|
|
287
|
+
]);
|
|
288
|
+
expect(names).not.toContain("cc_memory_read");
|
|
289
|
+
expect(names).not.toContain("cc_metadata_sync");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("keeps old ClawChat tools registered and guarded against direct HTTP or shell fallbacks", () => {
|
|
293
|
+
const { api, registered } = buildApi({
|
|
294
|
+
configChannel: configuredChannel({ ownerUserId: "owner" }),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
registerOpenclawClawlingTools(api);
|
|
298
|
+
|
|
299
|
+
const oldTools = registered.filter((tool) => !/memory|metadata/.test(tool.name));
|
|
300
|
+
expect(oldTools.map((tool) => tool.name).sort()).toEqual(
|
|
301
|
+
ALWAYS_VISIBLE_TOOL_NAMES.filter((name) => !/memory|metadata/.test(name)),
|
|
302
|
+
);
|
|
303
|
+
for (const tool of oldTools) {
|
|
304
|
+
expect(tool.description).toMatch(/Do not use execute/);
|
|
305
|
+
expect(tool.description).toMatch(/shell commands/);
|
|
306
|
+
expect(tool.description).toMatch(/curl/);
|
|
307
|
+
expect(tool.description).toMatch(/direct ClawChat HTTP calls/);
|
|
308
|
+
expect(tool.description).toMatch(/generic fallback tools/);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("memory and metadata schemas require explicit targets and bounded modes", () => {
|
|
313
|
+
const readSchema = schemas.ClawchatMemoryReadSchema;
|
|
314
|
+
const searchSchema = schemas.ClawchatMemorySearchSchema;
|
|
315
|
+
const writeSchema = schemas.ClawchatMemoryWriteSchema;
|
|
316
|
+
const editSchema = schemas.ClawchatMemoryEditSchema;
|
|
317
|
+
const syncSchema = schemas.ClawchatMetadataSyncSchema;
|
|
318
|
+
const updateSchema = schemas.ClawchatMetadataUpdateSchema;
|
|
319
|
+
expect(readSchema).toBeDefined();
|
|
320
|
+
expect(searchSchema).toBeDefined();
|
|
321
|
+
expect(writeSchema).toBeDefined();
|
|
322
|
+
expect(editSchema).toBeDefined();
|
|
323
|
+
expect(syncSchema).toBeDefined();
|
|
324
|
+
expect(updateSchema).toBeDefined();
|
|
325
|
+
|
|
326
|
+
for (const schema of [searchSchema, readSchema, writeSchema, editSchema, syncSchema, updateSchema]) {
|
|
327
|
+
expect((schema as { type?: unknown }).type).toBe("object");
|
|
328
|
+
expect(schema).not.toHaveProperty("anyOf");
|
|
329
|
+
expect(schema).not.toHaveProperty("oneOf");
|
|
330
|
+
}
|
|
331
|
+
expect(Value.Check(searchSchema, { query: "才哥" })).toBe(true);
|
|
332
|
+
expect(Value.Check(searchSchema, { query: "才哥", targetTypes: ["user"], maxResults: 5 })).toBe(true);
|
|
333
|
+
expect(Value.Check(searchSchema, { query: "" })).toBe(false);
|
|
334
|
+
expect(Value.Check(searchSchema, { query: "才哥", targetTypes: ["profile"] })).toBe(false);
|
|
335
|
+
|
|
336
|
+
for (const targetType of ["owner", "user", "group"] as const) {
|
|
337
|
+
expect(Value.Check(readSchema, { targetType, targetId: targetType === "owner" ? "owner" : "id_1" })).toBe(true);
|
|
338
|
+
}
|
|
339
|
+
expect(Value.Check(readSchema, { targetType: "profile", targetId: "id_1" })).toBe(false);
|
|
340
|
+
expect(Value.Check(readSchema, { targetType: "user" })).toBe(false);
|
|
341
|
+
expect(Value.Check(readSchema, { targetType: "user", targetId: "id_1", filePath: "users/id_1.md" })).toBe(false);
|
|
342
|
+
|
|
343
|
+
expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "append", content: "note" })).toBe(true);
|
|
344
|
+
expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "replace", content: "" })).toBe(true);
|
|
345
|
+
expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "merge", content: "note" })).toBe(false);
|
|
346
|
+
|
|
347
|
+
expect(Value.Check(syncSchema, { targetType: "owner", targetId: "owner", direction: "pull" })).toBe(true);
|
|
348
|
+
expect(Value.Check(syncSchema, { targetType: "group", targetId: "grp_1", direction: "push" })).toBe(true);
|
|
349
|
+
expect(Value.Check(syncSchema, { targetType: "group", targetId: "grp_1", direction: "refresh" })).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("metadata update schema requires a non-empty whitelisted patch", () => {
|
|
353
|
+
const schema = schemas.ClawchatMetadataUpdateSchema;
|
|
354
|
+
expect(schema).toBeDefined();
|
|
355
|
+
|
|
356
|
+
expect(Value.Check(schema, {
|
|
357
|
+
targetType: "owner",
|
|
358
|
+
targetId: "owner",
|
|
359
|
+
patch: { agent_behavior: "Answer concisely." },
|
|
360
|
+
})).toBe(true);
|
|
361
|
+
expect(Value.Check(schema, {
|
|
362
|
+
targetType: "user",
|
|
363
|
+
targetId: "usr_1",
|
|
364
|
+
patch: { avatar_url: "https://cdn/avatar.png", bio: "" },
|
|
365
|
+
})).toBe(true);
|
|
366
|
+
expect(Value.Check(schema, {
|
|
367
|
+
targetType: "group",
|
|
368
|
+
targetId: "grp_1",
|
|
369
|
+
patch: { group_title: "Team", group_description: "Planning" },
|
|
370
|
+
})).toBe(true);
|
|
371
|
+
expect(Value.Check(schema, { targetType: "owner", targetId: "owner" })).toBe(false);
|
|
372
|
+
expect(Value.Check(schema, { targetType: "owner", targetId: "owner", patch: {} })).toBe(false);
|
|
373
|
+
for (const key of ["visibility", "status", "platform", "raw", "kind"]) {
|
|
374
|
+
expect(Value.Check(schema, {
|
|
375
|
+
targetType: "owner",
|
|
376
|
+
targetId: "owner",
|
|
377
|
+
patch: { [key]: "not allowed" },
|
|
378
|
+
})).toBe(false);
|
|
379
|
+
expect(Value.Check(schema, {
|
|
380
|
+
targetType: "group",
|
|
381
|
+
targetId: "grp_1",
|
|
382
|
+
patch: { [key]: "not allowed" },
|
|
383
|
+
})).toBe(false);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("memory tool safety errors do not expose workspace paths", async () => {
|
|
388
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawchat-plugin-openclaw-workspace-"));
|
|
389
|
+
tempRoots.push(workspaceDir);
|
|
390
|
+
fs.symlinkSync(os.tmpdir(), path.join(workspaceDir, "users"));
|
|
391
|
+
const { api, registered } = buildApi({
|
|
392
|
+
configChannel: configuredChannel({ ownerUserId: "owner" }),
|
|
393
|
+
workspaceDir,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
registerOpenclawClawlingTools(api);
|
|
397
|
+
const tool = registered.find((t) => t.name === "clawchat_memory_read")!;
|
|
398
|
+
const result = await tool.execute("call-1", { targetType: "user", targetId: "usr_1" });
|
|
399
|
+
|
|
400
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
401
|
+
error?: string;
|
|
402
|
+
message?: string;
|
|
403
|
+
};
|
|
404
|
+
expect(parsed).toMatchObject({
|
|
405
|
+
error: "validation",
|
|
406
|
+
message: "clawchat-plugin-openclaw: unsafe ClawChat memory target",
|
|
407
|
+
});
|
|
408
|
+
expect(parsed.message).not.toContain(workspaceDir);
|
|
409
|
+
expect(parsed.message).not.toContain(os.tmpdir());
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("clawchat_mention_message validates empty mentions before waiting for a client", async () => {
|
|
413
|
+
const { api, registered } = buildApi({
|
|
414
|
+
configChannel: configuredChannel(),
|
|
415
|
+
});
|
|
416
|
+
registerOpenclawClawlingTools(api);
|
|
417
|
+
const tool = registered.find((t) => t.name === "clawchat_mention_message")!;
|
|
418
|
+
|
|
419
|
+
const result = await Promise.race([
|
|
420
|
+
tool.execute("call-1", { chatId: "group-1", mentions: [] }),
|
|
421
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), 100)),
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
expect(result).not.toBe("timeout");
|
|
425
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
426
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
427
|
+
expect(parsed.error).toBe("validation");
|
|
428
|
+
expect(parsed.message).toMatch(/mention/i);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("clawchat_mention_message success returns terminal send result and marks reply suppression", async () => {
|
|
432
|
+
const client = mockMentionClient();
|
|
433
|
+
vi.spyOn(runtime, "getOpenclawClawlingClient").mockReturnValue(client as never);
|
|
434
|
+
const { api, registered } = buildApi({
|
|
435
|
+
configChannel: configuredChannel(),
|
|
436
|
+
});
|
|
437
|
+
registerOpenclawClawlingTools(api);
|
|
438
|
+
const tool = registered.find((t) => t.name === "clawchat_mention_message")!;
|
|
439
|
+
|
|
440
|
+
const result = await runWithTerminalClawChatSendScope("scope-1", () =>
|
|
441
|
+
tool.execute("call-1", {
|
|
442
|
+
chatId: "group-1",
|
|
443
|
+
mentions: [{ userId: "user-1", display: "Alice" }],
|
|
444
|
+
text: "please check",
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
449
|
+
sent?: boolean;
|
|
450
|
+
terminal?: boolean;
|
|
451
|
+
noFollowupReply?: boolean;
|
|
452
|
+
instruction?: string;
|
|
453
|
+
messageId?: string;
|
|
454
|
+
mentions?: string[];
|
|
455
|
+
};
|
|
456
|
+
expect(parsed).toMatchObject({
|
|
457
|
+
sent: true,
|
|
458
|
+
terminal: true,
|
|
459
|
+
noFollowupReply: true,
|
|
460
|
+
messageId: "msg-mention",
|
|
461
|
+
mentions: ["user-1"],
|
|
462
|
+
});
|
|
463
|
+
expect(parsed.instruction).toContain("output only <clawchat:no-reply/>");
|
|
464
|
+
expect(consumeTerminalClawChatSend({
|
|
465
|
+
accountId: "default",
|
|
466
|
+
chatId: "group-1",
|
|
467
|
+
scopeId: "scope-1",
|
|
468
|
+
})?.messageId).toBe("msg-mention");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("clawchat_mention_message returns config error for token-only unconfigured account", async () => {
|
|
472
|
+
const { api, registered } = buildApi({
|
|
473
|
+
configChannel: { websocketUrl: "wss://w", token: "tk" },
|
|
474
|
+
});
|
|
475
|
+
registerOpenclawClawlingTools(api);
|
|
476
|
+
const tool = registered.find((t) => t.name === "clawchat_mention_message")!;
|
|
477
|
+
|
|
478
|
+
const result = await Promise.race([
|
|
479
|
+
tool.execute("call-1", {
|
|
480
|
+
chatId: "group-1",
|
|
481
|
+
mentions: [{ userId: "user-1" }],
|
|
482
|
+
}),
|
|
483
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), 100)),
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
expect(result).not.toBe("timeout");
|
|
487
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
488
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
489
|
+
expect(parsed.error).toBe("config");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("clawchat_mention_message returns validation error for non-string replyToMessageId", async () => {
|
|
493
|
+
const { api, registered } = buildApi({
|
|
494
|
+
configChannel: configuredChannel(),
|
|
495
|
+
});
|
|
496
|
+
registerOpenclawClawlingTools(api);
|
|
497
|
+
const tool = registered.find((t) => t.name === "clawchat_mention_message")!;
|
|
498
|
+
|
|
499
|
+
const result = await tool.execute("call-1", {
|
|
500
|
+
chatId: "group-1",
|
|
501
|
+
mentions: [{ userId: "user-1" }],
|
|
502
|
+
replyToMessageId: null,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
506
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
507
|
+
expect(parsed.error).toBe("validation");
|
|
508
|
+
expect(parsed.message).toMatch(/replyToMessageId/i);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("clawchat_mention_message accepts padded replyToMessageId before config guard", async () => {
|
|
512
|
+
const { api, registered } = buildApi({
|
|
513
|
+
configChannel: { websocketUrl: "wss://w", token: "tk" },
|
|
514
|
+
});
|
|
515
|
+
registerOpenclawClawlingTools(api);
|
|
516
|
+
const tool = registered.find((t) => t.name === "clawchat_mention_message")!;
|
|
517
|
+
|
|
518
|
+
const result = await tool.execute("call-1", {
|
|
519
|
+
chatId: "group-1",
|
|
520
|
+
mentions: [{ userId: "user-1" }],
|
|
521
|
+
replyToMessageId: " msg-1 ",
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
525
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
526
|
+
expect(parsed.error).toBe("config");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("clawchat_mention_message returns config error when websocket client is not ready", async () => {
|
|
530
|
+
const { api, registered } = buildApi({
|
|
531
|
+
configChannel: configuredChannel(),
|
|
532
|
+
});
|
|
533
|
+
registerOpenclawClawlingTools(api);
|
|
534
|
+
const tool = registered.find((t) => t.name === "clawchat_mention_message")!;
|
|
535
|
+
|
|
536
|
+
const result = await Promise.race([
|
|
537
|
+
tool.execute("call-1", {
|
|
538
|
+
chatId: "group-1",
|
|
539
|
+
mentions: [{ userId: "user-1" }],
|
|
540
|
+
}),
|
|
541
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), 100)),
|
|
542
|
+
]);
|
|
543
|
+
|
|
544
|
+
expect(result).not.toBe("timeout");
|
|
545
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
546
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
547
|
+
expect(parsed).toEqual({
|
|
548
|
+
error: "config",
|
|
549
|
+
message: "ClawChat websocket client is not ready",
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("account tools return a config error before activation instead of disappearing", async () => {
|
|
554
|
+
const { api, registered } = buildApi({
|
|
555
|
+
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
registerOpenclawClawlingTools(api);
|
|
559
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
560
|
+
const result = await tool.execute("call-1", {});
|
|
561
|
+
|
|
562
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
563
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
564
|
+
expect(parsed.error).toBe("config");
|
|
565
|
+
expect(parsed.message).toMatch(/token is required/i);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("records successful clawchat tool calls without changing the returned result", async () => {
|
|
569
|
+
const store = { recordToolCall: vi.fn() };
|
|
570
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
571
|
+
new Response(
|
|
572
|
+
JSON.stringify({
|
|
573
|
+
code: 0,
|
|
574
|
+
msg: "ok",
|
|
575
|
+
data: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
576
|
+
}),
|
|
577
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
try {
|
|
581
|
+
const { api, registered } = buildApi({
|
|
582
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
583
|
+
});
|
|
584
|
+
registerOpenclawClawlingTools(api, { store });
|
|
585
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
586
|
+
|
|
587
|
+
const result = await tool.execute("call-1", {});
|
|
588
|
+
|
|
589
|
+
expect((result as { details: unknown }).details).toEqual({
|
|
590
|
+
user_id: "u",
|
|
591
|
+
nickname: "Bot",
|
|
592
|
+
avatar_url: "",
|
|
593
|
+
bio: "",
|
|
594
|
+
});
|
|
595
|
+
expect(store.recordToolCall).toHaveBeenCalledTimes(1);
|
|
596
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(
|
|
597
|
+
expect.objectContaining({
|
|
598
|
+
platform: "openclaw",
|
|
599
|
+
accountId: "default",
|
|
600
|
+
toolName: "clawchat_get_account_profile",
|
|
601
|
+
args: {},
|
|
602
|
+
result: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
603
|
+
error: null,
|
|
604
|
+
startedAt: expect.any(Number),
|
|
605
|
+
endedAt: expect.any(Number),
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
} finally {
|
|
609
|
+
fetchMock.mockRestore();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("gets a conversation without writing SQLite conversation state", async () => {
|
|
614
|
+
const store = {
|
|
615
|
+
recordToolCall: vi.fn(),
|
|
616
|
+
};
|
|
617
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
618
|
+
new Response(
|
|
619
|
+
JSON.stringify({
|
|
620
|
+
code: 0,
|
|
621
|
+
msg: "ok",
|
|
622
|
+
data: {
|
|
623
|
+
conversation: {
|
|
624
|
+
id: "group_1",
|
|
625
|
+
type: "group",
|
|
626
|
+
title: "Project Group",
|
|
627
|
+
creator_id: "owner",
|
|
628
|
+
created_at: "2026-05-01T00:00:00Z",
|
|
629
|
+
updated_at: "2026-05-02T00:00:00Z",
|
|
630
|
+
participants: [
|
|
631
|
+
{ conversation_id: "group_1", user_id: "owner", role: "owner", joined_at: "2026-05-01T00:00:00Z" },
|
|
632
|
+
],
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
}),
|
|
636
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
try {
|
|
640
|
+
const { api, registered } = buildApi({
|
|
641
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
642
|
+
});
|
|
643
|
+
registerOpenclawClawlingTools(api, { store });
|
|
644
|
+
const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
|
|
645
|
+
|
|
646
|
+
await tool.execute("call-1", { conversationId: "group_1" });
|
|
647
|
+
|
|
648
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
649
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations/group_1");
|
|
650
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(expect.objectContaining({
|
|
651
|
+
platform: "openclaw",
|
|
652
|
+
accountId: "default",
|
|
653
|
+
toolName: "clawchat_get_conversation",
|
|
654
|
+
}));
|
|
655
|
+
} finally {
|
|
656
|
+
fetchMock.mockRestore();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("returns the standard error result when get conversation is not found", async () => {
|
|
661
|
+
const store = {
|
|
662
|
+
recordToolCall: vi.fn(),
|
|
663
|
+
};
|
|
664
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
665
|
+
new Response(JSON.stringify({ code: 40401, msg: "conversation not found", data: null }), {
|
|
666
|
+
status: 200,
|
|
667
|
+
headers: { "content-type": "application/json" },
|
|
668
|
+
}),
|
|
669
|
+
);
|
|
670
|
+
try {
|
|
671
|
+
const { api, registered } = buildApi({
|
|
672
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
673
|
+
});
|
|
674
|
+
registerOpenclawClawlingTools(api, { store });
|
|
675
|
+
const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
|
|
676
|
+
|
|
677
|
+
const result = await tool.execute("call-1", { conversationId: "missing" });
|
|
678
|
+
|
|
679
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
680
|
+
error?: string;
|
|
681
|
+
message?: string;
|
|
682
|
+
meta?: { code?: number };
|
|
683
|
+
};
|
|
684
|
+
expect(parsed).toMatchObject({
|
|
685
|
+
error: "api",
|
|
686
|
+
message: "conversation not found",
|
|
687
|
+
meta: { code: 40401 },
|
|
688
|
+
});
|
|
689
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(expect.objectContaining({
|
|
690
|
+
platform: "openclaw",
|
|
691
|
+
accountId: "default",
|
|
692
|
+
toolName: "clawchat_get_conversation",
|
|
693
|
+
error: "api: conversation not found",
|
|
694
|
+
}));
|
|
695
|
+
} finally {
|
|
696
|
+
fetchMock.mockRestore();
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("does not require SQLite conversation state methods from account, user, friend, search, avatar, or media tools", async () => {
|
|
701
|
+
const store = {
|
|
702
|
+
recordToolCall: vi.fn(),
|
|
703
|
+
};
|
|
704
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
705
|
+
new Response(
|
|
706
|
+
JSON.stringify({
|
|
707
|
+
code: 0,
|
|
708
|
+
msg: "ok",
|
|
709
|
+
data: { id: "u", nickname: "Bot", avatar_url: "", bio: "" },
|
|
710
|
+
}),
|
|
711
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
712
|
+
),
|
|
713
|
+
);
|
|
714
|
+
try {
|
|
715
|
+
const { api, registered } = buildApi({
|
|
716
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
717
|
+
});
|
|
718
|
+
registerOpenclawClawlingTools(api, { store });
|
|
719
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
720
|
+
|
|
721
|
+
await tool.execute("call-1", {});
|
|
722
|
+
|
|
723
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(expect.objectContaining({
|
|
724
|
+
platform: "openclaw",
|
|
725
|
+
accountId: "default",
|
|
726
|
+
toolName: "clawchat_get_account_profile",
|
|
727
|
+
}));
|
|
728
|
+
} finally {
|
|
729
|
+
fetchMock.mockRestore();
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("records clawchat tool failures while preserving the returned error result", async () => {
|
|
734
|
+
const store = { recordToolCall: vi.fn() };
|
|
735
|
+
const { api, registered } = buildApi({
|
|
736
|
+
configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
|
|
737
|
+
});
|
|
738
|
+
registerOpenclawClawlingTools(api, { store });
|
|
739
|
+
const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
740
|
+
|
|
741
|
+
const result = await tool.execute("call-1", {});
|
|
742
|
+
|
|
743
|
+
const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
|
|
744
|
+
error?: string;
|
|
745
|
+
message?: string;
|
|
746
|
+
};
|
|
747
|
+
expect(parsed).toMatchObject({ error: "config" });
|
|
748
|
+
expect(store.recordToolCall).toHaveBeenCalledWith(
|
|
749
|
+
expect.objectContaining({
|
|
750
|
+
platform: "openclaw",
|
|
751
|
+
accountId: "default",
|
|
752
|
+
toolName: "clawchat_get_account_profile",
|
|
753
|
+
args: {},
|
|
754
|
+
result: parsed,
|
|
755
|
+
error: expect.stringContaining("config"),
|
|
756
|
+
}),
|
|
757
|
+
);
|
|
758
|
+
expect(registered.every((tool) => tool.name.startsWith("clawchat_"))).toBe(true);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("clawchat_update_account_profile description names account profile triggers (EN + ZH)", () => {
|
|
762
|
+
const { api } = buildApi({ configChannel: configuredChannel() });
|
|
763
|
+
const fullTools: Array<{ name: string; description?: string }> = [];
|
|
764
|
+
api.registerTool = (tool: { name: string; description?: string }) => {
|
|
765
|
+
fullTools.push(tool);
|
|
766
|
+
};
|
|
767
|
+
registerOpenclawClawlingTools(api);
|
|
768
|
+
const update = fullTools.find((t) => t.name === "clawchat_update_account_profile")!;
|
|
769
|
+
expect(update).toBeDefined();
|
|
770
|
+
expect(update.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
|
|
771
|
+
expect(update.description).toMatch(/ClawChat (account )?(nickname|name)/i);
|
|
772
|
+
expect(update.description).toMatch(/ClawChat 昵称|账号昵称|账号名字/);
|
|
773
|
+
expect(update.description).toMatch(/avatar|profile picture/i);
|
|
774
|
+
expect(update.description).toMatch(/ClawChat 头像|账号头像/);
|
|
775
|
+
expect(update.description).toMatch(/clawchat_upload_avatar_image/);
|
|
776
|
+
expect(update.description).toMatch(/bio|self-introduction/i);
|
|
777
|
+
expect(update.description).toMatch(/ClawChat 简介|账号简介|个人简介/);
|
|
778
|
+
expect(update.description).not.toMatch(
|
|
779
|
+
new RegExp(["agent's own", "rename " + "yourself", "this agent's own"].join("|"), "i"),
|
|
780
|
+
);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("account query and upload tool descriptions include precise trigger semantics", () => {
|
|
784
|
+
const { api, registered: fullTools } = buildApi({ configChannel: configuredChannel() });
|
|
785
|
+
registerOpenclawClawlingTools(api);
|
|
786
|
+
|
|
787
|
+
const accountProfile = fullTools.find((t) => t.name === "clawchat_get_account_profile")!;
|
|
788
|
+
const userProfile = fullTools.find((t) => t.name === "clawchat_get_user_profile")!;
|
|
789
|
+
const memoryWrite = fullTools.find((t) => t.name === "clawchat_memory_write")!;
|
|
790
|
+
const metadataSync = fullTools.find((t) => t.name === "clawchat_metadata_sync")!;
|
|
791
|
+
const friends = fullTools.find((t) => t.name === "clawchat_list_account_friends")!;
|
|
792
|
+
const avatar = fullTools.find((t) => t.name === "clawchat_upload_avatar_image")!;
|
|
793
|
+
const media = fullTools.find((t) => t.name === "clawchat_upload_media_file")!;
|
|
794
|
+
|
|
795
|
+
expect(accountProfile.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
|
|
796
|
+
expect(accountProfile.description).toMatch(/profile|nickname|avatar|bio/i);
|
|
797
|
+
expect(accountProfile.description).not.toMatch(/agent's own|this agent's/i);
|
|
798
|
+
|
|
799
|
+
expect(userProfile.description).toMatch(/userId/);
|
|
800
|
+
expect(userProfile.description).toMatch(/public profile/i);
|
|
801
|
+
expect(userProfile.description).toMatch(/do not guess|do not infer/i);
|
|
802
|
+
expect(userProfile.description).toMatch(/read-only lookup/i);
|
|
803
|
+
expect(userProfile.description).toMatch(/clawchat_metadata_sync.*direction=pull/i);
|
|
804
|
+
|
|
805
|
+
expect(memoryWrite.description).toMatch(/agent-authored body/i);
|
|
806
|
+
expect(memoryWrite.description).toMatch(/never modifies the metadata block/i);
|
|
807
|
+
expect(memoryWrite.description).toMatch(/Do not use.*nickname.*avatar_url.*bio.*profile_type/i);
|
|
808
|
+
expect(memoryWrite.description).toMatch(/refresh.*local ClawChat.*profile.*clawchat_metadata_sync.*direction=pull/i);
|
|
809
|
+
|
|
810
|
+
expect(metadataSync.description).toMatch(/refresh, sync, or update local ClawChat current agent profile\/behavior, agent-owner profile information/i);
|
|
811
|
+
expect(metadataSync.description).toMatch(/rewrites only the metadata block/i);
|
|
812
|
+
expect(metadataSync.description).toMatch(/Do not combine clawchat_get_user_profile with clawchat_memory_write/i);
|
|
813
|
+
|
|
814
|
+
expect(friends.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
|
|
815
|
+
expect(friends.description).toMatch(/friends|contacts/i);
|
|
816
|
+
expect(friends.description).not.toMatch(/paginated|page=1|pageSize=20/i);
|
|
817
|
+
expect(friends.parameters?.properties ?? {}).toEqual({});
|
|
818
|
+
|
|
819
|
+
expect(avatar.description).toMatch(/local image/i);
|
|
820
|
+
expect(avatar.description).toMatch(/avatar URL|hosted avatar URL|public URL/i);
|
|
821
|
+
expect(avatar.description).toMatch(/clawchat_update_account_profile/);
|
|
822
|
+
expect(avatar.description).toMatch(/does not update|does not set/i);
|
|
823
|
+
|
|
824
|
+
expect(media.description).toMatch(/local file|media file/i);
|
|
825
|
+
expect(media.description).toMatch(/public URL|shareable URL/i);
|
|
826
|
+
expect(media.description).toMatch(/not.*avatar|do not use.*avatar/i);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it("search and moment tool descriptions match reviewed trigger semantics", () => {
|
|
830
|
+
const { api, registered: fullTools } = buildApi({ configChannel: configuredChannel() });
|
|
831
|
+
registerOpenclawClawlingTools(api);
|
|
832
|
+
|
|
833
|
+
const search = fullTools.find((t) => t.name === "clawchat_search_users")!;
|
|
834
|
+
const memorySearch = fullTools.find((t) => t.name === "clawchat_memory_search")!;
|
|
835
|
+
const memoryRead = fullTools.find((t) => t.name === "clawchat_memory_read")!;
|
|
836
|
+
const listMoments = fullTools.find((t) => t.name === "clawchat_list_moments")!;
|
|
837
|
+
const createMoment = fullTools.find((t) => t.name === "clawchat_create_moment")!;
|
|
838
|
+
const deleteMoment = fullTools.find((t) => t.name === "clawchat_delete_moment")!;
|
|
839
|
+
const reaction = fullTools.find((t) => t.name === "clawchat_toggle_moment_reaction")!;
|
|
840
|
+
const comment = fullTools.find((t) => t.name === "clawchat_create_moment_comment")!;
|
|
841
|
+
const reply = fullTools.find((t) => t.name === "clawchat_reply_moment_comment")!;
|
|
842
|
+
const deleteComment = fullTools.find((t) => t.name === "clawchat_delete_moment_comment")!;
|
|
843
|
+
|
|
844
|
+
for (const tool of [
|
|
845
|
+
search,
|
|
846
|
+
listMoments,
|
|
847
|
+
createMoment,
|
|
848
|
+
deleteMoment,
|
|
849
|
+
reaction,
|
|
850
|
+
comment,
|
|
851
|
+
reply,
|
|
852
|
+
deleteComment,
|
|
853
|
+
]) {
|
|
854
|
+
expect(tool.description).toMatch(/Do not use execute/);
|
|
855
|
+
expect(tool.description).toMatch(/direct ClawChat HTTP calls/);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
expect(search.description).toMatch(/Search ClawChat users by username or nickname/);
|
|
859
|
+
expect(search.description).toMatch(/server-side ClawChat users/i);
|
|
860
|
+
expect(search.description).toMatch(/does not search local ClawChat memory/i);
|
|
861
|
+
expect(search.description).toMatch(/clawchat_memory_search/);
|
|
862
|
+
expect(search.description).toMatch(/do not guess a userId/);
|
|
863
|
+
expect(memorySearch.description).toMatch(/Search local ClawChat memory Markdown files/i);
|
|
864
|
+
expect(memorySearch.description).toMatch(/owner\.md, users\/\*\.md, and groups\/\*\.md/);
|
|
865
|
+
expect(memorySearch.description).toMatch(/remembered person, alias, relationship, prior note, group rule/i);
|
|
866
|
+
expect(memorySearch.description).toMatch(/does not contact the ClawChat server/i);
|
|
867
|
+
expect(memoryRead.description).toMatch(/target returned by clawchat_memory_search/i);
|
|
868
|
+
expect(memoryRead.description).toMatch(/use clawchat_memory_search first/i);
|
|
869
|
+
expect(listMoments.description).toMatch(/moments\/dynamics\/feed/);
|
|
870
|
+
expect(listMoments.description).toMatch(/friends-only feed endpoint/);
|
|
871
|
+
expect(createMoment.description).toMatch(/At least one of text or images/);
|
|
872
|
+
expect(createMoment.description).toMatch(/do not pass local file paths as images/);
|
|
873
|
+
expect(deleteMoment.description).toMatch(/Do not guess the id/);
|
|
874
|
+
expect(reaction.description).toMatch(/adds the reaction if missing and removes it/);
|
|
875
|
+
expect(comment.description).toMatch(/top-level comment/);
|
|
876
|
+
expect(comment.description).toMatch(/Use clawchat_reply_moment_comment/);
|
|
877
|
+
expect(reply.description).toMatch(/single-level reply/);
|
|
878
|
+
expect(reply.description).toMatch(/do not use this for top-level comments/);
|
|
879
|
+
expect(deleteComment.description).toMatch(/moment and comment ids/);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it("clawchat_upload_avatar_image rejects oversized files before upload", async () => {
|
|
883
|
+
const fs = await import("node:fs/promises");
|
|
884
|
+
const path = await import("node:path");
|
|
885
|
+
const os = await import("node:os");
|
|
886
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawchat-plugin-openclaw-"));
|
|
887
|
+
const big = path.join(tmp, "avatar-big.bin");
|
|
888
|
+
const handle = await fs.open(big, "w");
|
|
889
|
+
await handle.truncate(21 * 1024 * 1024);
|
|
890
|
+
await handle.close();
|
|
891
|
+
try {
|
|
892
|
+
const { api, registered } = buildApi({
|
|
893
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
894
|
+
});
|
|
895
|
+
registerOpenclawClawlingTools(api);
|
|
896
|
+
const tool = registered.find((t) => t.name === "clawchat_upload_avatar_image")!;
|
|
897
|
+
const result = await tool.execute("call-1", { filePath: big });
|
|
898
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
899
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
900
|
+
expect(parsed.error).toBe("validation");
|
|
901
|
+
expect(parsed.message).toMatch(/20 ?MB|too large/i);
|
|
902
|
+
} finally {
|
|
903
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
it("clawchat_upload_media_file rejects oversized files before upload", async () => {
|
|
909
|
+
const fs = await import("node:fs/promises");
|
|
910
|
+
const path = await import("node:path");
|
|
911
|
+
const os = await import("node:os");
|
|
912
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawchat-plugin-openclaw-"));
|
|
913
|
+
const big = path.join(tmp, "big.bin");
|
|
914
|
+
// create a sparse 21MB file (we never read it; size check fires first)
|
|
915
|
+
const handle = await fs.open(big, "w");
|
|
916
|
+
await handle.truncate(21 * 1024 * 1024);
|
|
917
|
+
await handle.close();
|
|
918
|
+
try {
|
|
919
|
+
const { api, registered } = buildApi({
|
|
920
|
+
configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
|
|
921
|
+
});
|
|
922
|
+
registerOpenclawClawlingTools(api);
|
|
923
|
+
const tool = registered.find((t) => t.name === "clawchat_upload_media_file")!;
|
|
924
|
+
const result = await tool.execute("call-1", { filePath: big });
|
|
925
|
+
const text = (result as { content: { text: string }[] }).content[0]!.text;
|
|
926
|
+
const parsed = JSON.parse(text) as { error?: string; message?: string };
|
|
927
|
+
expect(parsed.error).toBe("validation");
|
|
928
|
+
expect(parsed.message).toMatch(/20 ?MB|too large/i);
|
|
929
|
+
} finally {
|
|
930
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
});
|