@copilotkit/react-core 1.57.3 → 1.58.0-canary.thread-id-propagation
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/LICENSE +21 -0
- package/dist/{copilotkit-CtXcs1ea.cjs → copilotkit-B4ouY7qC.cjs} +14 -3
- package/dist/copilotkit-B4ouY7qC.cjs.map +1 -0
- package/dist/copilotkit-BK9CVq9A.d.cts.map +1 -1
- package/dist/{copilotkit-CC8DjOiC.mjs → copilotkit-L4mM_JqG.mjs} +14 -3
- package/dist/copilotkit-L4mM_JqG.mjs.map +1 -0
- package/dist/copilotkit-WlmeVijs.d.mts.map +1 -1
- package/dist/index.cjs +3 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -77
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +15 -78
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/headless.cjs +11 -0
- package/dist/v2/headless.cjs.map +1 -1
- package/dist/v2/headless.d.cts.map +1 -1
- package/dist/v2/headless.d.mts.map +1 -1
- package/dist/v2/headless.mjs +11 -0
- package/dist/v2/headless.mjs.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +13 -2
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +12 -13
- package/skills/react-core/SKILL.md +108 -0
- package/skills/react-core/references/agent-access.md +288 -0
- package/skills/react-core/references/attachments.md +291 -0
- package/skills/react-core/references/capabilities.md +138 -0
- package/skills/react-core/references/chat-components.md +221 -0
- package/skills/react-core/references/client-side-tools.md +358 -0
- package/skills/react-core/references/custom-message-renderers.md +226 -0
- package/skills/react-core/references/debug-mode.md +153 -0
- package/skills/react-core/references/human-in-the-loop.md +312 -0
- package/skills/react-core/references/provider-setup.md +326 -0
- package/skills/react-core/references/rendering-activity-messages.md +207 -0
- package/skills/react-core/references/rendering-tool-calls.md +319 -0
- package/skills/react-core/references/suggestions.md +211 -0
- package/skills/react-core/references/switching-agents-recipes.md +160 -0
- package/skills/react-core/references/switching-agents.md +231 -0
- package/skills/react-core/references/threads.md +226 -0
- package/.attw.json +0 -3
- package/CHANGELOG.md +0 -5043
- package/dist/copilotkit-CC8DjOiC.mjs.map +0 -1
- package/dist/copilotkit-CtXcs1ea.cjs.map +0 -1
- package/scripts/scope-preflight.mjs +0 -100
- package/src/components/CopilotListeners.tsx +0 -137
- package/src/components/__tests__/CopilotListeners.test.tsx +0 -38
- package/src/components/copilot-provider/__tests__/copilot-messages-key.test.tsx +0 -92
- package/src/components/copilot-provider/__tests__/copilotkit-error.test.tsx +0 -77
- package/src/components/copilot-provider/__tests__/error-visibility-prod.test.tsx +0 -70
- package/src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx +0 -107
- package/src/components/copilot-provider/copilot-messages.tsx +0 -314
- package/src/components/copilot-provider/copilotkit-props.tsx +0 -214
- package/src/components/copilot-provider/copilotkit.tsx +0 -853
- package/src/components/copilot-provider/index.ts +0 -3
- package/src/components/dev-console/console-trigger.tsx +0 -283
- package/src/components/dev-console/developer-console-modal.tsx +0 -1016
- package/src/components/dev-console/icons.tsx +0 -106
- package/src/components/error-boundary/error-boundary.tsx +0 -99
- package/src/components/error-boundary/error-utils.tsx +0 -105
- package/src/components/index.ts +0 -1
- package/src/components/toast/exclamation-mark-icon.tsx +0 -27
- package/src/components/toast/toast-provider.tsx +0 -448
- package/src/components/usage-banner.tsx +0 -266
- package/src/context/__tests__/threads-context.test.tsx +0 -141
- package/src/context/coagent-state-renders-context.tsx +0 -89
- package/src/context/copilot-context.tsx +0 -365
- package/src/context/copilot-messages-context.tsx +0 -35
- package/src/context/index.ts +0 -22
- package/src/context/threads-context.tsx +0 -69
- package/src/hooks/__tests__/use-coagent-config.test.ts +0 -352
- package/src/hooks/__tests__/use-coagent-state-render-bridge.helpers.test.ts +0 -107
- package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +0 -1209
- package/src/hooks/__tests__/use-coagent-state-render.test.tsx +0 -356
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +0 -241
- package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -72
- package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +0 -102
- package/src/hooks/index.ts +0 -33
- package/src/hooks/use-agent-nodename.ts +0 -33
- package/src/hooks/use-coagent-state-render-bridge.helpers.ts +0 -345
- package/src/hooks/use-coagent-state-render-bridge.tsx +0 -222
- package/src/hooks/use-coagent-state-render-registry.ts +0 -230
- package/src/hooks/use-coagent-state-render.ts +0 -163
- package/src/hooks/use-coagent.ts +0 -377
- package/src/hooks/use-configure-chat-suggestions.tsx +0 -96
- package/src/hooks/use-copilot-action.ts +0 -245
- package/src/hooks/use-copilot-additional-instructions.ts +0 -98
- package/src/hooks/use-copilot-authenticated-action.ts +0 -73
- package/src/hooks/use-copilot-chat-headless_c.ts +0 -264
- package/src/hooks/use-copilot-chat-suggestions.tsx +0 -134
- package/src/hooks/use-copilot-chat.ts +0 -132
- package/src/hooks/use-copilot-chat_internal.ts +0 -875
- package/src/hooks/use-copilot-readable.ts +0 -135
- package/src/hooks/use-copilot-runtime-client.ts +0 -178
- package/src/hooks/use-default-tool.ts +0 -13
- package/src/hooks/use-flat-category-store.ts +0 -109
- package/src/hooks/use-frontend-tool.ts +0 -113
- package/src/hooks/use-human-in-the-loop.ts +0 -138
- package/src/hooks/use-langgraph-interrupt.ts +0 -103
- package/src/hooks/use-lazy-tool-renderer.tsx +0 -30
- package/src/hooks/use-make-copilot-document-readable.ts +0 -30
- package/src/hooks/use-render-tool-call.ts +0 -89
- package/src/hooks/use-tree.ts +0 -222
- package/src/index.tsx +0 -7
- package/src/lib/copilot-task.ts +0 -215
- package/src/lib/index.ts +0 -1
- package/src/lib/status-checker.ts +0 -67
- package/src/setupTests.ts +0 -37
- package/src/test-helpers/copilot-context.ts +0 -91
- package/src/types/chat-suggestion-configuration.ts +0 -23
- package/src/types/coagent-action.ts +0 -35
- package/src/types/coagent-state.ts +0 -13
- package/src/types/crew.ts +0 -89
- package/src/types/document-pointer.ts +0 -7
- package/src/types/frontend-action.ts +0 -213
- package/src/types/index.ts +0 -17
- package/src/types/interrupt-action.ts +0 -58
- package/src/types/system-message.ts +0 -4
- package/src/utils/dev-console.ts +0 -19
- package/src/utils/index.ts +0 -2
- package/src/utils/suggestions-constants.ts +0 -8
- package/src/utils/utils.test.ts +0 -7
- package/src/utils/utils.ts +0 -6
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +0 -240
- package/src/v2/__tests__/globalSetup.ts +0 -14
- package/src/v2/__tests__/setup.ts +0 -93
- package/src/v2/__tests__/utils/test-helpers.tsx +0 -570
- package/src/v2/a2ui/A2UICatalogContext.tsx +0 -79
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +0 -294
- package/src/v2/a2ui/A2UIToolCallRenderer.tsx +0 -290
- package/src/v2/components/CopilotKitInspector.tsx +0 -52
- package/src/v2/components/MCPAppsActivityRenderer.tsx +0 -815
- package/src/v2/components/OpenGenerativeUIRenderer.tsx +0 -598
- package/src/v2/components/WildcardToolCallRender.tsx +0 -86
- package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +0 -665
- package/src/v2/components/chat/CopilotChat.tsx +0 -664
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +0 -393
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +0 -374
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +0 -159
- package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +0 -350
- package/src/v2/components/chat/CopilotChatInput.tsx +0 -1412
- package/src/v2/components/chat/CopilotChatMessageView.tsx +0 -716
- package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +0 -265
- package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +0 -59
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +0 -134
- package/src/v2/components/chat/CopilotChatToggleButton.tsx +0 -171
- package/src/v2/components/chat/CopilotChatToolCallsView.tsx +0 -40
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +0 -445
- package/src/v2/components/chat/CopilotChatView.tsx +0 -890
- package/src/v2/components/chat/CopilotModalHeader.tsx +0 -129
- package/src/v2/components/chat/CopilotPopup.tsx +0 -81
- package/src/v2/components/chat/CopilotPopupView.tsx +0 -317
- package/src/v2/components/chat/CopilotSidebar.tsx +0 -80
- package/src/v2/components/chat/CopilotSidebarView.tsx +0 -269
- package/src/v2/components/chat/Lightbox.tsx +0 -103
- package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +0 -66
- package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +0 -168
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +0 -1239
- package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +0 -73
- package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +0 -432
- package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +0 -183
- package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +0 -184
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +0 -649
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +0 -624
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +0 -702
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.thumbs.test.tsx +0 -72
- package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +0 -241
- package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +0 -107
- package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +0 -929
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +0 -1567
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +0 -1004
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +0 -279
- package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +0 -336
- package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +0 -249
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +0 -530
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +0 -785
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +0 -2416
- package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +0 -621
- package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +0 -56
- package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +0 -264
- package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +0 -853
- package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +0 -94
- package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +0 -1050
- package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +0 -484
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +0 -612
- package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.tsx +0 -159
- package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +0 -502
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +0 -1068
- package/src/v2/components/chat/__tests__/MCPAppsProxy.e2e.test.tsx +0 -589
- package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +0 -403
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -137
- package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +0 -37
- package/src/v2/components/chat/__tests__/setup.ts +0 -1
- package/src/v2/components/chat/index.ts +0 -90
- package/src/v2/components/chat/last-user-message-context.ts +0 -21
- package/src/v2/components/chat/normalize-auto-scroll.ts +0 -17
- package/src/v2/components/chat/scroll-element-context.ts +0 -13
- package/src/v2/components/index.ts +0 -8
- package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -286
- package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -464
- package/src/v2/components/intelligence-indicator/index.ts +0 -2
- package/src/v2/components/license-warning-banner.tsx +0 -217
- package/src/v2/components/ui/button.tsx +0 -124
- package/src/v2/components/ui/dropdown-menu.tsx +0 -258
- package/src/v2/components/ui/tooltip.tsx +0 -60
- package/src/v2/context.ts +0 -62
- package/src/v2/headless.ts +0 -64
- package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +0 -152
- package/src/v2/hooks/__tests__/standard-schema.test.tsx +0 -282
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +0 -140
- package/src/v2/hooks/__tests__/use-agent-context.test.tsx +0 -401
- package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +0 -44
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +0 -211
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +0 -1029
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +0 -159
- package/src/v2/hooks/__tests__/use-attachments.test.tsx +0 -169
- package/src/v2/hooks/__tests__/use-capabilities.test.tsx +0 -76
- package/src/v2/hooks/__tests__/use-component.test.tsx +0 -126
- package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +0 -696
- package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +0 -153
- package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -167
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +0 -2148
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +0 -1261
- package/src/v2/hooks/__tests__/use-interrupt.test.tsx +0 -397
- package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +0 -56
- package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +0 -192
- package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +0 -219
- package/src/v2/hooks/__tests__/use-render-custom-messages.test.tsx +0 -55
- package/src/v2/hooks/__tests__/use-render-tool.test.tsx +0 -259
- package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +0 -524
- package/src/v2/hooks/__tests__/use-threads.test.tsx +0 -757
- package/src/v2/hooks/__tests__/zod-regression.test.tsx +0 -311
- package/src/v2/hooks/index.ts +0 -24
- package/src/v2/hooks/use-agent-context.tsx +0 -45
- package/src/v2/hooks/use-agent.tsx +0 -227
- package/src/v2/hooks/use-attachments.tsx +0 -269
- package/src/v2/hooks/use-capabilities.tsx +0 -25
- package/src/v2/hooks/use-component.tsx +0 -91
- package/src/v2/hooks/use-configure-suggestions.tsx +0 -236
- package/src/v2/hooks/use-default-render-tool.tsx +0 -271
- package/src/v2/hooks/use-frontend-tool.tsx +0 -46
- package/src/v2/hooks/use-human-in-the-loop.tsx +0 -81
- package/src/v2/hooks/use-interrupt.tsx +0 -305
- package/src/v2/hooks/use-keyboard-height.tsx +0 -67
- package/src/v2/hooks/use-pin-to-send.ts +0 -94
- package/src/v2/hooks/use-render-activity-message.tsx +0 -72
- package/src/v2/hooks/use-render-custom-messages.tsx +0 -93
- package/src/v2/hooks/use-render-tool-call.tsx +0 -208
- package/src/v2/hooks/use-render-tool.tsx +0 -184
- package/src/v2/hooks/use-suggestions.tsx +0 -91
- package/src/v2/hooks/use-threads.tsx +0 -325
- package/src/v2/hooks/useKatexStyles.ts +0 -27
- package/src/v2/index.css +0 -1
- package/src/v2/index.ts +0 -27
- package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +0 -495
- package/src/v2/lib/__tests__/processPartialHtml.test.ts +0 -112
- package/src/v2/lib/__tests__/renderSlot.test.tsx +0 -588
- package/src/v2/lib/__tests__/slots.test.ts +0 -56
- package/src/v2/lib/processPartialHtml.ts +0 -45
- package/src/v2/lib/react-core.ts +0 -156
- package/src/v2/lib/slots.tsx +0 -184
- package/src/v2/lib/transcription-client.ts +0 -184
- package/src/v2/lib/utils.ts +0 -8
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +0 -196
- package/src/v2/providers/CopilotKitProvider.tsx +0 -800
- package/src/v2/providers/SandboxFunctionsContext.ts +0 -10
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +0 -652
- package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +0 -101
- package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +0 -69
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +0 -881
- package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +0 -198
- package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +0 -740
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +0 -713
- package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +0 -294
- package/src/v2/providers/index.ts +0 -21
- package/src/v2/styles/globals.css +0 -349
- package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +0 -525
- package/src/v2/types/defineToolCallRenderer.ts +0 -68
- package/src/v2/types/frontend-tool.ts +0 -8
- package/src/v2/types/human-in-the-loop.ts +0 -33
- package/src/v2/types/index.ts +0 -8
- package/src/v2/types/interrupt.ts +0 -15
- package/src/v2/types/react-activity-message-renderer.ts +0 -27
- package/src/v2/types/react-custom-message-renderer.ts +0 -17
- package/src/v2/types/react-tool-call-renderer.ts +0 -35
- package/src/v2/types/sandbox-function.ts +0 -11
- package/tsconfig.json +0 -8
- package/tsdown.config.ts +0 -193
- package/typedoc.json +0 -4
- package/vitest.config.mjs +0 -31
|
@@ -1,757 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
-
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { useCopilotKit } from "../../context";
|
|
5
|
-
import {
|
|
6
|
-
CopilotKitCoreRuntimeConnectionStatus,
|
|
7
|
-
ɵMAX_SOCKET_RETRIES,
|
|
8
|
-
} from "@copilotkit/core";
|
|
9
|
-
|
|
10
|
-
vi.mock("../../context", () => ({
|
|
11
|
-
useCopilotKit: vi.fn(),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
|
|
15
|
-
|
|
16
|
-
// Shape of the mock socket exposed via the hoisted `phoenix.sockets`
|
|
17
|
-
// array. Defined as a named type so test-side assertions can drop the
|
|
18
|
-
// blanket `any[]` cast and surface socket-API typos at compile time.
|
|
19
|
-
interface MockChannelLike {
|
|
20
|
-
topic: string;
|
|
21
|
-
params: Record<string, unknown>;
|
|
22
|
-
left: boolean;
|
|
23
|
-
serverPush(event: string, payload: unknown): void;
|
|
24
|
-
}
|
|
25
|
-
interface MockSocketLike {
|
|
26
|
-
url: string;
|
|
27
|
-
connected: boolean;
|
|
28
|
-
disconnected: boolean;
|
|
29
|
-
channels: MockChannelLike[];
|
|
30
|
-
triggerError(error?: unknown): void;
|
|
31
|
-
triggerOpen(): void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const phoenix = vi.hoisted(() => ({
|
|
35
|
-
sockets: [] as MockSocketLike[],
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
vi.mock("phoenix", () => {
|
|
39
|
-
class MockPush {
|
|
40
|
-
private callbacks = new Map<string, (payload?: unknown) => void>();
|
|
41
|
-
|
|
42
|
-
receive(status: string, callback: (payload?: unknown) => void): MockPush {
|
|
43
|
-
this.callbacks.set(status, callback);
|
|
44
|
-
return this;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
trigger(status: string, payload?: unknown): void {
|
|
48
|
-
this.callbacks.get(status)?.(payload);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
class MockChannel {
|
|
53
|
-
topic: string;
|
|
54
|
-
params: Record<string, unknown>;
|
|
55
|
-
left = false;
|
|
56
|
-
|
|
57
|
-
private handlers = new Map<
|
|
58
|
-
string,
|
|
59
|
-
Array<{ ref: number; callback: (payload: unknown) => void }>
|
|
60
|
-
>();
|
|
61
|
-
private nextRef = 1;
|
|
62
|
-
|
|
63
|
-
constructor(topic = "", params: Record<string, unknown> = {}) {
|
|
64
|
-
this.topic = topic;
|
|
65
|
-
this.params = params;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
on(event: string, callback: (payload: unknown) => void): number {
|
|
69
|
-
if (!this.handlers.has(event)) {
|
|
70
|
-
this.handlers.set(event, []);
|
|
71
|
-
}
|
|
72
|
-
const ref = this.nextRef++;
|
|
73
|
-
this.handlers.get(event)!.push({ ref, callback });
|
|
74
|
-
return ref;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
off(event: string, ref?: number): void {
|
|
78
|
-
if (!this.handlers.has(event)) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (ref === undefined) {
|
|
82
|
-
this.handlers.delete(event);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Re-check after the early returns above: `off(event)` deletes the
|
|
87
|
-
// entry, so a subsequent `off(event, ref)` would otherwise hit a
|
|
88
|
-
// non-null assertion that lies and `.filter` on undefined.
|
|
89
|
-
const entries = this.handlers.get(event);
|
|
90
|
-
if (entries === undefined) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
this.handlers.set(
|
|
94
|
-
event,
|
|
95
|
-
entries.filter((entry) => entry.ref !== ref),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
join(): MockPush {
|
|
100
|
-
// Each rejoin must produce a distinct push instance — sharing
|
|
101
|
-
// one across joins lets stale "ok"/"error" callbacks from a
|
|
102
|
-
// prior join fire against a new join's listeners.
|
|
103
|
-
return new MockPush();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
leave(): void {
|
|
107
|
-
this.left = true;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
serverPush(event: string, payload: unknown): void {
|
|
111
|
-
for (const entry of this.handlers.get(event) ?? []) {
|
|
112
|
-
entry.callback(payload);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
class MockSocket {
|
|
118
|
-
url: string;
|
|
119
|
-
opts: Record<string, unknown>;
|
|
120
|
-
connected = false;
|
|
121
|
-
disconnected = false;
|
|
122
|
-
channels: MockChannel[] = [];
|
|
123
|
-
|
|
124
|
-
private errorHandlers: Array<(error?: unknown) => void> = [];
|
|
125
|
-
private openHandlers: Array<() => void> = [];
|
|
126
|
-
|
|
127
|
-
constructor(url = "", opts: Record<string, unknown> = {}) {
|
|
128
|
-
this.url = url;
|
|
129
|
-
this.opts = opts;
|
|
130
|
-
phoenix.sockets.push(this);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
connect(): void {
|
|
134
|
-
// Phoenix sockets fire `onOpen` exactly once per WebSocket upgrade,
|
|
135
|
-
// and the upgrade is asynchronous. Tests must drive that transition
|
|
136
|
-
// explicitly via `triggerOpen()` so we exercise one open per
|
|
137
|
-
// connection — auto-firing here would either double-fire (when a
|
|
138
|
-
// test also calls `triggerOpen()`) or hide cases where production
|
|
139
|
-
// code forgets to await the open before joining a channel.
|
|
140
|
-
this.connected = true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
disconnect(): void {
|
|
144
|
-
// Real Phoenix sockets flip `connected` back to false on disconnect —
|
|
145
|
-
// a mock that only sets `disconnected = true` lets a regression that
|
|
146
|
-
// forgets to clear `connected` slip through, since assertions like
|
|
147
|
-
// `socket.connected === false` would be vacuously satisfied by the
|
|
148
|
-
// initial value but never re-checked after a reconnect cycle.
|
|
149
|
-
this.connected = false;
|
|
150
|
-
this.disconnected = true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
channel(topic: string, params: Record<string, unknown> = {}): MockChannel {
|
|
154
|
-
const channel = new MockChannel(topic, params);
|
|
155
|
-
this.channels.push(channel);
|
|
156
|
-
return channel;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
onError(callback: (error?: unknown) => void): void {
|
|
160
|
-
this.errorHandlers.push(callback);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
onOpen(callback: () => void): void {
|
|
164
|
-
this.openHandlers.push(callback);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
triggerError(error?: unknown): void {
|
|
168
|
-
for (const handler of this.errorHandlers) {
|
|
169
|
-
handler(error);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
triggerOpen(): void {
|
|
174
|
-
for (const handler of this.openHandlers) {
|
|
175
|
-
handler();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return { Socket: MockSocket };
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const fetchMock = vi.fn();
|
|
184
|
-
// Use `vi.stubGlobal` so the original `fetch` is restored automatically
|
|
185
|
-
// by `vi.unstubAllGlobals()` below — direct `globalThis.fetch = ...`
|
|
186
|
-
// assignment leaks the mock across test files in the same vitest worker.
|
|
187
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
188
|
-
|
|
189
|
-
function getMockSockets(): MockSocketLike[] {
|
|
190
|
-
return phoenix.sockets;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function setupCopilotKit(runtimeUrl = "http://localhost:4000") {
|
|
194
|
-
mockUseCopilotKit.mockReturnValue({
|
|
195
|
-
copilotkit: {
|
|
196
|
-
runtimeUrl,
|
|
197
|
-
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
198
|
-
headers: { Authorization: "Bearer test-token" },
|
|
199
|
-
intelligence: {
|
|
200
|
-
wsUrl: "ws://localhost:4000/client",
|
|
201
|
-
},
|
|
202
|
-
registerThreadStore: vi.fn(),
|
|
203
|
-
unregisterThreadStore: vi.fn(),
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function jsonResponse(body: unknown, status = 200) {
|
|
209
|
-
return Promise.resolve({
|
|
210
|
-
ok: status >= 200 && status < 300,
|
|
211
|
-
status,
|
|
212
|
-
json: () => Promise.resolve(body),
|
|
213
|
-
text: () => Promise.resolve(JSON.stringify(body)),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const defaultInput = { agentId: "agent-1" };
|
|
218
|
-
|
|
219
|
-
const sampleThreads = [
|
|
220
|
-
{
|
|
221
|
-
id: "t-1",
|
|
222
|
-
organizationId: "org-1",
|
|
223
|
-
agentId: "agent-1",
|
|
224
|
-
createdById: "user-1",
|
|
225
|
-
name: "Thread One",
|
|
226
|
-
archived: false,
|
|
227
|
-
createdAt: "2026-01-01T00:00:00Z",
|
|
228
|
-
updatedAt: "2026-01-01T00:00:00Z",
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
id: "t-2",
|
|
232
|
-
organizationId: "org-1",
|
|
233
|
-
agentId: "agent-1",
|
|
234
|
-
createdById: "user-1",
|
|
235
|
-
name: "Thread Two",
|
|
236
|
-
archived: false,
|
|
237
|
-
createdAt: "2026-01-02T00:00:00Z",
|
|
238
|
-
updatedAt: "2026-01-02T00:00:00Z",
|
|
239
|
-
},
|
|
240
|
-
];
|
|
241
|
-
|
|
242
|
-
const { useThreads } = await import("../use-threads");
|
|
243
|
-
|
|
244
|
-
describe("useThreads", () => {
|
|
245
|
-
beforeEach(() => {
|
|
246
|
-
phoenix.sockets.splice(0);
|
|
247
|
-
fetchMock.mockReset();
|
|
248
|
-
// Reset before re-priming. setupCopilotKit() uses mockReturnValue, so a
|
|
249
|
-
// future test that uses mockReturnValueOnce would otherwise leak any
|
|
250
|
-
// un-consumed queued returns into the next test.
|
|
251
|
-
mockUseCopilotKit.mockReset();
|
|
252
|
-
setupCopilotKit();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
afterAll(() => {
|
|
256
|
-
// Pair with `vi.stubGlobal("fetch", fetchMock)` above. Without this
|
|
257
|
-
// restoration the mock leaks into any sibling test file that runs in
|
|
258
|
-
// the same vitest worker and assumes a real `fetch`.
|
|
259
|
-
vi.unstubAllGlobals();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("fetches threads and subscribes to the user metadata channel", async () => {
|
|
263
|
-
fetchMock
|
|
264
|
-
.mockReturnValueOnce(
|
|
265
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
266
|
-
)
|
|
267
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
268
|
-
|
|
269
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
270
|
-
|
|
271
|
-
await waitFor(() => {
|
|
272
|
-
expect(result.current.isLoading).toBe(false);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
expect(result.current.threads.map((thread) => thread.id)).toEqual([
|
|
276
|
-
"t-2",
|
|
277
|
-
"t-1",
|
|
278
|
-
]);
|
|
279
|
-
expect(result.current.error).toBeNull();
|
|
280
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
281
|
-
expect.stringContaining("/threads?agentId=agent-1"),
|
|
282
|
-
expect.objectContaining({ method: "GET" }),
|
|
283
|
-
);
|
|
284
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
285
|
-
expect.stringContaining("/threads/subscribe"),
|
|
286
|
-
expect.objectContaining({ method: "POST" }),
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
const socket = getMockSockets()[0];
|
|
290
|
-
expect(socket.connected).toBe(true);
|
|
291
|
-
expect(socket.channels[0].topic).toBe("user_meta:jc-1");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("stores fetch failures in error state", async () => {
|
|
295
|
-
fetchMock.mockReturnValue(jsonResponse({}, 500));
|
|
296
|
-
|
|
297
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
298
|
-
|
|
299
|
-
await waitFor(() => {
|
|
300
|
-
expect(result.current.isLoading).toBe(false);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
expect(result.current.error?.message).toContain("500");
|
|
304
|
-
expect(result.current.threads).toEqual([]);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("does not fetch when runtimeUrl is not configured", async () => {
|
|
308
|
-
setupCopilotKit("");
|
|
309
|
-
|
|
310
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
311
|
-
|
|
312
|
-
await waitFor(() => {
|
|
313
|
-
expect(result.current.isLoading).toBe(false);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
317
|
-
expect(result.current.error?.message).toBe("Runtime URL is not configured");
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("updates local state directly from realtime metadata events", async () => {
|
|
321
|
-
fetchMock
|
|
322
|
-
.mockReturnValueOnce(
|
|
323
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
324
|
-
)
|
|
325
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
326
|
-
|
|
327
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
328
|
-
|
|
329
|
-
await waitFor(() => {
|
|
330
|
-
expect(result.current.isLoading).toBe(false);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
const channel = getMockSockets()[0].channels[0];
|
|
334
|
-
|
|
335
|
-
act(() => {
|
|
336
|
-
channel.serverPush("thread_metadata", {
|
|
337
|
-
operation: "updated",
|
|
338
|
-
threadId: "t-1",
|
|
339
|
-
userId: "user-1",
|
|
340
|
-
organizationId: "org-1",
|
|
341
|
-
occurredAt: "2026-01-03T00:00:00Z",
|
|
342
|
-
thread: {
|
|
343
|
-
...sampleThreads[0],
|
|
344
|
-
name: "Renamed Thread",
|
|
345
|
-
updatedAt: "2026-01-03T00:00:00Z",
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
await waitFor(() => {
|
|
351
|
-
expect(result.current.threads[0].name).toBe("Renamed Thread");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("applies realtime metadata without client-side user filtering", async () => {
|
|
358
|
-
fetchMock
|
|
359
|
-
.mockReturnValueOnce(
|
|
360
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
361
|
-
)
|
|
362
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
363
|
-
|
|
364
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
365
|
-
|
|
366
|
-
await waitFor(() => {
|
|
367
|
-
expect(result.current.isLoading).toBe(false);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
act(() => {
|
|
371
|
-
getMockSockets()[0].channels[0].serverPush("thread_metadata", {
|
|
372
|
-
operation: "deleted",
|
|
373
|
-
threadId: "t-2",
|
|
374
|
-
userId: "user-2",
|
|
375
|
-
organizationId: "org-1",
|
|
376
|
-
occurredAt: "2026-01-03T00:00:00Z",
|
|
377
|
-
deleted: { id: "t-2" },
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
await waitFor(() => {
|
|
382
|
-
expect(result.current.threads).toHaveLength(1);
|
|
383
|
-
});
|
|
384
|
-
// Identity-check the remaining thread so a regression that removes
|
|
385
|
-
// the wrong thread (e.g. a swapped index) is caught.
|
|
386
|
-
expect(result.current.threads[0].id).toBe("t-1");
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it("renames a thread through the runtime contract", async () => {
|
|
390
|
-
fetchMock
|
|
391
|
-
.mockReturnValueOnce(
|
|
392
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
393
|
-
)
|
|
394
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
|
|
395
|
-
.mockReturnValueOnce(jsonResponse({}));
|
|
396
|
-
|
|
397
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
398
|
-
|
|
399
|
-
await waitFor(() => {
|
|
400
|
-
expect(result.current.isLoading).toBe(false);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
await act(async () => {
|
|
404
|
-
await result.current.renameThread("t-1", "Renamed");
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// Find the PATCH call by URL+method rather than a hardcoded index —
|
|
408
|
-
// a future change to the fetch order (or an extra startup fetch) must
|
|
409
|
-
// not silently miss the actual rename request.
|
|
410
|
-
const renameCall = fetchMock.mock.calls.find(
|
|
411
|
-
(args: unknown[]) =>
|
|
412
|
-
typeof args[0] === "string" &&
|
|
413
|
-
(args[0] as string).includes("/threads/t-1") &&
|
|
414
|
-
(args[1] as { method?: string } | undefined)?.method === "PATCH",
|
|
415
|
-
);
|
|
416
|
-
expect(renameCall).toBeDefined();
|
|
417
|
-
const [, renameOptions] = renameCall!;
|
|
418
|
-
expect(JSON.parse((renameOptions as { body: string }).body)).toMatchObject({
|
|
419
|
-
agentId: "agent-1",
|
|
420
|
-
name: "Renamed",
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("archives and deletes threads through the runtime contract", async () => {
|
|
425
|
-
fetchMock
|
|
426
|
-
.mockReturnValueOnce(
|
|
427
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
428
|
-
)
|
|
429
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
|
|
430
|
-
.mockReturnValueOnce(jsonResponse({}))
|
|
431
|
-
.mockReturnValueOnce(jsonResponse({}));
|
|
432
|
-
|
|
433
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
434
|
-
|
|
435
|
-
await waitFor(() => {
|
|
436
|
-
expect(result.current.isLoading).toBe(false);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
await act(async () => {
|
|
440
|
-
await result.current.archiveThread("t-2");
|
|
441
|
-
await result.current.deleteThread("t-1");
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Filter by URL+method rather than fixed indices (mirrors the rename
|
|
445
|
-
// test above). A future change to the startup fetch order — adding an
|
|
446
|
-
// /info call, splitting the join token, etc. — must not silently miss
|
|
447
|
-
// the actual archive/delete requests.
|
|
448
|
-
const archiveCall = fetchMock.mock.calls.find(
|
|
449
|
-
(args: unknown[]) =>
|
|
450
|
-
typeof args[0] === "string" &&
|
|
451
|
-
(args[0] as string).includes("/threads/t-2/archive") &&
|
|
452
|
-
(args[1] as { method?: string } | undefined)?.method === "POST",
|
|
453
|
-
);
|
|
454
|
-
expect(archiveCall).toBeDefined();
|
|
455
|
-
expect(
|
|
456
|
-
JSON.parse((archiveCall![1] as { body: string }).body),
|
|
457
|
-
).toMatchObject({ agentId: "agent-1" });
|
|
458
|
-
|
|
459
|
-
const deleteCall = fetchMock.mock.calls.find(
|
|
460
|
-
(args: unknown[]) =>
|
|
461
|
-
typeof args[0] === "string" &&
|
|
462
|
-
(args[0] as string).includes("/threads/t-1") &&
|
|
463
|
-
(args[1] as { method?: string } | undefined)?.method === "DELETE",
|
|
464
|
-
);
|
|
465
|
-
expect(deleteCall).toBeDefined();
|
|
466
|
-
expect(JSON.parse((deleteCall![1] as { body: string }).body)).toMatchObject(
|
|
467
|
-
{ agentId: "agent-1" },
|
|
468
|
-
);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it("exposes thread-scoped pagination properties", async () => {
|
|
472
|
-
fetchMock
|
|
473
|
-
.mockReturnValueOnce(
|
|
474
|
-
jsonResponse({
|
|
475
|
-
threads: sampleThreads,
|
|
476
|
-
joinCode: "jc-1",
|
|
477
|
-
nextCursor: "cursor-abc",
|
|
478
|
-
}),
|
|
479
|
-
)
|
|
480
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
481
|
-
|
|
482
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
483
|
-
|
|
484
|
-
await waitFor(() => {
|
|
485
|
-
expect(result.current.isLoading).toBe(false);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
expect(result.current).toHaveProperty("hasMoreThreads");
|
|
489
|
-
expect(result.current).toHaveProperty("isFetchingMoreThreads");
|
|
490
|
-
expect(result.current).toHaveProperty("fetchMoreThreads");
|
|
491
|
-
expect(result.current).not.toHaveProperty("hasNextPage");
|
|
492
|
-
expect(result.current).not.toHaveProperty("isFetchingNextPage");
|
|
493
|
-
expect(result.current).not.toHaveProperty("fetchNextPage");
|
|
494
|
-
|
|
495
|
-
expect(result.current.hasMoreThreads).toBe(true);
|
|
496
|
-
expect(result.current.isFetchingMoreThreads).toBe(false);
|
|
497
|
-
expect(typeof result.current.fetchMoreThreads).toBe("function");
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it("fetchMoreThreads fetches the next page with the cursor and appends threads", async () => {
|
|
501
|
-
const nextPageThreads = [
|
|
502
|
-
{
|
|
503
|
-
id: "t-3",
|
|
504
|
-
organizationId: "org-1",
|
|
505
|
-
agentId: "agent-1",
|
|
506
|
-
createdById: "user-1",
|
|
507
|
-
name: "Thread Three",
|
|
508
|
-
archived: false,
|
|
509
|
-
createdAt: "2026-01-03T00:00:00Z",
|
|
510
|
-
updatedAt: "2026-01-03T00:00:00Z",
|
|
511
|
-
},
|
|
512
|
-
];
|
|
513
|
-
|
|
514
|
-
fetchMock
|
|
515
|
-
.mockReturnValueOnce(
|
|
516
|
-
jsonResponse({
|
|
517
|
-
threads: sampleThreads,
|
|
518
|
-
joinCode: "jc-1",
|
|
519
|
-
nextCursor: "cursor-abc",
|
|
520
|
-
}),
|
|
521
|
-
)
|
|
522
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
|
|
523
|
-
.mockReturnValueOnce(
|
|
524
|
-
jsonResponse({ threads: nextPageThreads, joinCode: "jc-1" }),
|
|
525
|
-
);
|
|
526
|
-
|
|
527
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
528
|
-
|
|
529
|
-
await waitFor(() => {
|
|
530
|
-
expect(result.current.isLoading).toBe(false);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
expect(result.current.threads).toHaveLength(2);
|
|
534
|
-
expect(result.current.hasMoreThreads).toBe(true);
|
|
535
|
-
|
|
536
|
-
act(() => {
|
|
537
|
-
result.current.fetchMoreThreads();
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
await waitFor(() => {
|
|
541
|
-
expect(result.current.threads).toHaveLength(3);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
const nextPageCall = fetchMock.mock.calls.find(
|
|
545
|
-
(args: unknown[]) =>
|
|
546
|
-
typeof args[0] === "string" &&
|
|
547
|
-
(args[0] as string).includes("cursor=cursor-abc"),
|
|
548
|
-
);
|
|
549
|
-
expect(nextPageCall).toBeDefined();
|
|
550
|
-
expect(nextPageCall![0]).toContain("agentId=agent-1");
|
|
551
|
-
expect(result.current.threads.map((t: { id: string }) => t.id)).toContain(
|
|
552
|
-
"t-3",
|
|
553
|
-
);
|
|
554
|
-
expect(result.current.threads).toHaveLength(3);
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
it("does not expose organizationId or createdById on threads", async () => {
|
|
558
|
-
fetchMock
|
|
559
|
-
.mockReturnValueOnce(
|
|
560
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
561
|
-
)
|
|
562
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
563
|
-
|
|
564
|
-
const { result } = renderHook(() => useThreads(defaultInput));
|
|
565
|
-
|
|
566
|
-
await waitFor(() => {
|
|
567
|
-
expect(result.current.isLoading).toBe(false);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
for (const thread of result.current.threads) {
|
|
571
|
-
expect(thread).not.toHaveProperty("organizationId");
|
|
572
|
-
expect(thread).not.toHaveProperty("createdById");
|
|
573
|
-
expect(thread).toHaveProperty("id");
|
|
574
|
-
expect(thread).toHaveProperty("agentId");
|
|
575
|
-
expect(thread).toHaveProperty("name");
|
|
576
|
-
expect(thread).toHaveProperty("archived");
|
|
577
|
-
expect(thread).toHaveProperty("createdAt");
|
|
578
|
-
expect(thread).toHaveProperty("updatedAt");
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
it("tears down sockets after repeated connection failures", async () => {
|
|
583
|
-
fetchMock
|
|
584
|
-
.mockReturnValueOnce(
|
|
585
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
586
|
-
)
|
|
587
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
588
|
-
|
|
589
|
-
renderHook(() => useThreads(defaultInput));
|
|
590
|
-
|
|
591
|
-
await waitFor(() => {
|
|
592
|
-
expect(getMockSockets().length).toBe(1);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
const socket = getMockSockets()[0];
|
|
596
|
-
const channel = socket.channels[0];
|
|
597
|
-
|
|
598
|
-
// Threshold is sourced from production (ɵMAX_SOCKET_RETRIES) so a
|
|
599
|
-
// future change to the retry budget cannot silently desync the test.
|
|
600
|
-
// We fire all errors inside a single act to keep the rxjs cleanup
|
|
601
|
-
// synchronous with the assertions, then check the pre-threshold and
|
|
602
|
-
// post-threshold states by inspecting the socket between iterations.
|
|
603
|
-
act(() => {
|
|
604
|
-
for (let index = 0; index < ɵMAX_SOCKET_RETRIES - 1; index += 1) {
|
|
605
|
-
socket.triggerError();
|
|
606
|
-
}
|
|
607
|
-
// Pre-threshold: teardown must NOT be premature.
|
|
608
|
-
expect(channel.left).toBe(false);
|
|
609
|
-
expect(socket.disconnected).toBe(false);
|
|
610
|
-
// The Nth error crosses the threshold and triggers teardown.
|
|
611
|
-
socket.triggerError();
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
expect(channel.left).toBe(true);
|
|
615
|
-
expect(socket.disconnected).toBe(true);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
it("tears down the active socket on unmount", async () => {
|
|
619
|
-
fetchMock
|
|
620
|
-
.mockReturnValueOnce(
|
|
621
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
622
|
-
)
|
|
623
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
624
|
-
|
|
625
|
-
const { unmount } = renderHook(() => useThreads(defaultInput));
|
|
626
|
-
|
|
627
|
-
await waitFor(() => {
|
|
628
|
-
expect(getMockSockets().length).toBe(1);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
const socket = getMockSockets()[0];
|
|
632
|
-
const channel = socket.channels[0];
|
|
633
|
-
|
|
634
|
-
unmount();
|
|
635
|
-
|
|
636
|
-
expect(channel.left).toBe(true);
|
|
637
|
-
expect(socket.disconnected).toBe(true);
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
it("registers thread store on mount and unregisters on unmount", async () => {
|
|
641
|
-
const registerThreadStore = vi.fn();
|
|
642
|
-
const unregisterThreadStore = vi.fn();
|
|
643
|
-
// Use mockReturnValue (not mockReturnValueOnce) so the same spies are
|
|
644
|
-
// returned across all renders, including the cleanup render where
|
|
645
|
-
// unmount triggers the effect's cleanup function.
|
|
646
|
-
// runtimeConnectionStatus is set explicitly to Connected — the hook
|
|
647
|
-
// treats anything other than Connected as "do not dispatch context",
|
|
648
|
-
// and we want this test to exercise a fully-wired flow.
|
|
649
|
-
mockUseCopilotKit.mockReturnValue({
|
|
650
|
-
copilotkit: {
|
|
651
|
-
runtimeUrl: "http://localhost:4000",
|
|
652
|
-
runtimeConnectionStatus:
|
|
653
|
-
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
654
|
-
headers: { Authorization: "Bearer test-token" },
|
|
655
|
-
intelligence: { wsUrl: "ws://localhost:4000/client" },
|
|
656
|
-
registerThreadStore,
|
|
657
|
-
unregisterThreadStore,
|
|
658
|
-
},
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
fetchMock
|
|
662
|
-
.mockReturnValueOnce(
|
|
663
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
664
|
-
)
|
|
665
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
666
|
-
|
|
667
|
-
const { unmount } = renderHook(() => useThreads(defaultInput));
|
|
668
|
-
|
|
669
|
-
await waitFor(() => {
|
|
670
|
-
expect(registerThreadStore).toHaveBeenCalledWith(
|
|
671
|
-
"agent-1",
|
|
672
|
-
expect.objectContaining({ select: expect.any(Function) }),
|
|
673
|
-
);
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
unmount();
|
|
677
|
-
|
|
678
|
-
expect(unregisterThreadStore).toHaveBeenCalledWith("agent-1");
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
it("waits for runtimeConnectionStatus=Connected before fetching /threads", async () => {
|
|
682
|
-
// Start in Connecting — hook should hold off on dispatching any request
|
|
683
|
-
// so the initial list fetch includes wsUrl and avoids a redundant second
|
|
684
|
-
// call once /info resolves.
|
|
685
|
-
mockUseCopilotKit.mockReturnValue({
|
|
686
|
-
copilotkit: {
|
|
687
|
-
runtimeUrl: "http://localhost:4000",
|
|
688
|
-
runtimeConnectionStatus:
|
|
689
|
-
CopilotKitCoreRuntimeConnectionStatus.Connecting,
|
|
690
|
-
headers: { Authorization: "Bearer test-token" },
|
|
691
|
-
intelligence: undefined,
|
|
692
|
-
registerThreadStore: vi.fn(),
|
|
693
|
-
unregisterThreadStore: vi.fn(),
|
|
694
|
-
},
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
fetchMock
|
|
698
|
-
.mockReturnValueOnce(
|
|
699
|
-
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
700
|
-
)
|
|
701
|
-
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
702
|
-
|
|
703
|
-
const { result, rerender } = renderHook(() => useThreads(defaultInput));
|
|
704
|
-
|
|
705
|
-
// Flush React effects + microtasks deterministically. A bare
|
|
706
|
-
// setTimeout(20) raced the store-effect under load on slow machines.
|
|
707
|
-
// Chained microtask flushes inside `act` give every queued effect a
|
|
708
|
-
// chance to run without depending on real-time delay.
|
|
709
|
-
await act(async () => {
|
|
710
|
-
await Promise.resolve();
|
|
711
|
-
await Promise.resolve();
|
|
712
|
-
await Promise.resolve();
|
|
713
|
-
});
|
|
714
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
715
|
-
|
|
716
|
-
// While waiting for Connected, the hook must surface isLoading=true so
|
|
717
|
-
// consumers don't render an empty-state flash before the first fetch
|
|
718
|
-
// is even dispatched. The store's own isLoading is false at this
|
|
719
|
-
// point (no contextChanged action yet), so the hook synthesizes it.
|
|
720
|
-
expect(result.current.isLoading).toBe(true);
|
|
721
|
-
expect(result.current.threads).toEqual([]);
|
|
722
|
-
|
|
723
|
-
// Flip to Connected with wsUrl populated, re-render. The effect now
|
|
724
|
-
// dispatches exactly one list fetch (+ one subscribe after it lands).
|
|
725
|
-
mockUseCopilotKit.mockReturnValue({
|
|
726
|
-
copilotkit: {
|
|
727
|
-
runtimeUrl: "http://localhost:4000",
|
|
728
|
-
runtimeConnectionStatus:
|
|
729
|
-
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
730
|
-
headers: { Authorization: "Bearer test-token" },
|
|
731
|
-
intelligence: { wsUrl: "ws://localhost:4000/client" },
|
|
732
|
-
registerThreadStore: vi.fn(),
|
|
733
|
-
unregisterThreadStore: vi.fn(),
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
rerender();
|
|
738
|
-
|
|
739
|
-
await waitFor(() => {
|
|
740
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
741
|
-
expect.stringContaining("/threads?agentId=agent-1"),
|
|
742
|
-
expect.objectContaining({ method: "GET" }),
|
|
743
|
-
);
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
// Exactly the expected pair — no speculative list call before Connected.
|
|
747
|
-
const listCalls = fetchMock.mock.calls.filter(
|
|
748
|
-
([url]) => typeof url === "string" && /\/threads\?agentId=/.test(url),
|
|
749
|
-
);
|
|
750
|
-
expect(listCalls).toHaveLength(1);
|
|
751
|
-
|
|
752
|
-
// After the fetch settles, isLoading returns to false.
|
|
753
|
-
await waitFor(() => {
|
|
754
|
-
expect(result.current.isLoading).toBe(false);
|
|
755
|
-
});
|
|
756
|
-
});
|
|
757
|
-
});
|