@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,827 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
3
|
+
import { ClawlingApiError } from "./api-types.ts";
|
|
4
|
+
|
|
5
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
6
|
+
return new Response(JSON.stringify(body), {
|
|
7
|
+
status,
|
|
8
|
+
headers: { "content-type": "application/json" },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("clawchat-plugin-openclaw api-client", () => {
|
|
13
|
+
it("getMyProfile sends GET /me with bearer token and unwraps data", async () => {
|
|
14
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
15
|
+
jsonResponse({
|
|
16
|
+
code: 0,
|
|
17
|
+
message: "ok",
|
|
18
|
+
data: { user_id: "u1", display_name: "Alice" },
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
const client = createOpenclawClawlingApiClient({
|
|
22
|
+
baseUrl: "https://api.example.com",
|
|
23
|
+
token: "tk",
|
|
24
|
+
fetchImpl,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const profile = await client.getMyProfile();
|
|
28
|
+
|
|
29
|
+
expect(fetchImpl).toHaveBeenCalledWith(
|
|
30
|
+
"https://api.example.com/v1/users/me",
|
|
31
|
+
expect.objectContaining({
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: expect.objectContaining({
|
|
34
|
+
authorization: "Bearer tk",
|
|
35
|
+
// Global X-Device-Id is sent on every request, not just connect.
|
|
36
|
+
"x-device-id": "clawchat-plugin-openclaw",
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
expect(profile).toEqual({ user_id: "u1", display_name: "Alice" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("getUserInfo url-encodes the userId", async () => {
|
|
44
|
+
const fetchImpl = vi
|
|
45
|
+
.fn()
|
|
46
|
+
.mockResolvedValue(
|
|
47
|
+
jsonResponse({ code: 0, message: "ok", data: { user_id: "u/2", display_name: "Bob" } }),
|
|
48
|
+
);
|
|
49
|
+
const client = createOpenclawClawlingApiClient({
|
|
50
|
+
baseUrl: "https://api.example.com",
|
|
51
|
+
token: "tk",
|
|
52
|
+
fetchImpl,
|
|
53
|
+
});
|
|
54
|
+
await client.getUserInfo("u/2");
|
|
55
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/u%2F2");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("getUserProfile aliases GET /v1/users/{id}", async () => {
|
|
59
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
60
|
+
jsonResponse({ code: 0, message: "ok", data: { id: "u/2", nickname: "Bob" } }),
|
|
61
|
+
);
|
|
62
|
+
const client = createOpenclawClawlingApiClient({
|
|
63
|
+
baseUrl: "https://api.example.com",
|
|
64
|
+
token: "tk",
|
|
65
|
+
fetchImpl,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const profile = await client.getUserProfile("u/2");
|
|
69
|
+
|
|
70
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/u%2F2");
|
|
71
|
+
expect(profile).toEqual({ id: "u/2", nickname: "Bob" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("getAgentDetail calls GET /v1/agents/{agent_id}", async () => {
|
|
75
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
76
|
+
jsonResponse({
|
|
77
|
+
code: 0,
|
|
78
|
+
message: "ok",
|
|
79
|
+
data: { agent: { user_id: "agt/1", owner_id: "owner", type: "agent" } },
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
const client = createOpenclawClawlingApiClient({
|
|
83
|
+
baseUrl: "https://api.example.com",
|
|
84
|
+
token: "tk",
|
|
85
|
+
fetchImpl,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await client.getAgentDetail("agt/1");
|
|
89
|
+
|
|
90
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/agt%2F1");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("metadata REST helpers", () => {
|
|
94
|
+
it("patchAgent sends only nickname/avatar_url/bio to PATCH /v1/agents/{agent_id}", async () => {
|
|
95
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
96
|
+
jsonResponse({
|
|
97
|
+
code: 0,
|
|
98
|
+
message: "ok",
|
|
99
|
+
data: {
|
|
100
|
+
agent: {
|
|
101
|
+
id: "agt/1",
|
|
102
|
+
owner_id: "owner",
|
|
103
|
+
user_id: "agent-user",
|
|
104
|
+
type: "bot",
|
|
105
|
+
nickname: "OpenClaw Bot",
|
|
106
|
+
avatar_url: "https://cdn/agent.png",
|
|
107
|
+
bio: "metadata bio",
|
|
108
|
+
visibility: "public",
|
|
109
|
+
status: "active",
|
|
110
|
+
platform: "openclaw",
|
|
111
|
+
created_at: "2026-05-24T00:00:00Z",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
const client = createOpenclawClawlingApiClient({
|
|
117
|
+
baseUrl: "https://api.example.com",
|
|
118
|
+
token: "tk",
|
|
119
|
+
fetchImpl,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await client.patchAgent("agt/1", {
|
|
123
|
+
nickname: "OpenClaw Bot",
|
|
124
|
+
avatar_url: "https://cdn/agent.png",
|
|
125
|
+
bio: "metadata bio",
|
|
126
|
+
filePath: "owner.md",
|
|
127
|
+
} as never);
|
|
128
|
+
|
|
129
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/agt%2F1");
|
|
130
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
131
|
+
expect(init.method).toBe("PATCH");
|
|
132
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
133
|
+
nickname: "OpenClaw Bot",
|
|
134
|
+
avatar_url: "https://cdn/agent.png",
|
|
135
|
+
bio: "metadata bio",
|
|
136
|
+
});
|
|
137
|
+
expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
|
|
138
|
+
expect(result.agent.nickname).toBe("OpenClaw Bot");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("updateAgentBehavior patches current agent behavior through /v1/agents/me/behavior", async () => {
|
|
142
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
143
|
+
jsonResponse({
|
|
144
|
+
code: 0,
|
|
145
|
+
message: "ok",
|
|
146
|
+
data: {
|
|
147
|
+
agent: {
|
|
148
|
+
id: "agt/1",
|
|
149
|
+
owner_id: "owner",
|
|
150
|
+
user_id: "agent-user",
|
|
151
|
+
behavior: "Act clearly.",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
const client = createOpenclawClawlingApiClient({
|
|
157
|
+
baseUrl: "https://api.example.com",
|
|
158
|
+
token: "tk",
|
|
159
|
+
fetchImpl,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = await client.updateAgentBehavior("Act clearly.");
|
|
163
|
+
|
|
164
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/me/behavior");
|
|
165
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
166
|
+
expect(init.method).toBe("PATCH");
|
|
167
|
+
expect(JSON.parse(init.body as string)).toEqual({ behavior: "Act clearly." });
|
|
168
|
+
expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
|
|
169
|
+
expect(result.agent.behavior).toBe("Act clearly.");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("patchConversation sends only title/description to PATCH /v1/conversations/{id}", async () => {
|
|
173
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
174
|
+
jsonResponse({
|
|
175
|
+
code: 0,
|
|
176
|
+
message: "ok",
|
|
177
|
+
data: {
|
|
178
|
+
conversation: {
|
|
179
|
+
id: "cnv/1",
|
|
180
|
+
type: "group",
|
|
181
|
+
title: "Metadata Team",
|
|
182
|
+
description: "Group metadata",
|
|
183
|
+
creator_id: "usr_creator",
|
|
184
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
185
|
+
updated_at: "2026-05-24T00:00:00Z",
|
|
186
|
+
participants: [],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
const client = createOpenclawClawlingApiClient({
|
|
192
|
+
baseUrl: "https://api.example.com",
|
|
193
|
+
token: "tk",
|
|
194
|
+
fetchImpl,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const result = await client.patchConversation("cnv/1", {
|
|
198
|
+
title: "Metadata Team",
|
|
199
|
+
description: "Group metadata",
|
|
200
|
+
filePath: "groups/cnv_1.md",
|
|
201
|
+
} as never);
|
|
202
|
+
|
|
203
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv%2F1");
|
|
204
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
205
|
+
expect(init.method).toBe("PATCH");
|
|
206
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
207
|
+
title: "Metadata Team",
|
|
208
|
+
description: "Group metadata",
|
|
209
|
+
});
|
|
210
|
+
expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
|
|
211
|
+
expect(result.conversation.description).toBe("Group metadata");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("metadata user updates still use updateMyProfile with PATCH /v1/users/me", async () => {
|
|
215
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
216
|
+
jsonResponse({
|
|
217
|
+
code: 0,
|
|
218
|
+
message: "ok",
|
|
219
|
+
data: { id: "usr_1", nickname: "Alice", avatar_url: "https://cdn/a.png", bio: "hello" },
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
const client = createOpenclawClawlingApiClient({
|
|
223
|
+
baseUrl: "https://api.example.com",
|
|
224
|
+
token: "tk",
|
|
225
|
+
fetchImpl,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await client.updateMyProfile({
|
|
229
|
+
nickname: "Alice",
|
|
230
|
+
avatar_url: "https://cdn/a.png",
|
|
231
|
+
bio: "hello",
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/me");
|
|
235
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
236
|
+
expect(init.method).toBe("PATCH");
|
|
237
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
238
|
+
nickname: "Alice",
|
|
239
|
+
avatar_url: "https://cdn/a.png",
|
|
240
|
+
bio: "hello",
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("metadata getConversation preserves participants for caller mapping", async () => {
|
|
245
|
+
const participants = [
|
|
246
|
+
{
|
|
247
|
+
conversation_id: "cnv_1",
|
|
248
|
+
user_id: "usr_1",
|
|
249
|
+
role: "owner",
|
|
250
|
+
joined_at: "2026-05-20T00:00:00Z",
|
|
251
|
+
user: { id: "usr_1", nickname: "Alice", avatar_url: "https://cdn/a.png" },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
conversation_id: "cnv_1",
|
|
255
|
+
user_id: "usr_2",
|
|
256
|
+
role: "member",
|
|
257
|
+
joined_at: "2026-05-21T00:00:00Z",
|
|
258
|
+
user: { id: "usr_2", nickname: "Bob", avatar_url: "https://cdn/b.png" },
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
262
|
+
jsonResponse({
|
|
263
|
+
code: 0,
|
|
264
|
+
message: "ok",
|
|
265
|
+
data: {
|
|
266
|
+
conversation: {
|
|
267
|
+
id: "cnv_1",
|
|
268
|
+
type: "group",
|
|
269
|
+
title: "Team",
|
|
270
|
+
creator_id: "usr_1",
|
|
271
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
272
|
+
updated_at: "2026-05-24T00:00:00Z",
|
|
273
|
+
participants,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
const client = createOpenclawClawlingApiClient({
|
|
279
|
+
baseUrl: "https://api.example.com",
|
|
280
|
+
token: "tk",
|
|
281
|
+
fetchImpl,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await client.getConversation("cnv_1");
|
|
285
|
+
|
|
286
|
+
expect(result.conversation.participants).toEqual(participants);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("metadata patch helpers reject blank ids and empty allowed patches before sending", async () => {
|
|
290
|
+
const fetchImpl = vi.fn();
|
|
291
|
+
const client = createOpenclawClawlingApiClient({
|
|
292
|
+
baseUrl: "https://api.example.com",
|
|
293
|
+
token: "tk",
|
|
294
|
+
fetchImpl,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await expect(client.patchAgent(" ", { nickname: "Bot" })).rejects.toMatchObject({
|
|
298
|
+
kind: "validation",
|
|
299
|
+
});
|
|
300
|
+
await expect(client.patchAgent("agt_1", {})).rejects.toMatchObject({
|
|
301
|
+
kind: "validation",
|
|
302
|
+
});
|
|
303
|
+
await expect(client.patchConversation("", { title: "Team" })).rejects.toMatchObject({
|
|
304
|
+
kind: "validation",
|
|
305
|
+
});
|
|
306
|
+
await expect(
|
|
307
|
+
client.patchConversation("cnv_1", { filePath: "groups/cnv_1.md" } as never),
|
|
308
|
+
).rejects.toMatchObject({
|
|
309
|
+
kind: "validation",
|
|
310
|
+
});
|
|
311
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("listFriends uses the friendships endpoint", async () => {
|
|
316
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
317
|
+
jsonResponse({
|
|
318
|
+
code: 0,
|
|
319
|
+
message: "ok",
|
|
320
|
+
data: { friends: [{ id: "u1", nickname: "Alice", type: "user" }] },
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
const client = createOpenclawClawlingApiClient({
|
|
324
|
+
baseUrl: "https://api.example.com",
|
|
325
|
+
token: "tk",
|
|
326
|
+
fetchImpl,
|
|
327
|
+
});
|
|
328
|
+
const result = await client.listFriends();
|
|
329
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/friendships");
|
|
330
|
+
expect(result).toEqual({ friends: [{ id: "u1", nickname: "Alice", type: "user" }] });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("searchUsers sends q + limit as query string", async () => {
|
|
334
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
335
|
+
jsonResponse({
|
|
336
|
+
code: 0,
|
|
337
|
+
message: "ok",
|
|
338
|
+
data: { users: [{ id: "u1", nickname: "Alice", type: "user" }] },
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
const client = createOpenclawClawlingApiClient({
|
|
342
|
+
baseUrl: "https://api.example.com",
|
|
343
|
+
token: "tk",
|
|
344
|
+
fetchImpl,
|
|
345
|
+
});
|
|
346
|
+
const result = await client.searchUsers({ q: "alice", limit: 20 });
|
|
347
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
348
|
+
"https://api.example.com/v1/users/search?q=alice&limit=20",
|
|
349
|
+
);
|
|
350
|
+
expect(result.users[0]!.nickname).toBe("Alice");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("listMoments sends before + limit as query string", async () => {
|
|
354
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
355
|
+
jsonResponse({
|
|
356
|
+
code: 0,
|
|
357
|
+
message: "ok",
|
|
358
|
+
data: { moments: [{ id: 122, author_id: "u1", created_at: "2026-05-15T00:00:00Z" }] },
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
const client = createOpenclawClawlingApiClient({
|
|
362
|
+
baseUrl: "https://api.example.com",
|
|
363
|
+
token: "tk",
|
|
364
|
+
fetchImpl,
|
|
365
|
+
});
|
|
366
|
+
await client.listMoments({ before: 123, limit: 30 });
|
|
367
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
368
|
+
"https://api.example.com/v1/moments?before=123&limit=30",
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("createMoment POSTs text and image URLs", async () => {
|
|
373
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
374
|
+
jsonResponse({
|
|
375
|
+
code: 0,
|
|
376
|
+
message: "ok",
|
|
377
|
+
data: { moment: { id: 1, author_id: "u1", text: "hello", created_at: "now" } },
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
const client = createOpenclawClawlingApiClient({
|
|
381
|
+
baseUrl: "https://api.example.com",
|
|
382
|
+
token: "tk",
|
|
383
|
+
fetchImpl,
|
|
384
|
+
});
|
|
385
|
+
await client.createMoment({ text: "hello", images: ["https://cdn/a.png"] });
|
|
386
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/moments");
|
|
387
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
388
|
+
expect(init.method).toBe("POST");
|
|
389
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
390
|
+
text: "hello",
|
|
391
|
+
images: ["https://cdn/a.png"],
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("deleteMoment sends DELETE /moments/{id}", async () => {
|
|
396
|
+
const fetchImpl = vi
|
|
397
|
+
.fn()
|
|
398
|
+
.mockResolvedValue(jsonResponse({ code: 0, message: "ok", data: { ok: true } }));
|
|
399
|
+
const client = createOpenclawClawlingApiClient({
|
|
400
|
+
baseUrl: "https://api.example.com",
|
|
401
|
+
token: "tk",
|
|
402
|
+
fetchImpl,
|
|
403
|
+
});
|
|
404
|
+
await client.deleteMoment(123);
|
|
405
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/moments/123");
|
|
406
|
+
expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("toggleMomentReaction POSTs emoji body", async () => {
|
|
410
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
411
|
+
jsonResponse({
|
|
412
|
+
code: 0,
|
|
413
|
+
message: "ok",
|
|
414
|
+
data: { reactions: [{ emoji: "👍", count: 1, mine: true }] },
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
const client = createOpenclawClawlingApiClient({
|
|
418
|
+
baseUrl: "https://api.example.com",
|
|
419
|
+
token: "tk",
|
|
420
|
+
fetchImpl,
|
|
421
|
+
});
|
|
422
|
+
await client.toggleMomentReaction({ momentId: 123, emoji: "👍" });
|
|
423
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
424
|
+
"https://api.example.com/v1/moments/123/reactions",
|
|
425
|
+
);
|
|
426
|
+
expect(JSON.parse((fetchImpl.mock.calls[0]![1] as RequestInit).body as string)).toEqual({
|
|
427
|
+
emoji: "👍",
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("createMomentComment and replyMomentComment POST expected bodies", async () => {
|
|
432
|
+
const fetchImpl = vi.fn().mockImplementation(() =>
|
|
433
|
+
Promise.resolve(
|
|
434
|
+
jsonResponse({
|
|
435
|
+
code: 0,
|
|
436
|
+
message: "ok",
|
|
437
|
+
data: { comment: { id: 456, author_id: "u1", text: "nice", created_at: "now" } },
|
|
438
|
+
}),
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
const client = createOpenclawClawlingApiClient({
|
|
442
|
+
baseUrl: "https://api.example.com",
|
|
443
|
+
token: "tk",
|
|
444
|
+
fetchImpl,
|
|
445
|
+
});
|
|
446
|
+
await client.createMomentComment({ momentId: 123, text: "nice" });
|
|
447
|
+
await client.replyMomentComment({ momentId: 123, replyToCommentId: 456, text: "yes" });
|
|
448
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
449
|
+
"https://api.example.com/v1/moments/123/comments",
|
|
450
|
+
);
|
|
451
|
+
expect(JSON.parse((fetchImpl.mock.calls[0]![1] as RequestInit).body as string)).toEqual({
|
|
452
|
+
text: "nice",
|
|
453
|
+
});
|
|
454
|
+
expect(JSON.parse((fetchImpl.mock.calls[1]![1] as RequestInit).body as string)).toEqual({
|
|
455
|
+
text: "yes",
|
|
456
|
+
reply_to_comment_id: 456,
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("deleteMomentComment sends DELETE /moments/{id}/comments/{cid}", async () => {
|
|
461
|
+
const fetchImpl = vi
|
|
462
|
+
.fn()
|
|
463
|
+
.mockResolvedValue(jsonResponse({ code: 0, message: "ok", data: { ok: true } }));
|
|
464
|
+
const client = createOpenclawClawlingApiClient({
|
|
465
|
+
baseUrl: "https://api.example.com",
|
|
466
|
+
token: "tk",
|
|
467
|
+
fetchImpl,
|
|
468
|
+
});
|
|
469
|
+
await client.deleteMomentComment({ momentId: 123, commentId: 456 });
|
|
470
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
471
|
+
"https://api.example.com/v1/moments/123/comments/456",
|
|
472
|
+
);
|
|
473
|
+
expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("updateMyProfile sends PATCH /me with JSON body without requiring configured userId", async () => {
|
|
477
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
478
|
+
jsonResponse({
|
|
479
|
+
code: 0,
|
|
480
|
+
message: "ok",
|
|
481
|
+
data: { user_id: "u1", display_name: "Alice2" },
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
const client = createOpenclawClawlingApiClient({
|
|
485
|
+
baseUrl: "https://api.example.com",
|
|
486
|
+
token: "tk",
|
|
487
|
+
fetchImpl,
|
|
488
|
+
});
|
|
489
|
+
await client.updateMyProfile({ display_name: "Alice2" });
|
|
490
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/me");
|
|
491
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
492
|
+
expect(init.method).toBe("PATCH");
|
|
493
|
+
expect(JSON.parse(init.body as string)).toEqual({ display_name: "Alice2" });
|
|
494
|
+
expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("updateMyProfile forwards bio in the JSON body", async () => {
|
|
498
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
499
|
+
jsonResponse({
|
|
500
|
+
code: 0,
|
|
501
|
+
message: "ok",
|
|
502
|
+
data: { user_id: "u1", bio: "hello there" },
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
505
|
+
const client = createOpenclawClawlingApiClient({
|
|
506
|
+
baseUrl: "https://api.example.com",
|
|
507
|
+
token: "tk",
|
|
508
|
+
userId: "agent-1",
|
|
509
|
+
fetchImpl,
|
|
510
|
+
});
|
|
511
|
+
await client.updateMyProfile({ bio: "hello there" });
|
|
512
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
513
|
+
expect(JSON.parse(init.body as string)).toEqual({ bio: "hello there" });
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("getConversation sends GET /v1/conversations/{id}", async () => {
|
|
517
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
518
|
+
jsonResponse({
|
|
519
|
+
code: 0,
|
|
520
|
+
message: "ok",
|
|
521
|
+
data: {
|
|
522
|
+
conversation: {
|
|
523
|
+
id: "cnv_1",
|
|
524
|
+
type: "group",
|
|
525
|
+
title: "Team",
|
|
526
|
+
creator_id: "usr_creator",
|
|
527
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
528
|
+
updated_at: "2026-05-20T00:01:00Z",
|
|
529
|
+
participants: [
|
|
530
|
+
{
|
|
531
|
+
conversation_id: "cnv_1",
|
|
532
|
+
user_id: "usr_1",
|
|
533
|
+
role: "owner",
|
|
534
|
+
joined_at: "2026-05-20T00:00:00Z",
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
}),
|
|
540
|
+
);
|
|
541
|
+
const client = createOpenclawClawlingApiClient({
|
|
542
|
+
baseUrl: "https://api.example.com",
|
|
543
|
+
token: "tk",
|
|
544
|
+
fetchImpl,
|
|
545
|
+
});
|
|
546
|
+
const result = await client.getConversation("cnv_1");
|
|
547
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv_1");
|
|
548
|
+
expect(result.conversation.title).toBe("Team");
|
|
549
|
+
expect(result.conversation.creator_id).toBe("usr_creator");
|
|
550
|
+
expect(result.conversation.participants[0]!.user_id).toBe("usr_1");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("does not expose conversation admin mutation methods", () => {
|
|
554
|
+
const client = createOpenclawClawlingApiClient({
|
|
555
|
+
baseUrl: "https://api.example.com",
|
|
556
|
+
token: "tk",
|
|
557
|
+
fetchImpl: vi.fn(),
|
|
558
|
+
});
|
|
559
|
+
expect(client).not.toHaveProperty("createConversation");
|
|
560
|
+
expect(client).not.toHaveProperty("updateConversation");
|
|
561
|
+
expect(client).not.toHaveProperty("leaveConversation");
|
|
562
|
+
expect(client).not.toHaveProperty("dissolveConversation");
|
|
563
|
+
expect(client).not.toHaveProperty("addConversationUsers");
|
|
564
|
+
expect(client).not.toHaveProperty("removeConversationUsers");
|
|
565
|
+
expect(client).not.toHaveProperty("listConversationUsers");
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("uploadMedia POSTs multipart with field name 'file'", async () => {
|
|
569
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
570
|
+
jsonResponse({
|
|
571
|
+
code: 0,
|
|
572
|
+
message: "ok",
|
|
573
|
+
data: { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
|
|
574
|
+
}),
|
|
575
|
+
);
|
|
576
|
+
const client = createOpenclawClawlingApiClient({
|
|
577
|
+
baseUrl: "https://api.example.com",
|
|
578
|
+
token: "tk",
|
|
579
|
+
fetchImpl,
|
|
580
|
+
});
|
|
581
|
+
const result = await client.uploadMedia({
|
|
582
|
+
buffer: Buffer.from("hi-bytes-12!"),
|
|
583
|
+
filename: "x.png",
|
|
584
|
+
mime: "image/png",
|
|
585
|
+
});
|
|
586
|
+
// uploadMedia intentionally targets `/media/upload` WITHOUT the `/v1`
|
|
587
|
+
// prefix — the upstream mount is unversioned.
|
|
588
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/media/upload");
|
|
589
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
590
|
+
expect(init.method).toBe("POST");
|
|
591
|
+
expect(init.body).toBeInstanceOf(FormData);
|
|
592
|
+
const fd = init.body as FormData;
|
|
593
|
+
const file = fd.get("file") as File;
|
|
594
|
+
expect(file).toBeInstanceOf(File);
|
|
595
|
+
expect(file.name).toBe("x.png");
|
|
596
|
+
expect(file.type).toBe("image/png");
|
|
597
|
+
expect(result).toEqual({ kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" });
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it.each([
|
|
601
|
+
["kind", { url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" }],
|
|
602
|
+
["url", { kind: "image", name: "x.png", size: 12, mime: "image/png" }],
|
|
603
|
+
["name", { kind: "image", url: "https://cdn/x.png", size: 12, mime: "image/png" }],
|
|
604
|
+
["mime", { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12 }],
|
|
605
|
+
["size", { kind: "image", url: "https://cdn/x.png", name: "x.png", mime: "image/png" }],
|
|
606
|
+
])("uploadMedia treats missing %s in response data as protocol error", async (_field, data) => {
|
|
607
|
+
const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ code: 0, message: "ok", data }));
|
|
608
|
+
const client = createOpenclawClawlingApiClient({
|
|
609
|
+
baseUrl: "https://api.example.com",
|
|
610
|
+
token: "tk",
|
|
611
|
+
fetchImpl,
|
|
612
|
+
});
|
|
613
|
+
await expect(
|
|
614
|
+
client.uploadMedia({
|
|
615
|
+
buffer: Buffer.from("hi-bytes-12!"),
|
|
616
|
+
filename: "x.png",
|
|
617
|
+
mime: "image/png",
|
|
618
|
+
}),
|
|
619
|
+
).rejects.toMatchObject({ kind: "api" });
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("uploadMedia treats unsupported kind in response data as protocol error", async () => {
|
|
623
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
624
|
+
jsonResponse({
|
|
625
|
+
code: 0,
|
|
626
|
+
message: "ok",
|
|
627
|
+
data: { kind: "bogus", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
const client = createOpenclawClawlingApiClient({
|
|
631
|
+
baseUrl: "https://api.example.com",
|
|
632
|
+
token: "tk",
|
|
633
|
+
fetchImpl,
|
|
634
|
+
});
|
|
635
|
+
await expect(
|
|
636
|
+
client.uploadMedia({
|
|
637
|
+
buffer: Buffer.from("hi-bytes-12!"),
|
|
638
|
+
filename: "x.png",
|
|
639
|
+
mime: "image/png",
|
|
640
|
+
}),
|
|
641
|
+
).rejects.toMatchObject({ kind: "api" });
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("uploadAvatar POSTs multipart to /v1/files/upload-url", async () => {
|
|
645
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
646
|
+
jsonResponse({
|
|
647
|
+
code: 0,
|
|
648
|
+
message: "ok",
|
|
649
|
+
data: { url: "https://cdn/avatars/a.png", size: 99, mime: "image/png" },
|
|
650
|
+
}),
|
|
651
|
+
);
|
|
652
|
+
const client = createOpenclawClawlingApiClient({
|
|
653
|
+
baseUrl: "https://api.example.com",
|
|
654
|
+
token: "tk",
|
|
655
|
+
fetchImpl,
|
|
656
|
+
});
|
|
657
|
+
const result = await client.uploadAvatar({
|
|
658
|
+
buffer: Buffer.from("avatar-bytes-12"),
|
|
659
|
+
filename: "avatar.png",
|
|
660
|
+
mime: "image/png",
|
|
661
|
+
});
|
|
662
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/files/upload-url");
|
|
663
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
664
|
+
expect(init.method).toBe("POST");
|
|
665
|
+
expect(init.body).toBeInstanceOf(FormData);
|
|
666
|
+
const fd = init.body as FormData;
|
|
667
|
+
const file = fd.get("file") as File;
|
|
668
|
+
expect(file).toBeInstanceOf(File);
|
|
669
|
+
expect(file.name).toBe("avatar.png");
|
|
670
|
+
expect(file.type).toBe("image/png");
|
|
671
|
+
// X-Device-Id is present on avatar uploads too.
|
|
672
|
+
expect((init.headers as Record<string, string>)["x-device-id"]).toBe("clawchat-plugin-openclaw");
|
|
673
|
+
expect(result).not.toHaveProperty("kind");
|
|
674
|
+
expect(result).not.toHaveProperty("name");
|
|
675
|
+
expect(result).toEqual({ url: "https://cdn/avatars/a.png", size: 99, mime: "image/png" });
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("agentsConnect POSTs /agents/connect with { code, platform, type } body", async () => {
|
|
679
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
680
|
+
jsonResponse({
|
|
681
|
+
code: 0,
|
|
682
|
+
msg: "ok",
|
|
683
|
+
data: {
|
|
684
|
+
agent: {
|
|
685
|
+
id: "ag-1",
|
|
686
|
+
owner_id: "owner-1",
|
|
687
|
+
user_id: "agent-1",
|
|
688
|
+
type: "bot",
|
|
689
|
+
nickname: "Bot",
|
|
690
|
+
avatar_url: "",
|
|
691
|
+
bio: "",
|
|
692
|
+
visibility: "public",
|
|
693
|
+
status: "active",
|
|
694
|
+
platform: "openclaw",
|
|
695
|
+
created_at: "2026-04-17T00:00:00Z",
|
|
696
|
+
},
|
|
697
|
+
access_token: "tk-access",
|
|
698
|
+
refresh_token: "tk-refresh",
|
|
699
|
+
},
|
|
700
|
+
}),
|
|
701
|
+
);
|
|
702
|
+
const client = createOpenclawClawlingApiClient({
|
|
703
|
+
baseUrl: "https://api.example.com",
|
|
704
|
+
token: "tk",
|
|
705
|
+
fetchImpl,
|
|
706
|
+
});
|
|
707
|
+
const result = await client.agentsConnect({
|
|
708
|
+
code: "INV-1",
|
|
709
|
+
platform: "openclaw",
|
|
710
|
+
type: "bot",
|
|
711
|
+
});
|
|
712
|
+
expect(fetchImpl).toHaveBeenCalledWith(
|
|
713
|
+
"https://api.example.com/v1/agents/connect",
|
|
714
|
+
expect.objectContaining({
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: expect.objectContaining({
|
|
717
|
+
authorization: "Bearer tk",
|
|
718
|
+
"content-type": "application/json",
|
|
719
|
+
// Fixed device id — pins the issued credentials to this plugin.
|
|
720
|
+
"x-device-id": "clawchat-plugin-openclaw",
|
|
721
|
+
}),
|
|
722
|
+
body: JSON.stringify({ code: "INV-1", platform: "openclaw", type: "bot" }),
|
|
723
|
+
}),
|
|
724
|
+
);
|
|
725
|
+
expect(result.agent.user_id).toBe("agent-1");
|
|
726
|
+
expect(result.access_token).toBe("tk-access");
|
|
727
|
+
expect(result.refresh_token).toBe("tk-refresh");
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("agentsConnect rejects empty invite code / platform / type locally", async () => {
|
|
731
|
+
const fetchImpl = vi.fn();
|
|
732
|
+
const client = createOpenclawClawlingApiClient({
|
|
733
|
+
baseUrl: "https://api.example.com",
|
|
734
|
+
token: "tk",
|
|
735
|
+
fetchImpl,
|
|
736
|
+
});
|
|
737
|
+
await expect(
|
|
738
|
+
client.agentsConnect({ code: " ", platform: "openclaw", type: "bot" }),
|
|
739
|
+
).rejects.toMatchObject({ kind: "validation" });
|
|
740
|
+
await expect(
|
|
741
|
+
client.agentsConnect({ code: "INV", platform: "", type: "bot" }),
|
|
742
|
+
).rejects.toMatchObject({ kind: "validation" });
|
|
743
|
+
await expect(
|
|
744
|
+
client.agentsConnect({ code: "INV", platform: "openclaw", type: "" }),
|
|
745
|
+
).rejects.toMatchObject({ kind: "validation" });
|
|
746
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it("throws ClawlingApiError(api) on code !== 0", async () => {
|
|
750
|
+
const fetchImpl = vi
|
|
751
|
+
.fn()
|
|
752
|
+
.mockResolvedValue(jsonResponse({ code: 4001, message: "user not found", data: null }));
|
|
753
|
+
const client = createOpenclawClawlingApiClient({
|
|
754
|
+
baseUrl: "https://api.example.com",
|
|
755
|
+
token: "tk",
|
|
756
|
+
fetchImpl,
|
|
757
|
+
});
|
|
758
|
+
await expect(client.getUserInfo("missing")).rejects.toMatchObject({
|
|
759
|
+
kind: "api",
|
|
760
|
+
message: "user not found",
|
|
761
|
+
meta: expect.objectContaining({ code: 4001 }),
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("throws ClawlingApiError(auth) on 401", async () => {
|
|
766
|
+
const fetchImpl = vi.fn().mockResolvedValue(new Response("unauthorized", { status: 401 }));
|
|
767
|
+
const client = createOpenclawClawlingApiClient({
|
|
768
|
+
baseUrl: "https://api.example.com",
|
|
769
|
+
token: "tk",
|
|
770
|
+
fetchImpl,
|
|
771
|
+
});
|
|
772
|
+
await expect(client.getMyProfile()).rejects.toMatchObject({
|
|
773
|
+
kind: "auth",
|
|
774
|
+
meta: expect.objectContaining({ status: 401 }),
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it("throws ClawlingApiError(transport) on 500", async () => {
|
|
779
|
+
const fetchImpl = vi.fn().mockResolvedValue(new Response("internal error", { status: 500 }));
|
|
780
|
+
const client = createOpenclawClawlingApiClient({
|
|
781
|
+
baseUrl: "https://api.example.com",
|
|
782
|
+
token: "tk",
|
|
783
|
+
fetchImpl,
|
|
784
|
+
});
|
|
785
|
+
await expect(client.getMyProfile()).rejects.toMatchObject({
|
|
786
|
+
kind: "transport",
|
|
787
|
+
meta: expect.objectContaining({ status: 500 }),
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("throws ClawlingApiError(transport) when fetch itself rejects", async () => {
|
|
792
|
+
const fetchImpl = vi.fn().mockRejectedValue(new Error("ENETDOWN"));
|
|
793
|
+
const client = createOpenclawClawlingApiClient({
|
|
794
|
+
baseUrl: "https://api.example.com",
|
|
795
|
+
token: "tk",
|
|
796
|
+
fetchImpl,
|
|
797
|
+
});
|
|
798
|
+
await expect(client.getMyProfile()).rejects.toMatchObject({
|
|
799
|
+
kind: "transport",
|
|
800
|
+
message: expect.stringContaining("ENETDOWN"),
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it("strips trailing slash from baseUrl", async () => {
|
|
805
|
+
const fetchImpl = vi
|
|
806
|
+
.fn()
|
|
807
|
+
.mockResolvedValue(
|
|
808
|
+
jsonResponse({ code: 0, message: "ok", data: { user_id: "u1", display_name: "A" } }),
|
|
809
|
+
);
|
|
810
|
+
const client = createOpenclawClawlingApiClient({
|
|
811
|
+
baseUrl: "https://api.example.com/",
|
|
812
|
+
token: "tk",
|
|
813
|
+
fetchImpl,
|
|
814
|
+
});
|
|
815
|
+
await client.getMyProfile();
|
|
816
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/me");
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it("rejects baseUrl without http(s) scheme", () => {
|
|
820
|
+
expect(() =>
|
|
821
|
+
createOpenclawClawlingApiClient({
|
|
822
|
+
baseUrl: "ftp://wrong.example.com",
|
|
823
|
+
token: "tk",
|
|
824
|
+
}),
|
|
825
|
+
).toThrow(ClawlingApiError);
|
|
826
|
+
});
|
|
827
|
+
});
|