@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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/README.md +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -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 +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- 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 +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -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.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
package/src/manifest.test.ts
CHANGED
|
@@ -2,6 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
|
|
4
4
|
import packageJson from "../package.json" with { type: "json" };
|
|
5
|
+
import {
|
|
6
|
+
CLAWCHAT_AGENT_ID_ENV,
|
|
7
|
+
CLAWCHAT_OWNER_USER_ID_ENV,
|
|
8
|
+
openclawClawlingConfigSchema,
|
|
9
|
+
} from "./config.ts";
|
|
5
10
|
|
|
6
11
|
interface PackageJsonWithOpenclaw {
|
|
7
12
|
name: string;
|
|
@@ -37,7 +42,7 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
37
42
|
it("keeps plugin id / channel id / package name aligned", () => {
|
|
38
43
|
expect(pluginManifest.id).toBe("openclaw-clawchat");
|
|
39
44
|
expect(pluginManifest.channels).toContain("openclaw-clawchat");
|
|
40
|
-
expect(pluginManifest).
|
|
45
|
+
expect(pluginManifest.skills).toEqual(["./skills"]);
|
|
41
46
|
expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.label).toBe(
|
|
42
47
|
"Clawling Chat",
|
|
43
48
|
);
|
|
@@ -65,8 +70,8 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
65
70
|
expect(pkg.openclaw.runtimeSetupEntry).toBe("./dist/setup-entry.js");
|
|
66
71
|
expect(pkg.files).toContain("dist");
|
|
67
72
|
expect(pkg.files).toContain("setup-entry.ts");
|
|
73
|
+
expect(pkg.files).toContain("skills");
|
|
68
74
|
expect(pkg.files).toContain("INSTALL.md");
|
|
69
|
-
expect(pkg.files).not.toContain("skills");
|
|
70
75
|
expect(pkg.scripts.build).toBe("tsc -p tsconfig.build.json");
|
|
71
76
|
expect(pkg.scripts.prepack).toBe("npm run build");
|
|
72
77
|
expect(fs.existsSync(new URL("../tsconfig.build.json", import.meta.url))).toBe(true);
|
|
@@ -85,7 +90,7 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
85
90
|
selectionLabel: "Clawling Chat",
|
|
86
91
|
docsPath: "/channels/openclaw-clawchat",
|
|
87
92
|
docsLabel: "openclaw-clawchat",
|
|
88
|
-
blurb: "
|
|
93
|
+
blurb: "ClawChat Protocol v2 over WebSocket.",
|
|
89
94
|
order: 110,
|
|
90
95
|
cliAddOptions: [
|
|
91
96
|
{
|
|
@@ -100,10 +105,10 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
100
105
|
expect(pluginManifest.activation).toEqual({
|
|
101
106
|
onStartup: true,
|
|
102
107
|
onChannels: ["openclaw-clawchat"],
|
|
103
|
-
onCommands: ["clawchat-
|
|
108
|
+
onCommands: ["clawchat-activate"],
|
|
104
109
|
});
|
|
105
110
|
expect(pluginManifest.commandAliases).toEqual([
|
|
106
|
-
{ name: "clawchat-
|
|
111
|
+
{ name: "clawchat-activate", kind: "runtime-slash" },
|
|
107
112
|
]);
|
|
108
113
|
});
|
|
109
114
|
|
|
@@ -111,7 +116,9 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
111
116
|
expect(pluginManifest.channelEnvVars).toEqual({
|
|
112
117
|
"openclaw-clawchat": [
|
|
113
118
|
"CLAWCHAT_TOKEN",
|
|
119
|
+
CLAWCHAT_AGENT_ID_ENV,
|
|
114
120
|
"CLAWCHAT_USER_ID",
|
|
121
|
+
CLAWCHAT_OWNER_USER_ID_ENV,
|
|
115
122
|
"CLAWCHAT_REFRESH_TOKEN",
|
|
116
123
|
"CLAWCHAT_BASE_URL",
|
|
117
124
|
"CLAWCHAT_WEBSOCKET_URL",
|
|
@@ -119,6 +126,13 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
119
126
|
});
|
|
120
127
|
});
|
|
121
128
|
|
|
129
|
+
it("keeps host manifest channel schemas aligned with runtime config schema", () => {
|
|
130
|
+
expect(pluginManifest.configSchema).toEqual(openclawClawlingConfigSchema);
|
|
131
|
+
expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.schema).toEqual(
|
|
132
|
+
openclawClawlingConfigSchema,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
122
136
|
it("keeps setup entry on a lightweight setup surface", () => {
|
|
123
137
|
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
124
138
|
expect(pkg.files).not.toContain("setup-api.ts");
|
|
@@ -131,7 +145,7 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
131
145
|
expect(setupEntry).not.toMatch(/\.\/src\/outbound(?:\.ts)?/);
|
|
132
146
|
});
|
|
133
147
|
|
|
134
|
-
it("uses the
|
|
148
|
+
it("uses the OpenClaw channel entry helper for registration-mode splitting", () => {
|
|
135
149
|
const entry = fs.readFileSync(new URL("../index.ts", import.meta.url), "utf8");
|
|
136
150
|
expect(entry).toMatch(/defineChannelPluginEntry/);
|
|
137
151
|
expect(entry).toMatch(/registerFull/);
|
|
@@ -145,19 +159,55 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
145
159
|
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
146
160
|
expect(readme).not.toMatch(/clawchat_activate/i);
|
|
147
161
|
expect(docs).not.toMatch(/clawchat_activate/i);
|
|
148
|
-
expect(readme).toMatch(/\/clawchat-
|
|
149
|
-
expect(docs).toMatch(/\/clawchat-
|
|
162
|
+
expect(readme).toMatch(/\/clawchat-activate A1B2C3/i);
|
|
163
|
+
expect(docs).toMatch(/\/clawchat-activate A1B2C3/i);
|
|
150
164
|
expect(readme).toMatch(/OpenClaw 2026\.5\.5/i);
|
|
151
165
|
expect(docs).toMatch(/OpenClaw 2026\.5\.5/i);
|
|
152
166
|
expect(readme).toMatch(/Unknown channel: openclaw-clawchat/i);
|
|
153
167
|
expect(docs).toMatch(/Unknown channel: openclaw-clawchat/i);
|
|
154
168
|
});
|
|
155
169
|
|
|
156
|
-
it("
|
|
170
|
+
it("publishes the repository-provided ClawChat skill", () => {
|
|
157
171
|
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
158
|
-
expect(pluginManifest).
|
|
159
|
-
expect(pkg.files).
|
|
160
|
-
|
|
172
|
+
expect(pluginManifest.skills).toEqual(["./skills"]);
|
|
173
|
+
expect(pkg.files).toContain("skills");
|
|
174
|
+
|
|
175
|
+
const skillUrl = new URL("../skills/clawchat/SKILL.md", import.meta.url);
|
|
176
|
+
expect(fs.existsSync(skillUrl)).toBe(true);
|
|
177
|
+
const skill = fs.readFileSync(skillUrl, "utf8");
|
|
178
|
+
expect(skill).toMatch(
|
|
179
|
+
/^---\nname: clawchat\ndescription: Use when a request involves ClawChat profile, friends, user search, moments\/dynamics, comments, reactions, avatar, media, or read-only conversation lookup\.\n---/m,
|
|
180
|
+
);
|
|
181
|
+
expect(skill).toMatch(
|
|
182
|
+
/This skill guides agent behavior for ClawChat-aware tasks\. Use the registered ClawChat tools for profile, friends, user search, moments, comments, reactions, avatar, media, and read-only conversation list\/get operations instead of direct HTTP calls, shell scripts, or handwritten clients\./,
|
|
183
|
+
);
|
|
184
|
+
expect(skill).toMatch(/clawchat_get_account_profile/);
|
|
185
|
+
expect(skill).toMatch(/clawchat_search_users/);
|
|
186
|
+
expect(skill).toMatch(/clawchat_upload_avatar_image/);
|
|
187
|
+
expect(skill).toMatch(/clawchat_upload_media_file/);
|
|
188
|
+
expect(skill).toMatch(/clawchat_list_conversations/);
|
|
189
|
+
expect(skill).toMatch(/clawchat_get_conversation/);
|
|
190
|
+
expect(skill).toMatch(/conversations?\/groups?.*read-only/i);
|
|
191
|
+
expect(skill).not.toMatch(
|
|
192
|
+
/conversations?\/groups?.*(?:create|update|leave|dissolve|add members|remove members|administer)/i,
|
|
193
|
+
);
|
|
194
|
+
expect(skill).not.toMatch(/clawchat_create_group_conversation/);
|
|
195
|
+
expect(skill).not.toMatch(/clawchat_update_conversation/);
|
|
196
|
+
expect(skill).not.toMatch(/clawchat_leave_conversation/);
|
|
197
|
+
expect(skill).not.toMatch(/clawchat_dissolve_conversation/);
|
|
198
|
+
expect(skill).not.toMatch(/clawchat_add_conversation_member/);
|
|
199
|
+
expect(skill).not.toMatch(/clawchat_remove_conversation_member/);
|
|
200
|
+
expect(skill).not.toMatch(/clawchat_list_conversation_users/);
|
|
201
|
+
expect(skill).toMatch(/## Profile And Identity Sync/);
|
|
202
|
+
expect(skill).toMatch(/When updating the OpenClaw agent identity file/);
|
|
203
|
+
expect(skill).toMatch(/display name \/ nickname \| `clawchat_update_account_profile` with `nickname`/);
|
|
204
|
+
expect(skill).toMatch(/bio \/ self-introduction \| `clawchat_update_account_profile` with `bio`/);
|
|
205
|
+
expect(skill).toMatch(/local avatar image \| `clawchat_upload_avatar_image`, then `clawchat_update_account_profile` with `avatar_url`/);
|
|
206
|
+
expect(skill).toMatch(/Do not invent invite codes, tokens, moment ids, comment ids, user ids, emoji reactions, image URLs, or file paths/);
|
|
207
|
+
expect(skill).not.toMatch(/hermes/i);
|
|
208
|
+
expect(skill).not.toMatch(/target hermes/i);
|
|
209
|
+
expect(skill).not.toMatch(/choosing among registered clawchat_\*/);
|
|
210
|
+
expect(skill).not.toMatch(/\b(?:whe|regis|plu)\s*$/m);
|
|
161
211
|
});
|
|
162
212
|
|
|
163
213
|
it("declares ownership of registered ClawChat agent tools", () => {
|
|
@@ -166,6 +216,8 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
166
216
|
"clawchat_get_user_profile",
|
|
167
217
|
"clawchat_list_account_friends",
|
|
168
218
|
"clawchat_search_users",
|
|
219
|
+
"clawchat_list_conversations",
|
|
220
|
+
"clawchat_get_conversation",
|
|
169
221
|
"clawchat_list_moments",
|
|
170
222
|
"clawchat_create_moment",
|
|
171
223
|
"clawchat_delete_moment",
|
|
@@ -176,7 +228,27 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
176
228
|
"clawchat_update_account_profile",
|
|
177
229
|
"clawchat_upload_avatar_image",
|
|
178
230
|
"clawchat_upload_media_file",
|
|
231
|
+
"clawchat_memory_read",
|
|
232
|
+
"clawchat_memory_write",
|
|
233
|
+
"clawchat_memory_edit",
|
|
234
|
+
"clawchat_metadata_sync",
|
|
235
|
+
"clawchat_metadata_update",
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("declares exactly the five file-backed memory and metadata tools without old-prefix aliases", () => {
|
|
240
|
+
const memoryTools = (pluginManifest.contracts?.tools ?? []).filter((name) =>
|
|
241
|
+
/memory|metadata/.test(name),
|
|
242
|
+
);
|
|
243
|
+
expect(memoryTools).toEqual([
|
|
244
|
+
"clawchat_memory_read",
|
|
245
|
+
"clawchat_memory_write",
|
|
246
|
+
"clawchat_memory_edit",
|
|
247
|
+
"clawchat_metadata_sync",
|
|
248
|
+
"clawchat_metadata_update",
|
|
179
249
|
]);
|
|
250
|
+
expect(pluginManifest.contracts?.tools ?? []).not.toContain("cc_memory_read");
|
|
251
|
+
expect(pluginManifest.contracts?.tools ?? []).not.toContain("cc_metadata_sync");
|
|
180
252
|
});
|
|
181
253
|
|
|
182
254
|
it("keeps the optional OpenClaw source checkout local-only", () => {
|
|
@@ -214,8 +286,8 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
214
286
|
expect(docs).toMatch(/Current activation paths/i);
|
|
215
287
|
expect(readme).not.toMatch(/clawchat_activate/i);
|
|
216
288
|
expect(docs).not.toMatch(/clawchat_activate/i);
|
|
217
|
-
expect(readme).toMatch(/\/clawchat-
|
|
218
|
-
expect(docs).toMatch(/\/clawchat-
|
|
289
|
+
expect(readme).toMatch(/\/clawchat-activate A1B2C3/i);
|
|
290
|
+
expect(docs).toMatch(/\/clawchat-activate A1B2C3/i);
|
|
219
291
|
expect(readme).toMatch(/openclaw channels add --channel openclaw-clawchat --token "\$CLAWCHAT_INVITE_CODE"/i);
|
|
220
292
|
expect(docs).toMatch(/openclaw channels add --channel openclaw-clawchat --token "\$CLAWCHAT_INVITE_CODE"/i);
|
|
221
293
|
expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
|
|
@@ -85,7 +85,9 @@ describe("uploadOutboundMedia", () => {
|
|
|
85
85
|
function buildApiClient() {
|
|
86
86
|
return {
|
|
87
87
|
uploadMedia: vi.fn().mockResolvedValue({
|
|
88
|
+
kind: "image",
|
|
88
89
|
url: "https://cdn/uploaded.png",
|
|
90
|
+
name: "uploaded.png",
|
|
89
91
|
size: 12,
|
|
90
92
|
mime: "image/png",
|
|
91
93
|
}),
|
|
@@ -118,14 +120,41 @@ describe("uploadOutboundMedia", () => {
|
|
|
118
120
|
url: "https://cdn/uploaded.png",
|
|
119
121
|
mime: "image/png",
|
|
120
122
|
size: 12,
|
|
121
|
-
name: "
|
|
123
|
+
name: "uploaded.png",
|
|
122
124
|
},
|
|
123
125
|
{
|
|
124
126
|
kind: "image",
|
|
125
127
|
url: "https://cdn/uploaded.png",
|
|
126
128
|
mime: "image/png",
|
|
127
129
|
size: 12,
|
|
128
|
-
name: "
|
|
130
|
+
name: "uploaded.png",
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("uses server-returned kind and name for uploaded media fragments", async () => {
|
|
136
|
+
const { runtime } = buildRuntime();
|
|
137
|
+
const apiClient = buildApiClient();
|
|
138
|
+
(apiClient.uploadMedia as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
139
|
+
kind: "file",
|
|
140
|
+
url: "https://cdn/server.bin",
|
|
141
|
+
name: "server.bin",
|
|
142
|
+
size: 12,
|
|
143
|
+
mime: "image/png",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const fragments = await uploadOutboundMedia(["https://cdn/in.png"], {
|
|
147
|
+
apiClient,
|
|
148
|
+
runtime,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(fragments).toEqual([
|
|
152
|
+
{
|
|
153
|
+
kind: "file",
|
|
154
|
+
url: "https://cdn/server.bin",
|
|
155
|
+
mime: "image/png",
|
|
156
|
+
size: 12,
|
|
157
|
+
name: "server.bin",
|
|
129
158
|
},
|
|
130
159
|
]);
|
|
131
160
|
});
|
package/src/media-runtime.ts
CHANGED
|
@@ -7,15 +7,12 @@ import {
|
|
|
7
7
|
import type { OpenclawClawlingApiClient } from "./api-client.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Local structural superset of the
|
|
10
|
+
* Local structural superset of the protocol media fragment narrow types.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* outbound fragments; structural compatibility lets a single object
|
|
17
|
-
* satisfy whichever narrow type matches its runtime `kind`. We cast
|
|
18
|
-
* to `Fragment[]` only at the SDK boundary (outbound.ts).
|
|
12
|
+
* Each media fragment has a literal `kind` that distinguishes it. Building the
|
|
13
|
+
* wide shape locally avoids per-kind switch statements when constructing
|
|
14
|
+
* outbound fragments; structural compatibility lets a single object satisfy
|
|
15
|
+
* whichever narrow type matches its runtime `kind`.
|
|
19
16
|
*/
|
|
20
17
|
export interface MediaItem {
|
|
21
18
|
kind: "image" | "file" | "audio" | "video";
|
|
@@ -138,12 +135,12 @@ export async function uploadOutboundMedia(
|
|
|
138
135
|
mime: loaded.contentType,
|
|
139
136
|
});
|
|
140
137
|
const fragment: ClawlingMediaFragment = {
|
|
141
|
-
kind:
|
|
138
|
+
kind: uploaded.kind,
|
|
142
139
|
url: uploaded.url,
|
|
140
|
+
name: uploaded.name,
|
|
143
141
|
mime: uploaded.mime,
|
|
144
142
|
size: uploaded.size,
|
|
145
143
|
};
|
|
146
|
-
if (loaded.fileName) fragment.name = loaded.fileName;
|
|
147
144
|
out.push(fragment);
|
|
148
145
|
} catch (err) {
|
|
149
146
|
ctx.log?.error?.(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Fragment } from "
|
|
1
|
+
import type { Fragment } from "./protocol-types.ts";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import { extractMediaFragments, fragmentsToText, textToFragments } from "./message-mapper.ts";
|
|
4
4
|
|
|
@@ -119,7 +119,7 @@ describe("openclaw-clawchat message-mapper", () => {
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
it("extractMediaFragments skips media fragments without url", () => {
|
|
122
|
-
// Note:
|
|
122
|
+
// Note: the protocol type requires `url` on image fragments — using a partial
|
|
123
123
|
// object here exercises the runtime guard. Casting here is intentional
|
|
124
124
|
// (we want to verify defensive handling of malformed input).
|
|
125
125
|
const fragments: Fragment[] = [
|
package/src/message-mapper.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Fragment } from "
|
|
1
|
+
import type { Fragment } from "./protocol-types.ts";
|
|
2
2
|
import type { MediaItem } from "./media-runtime.ts";
|
|
3
3
|
|
|
4
4
|
interface FlattenOptions {
|
|
@@ -61,7 +61,7 @@ export function textToFragments(text: string): Fragment[] {
|
|
|
61
61
|
/**
|
|
62
62
|
* Extract media fragments from a body (image/file/audio/video). Skips
|
|
63
63
|
* entries missing `url`. Preserves all optional metadata fields the
|
|
64
|
-
*
|
|
64
|
+
* protocol carries through (mime/size/width/height/duration/name).
|
|
65
65
|
*/
|
|
66
66
|
export function extractMediaFragments(fragments: Fragment[]): MediaItem[] {
|
|
67
67
|
const out: MediaItem[] = [];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MockTransport } from "./mock-transport.ts";
|
|
3
|
+
|
|
4
|
+
describe("MockTransport", () => {
|
|
5
|
+
it("opens, records sent frames, emits inbound frames, and closes", async () => {
|
|
6
|
+
const transport = new MockTransport();
|
|
7
|
+
const onOpen = vi.fn();
|
|
8
|
+
const onMessage = vi.fn();
|
|
9
|
+
const onClose = vi.fn();
|
|
10
|
+
const onError = vi.fn();
|
|
11
|
+
|
|
12
|
+
await transport.connect("ws://test", { onOpen, onMessage, onClose, onError });
|
|
13
|
+
expect(transport.state).toBe("open");
|
|
14
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
15
|
+
|
|
16
|
+
transport.send("frame-1");
|
|
17
|
+
expect(transport.sent).toEqual(["frame-1"]);
|
|
18
|
+
|
|
19
|
+
transport.emitInbound("frame-2");
|
|
20
|
+
expect(onMessage).toHaveBeenCalledWith("frame-2");
|
|
21
|
+
|
|
22
|
+
const buffer = Buffer.from("frame-3");
|
|
23
|
+
transport.emitInbound(buffer);
|
|
24
|
+
expect(onMessage).toHaveBeenCalledWith(buffer);
|
|
25
|
+
|
|
26
|
+
const err = new Error("boom");
|
|
27
|
+
transport.emitError(err);
|
|
28
|
+
expect(onError).toHaveBeenCalledWith(err);
|
|
29
|
+
|
|
30
|
+
transport.close(1000, "done");
|
|
31
|
+
expect(transport.state).toBe("closed");
|
|
32
|
+
expect(onClose).toHaveBeenCalledWith(1000, "done");
|
|
33
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Transport, TransportEvents, TransportState } from "./protocol-types.ts";
|
|
2
|
+
|
|
3
|
+
export class MockTransport implements Transport {
|
|
4
|
+
private handlers?: TransportEvents;
|
|
5
|
+
private currentState: TransportState = "closed";
|
|
6
|
+
readonly sent: string[] = [];
|
|
7
|
+
|
|
8
|
+
get state(): TransportState {
|
|
9
|
+
return this.currentState;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async connect(_url: string, handlers: TransportEvents): Promise<void> {
|
|
13
|
+
this.handlers = handlers;
|
|
14
|
+
this.currentState = "open";
|
|
15
|
+
handlers.onOpen();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
send(data: string): void {
|
|
19
|
+
if (this.currentState !== "open") {
|
|
20
|
+
throw new Error("transport is not open");
|
|
21
|
+
}
|
|
22
|
+
this.sent.push(data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
close(code = 1000, reason = "client close"): void {
|
|
26
|
+
if (this.currentState === "closed") return;
|
|
27
|
+
this.currentState = "closed";
|
|
28
|
+
this.handlers?.onClose(code, reason);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
emitInbound(data: string | Buffer): void {
|
|
32
|
+
this.handlers?.onMessage(data);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emitError(err: Error): void {
|
|
36
|
+
this.handlers?.onError(err);
|
|
37
|
+
}
|
|
38
|
+
}
|