@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.
Files changed (99) hide show
  1. package/README.md +39 -17
  2. package/dist/index.js +3 -1
  3. package/dist/src/api-client.js +71 -12
  4. package/dist/src/api-types.test-d.js +10 -0
  5. package/dist/src/channel.js +5 -5
  6. package/dist/src/channel.setup.js +4 -17
  7. package/dist/src/clawchat-memory.js +290 -0
  8. package/dist/src/clawchat-metadata.js +235 -0
  9. package/dist/src/client.js +31 -93
  10. package/dist/src/commands.js +3 -3
  11. package/dist/src/config.js +58 -3
  12. package/dist/src/group-message-coalescer.js +107 -0
  13. package/dist/src/inbound.js +24 -28
  14. package/dist/src/login.runtime.js +82 -19
  15. package/dist/src/media-runtime.js +2 -3
  16. package/dist/src/message-mapper.js +1 -1
  17. package/dist/src/mock-transport.js +31 -0
  18. package/dist/src/outbound.js +281 -56
  19. package/dist/src/plugin-prompts.js +76 -0
  20. package/dist/src/profile-prompt.js +150 -0
  21. package/dist/src/profile-sync.js +169 -0
  22. package/dist/src/prompt-injection.js +25 -0
  23. package/dist/src/protocol-types.js +63 -0
  24. package/dist/src/protocol-types.typecheck.js +1 -0
  25. package/dist/src/protocol.js +2 -2
  26. package/dist/src/reply-dispatcher.js +143 -40
  27. package/dist/src/runtime.js +813 -109
  28. package/dist/src/storage.js +636 -0
  29. package/dist/src/tools-schema.js +70 -10
  30. package/dist/src/tools.js +600 -112
  31. package/dist/src/ws-alignment.js +8 -0
  32. package/dist/src/ws-client.js +588 -0
  33. package/index.ts +6 -1
  34. package/openclaw.plugin.json +44 -4
  35. package/package.json +4 -3
  36. package/prompts/platform.md +7 -0
  37. package/skills/clawchat/SKILL.md +90 -0
  38. package/src/api-client.test.ts +360 -15
  39. package/src/api-client.ts +127 -25
  40. package/src/api-types.test-d.ts +12 -0
  41. package/src/api-types.ts +71 -4
  42. package/src/buffered-stream.test.ts +1 -1
  43. package/src/buffered-stream.ts +1 -1
  44. package/src/channel.outbound.test.ts +270 -60
  45. package/src/channel.setup.ts +9 -18
  46. package/src/channel.test.ts +33 -25
  47. package/src/channel.ts +5 -7
  48. package/src/clawchat-memory.test.ts +372 -0
  49. package/src/clawchat-memory.ts +363 -0
  50. package/src/clawchat-metadata.test.ts +350 -0
  51. package/src/clawchat-metadata.ts +352 -0
  52. package/src/client.test.ts +57 -48
  53. package/src/client.ts +37 -129
  54. package/src/commands.test.ts +2 -2
  55. package/src/commands.ts +3 -3
  56. package/src/config.test.ts +169 -4
  57. package/src/config.ts +86 -6
  58. package/src/group-message-coalescer.test.ts +223 -0
  59. package/src/group-message-coalescer.ts +154 -0
  60. package/src/inbound.test.ts +106 -19
  61. package/src/inbound.ts +31 -35
  62. package/src/login.runtime.test.ts +294 -11
  63. package/src/login.runtime.ts +90 -21
  64. package/src/manifest.test.ts +86 -14
  65. package/src/media-runtime.test.ts +31 -2
  66. package/src/media-runtime.ts +7 -10
  67. package/src/message-mapper.test.ts +2 -2
  68. package/src/message-mapper.ts +2 -2
  69. package/src/mock-transport.test.ts +35 -0
  70. package/src/mock-transport.ts +38 -0
  71. package/src/outbound.test.ts +811 -95
  72. package/src/outbound.ts +332 -65
  73. package/src/plugin-entry.test.ts +3 -1
  74. package/src/plugin-prompts.test.ts +78 -0
  75. package/src/plugin-prompts.ts +92 -0
  76. package/src/profile-prompt.test.ts +435 -0
  77. package/src/profile-prompt.ts +208 -0
  78. package/src/profile-sync.test.ts +611 -0
  79. package/src/profile-sync.ts +268 -0
  80. package/src/prompt-injection.test.ts +39 -0
  81. package/src/prompt-injection.ts +45 -0
  82. package/src/protocol-types.test.ts +69 -0
  83. package/src/protocol-types.ts +296 -0
  84. package/src/protocol-types.typecheck.ts +89 -0
  85. package/src/protocol.ts +2 -2
  86. package/src/reply-dispatcher.test.ts +720 -135
  87. package/src/reply-dispatcher.ts +174 -42
  88. package/src/runtime.test.ts +3884 -337
  89. package/src/runtime.ts +956 -128
  90. package/src/storage.test.ts +692 -0
  91. package/src/storage.ts +989 -0
  92. package/src/streaming.test.ts +1 -1
  93. package/src/streaming.ts +1 -1
  94. package/src/tools-schema.ts +115 -13
  95. package/src/tools.test.ts +501 -10
  96. package/src/tools.ts +739 -133
  97. package/src/ws-alignment.ts +9 -0
  98. package/src/ws-client.test.ts +1218 -0
  99. package/src/ws-client.ts +662 -0
@@ -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).not.toHaveProperty("skills");
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: "Clawling Protocol v2 over WebSocket (chat-sdk).",
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-login"],
108
+ onCommands: ["clawchat-activate"],
104
109
  });
105
110
  expect(pluginManifest.commandAliases).toEqual([
106
- { name: "clawchat-login", kind: "runtime-slash" },
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 SDK channel entry helper for registration-mode splitting", () => {
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-login A1B2C3/i);
149
- expect(docs).toMatch(/\/clawchat-login A1B2C3/i);
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("does not publish repository-provided skills", () => {
170
+ it("publishes the repository-provided ClawChat skill", () => {
157
171
  const pkg = packageJson as PackageJsonWithOpenclaw;
158
- expect(pluginManifest).not.toHaveProperty("skills");
159
- expect(pkg.files).not.toContain("skills");
160
- expect(fs.existsSync(new URL("../skills/clawchat/SKILL.md", import.meta.url))).toBe(false);
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-login A1B2C3/i);
218
- expect(docs).toMatch(/\/clawchat-login A1B2C3/i);
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: "img.png",
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: "img.png",
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
  });
@@ -7,15 +7,12 @@ import {
7
7
  import type { OpenclawClawlingApiClient } from "./api-client.ts";
8
8
 
9
9
  /**
10
- * Local structural superset of the SDK's media fragment narrow types.
10
+ * Local structural superset of the protocol media fragment narrow types.
11
11
  *
12
- * `@clawling/chat-sdk@^0.2.0` exports each narrow type
13
- * (`ImageFragment` / `FileFragment` / `AudioFragment` / `VideoFragment`)
14
- * with a literal `kind` that distinguishes them. Building the wide
15
- * shape locally avoids per-kind switch statements when constructing
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: inferMediaKindFromMime(uploaded.mime),
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 "@newbase-clawchat/sdk";
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: SDK 0.2.0 requires `url` on image fragments — using a partial
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[] = [
@@ -1,4 +1,4 @@
1
- import type { Fragment } from "@newbase-clawchat/sdk";
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
- * SDK passes through (mime/size/width/height/duration/name).
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
+ }