@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,737 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ClawlingApiError } from "./api-types.ts";
|
|
4
|
+
import { runOpenclawClawlingLogin } from "./login.runtime.ts";
|
|
5
|
+
|
|
6
|
+
const CHANNEL_ID = "clawchat-plugin-openclaw";
|
|
7
|
+
|
|
8
|
+
function buildCfg(
|
|
9
|
+
section: Record<string, unknown> = { baseUrl: "https://api.example.com" },
|
|
10
|
+
): OpenClawConfig {
|
|
11
|
+
return {
|
|
12
|
+
channels: { [CHANNEL_ID]: section },
|
|
13
|
+
} as unknown as OpenClawConfig;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeApiClient(overrides: {
|
|
17
|
+
agentsConnect: ReturnType<typeof vi.fn>;
|
|
18
|
+
}): ReturnType<typeof import("./api-client.ts").createOpenclawClawlingApiClient> {
|
|
19
|
+
return {
|
|
20
|
+
getMyProfile: vi.fn(),
|
|
21
|
+
getUserInfo: vi.fn(),
|
|
22
|
+
listFriends: vi.fn(),
|
|
23
|
+
updateMyProfile: vi.fn(),
|
|
24
|
+
uploadMedia: vi.fn(),
|
|
25
|
+
agentsConnect: overrides.agentsConnect,
|
|
26
|
+
} as never;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("runOpenclawClawlingLogin", () => {
|
|
30
|
+
it("works with no prior setup (baseUrl / websocketUrl default built-in)", async () => {
|
|
31
|
+
const cfg = buildCfg({}); // empty section — resolver provides defaults
|
|
32
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
33
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
34
|
+
access_token: "t",
|
|
35
|
+
refresh_token: "r",
|
|
36
|
+
});
|
|
37
|
+
let capturedBaseUrl = "";
|
|
38
|
+
await runOpenclawClawlingLogin({
|
|
39
|
+
cfg,
|
|
40
|
+
runtime: { log: vi.fn() },
|
|
41
|
+
readInviteCode: async () => "INV",
|
|
42
|
+
apiClientFactory: (opts) => {
|
|
43
|
+
capturedBaseUrl = opts.baseUrl;
|
|
44
|
+
return makeApiClient({ agentsConnect });
|
|
45
|
+
},
|
|
46
|
+
persistConfig: vi.fn(),
|
|
47
|
+
});
|
|
48
|
+
// The api-client was constructed with a non-empty baseUrl sourced
|
|
49
|
+
// from the default, even though the cfg had none.
|
|
50
|
+
expect(capturedBaseUrl).toMatch(/^https?:\/\//);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("errors if invite code is blank", async () => {
|
|
54
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
55
|
+
await expect(
|
|
56
|
+
runOpenclawClawlingLogin({
|
|
57
|
+
cfg,
|
|
58
|
+
runtime: { log: vi.fn() },
|
|
59
|
+
readInviteCode: async () => " ",
|
|
60
|
+
}),
|
|
61
|
+
).rejects.toThrow(/invite code is required/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("calls agents/connect with the invite code and persists returned credentials", async () => {
|
|
65
|
+
const cfg = buildCfg({
|
|
66
|
+
baseUrl: "https://api.example.com",
|
|
67
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
68
|
+
});
|
|
69
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
70
|
+
agent: {
|
|
71
|
+
id: "ag-1",
|
|
72
|
+
owner_id: "owner-1",
|
|
73
|
+
user_id: "agent-123",
|
|
74
|
+
type: "bot",
|
|
75
|
+
nickname: "Bot",
|
|
76
|
+
avatar_url: "",
|
|
77
|
+
bio: "",
|
|
78
|
+
visibility: "public",
|
|
79
|
+
status: "active",
|
|
80
|
+
platform: "clawchat",
|
|
81
|
+
created_at: "2026-04-17T00:00:00Z",
|
|
82
|
+
},
|
|
83
|
+
access_token: "access-tok",
|
|
84
|
+
refresh_token: "refresh-tok",
|
|
85
|
+
});
|
|
86
|
+
const persistConfig = vi.fn();
|
|
87
|
+
const log = vi.fn();
|
|
88
|
+
|
|
89
|
+
await runOpenclawClawlingLogin({
|
|
90
|
+
cfg,
|
|
91
|
+
runtime: { log },
|
|
92
|
+
readInviteCode: async () => "INV-ABC",
|
|
93
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
94
|
+
persistConfig,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(agentsConnect).toHaveBeenCalledWith({
|
|
98
|
+
code: "INV-ABC",
|
|
99
|
+
platform: "openclaw",
|
|
100
|
+
type: "clawbot",
|
|
101
|
+
});
|
|
102
|
+
expect(persistConfig).toHaveBeenCalledTimes(1);
|
|
103
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
104
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
105
|
+
expect((savedCfg.plugins as { allow?: string[] })?.allow).toEqual([CHANNEL_ID]);
|
|
106
|
+
expect(section.token).toBe("access-tok");
|
|
107
|
+
expect(section.refreshToken).toBe("refresh-tok");
|
|
108
|
+
expect(section.userId).toBe("agent-123");
|
|
109
|
+
expect(section.ownerUserId).toBe("owner-1");
|
|
110
|
+
expect(section.groupMode).toBe("all");
|
|
111
|
+
expect(section.groupCommandMode).toBe("owner");
|
|
112
|
+
// Existing baseUrl and websocketUrl are preserved — agents/connect
|
|
113
|
+
// does not return them.
|
|
114
|
+
expect(section.baseUrl).toBe("https://api.example.com");
|
|
115
|
+
expect(section.websocketUrl).toBe("wss://ws.example.com/v2/client");
|
|
116
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("persists latest activation bootstrap metadata to the sqlite store after config write", async () => {
|
|
120
|
+
const cfg = buildCfg({
|
|
121
|
+
baseUrl: "https://api.example.com",
|
|
122
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
123
|
+
});
|
|
124
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
125
|
+
agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
|
|
126
|
+
access_token: "access-plain",
|
|
127
|
+
refresh_token: "refresh-plain",
|
|
128
|
+
conversation: { id: "conv-activation" },
|
|
129
|
+
});
|
|
130
|
+
const persistConfig = vi.fn();
|
|
131
|
+
const upsertActivation = vi.fn();
|
|
132
|
+
|
|
133
|
+
await runOpenclawClawlingLogin({
|
|
134
|
+
cfg,
|
|
135
|
+
runtime: { log: vi.fn() },
|
|
136
|
+
readInviteCode: async () => "INV-ABC",
|
|
137
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
138
|
+
persistConfig,
|
|
139
|
+
store: { upsertActivation },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(upsertActivation).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(persistConfig.mock.invocationCallOrder[0]).toBeLessThan(
|
|
144
|
+
upsertActivation.mock.invocationCallOrder[0],
|
|
145
|
+
);
|
|
146
|
+
expect(upsertActivation).toHaveBeenCalledWith({
|
|
147
|
+
platform: "openclaw",
|
|
148
|
+
accountId: "default",
|
|
149
|
+
userId: "agent-123",
|
|
150
|
+
ownerUserId: "owner-123",
|
|
151
|
+
accessToken: "access-plain",
|
|
152
|
+
refreshToken: "refresh-plain",
|
|
153
|
+
conversationId: "conv-activation",
|
|
154
|
+
loginMethod: "login",
|
|
155
|
+
});
|
|
156
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("baseUrl");
|
|
157
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("websocketUrl");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("uses the runtime config mutator with restart intent after credential writes", async () => {
|
|
161
|
+
const cfg = buildCfg({
|
|
162
|
+
baseUrl: "https://api.example.com",
|
|
163
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
164
|
+
});
|
|
165
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
166
|
+
agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
|
|
167
|
+
access_token: "access-tok",
|
|
168
|
+
refresh_token: "refresh-tok",
|
|
169
|
+
});
|
|
170
|
+
let mutatedCfg: OpenClawConfig | undefined;
|
|
171
|
+
const log = vi.fn();
|
|
172
|
+
const mutateConfigFile = vi.fn(async (params) => {
|
|
173
|
+
expect(params.afterWrite).toEqual({
|
|
174
|
+
mode: "restart",
|
|
175
|
+
reason: "clawchat-plugin-openclaw credentials changed",
|
|
176
|
+
});
|
|
177
|
+
const draft = structuredClone(cfg) as OpenClawConfig;
|
|
178
|
+
await params.mutate(draft, { snapshot: {} as never, previousHash: "before" });
|
|
179
|
+
mutatedCfg = draft;
|
|
180
|
+
return { nextConfig: draft } as never;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await runOpenclawClawlingLogin({
|
|
184
|
+
cfg,
|
|
185
|
+
runtime: { log },
|
|
186
|
+
readInviteCode: async () => "INV-ABC",
|
|
187
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
188
|
+
mutateConfigFile,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(mutateConfigFile).toHaveBeenCalledTimes(1);
|
|
192
|
+
const section = (mutatedCfg!.channels as Record<string, Record<string, unknown>>)[
|
|
193
|
+
CHANNEL_ID
|
|
194
|
+
]!;
|
|
195
|
+
expect(section.token).toBe("access-tok");
|
|
196
|
+
expect(section.refreshToken).toBe("refresh-tok");
|
|
197
|
+
expect(section.userId).toBe("agent-123");
|
|
198
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
199
|
+
expect(section.groupMode).toBe("all");
|
|
200
|
+
expect(section.groupCommandMode).toBe("owner");
|
|
201
|
+
const plugins = (mutatedCfg! as OpenClawConfig & {
|
|
202
|
+
plugins: { allow?: string[]; entries: Record<string, Record<string, unknown>> };
|
|
203
|
+
}).plugins;
|
|
204
|
+
expect(plugins.allow).toEqual([CHANNEL_ID]);
|
|
205
|
+
expect(plugins.entries[CHANNEL_ID]?.enabled).toBe(true);
|
|
206
|
+
expect(log).toHaveBeenCalledWith(
|
|
207
|
+
expect.stringContaining("Persisting ClawChat credentials and plugin activation"),
|
|
208
|
+
);
|
|
209
|
+
expect(log).toHaveBeenCalledWith(
|
|
210
|
+
expect.stringContaining("ClawChat credentials and plugin activation persisted"),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("preserves other configured channels when persisting ClawChat credentials", async () => {
|
|
215
|
+
const cfg = {
|
|
216
|
+
channels: {
|
|
217
|
+
telegram: {
|
|
218
|
+
enabled: true,
|
|
219
|
+
token: "telegram-token",
|
|
220
|
+
},
|
|
221
|
+
[CHANNEL_ID]: {
|
|
222
|
+
baseUrl: "https://api.example.com",
|
|
223
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
} as unknown as OpenClawConfig;
|
|
227
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
228
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
229
|
+
access_token: "access-tok",
|
|
230
|
+
refresh_token: "refresh-tok",
|
|
231
|
+
});
|
|
232
|
+
const persistConfig = vi.fn();
|
|
233
|
+
|
|
234
|
+
await runOpenclawClawlingLogin({
|
|
235
|
+
cfg,
|
|
236
|
+
runtime: { log: vi.fn() },
|
|
237
|
+
readInviteCode: async () => "INV-ABC",
|
|
238
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
239
|
+
persistConfig,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
243
|
+
expect(savedCfg.channels?.telegram).toEqual({
|
|
244
|
+
enabled: true,
|
|
245
|
+
token: "telegram-token",
|
|
246
|
+
});
|
|
247
|
+
expect(savedCfg.channels?.[CHANNEL_ID]).toMatchObject({
|
|
248
|
+
baseUrl: "https://api.example.com",
|
|
249
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
250
|
+
token: "access-tok",
|
|
251
|
+
userId: "agent-123",
|
|
252
|
+
ownerUserId: "owner-123",
|
|
253
|
+
refreshToken: "refresh-tok",
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("enables the channel when login fills a pre-activation disabled skeleton", async () => {
|
|
258
|
+
const cfg = buildCfg({
|
|
259
|
+
enabled: false,
|
|
260
|
+
baseUrl: "https://api.example.com",
|
|
261
|
+
});
|
|
262
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
263
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
264
|
+
access_token: "access-tok",
|
|
265
|
+
refresh_token: "refresh-tok",
|
|
266
|
+
});
|
|
267
|
+
const persistConfig = vi.fn();
|
|
268
|
+
|
|
269
|
+
await runOpenclawClawlingLogin({
|
|
270
|
+
cfg,
|
|
271
|
+
runtime: { log: vi.fn() },
|
|
272
|
+
readInviteCode: async () => "INV-ABC",
|
|
273
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
274
|
+
persistConfig,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
278
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
279
|
+
expect(section.enabled).toBe(true);
|
|
280
|
+
expect(section.token).toBe("access-tok");
|
|
281
|
+
expect(section.userId).toBe("agent-123");
|
|
282
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
283
|
+
expect(section.groupMode).toBe("all");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does not overwrite an explicit mention groupMode during login", async () => {
|
|
287
|
+
const cfg = buildCfg({
|
|
288
|
+
baseUrl: "https://api.example.com",
|
|
289
|
+
groupMode: "mention",
|
|
290
|
+
groupCommandMode: "off",
|
|
291
|
+
});
|
|
292
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
293
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
294
|
+
access_token: "access-tok",
|
|
295
|
+
refresh_token: "refresh-tok",
|
|
296
|
+
});
|
|
297
|
+
const persistConfig = vi.fn();
|
|
298
|
+
|
|
299
|
+
await runOpenclawClawlingLogin({
|
|
300
|
+
cfg,
|
|
301
|
+
runtime: { log: vi.fn() },
|
|
302
|
+
readInviteCode: async () => "INV-ABC",
|
|
303
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
304
|
+
persistConfig,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
308
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
309
|
+
expect(section.groupMode).toBe("mention");
|
|
310
|
+
expect(section.groupCommandMode).toBe("off");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("does not overwrite per-group groupMode settings during login", async () => {
|
|
314
|
+
const cfg = buildCfg({
|
|
315
|
+
baseUrl: "https://api.example.com",
|
|
316
|
+
groupMode: "all",
|
|
317
|
+
groups: {
|
|
318
|
+
"group-quiet": { groupMode: "mention" },
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
322
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
323
|
+
access_token: "access-tok",
|
|
324
|
+
refresh_token: "refresh-tok",
|
|
325
|
+
});
|
|
326
|
+
const persistConfig = vi.fn();
|
|
327
|
+
|
|
328
|
+
await runOpenclawClawlingLogin({
|
|
329
|
+
cfg,
|
|
330
|
+
runtime: { log: vi.fn() },
|
|
331
|
+
readInviteCode: async () => "INV-ABC",
|
|
332
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
333
|
+
persistConfig,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
337
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
338
|
+
expect(section.groups).toEqual({
|
|
339
|
+
"group-quiet": { groupMode: "mention" },
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("allows clawchat-plugin-openclaw plugin tools after successful login without replacing policy", async () => {
|
|
344
|
+
const cfg = {
|
|
345
|
+
...buildCfg({ baseUrl: "https://api.example.com" }),
|
|
346
|
+
tools: {
|
|
347
|
+
profile: "coding",
|
|
348
|
+
allow: [],
|
|
349
|
+
deny: ["exec"],
|
|
350
|
+
alsoAllow: ["browser"],
|
|
351
|
+
},
|
|
352
|
+
} as unknown as OpenClawConfig;
|
|
353
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
354
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
355
|
+
access_token: "access-tok",
|
|
356
|
+
refresh_token: "refresh-tok",
|
|
357
|
+
});
|
|
358
|
+
const persistConfig = vi.fn();
|
|
359
|
+
|
|
360
|
+
await runOpenclawClawlingLogin({
|
|
361
|
+
cfg,
|
|
362
|
+
runtime: { log: vi.fn() },
|
|
363
|
+
readInviteCode: async () => "INV-ABC",
|
|
364
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
365
|
+
persistConfig,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
369
|
+
expect(savedCfg.tools).toMatchObject({
|
|
370
|
+
profile: "coding",
|
|
371
|
+
allow: [],
|
|
372
|
+
deny: ["exec"],
|
|
373
|
+
alsoAllow: ["browser", "clawchat-plugin-openclaw"],
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("enables the runtime plugin entry after successful login without replacing plugin config", async () => {
|
|
378
|
+
const cfg = {
|
|
379
|
+
...buildCfg({ baseUrl: "https://api.example.com" }),
|
|
380
|
+
plugins: {
|
|
381
|
+
allow: ["browser"],
|
|
382
|
+
entries: {
|
|
383
|
+
[CHANNEL_ID]: {
|
|
384
|
+
enabled: false,
|
|
385
|
+
config: { keep: true },
|
|
386
|
+
hooks: { allowConversationAccess: true },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
} as unknown as OpenClawConfig;
|
|
391
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
392
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
393
|
+
access_token: "access-tok",
|
|
394
|
+
refresh_token: "refresh-tok",
|
|
395
|
+
});
|
|
396
|
+
const persistConfig = vi.fn();
|
|
397
|
+
|
|
398
|
+
await runOpenclawClawlingLogin({
|
|
399
|
+
cfg,
|
|
400
|
+
runtime: { log: vi.fn() },
|
|
401
|
+
readInviteCode: async () => "INV-ABC",
|
|
402
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
403
|
+
persistConfig,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig & {
|
|
407
|
+
plugins: { allow?: string[]; entries: Record<string, Record<string, unknown>> };
|
|
408
|
+
};
|
|
409
|
+
expect(savedCfg.plugins.allow).toEqual(["browser", "clawchat-plugin-openclaw"]);
|
|
410
|
+
expect(savedCfg.plugins.entries[CHANNEL_ID]).toEqual({
|
|
411
|
+
enabled: true,
|
|
412
|
+
config: { keep: true },
|
|
413
|
+
hooks: { allowConversationAccess: true },
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("surfaces agents/connect API errors with the kind and message", async () => {
|
|
418
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
419
|
+
const agentsConnect = vi.fn().mockRejectedValue(
|
|
420
|
+
new ClawlingApiError("api", "invite code expired", { code: 4002 }),
|
|
421
|
+
);
|
|
422
|
+
await expect(
|
|
423
|
+
runOpenclawClawlingLogin({
|
|
424
|
+
cfg,
|
|
425
|
+
runtime: { log: vi.fn() },
|
|
426
|
+
readInviteCode: async () => "expired",
|
|
427
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
428
|
+
persistConfig: vi.fn(),
|
|
429
|
+
}),
|
|
430
|
+
).rejects.toThrow(/agents\/connect failed \(api\): invite code expired/);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("rejects responses that omit required fields", async () => {
|
|
434
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
435
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
436
|
+
agent: { user_id: "", owner_id: "owner-123" },
|
|
437
|
+
access_token: "t",
|
|
438
|
+
refresh_token: "r",
|
|
439
|
+
});
|
|
440
|
+
await expect(
|
|
441
|
+
runOpenclawClawlingLogin({
|
|
442
|
+
cfg,
|
|
443
|
+
runtime: { log: vi.fn() },
|
|
444
|
+
readInviteCode: async () => "ok",
|
|
445
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
446
|
+
persistConfig: vi.fn(),
|
|
447
|
+
}),
|
|
448
|
+
).rejects.toThrow(/missing required fields/);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("rejects whitespace-only required agents/connect response fields", async () => {
|
|
452
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
453
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
454
|
+
agent: { user_id: " ", owner_id: "owner-123" },
|
|
455
|
+
access_token: "t",
|
|
456
|
+
refresh_token: "r",
|
|
457
|
+
});
|
|
458
|
+
await expect(
|
|
459
|
+
runOpenclawClawlingLogin({
|
|
460
|
+
cfg,
|
|
461
|
+
runtime: { log: vi.fn() },
|
|
462
|
+
readInviteCode: async () => "ok",
|
|
463
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
464
|
+
persistConfig: vi.fn(),
|
|
465
|
+
}),
|
|
466
|
+
).rejects.toThrow(/missing required fields/);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("rejects whitespace-only agents/connect access tokens", async () => {
|
|
470
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
471
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
472
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
473
|
+
access_token: " ",
|
|
474
|
+
refresh_token: "r",
|
|
475
|
+
});
|
|
476
|
+
await expect(
|
|
477
|
+
runOpenclawClawlingLogin({
|
|
478
|
+
cfg,
|
|
479
|
+
runtime: { log: vi.fn() },
|
|
480
|
+
readInviteCode: async () => "ok",
|
|
481
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
482
|
+
persistConfig: vi.fn(),
|
|
483
|
+
}),
|
|
484
|
+
).rejects.toThrow(/access_token/);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("rejects agents/connect responses that omit agent owner ids", async () => {
|
|
488
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
489
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
490
|
+
agent: { user_id: "agent-123" },
|
|
491
|
+
access_token: "t",
|
|
492
|
+
refresh_token: "r",
|
|
493
|
+
});
|
|
494
|
+
await expect(
|
|
495
|
+
runOpenclawClawlingLogin({
|
|
496
|
+
cfg,
|
|
497
|
+
runtime: { log: vi.fn() },
|
|
498
|
+
readInviteCode: async () => "ok",
|
|
499
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
500
|
+
persistConfig: vi.fn(),
|
|
501
|
+
}),
|
|
502
|
+
).rejects.toThrow(/agent\.owner_id/);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("rejects empty returned agent owner ids", async () => {
|
|
506
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
507
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
508
|
+
agent: { user_id: "agent-123", owner_id: "" },
|
|
509
|
+
access_token: "t",
|
|
510
|
+
refresh_token: "r",
|
|
511
|
+
});
|
|
512
|
+
await expect(
|
|
513
|
+
runOpenclawClawlingLogin({
|
|
514
|
+
cfg,
|
|
515
|
+
runtime: { log: vi.fn() },
|
|
516
|
+
readInviteCode: async () => "ok",
|
|
517
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
518
|
+
persistConfig: vi.fn(),
|
|
519
|
+
}),
|
|
520
|
+
).rejects.toThrow(/agent\.owner_id/);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("rejects whitespace-only returned agent owner ids", async () => {
|
|
524
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
525
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
526
|
+
agent: { user_id: "agent-123", owner_id: " " },
|
|
527
|
+
access_token: "t",
|
|
528
|
+
refresh_token: "r",
|
|
529
|
+
});
|
|
530
|
+
await expect(
|
|
531
|
+
runOpenclawClawlingLogin({
|
|
532
|
+
cfg,
|
|
533
|
+
runtime: { log: vi.fn() },
|
|
534
|
+
readInviteCode: async () => "ok",
|
|
535
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
536
|
+
persistConfig: vi.fn(),
|
|
537
|
+
}),
|
|
538
|
+
).rejects.toThrow(/missing required fields/);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("rejects whitespace-only activation conversation ids when present", async () => {
|
|
542
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
543
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
544
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
545
|
+
access_token: "t",
|
|
546
|
+
refresh_token: "r",
|
|
547
|
+
conversation: { id: " " },
|
|
548
|
+
});
|
|
549
|
+
await expect(
|
|
550
|
+
runOpenclawClawlingLogin({
|
|
551
|
+
cfg,
|
|
552
|
+
runtime: { log: vi.fn() },
|
|
553
|
+
readInviteCode: async () => "ok",
|
|
554
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
555
|
+
persistConfig: vi.fn(),
|
|
556
|
+
}),
|
|
557
|
+
).rejects.toThrow(/missing required fields/);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("trims persisted agents/connect credentials and activation conversation id", async () => {
|
|
561
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
562
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
563
|
+
agent: { user_id: " agent-123 ", owner_id: " owner-123 ", nickname: "Bot" },
|
|
564
|
+
access_token: " access-tok ",
|
|
565
|
+
refresh_token: " refresh-tok ",
|
|
566
|
+
conversation: { id: " conv-activation " },
|
|
567
|
+
});
|
|
568
|
+
const persistConfig = vi.fn();
|
|
569
|
+
const upsertActivation = vi.fn();
|
|
570
|
+
|
|
571
|
+
await runOpenclawClawlingLogin({
|
|
572
|
+
cfg,
|
|
573
|
+
runtime: { log: vi.fn() },
|
|
574
|
+
readInviteCode: async () => "ok",
|
|
575
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
576
|
+
persistConfig,
|
|
577
|
+
store: { upsertActivation },
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
581
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
582
|
+
expect(section.token).toBe("access-tok");
|
|
583
|
+
expect(section.refreshToken).toBe("refresh-tok");
|
|
584
|
+
expect(section.userId).toBe("agent-123");
|
|
585
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
586
|
+
expect(upsertActivation).toHaveBeenCalledWith({
|
|
587
|
+
platform: "openclaw",
|
|
588
|
+
accountId: "default",
|
|
589
|
+
userId: "agent-123",
|
|
590
|
+
ownerUserId: "owner-123",
|
|
591
|
+
accessToken: "access-tok",
|
|
592
|
+
refreshToken: "refresh-tok",
|
|
593
|
+
conversationId: "conv-activation",
|
|
594
|
+
loginMethod: "login",
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("does not preserve a stale refresh token when agents/connect returns a blank refresh token", async () => {
|
|
599
|
+
const cfg = buildCfg({
|
|
600
|
+
baseUrl: "https://api.example.com",
|
|
601
|
+
refreshToken: "stale-refresh",
|
|
602
|
+
});
|
|
603
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
604
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
605
|
+
access_token: "access-tok",
|
|
606
|
+
refresh_token: " ",
|
|
607
|
+
});
|
|
608
|
+
const persistConfig = vi.fn();
|
|
609
|
+
|
|
610
|
+
await runOpenclawClawlingLogin({
|
|
611
|
+
cfg,
|
|
612
|
+
runtime: { log: vi.fn() },
|
|
613
|
+
readInviteCode: async () => "ok",
|
|
614
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
615
|
+
persistConfig,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
619
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
620
|
+
expect(section.token).toBe("access-tok");
|
|
621
|
+
expect(section.userId).toBe("agent-123");
|
|
622
|
+
expect(section.refreshToken).toBeUndefined();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("persists the config automatically after receiving the token (no further prompts)", async () => {
|
|
626
|
+
const cfg = buildCfg({
|
|
627
|
+
baseUrl: "https://api.example.com",
|
|
628
|
+
websocketUrl: "wss://ws.example.com",
|
|
629
|
+
});
|
|
630
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
631
|
+
agent: { id: "agt-9", user_id: "agent-9", owner_id: "owner-9", nickname: "Nine" },
|
|
632
|
+
access_token: "ACC-0123456789",
|
|
633
|
+
refresh_token: "REF-xyz",
|
|
634
|
+
});
|
|
635
|
+
const persistCalls: OpenClawConfig[] = [];
|
|
636
|
+
const logMessages: string[] = [];
|
|
637
|
+
await runOpenclawClawlingLogin({
|
|
638
|
+
cfg,
|
|
639
|
+
runtime: { log: (m) => logMessages.push(m) },
|
|
640
|
+
readInviteCode: async () => "INV-AUTO",
|
|
641
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
642
|
+
persistConfig: (next) => {
|
|
643
|
+
persistCalls.push(next);
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
// persistConfig is invoked exactly once with the merged credentials.
|
|
647
|
+
expect(persistCalls).toHaveLength(1);
|
|
648
|
+
const section = (persistCalls[0]!.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
649
|
+
expect(section.token).toBe("ACC-0123456789");
|
|
650
|
+
expect(section.agentId).toBe("agt-9");
|
|
651
|
+
expect(section.userId).toBe("agent-9");
|
|
652
|
+
expect(section.ownerUserId).toBe("owner-9");
|
|
653
|
+
expect(section.refreshToken).toBe("REF-xyz");
|
|
654
|
+
// Existing URL fields are preserved.
|
|
655
|
+
expect(section.baseUrl).toBe("https://api.example.com");
|
|
656
|
+
expect(section.websocketUrl).toBe("wss://ws.example.com");
|
|
657
|
+
// Operator-visible log confirms the automatic config write.
|
|
658
|
+
expect(logMessages.some((m) => /Updating config/.test(m))).toBe(true);
|
|
659
|
+
expect(logMessages.some((m) => /Config file updated/.test(m))).toBe(true);
|
|
660
|
+
// Token in logs is redacted.
|
|
661
|
+
expect(logMessages.every((m) => !m.includes("ACC-0123456789"))).toBe(true);
|
|
662
|
+
expect(logMessages.every((m) => !m.includes("ACC-") && !m.includes("6789"))).toBe(true);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("login logs don't leak the /agents/connect endpoint path or base URL", async () => {
|
|
666
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
667
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
668
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
669
|
+
access_token: "t",
|
|
670
|
+
refresh_token: "r",
|
|
671
|
+
});
|
|
672
|
+
const logMessages: string[] = [];
|
|
673
|
+
await runOpenclawClawlingLogin({
|
|
674
|
+
cfg,
|
|
675
|
+
runtime: { log: (m) => logMessages.push(m) },
|
|
676
|
+
readInviteCode: async () => "INV",
|
|
677
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
678
|
+
persistConfig: vi.fn(),
|
|
679
|
+
});
|
|
680
|
+
expect(logMessages.every((m) => !m.includes("/agents/connect"))).toBe(true);
|
|
681
|
+
expect(logMessages.every((m) => !m.includes(cfg.channels!["clawchat-plugin-openclaw"].baseUrl))).toBe(
|
|
682
|
+
true,
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("trims the invite code before sending", async () => {
|
|
687
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
688
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
689
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
690
|
+
access_token: "t",
|
|
691
|
+
refresh_token: "r",
|
|
692
|
+
});
|
|
693
|
+
await runOpenclawClawlingLogin({
|
|
694
|
+
cfg,
|
|
695
|
+
runtime: { log: vi.fn() },
|
|
696
|
+
readInviteCode: async () => " INV-TRIM ",
|
|
697
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
698
|
+
persistConfig: vi.fn(),
|
|
699
|
+
});
|
|
700
|
+
expect(agentsConnect).toHaveBeenCalledWith({
|
|
701
|
+
code: "INV-TRIM",
|
|
702
|
+
platform: "openclaw",
|
|
703
|
+
type: "clawbot",
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () => {
|
|
709
|
+
it("performs a full login when called with a fixed readInviteCode (programmatic path)", async () => {
|
|
710
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
711
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
712
|
+
agent: { user_id: "agent-7", owner_id: "owner-7", nickname: "Seven" },
|
|
713
|
+
access_token: "acc-non-interactive",
|
|
714
|
+
refresh_token: "ref-ni",
|
|
715
|
+
});
|
|
716
|
+
const persistConfig = vi.fn();
|
|
717
|
+
await runOpenclawClawlingLogin({
|
|
718
|
+
cfg,
|
|
719
|
+
runtime: { log: () => {} },
|
|
720
|
+
readInviteCode: async () => "INV-PROGRAMMATIC",
|
|
721
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
722
|
+
persistConfig,
|
|
723
|
+
});
|
|
724
|
+
expect(agentsConnect).toHaveBeenCalledWith({
|
|
725
|
+
code: "INV-PROGRAMMATIC",
|
|
726
|
+
platform: "openclaw",
|
|
727
|
+
type: "clawbot",
|
|
728
|
+
});
|
|
729
|
+
expect(persistConfig).toHaveBeenCalledTimes(1);
|
|
730
|
+
const section = (persistConfig.mock.calls[0]![0] as OpenClawConfig).channels!
|
|
731
|
+
["clawchat-plugin-openclaw"] as Record<string, unknown>;
|
|
732
|
+
expect(section.token).toBe("acc-non-interactive");
|
|
733
|
+
expect(section.userId).toBe("agent-7");
|
|
734
|
+
expect(section.ownerUserId).toBe("owner-7");
|
|
735
|
+
expect(section.refreshToken).toBe("ref-ni");
|
|
736
|
+
});
|
|
737
|
+
});
|