@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.
Files changed (85) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +121 -19
  3. package/dist/index.js +10 -19
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +78 -10
  6. package/dist/src/api-types.test-d.js +10 -0
  7. package/dist/src/channel.js +25 -156
  8. package/dist/src/channel.setup.js +120 -0
  9. package/dist/src/client.js +37 -41
  10. package/dist/src/config.js +75 -17
  11. package/dist/src/inbound.js +79 -61
  12. package/dist/src/login.runtime.js +84 -19
  13. package/dist/src/media-runtime.js +8 -8
  14. package/dist/src/message-mapper.js +1 -1
  15. package/dist/src/mock-transport.js +31 -0
  16. package/dist/src/outbound.js +410 -26
  17. package/dist/src/protocol-types.js +63 -0
  18. package/dist/src/protocol-types.typecheck.js +1 -0
  19. package/dist/src/protocol.js +2 -7
  20. package/dist/src/reply-dispatcher.js +157 -54
  21. package/dist/src/runtime.js +795 -119
  22. package/dist/src/storage.js +689 -0
  23. package/dist/src/tools-schema.js +98 -16
  24. package/dist/src/tools.js +422 -135
  25. package/dist/src/ws-alignment.js +178 -0
  26. package/dist/src/ws-client.js +588 -0
  27. package/dist/src/ws-log.js +19 -0
  28. package/index.ts +10 -22
  29. package/openclaw.plugin.json +37 -2
  30. package/package.json +17 -4
  31. package/setup-entry.ts +4 -0
  32. package/skills/clawchat/SKILL.md +88 -0
  33. package/src/api-client.test.ts +274 -14
  34. package/src/api-client.ts +138 -23
  35. package/src/api-types.test-d.ts +12 -0
  36. package/src/api-types.ts +90 -4
  37. package/src/buffered-stream.test.ts +14 -12
  38. package/src/buffered-stream.ts +1 -1
  39. package/src/channel.outbound.test.ts +269 -60
  40. package/src/channel.setup.ts +146 -0
  41. package/src/channel.test.ts +130 -24
  42. package/src/channel.ts +30 -186
  43. package/src/client.test.ts +197 -11
  44. package/src/client.ts +50 -57
  45. package/src/config.test.ts +108 -6
  46. package/src/config.ts +95 -24
  47. package/src/inbound.test.ts +288 -37
  48. package/src/inbound.ts +96 -84
  49. package/src/login.runtime.test.ts +347 -13
  50. package/src/login.runtime.ts +105 -23
  51. package/src/manifest.test.ts +146 -74
  52. package/src/media-runtime.test.ts +57 -2
  53. package/src/media-runtime.ts +26 -17
  54. package/src/message-mapper.test.ts +2 -2
  55. package/src/message-mapper.ts +2 -2
  56. package/src/mock-transport.test.ts +35 -0
  57. package/src/mock-transport.ts +38 -0
  58. package/src/outbound.test.ts +694 -73
  59. package/src/outbound.ts +484 -31
  60. package/src/plugin-entry.test.ts +1 -0
  61. package/src/protocol-types.test.ts +69 -0
  62. package/src/protocol-types.ts +296 -0
  63. package/src/protocol-types.typecheck.ts +89 -0
  64. package/src/protocol.test.ts +1 -6
  65. package/src/protocol.ts +2 -7
  66. package/src/reply-dispatcher.test.ts +819 -119
  67. package/src/reply-dispatcher.ts +202 -60
  68. package/src/runtime.test.ts +2120 -41
  69. package/src/runtime.ts +935 -142
  70. package/src/scripts.test.ts +85 -0
  71. package/src/storage.test.ts +793 -0
  72. package/src/storage.ts +1095 -0
  73. package/src/streaming.test.ts +9 -8
  74. package/src/streaming.ts +1 -1
  75. package/src/tools-schema.ts +148 -20
  76. package/src/tools.test.ts +377 -50
  77. package/src/tools.ts +574 -154
  78. package/src/ws-alignment.test.ts +103 -0
  79. package/src/ws-alignment.ts +275 -0
  80. package/src/ws-client.test.ts +1218 -0
  81. package/src/ws-client.ts +662 -0
  82. package/src/ws-log.test.ts +32 -0
  83. package/src/ws-log.ts +31 -0
  84. package/skills/clawchat-account-tools/SKILL.md +0 -26
  85. 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("uses the runtime config mutator with auto reload intent for config writes", async () => {
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({ mode: "auto" });
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: vi.fn() },
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
  });