@cloudflare/ai-chat 0.0.1 → 0.0.4

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.
@@ -0,0 +1,543 @@
1
+ import { createExecutionContext, env } from "cloudflare:test";
2
+ import { describe, it, expect } from "vitest";
3
+ import worker from "./worker";
4
+ import type { UIMessage as ChatMessage } from "ai";
5
+
6
+ describe("Client-side tool duplicate message prevention", () => {
7
+ it("merges tool output into existing message by toolCallId", async () => {
8
+ const room = crypto.randomUUID();
9
+ const ctx = createExecutionContext();
10
+ const req = new Request(
11
+ `http://example.com/agents/test-chat-agent/${room}`,
12
+ { headers: { Upgrade: "websocket" } }
13
+ );
14
+ const res = await worker.fetch(req, env, ctx);
15
+ expect(res.status).toBe(101);
16
+ const ws = res.webSocket as WebSocket;
17
+ ws.accept();
18
+ await ctx.waitUntil(Promise.resolve());
19
+
20
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
21
+ const toolCallId = "call_merge_test";
22
+
23
+ // Persist assistant message with tool in input-available state
24
+ await agentStub.persistMessages([
25
+ {
26
+ id: "user-1",
27
+ role: "user",
28
+ parts: [{ type: "text", text: "Test" }]
29
+ },
30
+ {
31
+ id: "assistant-original",
32
+ role: "assistant",
33
+ parts: [
34
+ {
35
+ type: "tool-testTool",
36
+ toolCallId,
37
+ state: "input-available",
38
+ input: { param: "value" }
39
+ }
40
+ ] as ChatMessage["parts"]
41
+ }
42
+ ]);
43
+
44
+ // Persist message with different ID but same toolCallId (simulates second stream)
45
+ await agentStub.persistMessages([
46
+ {
47
+ id: "user-1",
48
+ role: "user",
49
+ parts: [{ type: "text", text: "Test" }]
50
+ },
51
+ {
52
+ id: "assistant-different-id",
53
+ role: "assistant",
54
+ parts: [
55
+ {
56
+ type: "tool-testTool",
57
+ toolCallId,
58
+ state: "output-available",
59
+ input: { param: "value" },
60
+ output: "result"
61
+ }
62
+ ] as ChatMessage["parts"]
63
+ }
64
+ ]);
65
+
66
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
67
+ const assistantMessages = messages.filter((m) => m.role === "assistant");
68
+
69
+ // Should have exactly 1 assistant message (merged, not duplicated)
70
+ expect(assistantMessages.length).toBe(1);
71
+ const toolPart = assistantMessages[0].parts[0] as {
72
+ state: string;
73
+ output?: unknown;
74
+ };
75
+ expect(toolPart.state).toBe("output-available");
76
+ expect(toolPart.output).toBe("result");
77
+
78
+ ws.close();
79
+ });
80
+
81
+ it("CF_AGENT_TOOL_RESULT applies tool result without auto-continuation by default", async () => {
82
+ const room = crypto.randomUUID();
83
+ const ctx = createExecutionContext();
84
+ const req = new Request(
85
+ `http://example.com/agents/test-chat-agent/${room}`,
86
+ { headers: { Upgrade: "websocket" } }
87
+ );
88
+ const res = await worker.fetch(req, env, ctx);
89
+ expect(res.status).toBe(101);
90
+ const ws = res.webSocket as WebSocket;
91
+ ws.accept();
92
+ await ctx.waitUntil(Promise.resolve());
93
+
94
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
95
+ const toolCallId = "call_tool_result_test";
96
+
97
+ // Persist assistant message with tool in input-available state
98
+ await agentStub.persistMessages([
99
+ {
100
+ id: "user-1",
101
+ role: "user",
102
+ parts: [{ type: "text", text: "Execute tool" }]
103
+ },
104
+ {
105
+ id: "assistant-1",
106
+ role: "assistant",
107
+ parts: [
108
+ {
109
+ type: "tool-testTool",
110
+ toolCallId,
111
+ state: "input-available",
112
+ input: { param: "value" }
113
+ }
114
+ ] as ChatMessage["parts"]
115
+ }
116
+ ]);
117
+
118
+ // Send CF_AGENT_TOOL_RESULT via WebSocket WITHOUT autoContinue flag
119
+ ws.send(
120
+ JSON.stringify({
121
+ type: "cf_agent_tool_result",
122
+ toolCallId,
123
+ toolName: "testTool",
124
+ output: { success: true }
125
+ // autoContinue not set - should NOT auto-continue
126
+ })
127
+ );
128
+
129
+ await new Promise((resolve) => setTimeout(resolve, 200));
130
+
131
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
132
+ const assistantMessages = messages.filter((m) => m.role === "assistant");
133
+
134
+ // Should have exactly 1 assistant message (no auto-continuation)
135
+ expect(assistantMessages.length).toBe(1);
136
+
137
+ const assistantMsg = assistantMessages[0];
138
+ expect(assistantMsg.id).toBe("assistant-1");
139
+
140
+ // Tool result should be applied
141
+ const toolPart = assistantMsg.parts[0] as {
142
+ state: string;
143
+ output?: unknown;
144
+ };
145
+ expect(toolPart.state).toBe("output-available");
146
+ expect(toolPart.output).toEqual({ success: true });
147
+
148
+ // No continuation parts (only the original tool part)
149
+ expect(assistantMsg.parts.length).toBe(1);
150
+
151
+ ws.close();
152
+ });
153
+
154
+ it("CF_AGENT_TOOL_RESULT auto-continues and merges when autoContinue is true", async () => {
155
+ const room = crypto.randomUUID();
156
+ const ctx = createExecutionContext();
157
+ const req = new Request(
158
+ `http://example.com/agents/test-chat-agent/${room}`,
159
+ { headers: { Upgrade: "websocket" } }
160
+ );
161
+ const res = await worker.fetch(req, env, ctx);
162
+ expect(res.status).toBe(101);
163
+ const ws = res.webSocket as WebSocket;
164
+ ws.accept();
165
+ await ctx.waitUntil(Promise.resolve());
166
+
167
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
168
+ const toolCallId = "call_tool_result_auto_continue";
169
+
170
+ // Persist assistant message with tool in input-available state
171
+ await agentStub.persistMessages([
172
+ {
173
+ id: "user-1",
174
+ role: "user",
175
+ parts: [{ type: "text", text: "Execute tool" }]
176
+ },
177
+ {
178
+ id: "assistant-1",
179
+ role: "assistant",
180
+ parts: [
181
+ {
182
+ type: "tool-testTool",
183
+ toolCallId,
184
+ state: "input-available",
185
+ input: { param: "value" }
186
+ }
187
+ ] as ChatMessage["parts"]
188
+ }
189
+ ]);
190
+
191
+ // Send CF_AGENT_TOOL_RESULT with autoContinue: true
192
+ ws.send(
193
+ JSON.stringify({
194
+ type: "cf_agent_tool_result",
195
+ toolCallId,
196
+ toolName: "testTool",
197
+ output: { success: true },
198
+ autoContinue: true
199
+ })
200
+ );
201
+
202
+ // Wait for tool result to be applied and continuation to happen
203
+ // Note: When there's no active stream, the continuation waits 500ms before proceeding
204
+ await new Promise((resolve) => setTimeout(resolve, 800));
205
+
206
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
207
+ const assistantMessages = messages.filter((m) => m.role === "assistant");
208
+
209
+ // Should still have exactly 1 assistant message (continuation merged into it)
210
+ expect(assistantMessages.length).toBe(1);
211
+
212
+ const assistantMsg = assistantMessages[0];
213
+ expect(assistantMsg.id).toBe("assistant-1");
214
+
215
+ // First part should be the tool with result applied
216
+ const toolPart = assistantMsg.parts[0] as {
217
+ state: string;
218
+ output?: unknown;
219
+ };
220
+ expect(toolPart.state).toBe("output-available");
221
+ expect(toolPart.output).toEqual({ success: true });
222
+
223
+ // Continuation parts should be appended (TestChatAgent returns text response)
224
+ expect(assistantMsg.parts.length).toBeGreaterThan(1);
225
+
226
+ ws.close();
227
+ });
228
+
229
+ it("strips OpenAI itemIds from persisted messages to prevent duplicate errors", async () => {
230
+ const room = crypto.randomUUID();
231
+ const ctx = createExecutionContext();
232
+ const req = new Request(
233
+ `http://example.com/agents/test-chat-agent/${room}`,
234
+ { headers: { Upgrade: "websocket" } }
235
+ );
236
+ const res = await worker.fetch(req, env, ctx);
237
+ expect(res.status).toBe(101);
238
+ const ws = res.webSocket as WebSocket;
239
+ ws.accept();
240
+ await ctx.waitUntil(Promise.resolve());
241
+
242
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
243
+
244
+ // Persist message with OpenAI itemId in providerMetadata (simulates OpenAI Responses API)
245
+ await agentStub.persistMessages([
246
+ {
247
+ id: "user-1",
248
+ role: "user",
249
+ parts: [{ type: "text", text: "Hello" }]
250
+ },
251
+ {
252
+ id: "assistant-1",
253
+ role: "assistant",
254
+ parts: [
255
+ {
256
+ type: "text",
257
+ text: "Hello! How can I help?",
258
+ providerMetadata: {
259
+ openai: {
260
+ itemId: "msg_abc123xyz" // This should be stripped
261
+ }
262
+ }
263
+ }
264
+ ] as ChatMessage["parts"]
265
+ }
266
+ ]);
267
+
268
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
269
+ const assistantMessage = messages.find((m) => m.role === "assistant");
270
+
271
+ expect(assistantMessage).toBeDefined();
272
+ const textPart = assistantMessage!.parts[0] as {
273
+ type: string;
274
+ text: string;
275
+ providerMetadata?: {
276
+ openai?: {
277
+ itemId?: string;
278
+ };
279
+ };
280
+ };
281
+
282
+ // The itemId should have been stripped during persistence
283
+ expect(textPart.text).toBe("Hello! How can I help?");
284
+ expect(textPart.providerMetadata?.openai?.itemId).toBeUndefined();
285
+
286
+ ws.close();
287
+ });
288
+
289
+ it("strips OpenAI itemIds from tool parts with callProviderMetadata", async () => {
290
+ const room = crypto.randomUUID();
291
+ const ctx = createExecutionContext();
292
+ const req = new Request(
293
+ `http://example.com/agents/test-chat-agent/${room}`,
294
+ { headers: { Upgrade: "websocket" } }
295
+ );
296
+ const res = await worker.fetch(req, env, ctx);
297
+ expect(res.status).toBe(101);
298
+ const ws = res.webSocket as WebSocket;
299
+ ws.accept();
300
+ await ctx.waitUntil(Promise.resolve());
301
+
302
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
303
+ const toolCallId = "call_openai_strip_test";
304
+
305
+ // Persist message with tool that has OpenAI itemId in callProviderMetadata
306
+ await agentStub.persistMessages([
307
+ {
308
+ id: "user-1",
309
+ role: "user",
310
+ parts: [{ type: "text", text: "What time is it?" }]
311
+ },
312
+ {
313
+ id: "assistant-1",
314
+ role: "assistant",
315
+ parts: [
316
+ {
317
+ type: "tool-getTime",
318
+ toolCallId,
319
+ state: "input-available",
320
+ input: { timezone: "UTC" },
321
+ callProviderMetadata: {
322
+ openai: {
323
+ itemId: "fc_xyz789" // This should be stripped
324
+ }
325
+ }
326
+ }
327
+ ] as ChatMessage["parts"]
328
+ }
329
+ ]);
330
+
331
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
332
+ const assistantMessage = messages.find((m) => m.role === "assistant");
333
+
334
+ expect(assistantMessage).toBeDefined();
335
+ const toolPart = assistantMessage!.parts[0] as {
336
+ type: string;
337
+ toolCallId: string;
338
+ callProviderMetadata?: {
339
+ openai?: {
340
+ itemId?: string;
341
+ };
342
+ };
343
+ };
344
+
345
+ // The itemId should have been stripped during persistence
346
+ expect(toolPart.toolCallId).toBe(toolCallId);
347
+ expect(toolPart.callProviderMetadata?.openai?.itemId).toBeUndefined();
348
+
349
+ ws.close();
350
+ });
351
+
352
+ it("preserves other providerMetadata when stripping itemId", async () => {
353
+ const room = crypto.randomUUID();
354
+ const ctx = createExecutionContext();
355
+ const req = new Request(
356
+ `http://example.com/agents/test-chat-agent/${room}`,
357
+ { headers: { Upgrade: "websocket" } }
358
+ );
359
+ const res = await worker.fetch(req, env, ctx);
360
+ expect(res.status).toBe(101);
361
+ const ws = res.webSocket as WebSocket;
362
+ ws.accept();
363
+ await ctx.waitUntil(Promise.resolve());
364
+
365
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
366
+
367
+ // Persist message with other metadata alongside itemId
368
+ await agentStub.persistMessages([
369
+ {
370
+ id: "user-1",
371
+ role: "user",
372
+ parts: [{ type: "text", text: "Hello" }]
373
+ },
374
+ {
375
+ id: "assistant-1",
376
+ role: "assistant",
377
+ parts: [
378
+ {
379
+ type: "text",
380
+ text: "Hello!",
381
+ providerMetadata: {
382
+ openai: {
383
+ itemId: "msg_strip_me", // Should be stripped
384
+ someOtherField: "keep_me" // Should be preserved
385
+ },
386
+ anthropic: {
387
+ someField: "also_keep_me" // Should be preserved
388
+ }
389
+ }
390
+ }
391
+ ] as ChatMessage["parts"]
392
+ }
393
+ ]);
394
+
395
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
396
+ const assistantMessage = messages.find((m) => m.role === "assistant");
397
+
398
+ expect(assistantMessage).toBeDefined();
399
+ const textPart = assistantMessage!.parts[0] as {
400
+ type: string;
401
+ providerMetadata?: {
402
+ openai?: {
403
+ itemId?: string;
404
+ someOtherField?: string;
405
+ };
406
+ anthropic?: {
407
+ someField?: string;
408
+ };
409
+ };
410
+ };
411
+
412
+ // itemId should be stripped
413
+ expect(textPart.providerMetadata?.openai?.itemId).toBeUndefined();
414
+ // Other fields should be preserved
415
+ expect(textPart.providerMetadata?.openai?.someOtherField).toBe("keep_me");
416
+ expect(textPart.providerMetadata?.anthropic?.someField).toBe(
417
+ "also_keep_me"
418
+ );
419
+
420
+ ws.close();
421
+ });
422
+
423
+ it("filters out empty reasoning parts to prevent AI SDK warnings", async () => {
424
+ const room = crypto.randomUUID();
425
+ const ctx = createExecutionContext();
426
+ const req = new Request(
427
+ `http://example.com/agents/test-chat-agent/${room}`,
428
+ { headers: { Upgrade: "websocket" } }
429
+ );
430
+ const res = await worker.fetch(req, env, ctx);
431
+ expect(res.status).toBe(101);
432
+ const ws = res.webSocket as WebSocket;
433
+ ws.accept();
434
+ await ctx.waitUntil(Promise.resolve());
435
+
436
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
437
+
438
+ // Persist message with empty reasoning part (simulates OpenAI Responses API)
439
+ await agentStub.persistMessages([
440
+ {
441
+ id: "user-1",
442
+ role: "user",
443
+ parts: [{ type: "text", text: "Think about this" }]
444
+ },
445
+ {
446
+ id: "assistant-1",
447
+ role: "assistant",
448
+ parts: [
449
+ {
450
+ type: "reasoning",
451
+ text: "", // Empty reasoning - should be filtered out
452
+ providerMetadata: {
453
+ openai: {
454
+ reasoningEncryptedContent: null
455
+ }
456
+ }
457
+ },
458
+ {
459
+ type: "text",
460
+ text: "Here is my response"
461
+ }
462
+ ] as ChatMessage["parts"]
463
+ }
464
+ ]);
465
+
466
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
467
+ const assistantMessage = messages.find((m) => m.role === "assistant");
468
+
469
+ expect(assistantMessage).toBeDefined();
470
+ // Empty reasoning part should have been filtered out
471
+ expect(assistantMessage!.parts.length).toBe(1);
472
+ expect(assistantMessage!.parts[0].type).toBe("text");
473
+
474
+ ws.close();
475
+ });
476
+
477
+ it("preserves non-empty reasoning parts", async () => {
478
+ const room = crypto.randomUUID();
479
+ const ctx = createExecutionContext();
480
+ const req = new Request(
481
+ `http://example.com/agents/test-chat-agent/${room}`,
482
+ { headers: { Upgrade: "websocket" } }
483
+ );
484
+ const res = await worker.fetch(req, env, ctx);
485
+ expect(res.status).toBe(101);
486
+ const ws = res.webSocket as WebSocket;
487
+ ws.accept();
488
+ await ctx.waitUntil(Promise.resolve());
489
+
490
+ const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
491
+
492
+ // Persist message with non-empty reasoning part
493
+ await agentStub.persistMessages([
494
+ {
495
+ id: "user-1",
496
+ role: "user",
497
+ parts: [{ type: "text", text: "Think about this" }]
498
+ },
499
+ {
500
+ id: "assistant-1",
501
+ role: "assistant",
502
+ parts: [
503
+ {
504
+ type: "reasoning",
505
+ text: "Let me think about this carefully...", // Non-empty - should be kept
506
+ providerMetadata: {
507
+ openai: {
508
+ itemId: "reason_123" // But itemId should still be stripped
509
+ }
510
+ }
511
+ },
512
+ {
513
+ type: "text",
514
+ text: "Here is my response"
515
+ }
516
+ ] as ChatMessage["parts"]
517
+ }
518
+ ]);
519
+
520
+ const messages = (await agentStub.getPersistedMessages()) as ChatMessage[];
521
+ const assistantMessage = messages.find((m) => m.role === "assistant");
522
+
523
+ expect(assistantMessage).toBeDefined();
524
+ // Non-empty reasoning part should be preserved
525
+ expect(assistantMessage!.parts.length).toBe(2);
526
+ expect(assistantMessage!.parts[0].type).toBe("reasoning");
527
+
528
+ const reasoningPart = assistantMessage!.parts[0] as {
529
+ type: string;
530
+ text: string;
531
+ providerMetadata?: {
532
+ openai?: {
533
+ itemId?: string;
534
+ };
535
+ };
536
+ };
537
+ expect(reasoningPart.text).toBe("Let me think about this carefully...");
538
+ // itemId should still be stripped
539
+ expect(reasoningPart.providerMetadata?.openai?.itemId).toBeUndefined();
540
+
541
+ ws.close();
542
+ });
543
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { MessageType } from "../types";
3
+ import type { UIMessage as ChatMessage } from "ai";
4
+ import { connectChatWS } from "./test-utils";
5
+
6
+ describe("Client Tools Broadcast", () => {
7
+ it("should not broadcast CF_AGENT_CHAT_MESSAGES back to the originating connection after chat request", async () => {
8
+ const room = crypto.randomUUID();
9
+ const { ws } = await connectChatWS(`/agents/test-chat-agent/${room}`);
10
+
11
+ const receivedMessages: Array<{ type: string; [key: string]: unknown }> =
12
+ [];
13
+ let resolvePromise: (value: boolean) => void;
14
+ const donePromise = new Promise<boolean>((res) => {
15
+ resolvePromise = res;
16
+ });
17
+
18
+ const timeout = setTimeout(() => resolvePromise(false), 2000);
19
+
20
+ ws.addEventListener("message", (e: MessageEvent) => {
21
+ const data = JSON.parse(e.data as string);
22
+ receivedMessages.push(data);
23
+
24
+ // Wait for the response to complete
25
+ if (data.type === MessageType.CF_AGENT_USE_CHAT_RESPONSE && data.done) {
26
+ // Give a small delay to catch any broadcast that might follow
27
+ setTimeout(() => {
28
+ clearTimeout(timeout);
29
+ resolvePromise(true);
30
+ }, 100);
31
+ }
32
+ });
33
+
34
+ const userMessage: ChatMessage = {
35
+ id: "msg1",
36
+ role: "user",
37
+ parts: [{ type: "text", text: "Hello" }]
38
+ };
39
+
40
+ // Send chat request from the client
41
+ ws.send(
42
+ JSON.stringify({
43
+ type: MessageType.CF_AGENT_USE_CHAT_REQUEST,
44
+ id: "req1",
45
+ init: {
46
+ method: "POST",
47
+ body: JSON.stringify({ messages: [userMessage] })
48
+ }
49
+ })
50
+ );
51
+
52
+ const done = await donePromise;
53
+ expect(done).toBe(true);
54
+
55
+ // The originating connection should NOT receive CF_AGENT_CHAT_MESSAGES
56
+ // It should only receive CF_AGENT_USE_CHAT_RESPONSE messages
57
+ const chatMessagesReceived = receivedMessages.filter(
58
+ (m) => m.type === MessageType.CF_AGENT_CHAT_MESSAGES
59
+ );
60
+
61
+ // This is the bug: the originating connection receives CF_AGENT_CHAT_MESSAGES
62
+ // which causes duplicate messages when combined with the stream response
63
+ expect(chatMessagesReceived.length).toBe(0);
64
+
65
+ ws.close();
66
+ });
67
+
68
+ it("should broadcast CF_AGENT_CHAT_MESSAGES to other connections but not the originator", async () => {
69
+ const room = crypto.randomUUID();
70
+
71
+ // Connect two clients to the same room
72
+ const { ws: ws1 } = await connectChatWS(`/agents/test-chat-agent/${room}`);
73
+ const { ws: ws2 } = await connectChatWS(`/agents/test-chat-agent/${room}`);
74
+
75
+ const ws1Messages: Array<{ type: string; [key: string]: unknown }> = [];
76
+ const ws2Messages: Array<{ type: string; [key: string]: unknown }> = [];
77
+
78
+ let resolvePromise: (value: boolean) => void;
79
+ const donePromise = new Promise<boolean>((res) => {
80
+ resolvePromise = res;
81
+ });
82
+
83
+ const timeout = setTimeout(() => resolvePromise(false), 2000);
84
+
85
+ ws1.addEventListener("message", (e: MessageEvent) => {
86
+ const data = JSON.parse(e.data as string);
87
+ ws1Messages.push(data);
88
+
89
+ if (data.type === MessageType.CF_AGENT_USE_CHAT_RESPONSE && data.done) {
90
+ setTimeout(() => {
91
+ clearTimeout(timeout);
92
+ resolvePromise(true);
93
+ }, 100);
94
+ }
95
+ });
96
+
97
+ ws2.addEventListener("message", (e: MessageEvent) => {
98
+ const data = JSON.parse(e.data as string);
99
+ ws2Messages.push(data);
100
+ });
101
+
102
+ const userMessage: ChatMessage = {
103
+ id: "msg1",
104
+ role: "user",
105
+ parts: [{ type: "text", text: "Hello" }]
106
+ };
107
+
108
+ // WS1 sends the chat request
109
+ ws1.send(
110
+ JSON.stringify({
111
+ type: MessageType.CF_AGENT_USE_CHAT_REQUEST,
112
+ id: "req1",
113
+ init: {
114
+ method: "POST",
115
+ body: JSON.stringify({ messages: [userMessage] })
116
+ }
117
+ })
118
+ );
119
+
120
+ const done = await donePromise;
121
+ expect(done).toBe(true);
122
+
123
+ // WS1 (originator) should NOT receive CF_AGENT_CHAT_MESSAGES
124
+ const ws1ChatMessages = ws1Messages.filter(
125
+ (m) => m.type === MessageType.CF_AGENT_CHAT_MESSAGES
126
+ );
127
+ expect(ws1ChatMessages.length).toBe(0);
128
+
129
+ // WS2 (other connection) SHOULD receive CF_AGENT_CHAT_MESSAGES
130
+ const ws2ChatMessages = ws2Messages.filter(
131
+ (m) => m.type === MessageType.CF_AGENT_CHAT_MESSAGES
132
+ );
133
+ expect(ws2ChatMessages.length).toBeGreaterThan(0);
134
+
135
+ ws1.close();
136
+ ws2.close();
137
+ });
138
+ });
@@ -0,0 +1,5 @@
1
+ import type { Env } from "./worker";
2
+
3
+ declare module "cloudflare:test" {
4
+ interface ProvidedEnv extends Env {}
5
+ }