@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.
Files changed (114) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +227 -0
  3. package/dist/index.js +20 -0
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +263 -0
  6. package/dist/src/api-types.js +17 -0
  7. package/dist/src/api-types.test-d.js +10 -0
  8. package/dist/src/buffered-stream.js +177 -0
  9. package/dist/src/channel.js +66 -0
  10. package/dist/src/channel.setup.js +119 -0
  11. package/dist/src/clawchat-memory.js +403 -0
  12. package/dist/src/clawchat-metadata.js +310 -0
  13. package/dist/src/client.js +35 -0
  14. package/dist/src/commands.js +35 -0
  15. package/dist/src/config.js +274 -0
  16. package/dist/src/group-message-coalescer.js +119 -0
  17. package/dist/src/inbound.js +170 -0
  18. package/dist/src/llm-context-debug.js +86 -0
  19. package/dist/src/login.runtime.js +204 -0
  20. package/dist/src/media-runtime.js +85 -0
  21. package/dist/src/message-mapper.js +146 -0
  22. package/dist/src/mock-transport.js +31 -0
  23. package/dist/src/outbound.js +628 -0
  24. package/dist/src/plugin-prompts.js +89 -0
  25. package/dist/src/profile-prompt.js +269 -0
  26. package/dist/src/profile-sync.js +110 -0
  27. package/dist/src/prompt-injection.js +25 -0
  28. package/dist/src/protocol-types.js +63 -0
  29. package/dist/src/protocol-types.typecheck.js +1 -0
  30. package/dist/src/protocol.js +33 -0
  31. package/dist/src/reply-dispatcher.js +422 -0
  32. package/dist/src/runtime.js +1254 -0
  33. package/dist/src/storage.js +525 -0
  34. package/dist/src/streaming.js +65 -0
  35. package/dist/src/terminal-send.js +36 -0
  36. package/dist/src/tools-schema.js +208 -0
  37. package/dist/src/tools.js +920 -0
  38. package/dist/src/ws-alignment.js +178 -0
  39. package/dist/src/ws-client.js +588 -0
  40. package/dist/src/ws-log.js +19 -0
  41. package/index.ts +24 -0
  42. package/openclaw.plugin.json +169 -0
  43. package/package.json +80 -0
  44. package/prompts/default-group-bio.md +19 -0
  45. package/prompts/default-owner-behavior.md +27 -0
  46. package/prompts/platform.md +13 -0
  47. package/setup-entry.ts +4 -0
  48. package/skills/clawchat/SKILL.md +91 -0
  49. package/src/api-client.test.ts +827 -0
  50. package/src/api-client.ts +414 -0
  51. package/src/api-types.ts +146 -0
  52. package/src/channel.outbound.test.ts +433 -0
  53. package/src/channel.setup.ts +145 -0
  54. package/src/channel.test.ts +262 -0
  55. package/src/channel.ts +81 -0
  56. package/src/clawchat-memory.test.ts +480 -0
  57. package/src/clawchat-memory.ts +533 -0
  58. package/src/clawchat-metadata.test.ts +477 -0
  59. package/src/clawchat-metadata.ts +429 -0
  60. package/src/client.test.ts +169 -0
  61. package/src/client.ts +56 -0
  62. package/src/commands.test.ts +39 -0
  63. package/src/commands.ts +41 -0
  64. package/src/config.test.ts +344 -0
  65. package/src/config.ts +404 -0
  66. package/src/group-message-coalescer.test.ts +237 -0
  67. package/src/group-message-coalescer.ts +171 -0
  68. package/src/inbound.test.ts +508 -0
  69. package/src/inbound.ts +278 -0
  70. package/src/llm-context-debug.test.ts +55 -0
  71. package/src/llm-context-debug.ts +139 -0
  72. package/src/login.runtime.test.ts +737 -0
  73. package/src/login.runtime.ts +277 -0
  74. package/src/manifest.test.ts +352 -0
  75. package/src/media-runtime.test.ts +207 -0
  76. package/src/media-runtime.ts +152 -0
  77. package/src/message-mapper.test.ts +201 -0
  78. package/src/message-mapper.ts +174 -0
  79. package/src/mock-transport.test.ts +35 -0
  80. package/src/mock-transport.ts +38 -0
  81. package/src/outbound.test.ts +1269 -0
  82. package/src/outbound.ts +803 -0
  83. package/src/plugin-entry.test.ts +38 -0
  84. package/src/plugin-prompts.test.ts +94 -0
  85. package/src/plugin-prompts.ts +107 -0
  86. package/src/profile-prompt.test.ts +274 -0
  87. package/src/profile-prompt.ts +351 -0
  88. package/src/profile-sync.test.ts +539 -0
  89. package/src/profile-sync.ts +191 -0
  90. package/src/prompt-injection.test.ts +39 -0
  91. package/src/prompt-injection.ts +45 -0
  92. package/src/protocol-types.test.ts +69 -0
  93. package/src/protocol-types.ts +296 -0
  94. package/src/protocol-types.typecheck.ts +89 -0
  95. package/src/protocol.test.ts +39 -0
  96. package/src/protocol.ts +42 -0
  97. package/src/reply-dispatcher.test.ts +1324 -0
  98. package/src/reply-dispatcher.ts +555 -0
  99. package/src/runtime.test.ts +4719 -0
  100. package/src/runtime.ts +1493 -0
  101. package/src/scripts.test.ts +85 -0
  102. package/src/storage.test.ts +560 -0
  103. package/src/storage.ts +807 -0
  104. package/src/terminal-send.test.ts +81 -0
  105. package/src/terminal-send.ts +56 -0
  106. package/src/tools-schema.ts +337 -0
  107. package/src/tools.test.ts +933 -0
  108. package/src/tools.ts +1185 -0
  109. package/src/ws-alignment.test.ts +103 -0
  110. package/src/ws-alignment.ts +275 -0
  111. package/src/ws-client.test.ts +1217 -0
  112. package/src/ws-client.ts +662 -0
  113. package/src/ws-log.test.ts +32 -0
  114. package/src/ws-log.ts +31 -0
@@ -0,0 +1,539 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { readClawChatMemoryFile } from "./clawchat-memory.ts";
6
+ import {
7
+ ensureGroupProfileForChat,
8
+ ensureUserProfileForSender,
9
+ refreshAgentBehaviorProfile,
10
+ refreshGroupProfile,
11
+ syncFirstSeenClawChatProfiles,
12
+ } from "./profile-sync.ts";
13
+
14
+ let roots: string[] = [];
15
+
16
+ function tempRoot(): string {
17
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawchat-plugin-openclaw-profile-sync-"));
18
+ roots.push(root);
19
+ return root;
20
+ }
21
+
22
+ afterEach(() => {
23
+ for (const root of roots) fs.rmSync(root, { recursive: true, force: true });
24
+ roots = [];
25
+ });
26
+
27
+ describe("ClawChat first-seen profile sync", () => {
28
+ it("metadata refresh writes group and participant memory files instead of SQLite profile metadata", async () => {
29
+ const memoryRoot = tempRoot();
30
+ const api = {
31
+ getConversation: vi.fn().mockResolvedValue({
32
+ conversation: {
33
+ id: "grp_memory",
34
+ type: "group",
35
+ title: "Memory Group",
36
+ description: "Group description",
37
+ creator_id: "usr_owner",
38
+ created_at: "2026-05-24T10:00:00.000Z",
39
+ updated_at: "2026-05-24T10:01:00.000Z",
40
+ participants: [
41
+ {
42
+ conversation_id: "grp_memory",
43
+ user_id: "usr_participant",
44
+ role: "member",
45
+ user: {
46
+ id: "usr_participant",
47
+ type: "user",
48
+ nickname: "Participant",
49
+ avatar_url: "https://example.test/participant.png",
50
+ bio: "Participant bio",
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ }),
56
+ getUserInfo: vi.fn().mockResolvedValue({
57
+ id: "usr_participant",
58
+ type: "user",
59
+ nickname: "Participant",
60
+ avatar_url: "https://example.test/participant.png",
61
+ bio: "Participant bio",
62
+ }),
63
+ };
64
+ const store = {};
65
+
66
+ await refreshGroupProfile({
67
+ platform: "openclaw",
68
+ accountId: "default",
69
+ chatId: "grp_memory",
70
+ api,
71
+ store,
72
+ memoryRoot,
73
+ });
74
+
75
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "group", targetId: "grp_memory" }))
76
+ .resolves.toMatchObject({
77
+ metadata: expect.objectContaining({ group_id: "grp_memory", group_title: "Memory Group" }),
78
+ });
79
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "usr_participant" }))
80
+ .resolves.toMatchObject({
81
+ metadata: expect.objectContaining({ id: "usr_participant", nickname: "Participant" }),
82
+ });
83
+ });
84
+
85
+ it("metadata refresh logs visibly and skips SQLite profile metadata when memoryRoot is missing", async () => {
86
+ const api = {
87
+ getAgentDetail: vi.fn().mockResolvedValue({
88
+ agent: {
89
+ id: "agent_row_1",
90
+ user_id: "usr_self",
91
+ owner_id: "usr_owner",
92
+ type: "agent",
93
+ nickname: "Hermes",
94
+ behavior: "Act helpfully.",
95
+ },
96
+ }),
97
+ };
98
+ const store = {};
99
+ const log = { error: vi.fn() };
100
+
101
+ await refreshAgentBehaviorProfile({
102
+ platform: "openclaw",
103
+ accountId: "default",
104
+ agentId: "agent_row_1",
105
+ accountUserId: "usr_self",
106
+ accountOwnerUserId: "usr_owner",
107
+ api,
108
+ store,
109
+ log,
110
+ });
111
+
112
+ expect(api.getAgentDetail).not.toHaveBeenCalled();
113
+ expect(log.error).toHaveBeenCalledWith(expect.stringContaining("metadata refresh requires memoryRoot"));
114
+ });
115
+
116
+ it("group refresh logs visibly and skips metadata files when memoryRoot is missing", async () => {
117
+ const api = {
118
+ getConversation: vi.fn().mockResolvedValue({
119
+ conversation: {
120
+ id: "grp_no_memory_root",
121
+ type: "group",
122
+ title: "No Memory Root",
123
+ creator_id: "usr_owner",
124
+ created_at: "2026-05-24T10:00:00.000Z",
125
+ updated_at: "2026-05-24T10:01:00.000Z",
126
+ participants: [
127
+ {
128
+ conversation_id: "grp_no_memory_root",
129
+ user_id: "usr_member",
130
+ role: "member",
131
+ joined_at: "2026-05-24T10:00:30.000Z",
132
+ },
133
+ ],
134
+ },
135
+ }),
136
+ };
137
+ const store = {};
138
+ const log = { error: vi.fn() };
139
+
140
+ await refreshGroupProfile({
141
+ platform: "openclaw",
142
+ accountId: "default",
143
+ chatId: "grp_no_memory_root",
144
+ api,
145
+ store,
146
+ log,
147
+ });
148
+
149
+ expect(log.error).toHaveBeenCalledWith(expect.stringContaining("group metadata refresh requires memoryRoot"));
150
+ expect(api.getConversation).toHaveBeenCalledWith("grp_no_memory_root");
151
+ });
152
+
153
+ it("minimal-inserts new group/user profiles before fetching details", async () => {
154
+ const memoryRoot = tempRoot();
155
+ const api = {
156
+ getConversation: vi.fn().mockResolvedValue({
157
+ conversation: {
158
+ id: "grp_1",
159
+ type: "group",
160
+ title: "Launch Room",
161
+ description: "Planning",
162
+ creator_id: "usr_owner",
163
+ created_at: "2026-05-22T10:00:00.000Z",
164
+ updated_at: "2026-05-22T10:01:00.000Z",
165
+ participants: [
166
+ {
167
+ conversation_id: "grp_1",
168
+ user_id: "usr_participant",
169
+ role: "member",
170
+ joined_at: "2026-05-22T10:00:30.000Z",
171
+ },
172
+ ],
173
+ },
174
+ }),
175
+ getUserInfo: vi.fn().mockImplementation(async (userId: string) => ({
176
+ id: userId,
177
+ type: userId === "agt_peer" ? "agent" : "user",
178
+ nickname: userId === "agt_peer" ? "Peer Agent" : "Participant",
179
+ avatar_url: userId === "agt_peer"
180
+ ? "https://example.test/agent.png"
181
+ : "https://example.test/participant.png",
182
+ bio: userId === "agt_peer" ? "Agent bio" : "Participant bio",
183
+ })),
184
+ };
185
+ const store = {};
186
+
187
+ await syncFirstSeenClawChatProfiles({
188
+ platform: "openclaw",
189
+ accountId: "default",
190
+ agentId: "agent_row_1",
191
+ accountUserId: "usr_self",
192
+ accountOwnerUserId: "usr_owner",
193
+ chat: { id: "grp_1", type: "group", lastSeenAt: 1000 },
194
+ sender: { id: "agt_peer", nickname: "Fallback Nick" },
195
+ api,
196
+ store,
197
+ memoryRoot,
198
+ });
199
+
200
+ expect(api.getConversation).toHaveBeenCalledWith("grp_1");
201
+ expect(api.getUserInfo).toHaveBeenCalledWith("agt_peer");
202
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "group", targetId: "grp_1" }))
203
+ .resolves.toMatchObject({ metadata: expect.objectContaining({ group_title: "Launch Room", group_description: "Planning" }) });
204
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "usr_participant" }))
205
+ .resolves.toMatchObject({
206
+ metadata: expect.objectContaining({ id: "usr_participant", nickname: "Participant" }),
207
+ });
208
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "agt_peer" }))
209
+ .resolves.toMatchObject({ metadata: expect.objectContaining({ id: "agt_peer", profile_type: "agent" }) });
210
+ });
211
+
212
+ it("refreshes direct user metadata even when the direct conversation is cached", async () => {
213
+ const memoryRoot = tempRoot();
214
+ const api = {
215
+ getConversation: vi.fn(),
216
+ getUserInfo: vi.fn().mockResolvedValue({
217
+ id: "usr_peer",
218
+ type: "user",
219
+ nickname: "Fetched Peer",
220
+ }),
221
+ };
222
+ const store = {
223
+ };
224
+
225
+ await syncFirstSeenClawChatProfiles({
226
+ platform: "openclaw",
227
+ accountId: "default",
228
+ agentId: "agent_row_1",
229
+ accountUserId: "usr_self",
230
+ accountOwnerUserId: "usr_owner",
231
+ chat: { id: "dm_1", type: "direct", lastSeenAt: 2000 },
232
+ sender: { id: "usr_peer", nickname: "Peer" },
233
+ api,
234
+ store,
235
+ memoryRoot,
236
+ });
237
+
238
+ expect(api.getConversation).not.toHaveBeenCalled();
239
+ expect(api.getUserInfo).toHaveBeenCalledWith("usr_peer"); await expect(readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "usr_peer" }))
240
+ .resolves.toMatchObject({ metadata: expect.objectContaining({ nickname: "Fetched Peer" }) });
241
+ });
242
+
243
+ it("first direct message fetches user detail but not conversation detail", async () => {
244
+ const memoryRoot = tempRoot();
245
+ const api = {
246
+ getConversation: vi.fn(),
247
+ getUserInfo: vi.fn().mockResolvedValue({
248
+ id: "usr_peer",
249
+ type: "user",
250
+ nickname: "Peer",
251
+ }),
252
+ };
253
+ const store = {
254
+ };
255
+
256
+ await syncFirstSeenClawChatProfiles({
257
+ platform: "openclaw",
258
+ accountId: "default",
259
+ agentId: "agent_row_1",
260
+ accountUserId: "usr_self",
261
+ accountOwnerUserId: "usr_owner",
262
+ chat: { id: "dm_1", type: "direct", lastSeenAt: 2100 },
263
+ sender: { id: "usr_peer", nickname: "Peer" },
264
+ api,
265
+ store,
266
+ memoryRoot,
267
+ });
268
+
269
+ expect(api.getUserInfo).toHaveBeenCalledWith("usr_peer");
270
+ expect(api.getConversation).not.toHaveBeenCalled();
271
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "usr_peer" }))
272
+ .resolves.toMatchObject({ metadata: expect.objectContaining({ id: "usr_peer", nickname: "Peer" }) });
273
+ });
274
+
275
+ it("keeps minimal profiles and resolves when detail fetches fail", async () => {
276
+ const memoryRoot = tempRoot();
277
+ const api = {
278
+ getConversation: vi.fn().mockRejectedValue(new Error("conversation unavailable")),
279
+ getUserInfo: vi.fn().mockRejectedValue(new Error("user unavailable")),
280
+ };
281
+ const store = {
282
+ };
283
+
284
+ await expect(syncFirstSeenClawChatProfiles({
285
+ platform: "openclaw",
286
+ accountId: "default",
287
+ accountUserId: "usr_self",
288
+ accountOwnerUserId: "usr_owner",
289
+ chat: { id: "grp_2", type: "group", lastSeenAt: 3000 },
290
+ sender: { id: "usr_peer", nickname: "Peer" },
291
+ api,
292
+ store,
293
+ memoryRoot,
294
+ })).resolves.toBeUndefined(); });
295
+
296
+ it("omits null group description from metadata files and SQLite details", async () => {
297
+ const memoryRoot = tempRoot();
298
+ const api = {
299
+ getConversation: vi.fn().mockResolvedValue({
300
+ conversation: {
301
+ id: "grp_clear_description",
302
+ type: "group",
303
+ title: "Launch Room",
304
+ description: null,
305
+ creator_id: "usr_owner",
306
+ created_at: "2026-05-22T10:00:00.000Z",
307
+ updated_at: "2026-05-22T10:01:00.000Z",
308
+ participants: [],
309
+ },
310
+ }),
311
+ };
312
+ const store = {};
313
+
314
+ await refreshGroupProfile({
315
+ platform: "openclaw",
316
+ accountId: "default",
317
+ chatId: "grp_clear_description",
318
+ api,
319
+ store,
320
+ memoryRoot,
321
+ });
322
+
323
+ const file = await readClawChatMemoryFile(memoryRoot, { targetType: "group", targetId: "grp_clear_description" });
324
+ expect(file.metadata).toEqual({
325
+ updated_at: "2026-05-22T10:01:00.000Z",
326
+ group_id: "grp_clear_description",
327
+ group_type: "group",
328
+ group_title: "Launch Room",
329
+ group_owner_id: "usr_owner",
330
+ group_created_at: "2026-05-22T10:00:00.000Z",
331
+ });
332
+ });
333
+
334
+ it("omits null participant profile type from metadata files", async () => {
335
+ const memoryRoot = tempRoot();
336
+ const api = {
337
+ getConversation: vi.fn().mockResolvedValue({
338
+ conversation: {
339
+ id: "grp_participant_type_clear",
340
+ type: "group",
341
+ title: "Launch Room",
342
+ creator_id: "usr_owner",
343
+ created_at: "2026-05-22T10:00:00.000Z",
344
+ updated_at: "2026-05-22T10:01:00.000Z",
345
+ participants: [
346
+ {
347
+ conversation_id: "grp_participant_type_clear",
348
+ user_id: "usr_participant",
349
+ role: "member",
350
+ joined_at: "2026-05-22T10:00:30.000Z",
351
+ user: {
352
+ id: "usr_participant",
353
+ type: null,
354
+ nickname: "Participant",
355
+ },
356
+ },
357
+ ],
358
+ },
359
+ }),
360
+ };
361
+ const store = {};
362
+
363
+ await refreshGroupProfile({
364
+ platform: "openclaw",
365
+ accountId: "default",
366
+ chatId: "grp_participant_type_clear",
367
+ api,
368
+ store,
369
+ memoryRoot,
370
+ });
371
+
372
+ const file = await readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "usr_participant" });
373
+ expect(Object.prototype.hasOwnProperty.call(file.metadata, "profile_type")).toBe(false);
374
+ });
375
+
376
+ it("omits null agent behavior from owner metadata files", async () => {
377
+ const memoryRoot = tempRoot();
378
+ const api = {
379
+ getAgentDetail: vi.fn().mockResolvedValue({
380
+ agent: {
381
+ user_id: "usr_self",
382
+ owner_id: "usr_owner",
383
+ type: "agent",
384
+ nickname: "Hermes",
385
+ behavior: null,
386
+ },
387
+ }),
388
+ getUserProfile: vi.fn().mockResolvedValue({
389
+ id: "usr_owner",
390
+ nickname: "Owner",
391
+ }),
392
+ };
393
+ const store = {};
394
+
395
+ await refreshAgentBehaviorProfile({
396
+ platform: "openclaw",
397
+ accountId: "default",
398
+ agentId: "agent_row_1",
399
+ accountUserId: "usr_self",
400
+ accountOwnerUserId: "usr_owner",
401
+ api,
402
+ store,
403
+ memoryRoot,
404
+ });
405
+
406
+ const file = await readClawChatMemoryFile(memoryRoot, { targetType: "owner", targetId: "owner" });
407
+ expect(file.metadata).toEqual(expect.objectContaining({
408
+ agent_id: "usr_self",
409
+ agent_owner_id: "usr_owner",
410
+ agent_owner_nickname: "Owner",
411
+ }));
412
+ expect(Object.prototype.hasOwnProperty.call(file.metadata, "agent_behavior")).toBe(false);
413
+ });
414
+
415
+ it("does not invent omitted user metadata fields", async () => {
416
+ const memoryRoot = tempRoot();
417
+ const api = {
418
+ getUserProfile: vi.fn().mockResolvedValue({
419
+ id: "usr_peer",
420
+ type: "user",
421
+ nickname: "Peer",
422
+ }),
423
+ };
424
+ const store = {};
425
+
426
+ await ensureUserProfileForSender({
427
+ platform: "openclaw",
428
+ accountId: "default",
429
+ agentId: "agent_row_1",
430
+ accountUserId: "usr_self",
431
+ accountOwnerUserId: "usr_owner",
432
+ sender: { id: "usr_peer", nickname: "Fallback" },
433
+ api,
434
+ store,
435
+ memoryRoot,
436
+ });
437
+
438
+ const file = await readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "usr_peer" });
439
+ expect(file.metadata).toEqual(expect.objectContaining({ id: "usr_peer", nickname: "Peer" }));
440
+ expect(Object.prototype.hasOwnProperty.call(file.metadata, "avatar_url")).toBe(false);
441
+ expect(Object.prototype.hasOwnProperty.call(file.metadata, "bio")).toBe(false);
442
+ });
443
+
444
+ it("exposes planned profile sync functions", async () => {
445
+ const memoryRoot = tempRoot();
446
+ const api = {
447
+ getUserProfile: vi.fn().mockResolvedValue({
448
+ id: "usr_peer",
449
+ type: "agent",
450
+ nickname: "Peer Agent",
451
+ avatar_url: "https://example.test/peer.png",
452
+ bio: "Peer bio",
453
+ }),
454
+ getConversation: vi.fn().mockResolvedValue({
455
+ conversation: {
456
+ id: "grp_3",
457
+ type: "group",
458
+ title: "Group 3",
459
+ description: "Description 3",
460
+ creator_id: "usr_owner",
461
+ created_at: "2026-05-22T10:00:00.000Z",
462
+ updated_at: "2026-05-22T10:01:00.000Z",
463
+ participants: [],
464
+ },
465
+ }),
466
+ getAgentDetail: vi.fn().mockResolvedValue({
467
+ agent: {
468
+ user_id: "usr_self",
469
+ owner_id: "usr_owner",
470
+ type: "agent",
471
+ nickname: "Hermes",
472
+ avatar_url: "https://example.test/hermes.png",
473
+ bio: "Agent bio",
474
+ behavior: "Act as Hermes.",
475
+ },
476
+ }),
477
+ };
478
+ const store = {
479
+ };
480
+
481
+ await ensureUserProfileForSender({
482
+ platform: "openclaw",
483
+ accountId: "default",
484
+ accountUserId: "usr_self",
485
+ accountOwnerUserId: "usr_owner",
486
+ sender: { id: "usr_peer", nickname: "Fallback" },
487
+ lastSeenAt: 4000,
488
+ api,
489
+ store,
490
+ memoryRoot,
491
+ });
492
+ await ensureGroupProfileForChat({
493
+ platform: "openclaw",
494
+ accountId: "default",
495
+ chat: { id: "grp_3", type: "group", lastSeenAt: 4100 },
496
+ api,
497
+ store,
498
+ memoryRoot,
499
+ });
500
+ await refreshAgentBehaviorProfile({
501
+ platform: "openclaw",
502
+ accountId: "default",
503
+ agentId: "agent_row_1",
504
+ accountUserId: "usr_self",
505
+ accountOwnerUserId: "usr_owner",
506
+ metadataVersion: 7,
507
+ api,
508
+ store,
509
+ memoryRoot,
510
+ });
511
+ await refreshGroupProfile({
512
+ platform: "openclaw",
513
+ accountId: "default",
514
+ chatId: "grp_3",
515
+ metadataVersion: 8,
516
+ api,
517
+ store,
518
+ memoryRoot,
519
+ });
520
+
521
+ expect(api.getUserProfile).toHaveBeenCalledWith("usr_peer");
522
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: "usr_peer" }))
523
+ .resolves.toMatchObject({
524
+ metadata: expect.objectContaining({
525
+ id: "usr_peer",
526
+ profile_type: "agent",
527
+ nickname: "Peer Agent",
528
+ }),
529
+ });
530
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "owner", targetId: "owner" }))
531
+ .resolves.toMatchObject({
532
+ metadata: expect.objectContaining({ agent_id: "usr_self", agent_behavior: "Act as Hermes." }),
533
+ });
534
+ await expect(readClawChatMemoryFile(memoryRoot, { targetType: "group", targetId: "grp_3" }))
535
+ .resolves.toMatchObject({
536
+ metadata: expect.objectContaining({ group_id: "grp_3", group_title: "Group 3", group_description: "Description 3" }),
537
+ });
538
+ });
539
+ });