@copilotkit/react-core 1.54.1 → 1.55.0-next.7

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 (183) hide show
  1. package/CHANGELOG.md +117 -116
  2. package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
  3. package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
  4. package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
  5. package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
  6. package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
  7. package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
  8. package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
  9. package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
  10. package/dist/index.cjs +27 -28
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -3
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +4 -5
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/index.umd.js +1941 -35
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/v2/index.cjs +77 -7
  21. package/dist/v2/index.css +1 -2
  22. package/dist/v2/index.d.cts +6 -4
  23. package/dist/v2/index.d.mts +6 -4
  24. package/dist/v2/index.mjs +7 -4
  25. package/dist/v2/index.umd.js +5725 -24
  26. package/dist/v2/index.umd.js.map +1 -1
  27. package/package.json +37 -9
  28. package/scripts/scope-preflight.mjs +101 -0
  29. package/src/components/CopilotListeners.tsx +2 -6
  30. package/src/components/copilot-provider/copilot-messages.tsx +1 -1
  31. package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
  32. package/src/components/copilot-provider/copilotkit.tsx +4 -4
  33. package/src/context/copilot-messages-context.tsx +1 -1
  34. package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
  35. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
  36. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
  37. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
  38. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
  39. package/src/hooks/use-agent-nodename.ts +1 -1
  40. package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
  41. package/src/hooks/use-coagent.ts +1 -1
  42. package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
  43. package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
  44. package/src/hooks/use-copilot-chat_internal.ts +2 -2
  45. package/src/hooks/use-copilot-readable.ts +1 -1
  46. package/src/hooks/use-frontend-tool.ts +2 -2
  47. package/src/hooks/use-human-in-the-loop.ts +2 -2
  48. package/src/hooks/use-langgraph-interrupt.ts +2 -5
  49. package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
  50. package/src/hooks/use-render-tool-call.ts +1 -1
  51. package/src/lib/copilot-task.ts +1 -1
  52. package/src/setupTests.ts +18 -14
  53. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
  54. package/src/v2/__tests__/globalSetup.ts +14 -0
  55. package/src/v2/__tests__/setup.ts +93 -0
  56. package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
  57. package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
  58. package/src/v2/components/CopilotKitInspector.tsx +50 -0
  59. package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
  60. package/src/v2/components/WildcardToolCallRender.tsx +86 -0
  61. package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
  62. package/src/v2/components/chat/CopilotChat.tsx +431 -0
  63. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
  64. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
  65. package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
  66. package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
  67. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
  68. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
  69. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
  70. package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
  71. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
  72. package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
  73. package/src/v2/components/chat/CopilotChatView.tsx +598 -0
  74. package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
  75. package/src/v2/components/chat/CopilotPopup.tsx +81 -0
  76. package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
  77. package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
  78. package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
  79. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
  80. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
  81. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
  82. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
  83. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
  84. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
  85. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
  86. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
  87. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
  88. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
  89. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
  90. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
  91. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
  92. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
  93. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
  94. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
  95. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
  96. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
  97. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
  98. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
  99. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
  100. package/src/v2/components/chat/__tests__/setup.ts +1 -0
  101. package/src/v2/components/chat/index.ts +79 -0
  102. package/src/v2/components/index.ts +7 -0
  103. package/src/v2/components/license-warning-banner.tsx +198 -0
  104. package/src/v2/components/ui/button.tsx +123 -0
  105. package/src/v2/components/ui/dropdown-menu.tsx +258 -0
  106. package/src/v2/components/ui/tooltip.tsx +60 -0
  107. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
  108. package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
  109. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
  110. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
  111. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
  112. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
  113. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
  114. package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
  115. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
  116. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
  117. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
  118. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
  119. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
  120. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
  121. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
  122. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
  123. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
  124. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
  125. package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
  126. package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
  127. package/src/v2/hooks/index.ts +18 -0
  128. package/src/v2/hooks/use-agent-context.tsx +45 -0
  129. package/src/v2/hooks/use-agent.tsx +155 -0
  130. package/src/v2/hooks/use-component.tsx +89 -0
  131. package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
  132. package/src/v2/hooks/use-default-render-tool.tsx +254 -0
  133. package/src/v2/hooks/use-frontend-tool.tsx +43 -0
  134. package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
  135. package/src/v2/hooks/use-interrupt.tsx +305 -0
  136. package/src/v2/hooks/use-keyboard-height.tsx +67 -0
  137. package/src/v2/hooks/use-render-activity-message.tsx +73 -0
  138. package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
  139. package/src/v2/hooks/use-render-tool-call.tsx +175 -0
  140. package/src/v2/hooks/use-render-tool.tsx +181 -0
  141. package/src/v2/hooks/use-suggestions.tsx +91 -0
  142. package/src/v2/hooks/use-threads.tsx +256 -0
  143. package/src/v2/hooks/useKatexStyles.ts +27 -0
  144. package/src/v2/index.css +1 -1
  145. package/src/v2/index.ts +18 -2
  146. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
  147. package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
  148. package/src/v2/lib/react-core.ts +156 -0
  149. package/src/v2/lib/slots.tsx +143 -0
  150. package/src/v2/lib/transcription-client.ts +184 -0
  151. package/src/v2/lib/utils.ts +8 -0
  152. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
  153. package/src/v2/providers/CopilotKitProvider.tsx +600 -0
  154. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
  155. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
  156. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
  157. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
  158. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
  159. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
  160. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
  161. package/src/v2/providers/index.ts +14 -0
  162. package/src/v2/styles/globals.css +230 -0
  163. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
  164. package/src/v2/types/defineToolCallRenderer.ts +65 -0
  165. package/src/v2/types/frontend-tool.ts +8 -0
  166. package/src/v2/types/human-in-the-loop.ts +33 -0
  167. package/src/v2/types/index.ts +7 -0
  168. package/src/v2/types/interrupt.ts +15 -0
  169. package/src/v2/types/react-activity-message-renderer.ts +27 -0
  170. package/src/v2/types/react-custom-message-renderer.ts +17 -0
  171. package/src/v2/types/react-tool-call-renderer.ts +32 -0
  172. package/tsdown.config.ts +34 -10
  173. package/vitest.config.mjs +4 -3
  174. package/LICENSE +0 -21
  175. package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
  176. package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
  177. package/dist/copilotkit-C94ayZbs.cjs +0 -2161
  178. package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
  179. package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
  180. package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
  181. package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
  182. package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
  183. package/dist/v2/index.css.map +0 -1
@@ -0,0 +1,433 @@
1
+ import React from "react";
2
+ import { act, renderHook, waitFor } from "@testing-library/react";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { useCopilotKit } from "../../providers/CopilotKitProvider";
5
+
6
+ vi.mock("../../providers/CopilotKitProvider", () => ({
7
+ useCopilotKit: vi.fn(),
8
+ }));
9
+
10
+ const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
11
+ const phoenix = vi.hoisted(() => ({
12
+ sockets: [] as any[],
13
+ }));
14
+
15
+ vi.mock("phoenix", () => {
16
+ class MockPush {
17
+ private callbacks = new Map<string, (payload?: unknown) => void>();
18
+
19
+ receive(status: string, callback: (payload?: unknown) => void): MockPush {
20
+ this.callbacks.set(status, callback);
21
+ return this;
22
+ }
23
+
24
+ trigger(status: string, payload?: unknown): void {
25
+ this.callbacks.get(status)?.(payload);
26
+ }
27
+ }
28
+
29
+ class MockChannel {
30
+ topic: string;
31
+ params: Record<string, unknown>;
32
+ left = false;
33
+ channels: MockChannel[] = [];
34
+
35
+ private handlers = new Map<
36
+ string,
37
+ Array<{ ref: number; callback: (payload: unknown) => void }>
38
+ >();
39
+ private joinPush = new MockPush();
40
+ private nextRef = 1;
41
+
42
+ constructor(topic = "", params: Record<string, unknown> = {}) {
43
+ this.topic = topic;
44
+ this.params = params;
45
+ }
46
+
47
+ on(event: string, callback: (payload: unknown) => void): number {
48
+ if (!this.handlers.has(event)) {
49
+ this.handlers.set(event, []);
50
+ }
51
+ const ref = this.nextRef++;
52
+ this.handlers.get(event)!.push({ ref, callback });
53
+ return ref;
54
+ }
55
+
56
+ off(event: string, ref?: number): void {
57
+ if (!this.handlers.has(event)) {
58
+ return;
59
+ }
60
+ if (ref === undefined) {
61
+ this.handlers.delete(event);
62
+ return;
63
+ }
64
+
65
+ this.handlers.set(
66
+ event,
67
+ this.handlers.get(event)!.filter((entry) => entry.ref !== ref),
68
+ );
69
+ }
70
+
71
+ join(): MockPush {
72
+ return this.joinPush;
73
+ }
74
+
75
+ leave(): void {
76
+ this.left = true;
77
+ }
78
+
79
+ serverPush(event: string, payload: unknown): void {
80
+ for (const entry of this.handlers.get(event) ?? []) {
81
+ entry.callback(payload);
82
+ }
83
+ }
84
+ }
85
+
86
+ class MockSocket {
87
+ url: string;
88
+ opts: Record<string, unknown>;
89
+ connected = false;
90
+ disconnected = false;
91
+ channels: MockChannel[] = [];
92
+
93
+ private errorHandlers: Array<(error?: unknown) => void> = [];
94
+ private openHandlers: Array<() => void> = [];
95
+
96
+ constructor(url = "", opts: Record<string, unknown> = {}) {
97
+ this.url = url;
98
+ this.opts = opts;
99
+ phoenix.sockets.push(this);
100
+ }
101
+
102
+ connect(): void {
103
+ this.connected = true;
104
+ }
105
+
106
+ disconnect(): void {
107
+ this.disconnected = true;
108
+ }
109
+
110
+ channel(topic: string, params: Record<string, unknown> = {}): MockChannel {
111
+ const channel = new MockChannel(topic, params);
112
+ this.channels.push(channel);
113
+ return channel;
114
+ }
115
+
116
+ onError(callback: (error?: unknown) => void): void {
117
+ this.errorHandlers.push(callback);
118
+ }
119
+
120
+ onOpen(callback: () => void): void {
121
+ this.openHandlers.push(callback);
122
+ }
123
+
124
+ triggerError(error?: unknown): void {
125
+ for (const handler of this.errorHandlers) {
126
+ handler(error);
127
+ }
128
+ }
129
+
130
+ triggerOpen(): void {
131
+ for (const handler of this.openHandlers) {
132
+ handler();
133
+ }
134
+ }
135
+ }
136
+
137
+ return { Socket: MockSocket };
138
+ });
139
+
140
+ const fetchMock = vi.fn();
141
+ globalThis.fetch = fetchMock;
142
+
143
+ function getMockSockets(): any[] {
144
+ return phoenix.sockets;
145
+ }
146
+
147
+ function setupCopilotKit(runtimeUrl = "http://localhost:4000") {
148
+ mockUseCopilotKit.mockReturnValue({
149
+ copilotkit: {
150
+ runtimeUrl,
151
+ headers: { Authorization: "Bearer test-token" },
152
+ intelligence: {
153
+ wsUrl: "ws://localhost:4000/client",
154
+ },
155
+ },
156
+ });
157
+ }
158
+
159
+ function jsonResponse(body: unknown, status = 200) {
160
+ return Promise.resolve({
161
+ ok: status >= 200 && status < 300,
162
+ status,
163
+ json: () => Promise.resolve(body),
164
+ text: () => Promise.resolve(JSON.stringify(body)),
165
+ });
166
+ }
167
+
168
+ const defaultInput = { agentId: "agent-1" };
169
+
170
+ const sampleThreads = [
171
+ {
172
+ id: "t-1",
173
+ organizationId: "org-1",
174
+ agentId: "agent-1",
175
+ createdById: "user-1",
176
+ name: "Thread One",
177
+ archived: false,
178
+ createdAt: "2026-01-01T00:00:00Z",
179
+ updatedAt: "2026-01-01T00:00:00Z",
180
+ },
181
+ {
182
+ id: "t-2",
183
+ organizationId: "org-1",
184
+ agentId: "agent-1",
185
+ createdById: "user-1",
186
+ name: "Thread Two",
187
+ archived: false,
188
+ createdAt: "2026-01-02T00:00:00Z",
189
+ updatedAt: "2026-01-02T00:00:00Z",
190
+ },
191
+ ];
192
+
193
+ const { useThreads } = await import("../use-threads");
194
+
195
+ describe("useThreads", () => {
196
+ beforeEach(() => {
197
+ phoenix.sockets.splice(0);
198
+ fetchMock.mockReset();
199
+ setupCopilotKit();
200
+ });
201
+
202
+ it("fetches threads and subscribes to the user metadata channel", async () => {
203
+ fetchMock
204
+ .mockReturnValueOnce(
205
+ jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
206
+ )
207
+ .mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
208
+
209
+ const { result } = renderHook(() => useThreads(defaultInput));
210
+
211
+ await waitFor(() => {
212
+ expect(result.current.isLoading).toBe(false);
213
+ });
214
+
215
+ expect(result.current.threads.map((thread) => thread.id)).toEqual([
216
+ "t-2",
217
+ "t-1",
218
+ ]);
219
+ expect(result.current.error).toBeNull();
220
+ expect(fetchMock).toHaveBeenCalledWith(
221
+ expect.stringContaining("/threads?agentId=agent-1"),
222
+ expect.objectContaining({ method: "GET" }),
223
+ );
224
+ expect(fetchMock).toHaveBeenCalledWith(
225
+ expect.stringContaining("/threads/subscribe"),
226
+ expect.objectContaining({ method: "POST" }),
227
+ );
228
+
229
+ const socket = getMockSockets()[0];
230
+ expect(socket.connected).toBe(true);
231
+ expect(socket.channels[0].topic).toBe("user_meta:jc-1");
232
+ });
233
+
234
+ it("stores fetch failures in error state", async () => {
235
+ fetchMock.mockReturnValue(jsonResponse({}, 500));
236
+
237
+ const { result } = renderHook(() => useThreads(defaultInput));
238
+
239
+ await waitFor(() => {
240
+ expect(result.current.isLoading).toBe(false);
241
+ });
242
+
243
+ expect(result.current.error?.message).toContain("500");
244
+ expect(result.current.threads).toEqual([]);
245
+ });
246
+
247
+ it("does not fetch when runtimeUrl is not configured", async () => {
248
+ setupCopilotKit("");
249
+
250
+ const { result } = renderHook(() => useThreads(defaultInput));
251
+
252
+ await waitFor(() => {
253
+ expect(result.current.isLoading).toBe(false);
254
+ });
255
+
256
+ expect(fetchMock).not.toHaveBeenCalled();
257
+ expect(result.current.error?.message).toBe("Runtime URL is not configured");
258
+ });
259
+
260
+ it("updates local state directly from realtime metadata events", async () => {
261
+ fetchMock
262
+ .mockReturnValueOnce(
263
+ jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
264
+ )
265
+ .mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
266
+
267
+ const { result } = renderHook(() => useThreads(defaultInput));
268
+
269
+ await waitFor(() => {
270
+ expect(result.current.isLoading).toBe(false);
271
+ });
272
+
273
+ const channel = getMockSockets()[0].channels[0];
274
+
275
+ act(() => {
276
+ channel.serverPush("thread_metadata", {
277
+ operation: "updated",
278
+ threadId: "t-1",
279
+ userId: "user-1",
280
+ organizationId: "org-1",
281
+ occurredAt: "2026-01-03T00:00:00Z",
282
+ thread: {
283
+ ...sampleThreads[0],
284
+ name: "Renamed Thread",
285
+ updatedAt: "2026-01-03T00:00:00Z",
286
+ },
287
+ });
288
+ });
289
+
290
+ await waitFor(() => {
291
+ expect(result.current.threads[0].name).toBe("Renamed Thread");
292
+ });
293
+
294
+ expect(fetchMock).toHaveBeenCalledTimes(2);
295
+ });
296
+
297
+ it("applies realtime metadata without client-side user filtering", async () => {
298
+ fetchMock
299
+ .mockReturnValueOnce(
300
+ jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
301
+ )
302
+ .mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
303
+
304
+ const { result } = renderHook(() => useThreads(defaultInput));
305
+
306
+ await waitFor(() => {
307
+ expect(result.current.isLoading).toBe(false);
308
+ });
309
+
310
+ act(() => {
311
+ getMockSockets()[0].channels[0].serverPush("thread_metadata", {
312
+ operation: "deleted",
313
+ threadId: "t-2",
314
+ userId: "user-2",
315
+ organizationId: "org-1",
316
+ occurredAt: "2026-01-03T00:00:00Z",
317
+ deleted: { id: "t-2" },
318
+ });
319
+ });
320
+
321
+ await waitFor(() => {
322
+ expect(result.current.threads).toHaveLength(1);
323
+ });
324
+ });
325
+
326
+ it("renames a thread through the runtime contract", async () => {
327
+ fetchMock
328
+ .mockReturnValueOnce(
329
+ jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
330
+ )
331
+ .mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
332
+ .mockReturnValueOnce(jsonResponse({}));
333
+
334
+ const { result } = renderHook(() => useThreads(defaultInput));
335
+
336
+ await waitFor(() => {
337
+ expect(result.current.isLoading).toBe(false);
338
+ });
339
+
340
+ await act(async () => {
341
+ await result.current.renameThread("t-1", "Renamed");
342
+ });
343
+
344
+ const [url, options] = fetchMock.mock.calls[2];
345
+ expect(url).toContain("/threads/t-1");
346
+ expect(options.method).toBe("PATCH");
347
+ expect(JSON.parse(options.body)).toMatchObject({
348
+ agentId: "agent-1",
349
+ name: "Renamed",
350
+ });
351
+ });
352
+
353
+ it("archives and deletes threads through the runtime contract", async () => {
354
+ fetchMock
355
+ .mockReturnValueOnce(
356
+ jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
357
+ )
358
+ .mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
359
+ .mockReturnValueOnce(jsonResponse({}))
360
+ .mockReturnValueOnce(jsonResponse({}));
361
+
362
+ const { result } = renderHook(() => useThreads(defaultInput));
363
+
364
+ await waitFor(() => {
365
+ expect(result.current.isLoading).toBe(false);
366
+ });
367
+
368
+ await act(async () => {
369
+ await result.current.archiveThread("t-2");
370
+ await result.current.deleteThread("t-1");
371
+ });
372
+
373
+ expect(fetchMock.mock.calls[2][0]).toContain("/threads/t-2/archive");
374
+ expect(fetchMock.mock.calls[2][1].method).toBe("POST");
375
+ expect(JSON.parse(fetchMock.mock.calls[2][1].body)).toMatchObject({
376
+ agentId: "agent-1",
377
+ });
378
+
379
+ expect(fetchMock.mock.calls[3][0]).toContain("/threads/t-1");
380
+ expect(fetchMock.mock.calls[3][1].method).toBe("DELETE");
381
+ expect(JSON.parse(fetchMock.mock.calls[3][1].body)).toMatchObject({
382
+ agentId: "agent-1",
383
+ });
384
+ });
385
+
386
+ it("tears down sockets after repeated connection failures", async () => {
387
+ fetchMock
388
+ .mockReturnValueOnce(
389
+ jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
390
+ )
391
+ .mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
392
+
393
+ renderHook(() => useThreads(defaultInput));
394
+
395
+ await waitFor(() => {
396
+ expect(getMockSockets().length).toBe(1);
397
+ });
398
+
399
+ const socket = getMockSockets()[0];
400
+ const channel = socket.channels[0];
401
+
402
+ act(() => {
403
+ for (let index = 0; index < 5; index += 1) {
404
+ socket.triggerError();
405
+ }
406
+ });
407
+
408
+ expect(channel.left).toBe(true);
409
+ expect(socket.disconnected).toBe(true);
410
+ });
411
+
412
+ it("tears down the active socket on unmount", async () => {
413
+ fetchMock
414
+ .mockReturnValueOnce(
415
+ jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
416
+ )
417
+ .mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
418
+
419
+ const { unmount } = renderHook(() => useThreads(defaultInput));
420
+
421
+ await waitFor(() => {
422
+ expect(getMockSockets().length).toBe(1);
423
+ });
424
+
425
+ const socket = getMockSockets()[0];
426
+ const channel = socket.channels[0];
427
+
428
+ unmount();
429
+
430
+ expect(channel.left).toBe(true);
431
+ expect(socket.disconnected).toBe(true);
432
+ });
433
+ });