@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +64 -0
- package/README.md +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
|
@@ -30,7 +30,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
30
30
|
it("works with no prior setup (baseUrl / websocketUrl default built-in)", async () => {
|
|
31
31
|
const cfg = buildCfg({}); // empty section — resolver provides defaults
|
|
32
32
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
33
|
-
agent: { user_id: "u" },
|
|
33
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
34
34
|
access_token: "t",
|
|
35
35
|
refresh_token: "r",
|
|
36
36
|
});
|
|
@@ -102,9 +102,12 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
102
102
|
expect(persistConfig).toHaveBeenCalledTimes(1);
|
|
103
103
|
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
104
104
|
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
105
|
+
expect((savedCfg.plugins as { allow?: string[] })?.allow).toEqual([CHANNEL_ID]);
|
|
105
106
|
expect(section.token).toBe("access-tok");
|
|
106
107
|
expect(section.refreshToken).toBe("refresh-tok");
|
|
107
108
|
expect(section.userId).toBe("agent-123");
|
|
109
|
+
expect(section.ownerUserId).toBe("owner-1");
|
|
110
|
+
expect(section.groupMode).toBe("all");
|
|
108
111
|
// Existing baseUrl and websocketUrl are preserved — agents/connect
|
|
109
112
|
// does not return them.
|
|
110
113
|
expect(section.baseUrl).toBe("https://api.example.com");
|
|
@@ -112,19 +115,64 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
112
115
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
|
|
113
116
|
});
|
|
114
117
|
|
|
115
|
-
it("
|
|
118
|
+
it("persists latest activation bootstrap metadata to the sqlite store after config write", async () => {
|
|
116
119
|
const cfg = buildCfg({
|
|
117
120
|
baseUrl: "https://api.example.com",
|
|
118
121
|
websocketUrl: "wss://ws.example.com/v2/client",
|
|
119
122
|
});
|
|
120
123
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
121
|
-
agent: { user_id: "agent-123", nickname: "Bot" },
|
|
124
|
+
agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
|
|
125
|
+
access_token: "access-plain",
|
|
126
|
+
refresh_token: "refresh-plain",
|
|
127
|
+
conversation: { id: "conv-activation" },
|
|
128
|
+
});
|
|
129
|
+
const persistConfig = vi.fn();
|
|
130
|
+
const upsertActivation = vi.fn();
|
|
131
|
+
|
|
132
|
+
await runOpenclawClawlingLogin({
|
|
133
|
+
cfg,
|
|
134
|
+
runtime: { log: vi.fn() },
|
|
135
|
+
readInviteCode: async () => "INV-ABC",
|
|
136
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
137
|
+
persistConfig,
|
|
138
|
+
store: { upsertActivation },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(upsertActivation).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(persistConfig.mock.invocationCallOrder[0]).toBeLessThan(
|
|
143
|
+
upsertActivation.mock.invocationCallOrder[0],
|
|
144
|
+
);
|
|
145
|
+
expect(upsertActivation).toHaveBeenCalledWith({
|
|
146
|
+
platform: "openclaw",
|
|
147
|
+
accountId: "default",
|
|
148
|
+
userId: "agent-123",
|
|
149
|
+
ownerUserId: "owner-123",
|
|
150
|
+
conversationId: "conv-activation",
|
|
151
|
+
loginMethod: "login",
|
|
152
|
+
});
|
|
153
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("accessToken");
|
|
154
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("refreshToken");
|
|
155
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("baseUrl");
|
|
156
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("websocketUrl");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("uses the runtime config mutator with restart intent after credential writes", async () => {
|
|
160
|
+
const cfg = buildCfg({
|
|
161
|
+
baseUrl: "https://api.example.com",
|
|
162
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
163
|
+
});
|
|
164
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
165
|
+
agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
|
|
122
166
|
access_token: "access-tok",
|
|
123
167
|
refresh_token: "refresh-tok",
|
|
124
168
|
});
|
|
125
169
|
let mutatedCfg: OpenClawConfig | undefined;
|
|
170
|
+
const log = vi.fn();
|
|
126
171
|
const mutateConfigFile = vi.fn(async (params) => {
|
|
127
|
-
expect(params.afterWrite).toEqual({
|
|
172
|
+
expect(params.afterWrite).toEqual({
|
|
173
|
+
mode: "restart",
|
|
174
|
+
reason: "openclaw-clawchat credentials changed",
|
|
175
|
+
});
|
|
128
176
|
const draft = structuredClone(cfg) as OpenClawConfig;
|
|
129
177
|
await params.mutate(draft, { snapshot: {} as never, previousHash: "before" });
|
|
130
178
|
mutatedCfg = draft;
|
|
@@ -133,7 +181,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
133
181
|
|
|
134
182
|
await runOpenclawClawlingLogin({
|
|
135
183
|
cfg,
|
|
136
|
-
runtime: { log
|
|
184
|
+
runtime: { log },
|
|
137
185
|
readInviteCode: async () => "INV-ABC",
|
|
138
186
|
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
139
187
|
mutateConfigFile,
|
|
@@ -146,6 +194,19 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
146
194
|
expect(section.token).toBe("access-tok");
|
|
147
195
|
expect(section.refreshToken).toBe("refresh-tok");
|
|
148
196
|
expect(section.userId).toBe("agent-123");
|
|
197
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
198
|
+
expect(section.groupMode).toBe("all");
|
|
199
|
+
const plugins = (mutatedCfg! as OpenClawConfig & {
|
|
200
|
+
plugins: { allow?: string[]; entries: Record<string, Record<string, unknown>> };
|
|
201
|
+
}).plugins;
|
|
202
|
+
expect(plugins.allow).toEqual([CHANNEL_ID]);
|
|
203
|
+
expect(plugins.entries[CHANNEL_ID]?.enabled).toBe(true);
|
|
204
|
+
expect(log).toHaveBeenCalledWith(
|
|
205
|
+
expect.stringContaining("Persisting ClawChat credentials and plugin activation"),
|
|
206
|
+
);
|
|
207
|
+
expect(log).toHaveBeenCalledWith(
|
|
208
|
+
expect.stringContaining("ClawChat credentials and plugin activation persisted"),
|
|
209
|
+
);
|
|
149
210
|
});
|
|
150
211
|
|
|
151
212
|
it("preserves other configured channels when persisting ClawChat credentials", async () => {
|
|
@@ -162,7 +223,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
162
223
|
},
|
|
163
224
|
} as unknown as OpenClawConfig;
|
|
164
225
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
165
|
-
agent: { user_id: "agent-123" },
|
|
226
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
166
227
|
access_token: "access-tok",
|
|
167
228
|
refresh_token: "refresh-tok",
|
|
168
229
|
});
|
|
@@ -186,6 +247,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
186
247
|
websocketUrl: "wss://ws.example.com/v2/client",
|
|
187
248
|
token: "access-tok",
|
|
188
249
|
userId: "agent-123",
|
|
250
|
+
ownerUserId: "owner-123",
|
|
189
251
|
refreshToken: "refresh-tok",
|
|
190
252
|
});
|
|
191
253
|
});
|
|
@@ -196,7 +258,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
196
258
|
baseUrl: "https://api.example.com",
|
|
197
259
|
});
|
|
198
260
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
199
|
-
agent: { user_id: "agent-123" },
|
|
261
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
200
262
|
access_token: "access-tok",
|
|
201
263
|
refresh_token: "refresh-tok",
|
|
202
264
|
});
|
|
@@ -215,6 +277,63 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
215
277
|
expect(section.enabled).toBe(true);
|
|
216
278
|
expect(section.token).toBe("access-tok");
|
|
217
279
|
expect(section.userId).toBe("agent-123");
|
|
280
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
281
|
+
expect(section.groupMode).toBe("all");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("does not overwrite an explicit mention groupMode during login", async () => {
|
|
285
|
+
const cfg = buildCfg({
|
|
286
|
+
baseUrl: "https://api.example.com",
|
|
287
|
+
groupMode: "mention",
|
|
288
|
+
});
|
|
289
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
290
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
291
|
+
access_token: "access-tok",
|
|
292
|
+
refresh_token: "refresh-tok",
|
|
293
|
+
});
|
|
294
|
+
const persistConfig = vi.fn();
|
|
295
|
+
|
|
296
|
+
await runOpenclawClawlingLogin({
|
|
297
|
+
cfg,
|
|
298
|
+
runtime: { log: vi.fn() },
|
|
299
|
+
readInviteCode: async () => "INV-ABC",
|
|
300
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
301
|
+
persistConfig,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
305
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
306
|
+
expect(section.groupMode).toBe("mention");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("does not overwrite per-group groupMode settings during login", async () => {
|
|
310
|
+
const cfg = buildCfg({
|
|
311
|
+
baseUrl: "https://api.example.com",
|
|
312
|
+
groupMode: "all",
|
|
313
|
+
groups: {
|
|
314
|
+
"group-quiet": { groupMode: "mention" },
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
318
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
319
|
+
access_token: "access-tok",
|
|
320
|
+
refresh_token: "refresh-tok",
|
|
321
|
+
});
|
|
322
|
+
const persistConfig = vi.fn();
|
|
323
|
+
|
|
324
|
+
await runOpenclawClawlingLogin({
|
|
325
|
+
cfg,
|
|
326
|
+
runtime: { log: vi.fn() },
|
|
327
|
+
readInviteCode: async () => "INV-ABC",
|
|
328
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
329
|
+
persistConfig,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
333
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
334
|
+
expect(section.groups).toEqual({
|
|
335
|
+
"group-quiet": { groupMode: "mention" },
|
|
336
|
+
});
|
|
218
337
|
});
|
|
219
338
|
|
|
220
339
|
it("allows openclaw-clawchat plugin tools after successful login without replacing policy", async () => {
|
|
@@ -228,7 +347,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
228
347
|
},
|
|
229
348
|
} as unknown as OpenClawConfig;
|
|
230
349
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
231
|
-
agent: { user_id: "agent-123" },
|
|
350
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
232
351
|
access_token: "access-tok",
|
|
233
352
|
refresh_token: "refresh-tok",
|
|
234
353
|
});
|
|
@@ -251,6 +370,46 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
251
370
|
});
|
|
252
371
|
});
|
|
253
372
|
|
|
373
|
+
it("enables the runtime plugin entry after successful login without replacing plugin config", async () => {
|
|
374
|
+
const cfg = {
|
|
375
|
+
...buildCfg({ baseUrl: "https://api.example.com" }),
|
|
376
|
+
plugins: {
|
|
377
|
+
allow: ["browser"],
|
|
378
|
+
entries: {
|
|
379
|
+
[CHANNEL_ID]: {
|
|
380
|
+
enabled: false,
|
|
381
|
+
config: { keep: true },
|
|
382
|
+
hooks: { allowConversationAccess: true },
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
} as unknown as OpenClawConfig;
|
|
387
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
388
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
389
|
+
access_token: "access-tok",
|
|
390
|
+
refresh_token: "refresh-tok",
|
|
391
|
+
});
|
|
392
|
+
const persistConfig = vi.fn();
|
|
393
|
+
|
|
394
|
+
await runOpenclawClawlingLogin({
|
|
395
|
+
cfg,
|
|
396
|
+
runtime: { log: vi.fn() },
|
|
397
|
+
readInviteCode: async () => "INV-ABC",
|
|
398
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
399
|
+
persistConfig,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig & {
|
|
403
|
+
plugins: { allow?: string[]; entries: Record<string, Record<string, unknown>> };
|
|
404
|
+
};
|
|
405
|
+
expect(savedCfg.plugins.allow).toEqual(["browser", "openclaw-clawchat"]);
|
|
406
|
+
expect(savedCfg.plugins.entries[CHANNEL_ID]).toEqual({
|
|
407
|
+
enabled: true,
|
|
408
|
+
config: { keep: true },
|
|
409
|
+
hooks: { allowConversationAccess: true },
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
254
413
|
it("surfaces agents/connect API errors with the kind and message", async () => {
|
|
255
414
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
256
415
|
const agentsConnect = vi.fn().mockRejectedValue(
|
|
@@ -270,9 +429,118 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
270
429
|
it("rejects responses that omit required fields", async () => {
|
|
271
430
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
272
431
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
273
|
-
agent: { user_id: "" },
|
|
432
|
+
agent: { user_id: "", owner_id: "owner-123" },
|
|
433
|
+
access_token: "t",
|
|
434
|
+
refresh_token: "r",
|
|
435
|
+
});
|
|
436
|
+
await expect(
|
|
437
|
+
runOpenclawClawlingLogin({
|
|
438
|
+
cfg,
|
|
439
|
+
runtime: { log: vi.fn() },
|
|
440
|
+
readInviteCode: async () => "ok",
|
|
441
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
442
|
+
persistConfig: vi.fn(),
|
|
443
|
+
}),
|
|
444
|
+
).rejects.toThrow(/missing required fields/);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("rejects whitespace-only required agents/connect response fields", async () => {
|
|
448
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
449
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
450
|
+
agent: { user_id: " ", owner_id: "owner-123" },
|
|
451
|
+
access_token: "t",
|
|
452
|
+
refresh_token: "r",
|
|
453
|
+
});
|
|
454
|
+
await expect(
|
|
455
|
+
runOpenclawClawlingLogin({
|
|
456
|
+
cfg,
|
|
457
|
+
runtime: { log: vi.fn() },
|
|
458
|
+
readInviteCode: async () => "ok",
|
|
459
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
460
|
+
persistConfig: vi.fn(),
|
|
461
|
+
}),
|
|
462
|
+
).rejects.toThrow(/missing required fields/);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("rejects whitespace-only agents/connect access tokens", async () => {
|
|
466
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
467
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
468
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
469
|
+
access_token: " ",
|
|
470
|
+
refresh_token: "r",
|
|
471
|
+
});
|
|
472
|
+
await expect(
|
|
473
|
+
runOpenclawClawlingLogin({
|
|
474
|
+
cfg,
|
|
475
|
+
runtime: { log: vi.fn() },
|
|
476
|
+
readInviteCode: async () => "ok",
|
|
477
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
478
|
+
persistConfig: vi.fn(),
|
|
479
|
+
}),
|
|
480
|
+
).rejects.toThrow(/access_token/);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("rejects agents/connect responses that omit agent owner ids", async () => {
|
|
484
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
485
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
486
|
+
agent: { user_id: "agent-123" },
|
|
487
|
+
access_token: "t",
|
|
488
|
+
refresh_token: "r",
|
|
489
|
+
});
|
|
490
|
+
await expect(
|
|
491
|
+
runOpenclawClawlingLogin({
|
|
492
|
+
cfg,
|
|
493
|
+
runtime: { log: vi.fn() },
|
|
494
|
+
readInviteCode: async () => "ok",
|
|
495
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
496
|
+
persistConfig: vi.fn(),
|
|
497
|
+
}),
|
|
498
|
+
).rejects.toThrow(/agent\.owner_id/);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("rejects empty returned agent owner ids", async () => {
|
|
502
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
503
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
504
|
+
agent: { user_id: "agent-123", owner_id: "" },
|
|
505
|
+
access_token: "t",
|
|
506
|
+
refresh_token: "r",
|
|
507
|
+
});
|
|
508
|
+
await expect(
|
|
509
|
+
runOpenclawClawlingLogin({
|
|
510
|
+
cfg,
|
|
511
|
+
runtime: { log: vi.fn() },
|
|
512
|
+
readInviteCode: async () => "ok",
|
|
513
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
514
|
+
persistConfig: vi.fn(),
|
|
515
|
+
}),
|
|
516
|
+
).rejects.toThrow(/agent\.owner_id/);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("rejects whitespace-only returned agent owner ids", async () => {
|
|
520
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
521
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
522
|
+
agent: { user_id: "agent-123", owner_id: " " },
|
|
523
|
+
access_token: "t",
|
|
524
|
+
refresh_token: "r",
|
|
525
|
+
});
|
|
526
|
+
await expect(
|
|
527
|
+
runOpenclawClawlingLogin({
|
|
528
|
+
cfg,
|
|
529
|
+
runtime: { log: vi.fn() },
|
|
530
|
+
readInviteCode: async () => "ok",
|
|
531
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
532
|
+
persistConfig: vi.fn(),
|
|
533
|
+
}),
|
|
534
|
+
).rejects.toThrow(/missing required fields/);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("rejects whitespace-only activation conversation ids when present", async () => {
|
|
538
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
539
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
540
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
274
541
|
access_token: "t",
|
|
275
542
|
refresh_token: "r",
|
|
543
|
+
conversation: { id: " " },
|
|
276
544
|
});
|
|
277
545
|
await expect(
|
|
278
546
|
runOpenclawClawlingLogin({
|
|
@@ -285,13 +553,76 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
285
553
|
).rejects.toThrow(/missing required fields/);
|
|
286
554
|
});
|
|
287
555
|
|
|
556
|
+
it("trims persisted agents/connect credentials and activation conversation id", async () => {
|
|
557
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
558
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
559
|
+
agent: { user_id: " agent-123 ", owner_id: " owner-123 ", nickname: "Bot" },
|
|
560
|
+
access_token: " access-tok ",
|
|
561
|
+
refresh_token: " refresh-tok ",
|
|
562
|
+
conversation: { id: " conv-activation " },
|
|
563
|
+
});
|
|
564
|
+
const persistConfig = vi.fn();
|
|
565
|
+
const upsertActivation = vi.fn();
|
|
566
|
+
|
|
567
|
+
await runOpenclawClawlingLogin({
|
|
568
|
+
cfg,
|
|
569
|
+
runtime: { log: vi.fn() },
|
|
570
|
+
readInviteCode: async () => "ok",
|
|
571
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
572
|
+
persistConfig,
|
|
573
|
+
store: { upsertActivation },
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
577
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
578
|
+
expect(section.token).toBe("access-tok");
|
|
579
|
+
expect(section.refreshToken).toBe("refresh-tok");
|
|
580
|
+
expect(section.userId).toBe("agent-123");
|
|
581
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
582
|
+
expect(upsertActivation).toHaveBeenCalledWith({
|
|
583
|
+
platform: "openclaw",
|
|
584
|
+
accountId: "default",
|
|
585
|
+
userId: "agent-123",
|
|
586
|
+
ownerUserId: "owner-123",
|
|
587
|
+
conversationId: "conv-activation",
|
|
588
|
+
loginMethod: "login",
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("does not preserve a stale refresh token when agents/connect returns a blank refresh token", async () => {
|
|
593
|
+
const cfg = buildCfg({
|
|
594
|
+
baseUrl: "https://api.example.com",
|
|
595
|
+
refreshToken: "stale-refresh",
|
|
596
|
+
});
|
|
597
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
598
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
599
|
+
access_token: "access-tok",
|
|
600
|
+
refresh_token: " ",
|
|
601
|
+
});
|
|
602
|
+
const persistConfig = vi.fn();
|
|
603
|
+
|
|
604
|
+
await runOpenclawClawlingLogin({
|
|
605
|
+
cfg,
|
|
606
|
+
runtime: { log: vi.fn() },
|
|
607
|
+
readInviteCode: async () => "ok",
|
|
608
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
609
|
+
persistConfig,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
613
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
614
|
+
expect(section.token).toBe("access-tok");
|
|
615
|
+
expect(section.userId).toBe("agent-123");
|
|
616
|
+
expect(section.refreshToken).toBeUndefined();
|
|
617
|
+
});
|
|
618
|
+
|
|
288
619
|
it("persists the config automatically after receiving the token (no further prompts)", async () => {
|
|
289
620
|
const cfg = buildCfg({
|
|
290
621
|
baseUrl: "https://api.example.com",
|
|
291
622
|
websocketUrl: "wss://ws.example.com",
|
|
292
623
|
});
|
|
293
624
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
294
|
-
agent: { user_id: "agent-9", nickname: "Nine" },
|
|
625
|
+
agent: { user_id: "agent-9", owner_id: "owner-9", nickname: "Nine" },
|
|
295
626
|
access_token: "ACC-0123456789",
|
|
296
627
|
refresh_token: "REF-xyz",
|
|
297
628
|
});
|
|
@@ -311,6 +642,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
311
642
|
const section = (persistCalls[0]!.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
312
643
|
expect(section.token).toBe("ACC-0123456789");
|
|
313
644
|
expect(section.userId).toBe("agent-9");
|
|
645
|
+
expect(section.ownerUserId).toBe("owner-9");
|
|
314
646
|
expect(section.refreshToken).toBe("REF-xyz");
|
|
315
647
|
// Existing URL fields are preserved.
|
|
316
648
|
expect(section.baseUrl).toBe("https://api.example.com");
|
|
@@ -320,12 +652,13 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
320
652
|
expect(logMessages.some((m) => /Config file updated/.test(m))).toBe(true);
|
|
321
653
|
// Token in logs is redacted.
|
|
322
654
|
expect(logMessages.every((m) => !m.includes("ACC-0123456789"))).toBe(true);
|
|
655
|
+
expect(logMessages.every((m) => !m.includes("ACC-") && !m.includes("6789"))).toBe(true);
|
|
323
656
|
});
|
|
324
657
|
|
|
325
658
|
it("login logs don't leak the /agents/connect endpoint path or base URL", async () => {
|
|
326
659
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
327
660
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
328
|
-
agent: { user_id: "u" },
|
|
661
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
329
662
|
access_token: "t",
|
|
330
663
|
refresh_token: "r",
|
|
331
664
|
});
|
|
@@ -346,7 +679,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
346
679
|
it("trims the invite code before sending", async () => {
|
|
347
680
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
348
681
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
349
|
-
agent: { user_id: "u" },
|
|
682
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
350
683
|
access_token: "t",
|
|
351
684
|
refresh_token: "r",
|
|
352
685
|
});
|
|
@@ -369,7 +702,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
|
|
|
369
702
|
it("performs a full login when called with a fixed readInviteCode (programmatic path)", async () => {
|
|
370
703
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
371
704
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
372
|
-
agent: { user_id: "agent-7", nickname: "Seven" },
|
|
705
|
+
agent: { user_id: "agent-7", owner_id: "owner-7", nickname: "Seven" },
|
|
373
706
|
access_token: "acc-non-interactive",
|
|
374
707
|
refresh_token: "ref-ni",
|
|
375
708
|
});
|
|
@@ -391,6 +724,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
|
|
|
391
724
|
["openclaw-clawchat"] as Record<string, unknown>;
|
|
392
725
|
expect(section.token).toBe("acc-non-interactive");
|
|
393
726
|
expect(section.userId).toBe("agent-7");
|
|
727
|
+
expect(section.ownerUserId).toBe("owner-7");
|
|
394
728
|
expect(section.refreshToken).toBe("ref-ni");
|
|
395
729
|
});
|
|
396
730
|
});
|