@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,1029 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { render, act, screen } from "@testing-library/react";
|
|
3
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
4
|
-
import { useAgent, UseAgentUpdate } from "../use-agent";
|
|
5
|
-
import { useCopilotKit } from "../../context";
|
|
6
|
-
import { MockStepwiseAgent } from "../../__tests__/utils/test-helpers";
|
|
7
|
-
import {
|
|
8
|
-
CopilotKitCore,
|
|
9
|
-
CopilotKitCoreRuntimeConnectionStatus,
|
|
10
|
-
} from "@copilotkit/core";
|
|
11
|
-
import type { Message } from "@ag-ui/core";
|
|
12
|
-
import type { RunAgentInput } from "@ag-ui/client";
|
|
13
|
-
|
|
14
|
-
vi.mock("../../context", () => ({
|
|
15
|
-
useCopilotKit: vi.fn(),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
vi.mock("../../providers/CopilotChatConfigurationProvider", () => ({
|
|
19
|
-
useCopilotChatConfiguration: vi.fn(() => undefined),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Message factories — eliminates `as any` on every message literal
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
function userMsg(id: string, content = `msg-${id}`): Message {
|
|
29
|
-
return { id, role: "user" as const, content };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function assistantMsg(id: string, content = `msg-${id}`): Message {
|
|
33
|
-
return { id, role: "assistant" as const, content };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Create N alternating user/assistant messages (ids "1" … "N") */
|
|
37
|
-
function createMessages(count: number): Message[] {
|
|
38
|
-
return Array.from({ length: count }, (_, i) =>
|
|
39
|
-
i % 2 === 0
|
|
40
|
-
? userMsg(String(i + 1), `tok${i + 1}`)
|
|
41
|
-
: assistantMsg(String(i + 1), `tok${i + 1}`),
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Subscriber notification helpers
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/** Helper: fire onMessagesChanged on all agent subscribers */
|
|
50
|
-
function notifyMessagesChanged(agent: MockStepwiseAgent) {
|
|
51
|
-
agent.subscribers.forEach((s) =>
|
|
52
|
-
s.onMessagesChanged?.({
|
|
53
|
-
messages: agent.messages,
|
|
54
|
-
state: agent.state,
|
|
55
|
-
agent,
|
|
56
|
-
}),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Helper: fire onStateChanged on all agent subscribers */
|
|
61
|
-
function notifyStateChanged(agent: MockStepwiseAgent) {
|
|
62
|
-
agent.subscribers.forEach((s) =>
|
|
63
|
-
s.onStateChanged?.({
|
|
64
|
-
state: agent.state,
|
|
65
|
-
messages: agent.messages,
|
|
66
|
-
agent,
|
|
67
|
-
}),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function createMockRunAgentInput(
|
|
72
|
-
overrides?: Partial<RunAgentInput>,
|
|
73
|
-
): RunAgentInput {
|
|
74
|
-
return {
|
|
75
|
-
threadId: "t-1",
|
|
76
|
-
runId: "r-1",
|
|
77
|
-
state: {},
|
|
78
|
-
messages: [],
|
|
79
|
-
tools: [],
|
|
80
|
-
context: [],
|
|
81
|
-
forwardedProps: {},
|
|
82
|
-
...overrides,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Helper: fire onRunInitialized on all agent subscribers */
|
|
87
|
-
function notifyRunInitialized(agent: MockStepwiseAgent) {
|
|
88
|
-
agent.subscribers.forEach((s) =>
|
|
89
|
-
s.onRunInitialized?.({
|
|
90
|
-
messages: agent.messages,
|
|
91
|
-
state: agent.state,
|
|
92
|
-
agent,
|
|
93
|
-
input: createMockRunAgentInput(),
|
|
94
|
-
}),
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Test component factory
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
/** Helper: create a test component that tracks render count */
|
|
103
|
-
function createTestComponent(
|
|
104
|
-
options: {
|
|
105
|
-
updates?: UseAgentUpdate[];
|
|
106
|
-
throttleMs?: number;
|
|
107
|
-
renderCount?: { current: number };
|
|
108
|
-
} = {},
|
|
109
|
-
) {
|
|
110
|
-
const {
|
|
111
|
-
updates = [UseAgentUpdate.OnMessagesChanged],
|
|
112
|
-
throttleMs,
|
|
113
|
-
renderCount,
|
|
114
|
-
} = options;
|
|
115
|
-
|
|
116
|
-
return function TestComponent() {
|
|
117
|
-
if (renderCount) renderCount.current++;
|
|
118
|
-
const { agent } = useAgent({
|
|
119
|
-
agentId: "test-agent",
|
|
120
|
-
updates,
|
|
121
|
-
throttleMs,
|
|
122
|
-
});
|
|
123
|
-
return (
|
|
124
|
-
<>
|
|
125
|
-
<div data-testid="count">{agent.messages.length}</div>
|
|
126
|
-
<div data-testid="state">{JSON.stringify(agent.state)}</div>
|
|
127
|
-
</>
|
|
128
|
-
);
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Factory for the mock return value of useCopilotKit.
|
|
133
|
-
* Uses a real CopilotKitCore instance so subscribeToAgentWithOptions (with its throttle
|
|
134
|
-
* logic) is exercised end-to-end rather than mocked. */
|
|
135
|
-
function createMockContext(
|
|
136
|
-
agent: MockStepwiseAgent,
|
|
137
|
-
overrides: { defaultThrottleMs?: number } = {},
|
|
138
|
-
) {
|
|
139
|
-
const core = new CopilotKitCore({
|
|
140
|
-
runtimeUrl: "http://localhost:3000/api/copilot",
|
|
141
|
-
});
|
|
142
|
-
if (overrides.defaultThrottleMs !== undefined) {
|
|
143
|
-
core.setDefaultThrottleMs(overrides.defaultThrottleMs);
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
copilotkit: {
|
|
147
|
-
getAgent: () => agent,
|
|
148
|
-
runtimeUrl: "http://localhost:3000/api/copilot",
|
|
149
|
-
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
150
|
-
runtimeTransport: "rest",
|
|
151
|
-
headers: {},
|
|
152
|
-
agents: { [String(agent.agentId)]: agent },
|
|
153
|
-
defaultThrottleMs: core.defaultThrottleMs,
|
|
154
|
-
subscribeToAgentWithOptions: core.subscribeToAgentWithOptions.bind(core),
|
|
155
|
-
},
|
|
156
|
-
executingToolCallIds: new Set(),
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
describe("useAgent throttleMs", () => {
|
|
161
|
-
let mockAgent: MockStepwiseAgent;
|
|
162
|
-
|
|
163
|
-
beforeEach(() => {
|
|
164
|
-
vi.useFakeTimers();
|
|
165
|
-
mockAgent = new MockStepwiseAgent();
|
|
166
|
-
mockAgent.agentId = "test-agent";
|
|
167
|
-
|
|
168
|
-
mockUseCopilotKit.mockReturnValue(createMockContext(mockAgent));
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
afterEach(() => {
|
|
172
|
-
vi.useRealTimers();
|
|
173
|
-
vi.restoreAllMocks();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("without throttleMs, component reflects latest messages after notification", async () => {
|
|
177
|
-
const TestComponent = createTestComponent();
|
|
178
|
-
|
|
179
|
-
render(<TestComponent />);
|
|
180
|
-
expect(screen.getByTestId("count").textContent).toBe("0");
|
|
181
|
-
|
|
182
|
-
await act(async () => {
|
|
183
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
184
|
-
notifyMessagesChanged(mockAgent);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("with throttleMs: 0 (explicit), behaves identically to omitting throttleMs", async () => {
|
|
191
|
-
const TestComponent = createTestComponent({ throttleMs: 0 });
|
|
192
|
-
|
|
193
|
-
render(<TestComponent />);
|
|
194
|
-
expect(screen.getByTestId("count").textContent).toBe("0");
|
|
195
|
-
|
|
196
|
-
await act(async () => {
|
|
197
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
198
|
-
notifyMessagesChanged(mockAgent);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
202
|
-
|
|
203
|
-
// Second notification also fires immediately (no throttle)
|
|
204
|
-
await act(async () => {
|
|
205
|
-
mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
|
|
206
|
-
notifyMessagesChanged(mockAgent);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("with throttleMs, first notification fires immediately (leading edge)", async () => {
|
|
213
|
-
const TestComponent = createTestComponent({ throttleMs: 100 });
|
|
214
|
-
|
|
215
|
-
render(<TestComponent />);
|
|
216
|
-
expect(screen.getByTestId("count").textContent).toBe("0");
|
|
217
|
-
|
|
218
|
-
await act(async () => {
|
|
219
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
220
|
-
notifyMessagesChanged(mockAgent);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("with throttleMs, second notification within window is deferred until trailing edge", async () => {
|
|
227
|
-
const TestComponent = createTestComponent({ throttleMs: 100 });
|
|
228
|
-
|
|
229
|
-
render(<TestComponent />);
|
|
230
|
-
|
|
231
|
-
// First notification — leading edge, fires immediately
|
|
232
|
-
await act(async () => {
|
|
233
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
234
|
-
notifyMessagesChanged(mockAgent);
|
|
235
|
-
});
|
|
236
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
237
|
-
|
|
238
|
-
// Second notification 10ms later — within throttle window
|
|
239
|
-
await act(async () => {
|
|
240
|
-
vi.advanceTimersByTime(10);
|
|
241
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
242
|
-
notifyMessagesChanged(mockAgent);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// The throttle should have deferred this — component still shows 1
|
|
246
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
247
|
-
|
|
248
|
-
// Advance past the throttle window — trailing edge fires
|
|
249
|
-
await act(async () => {
|
|
250
|
-
vi.advanceTimersByTime(100);
|
|
251
|
-
});
|
|
252
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("with throttleMs, rapid burst of many notifications results in exactly 2 renders (leading + trailing)", async () => {
|
|
256
|
-
const renderCount = { current: 0 };
|
|
257
|
-
const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
|
|
258
|
-
|
|
259
|
-
render(<TestComponent />);
|
|
260
|
-
const rendersAfterMount = renderCount.current;
|
|
261
|
-
|
|
262
|
-
// Leading edge — fires immediately
|
|
263
|
-
await act(async () => {
|
|
264
|
-
mockAgent.messages = [userMsg("1", "tok1")];
|
|
265
|
-
notifyMessagesChanged(mockAgent);
|
|
266
|
-
});
|
|
267
|
-
expect(renderCount.current).toBe(rendersAfterMount + 1);
|
|
268
|
-
|
|
269
|
-
// Fire 10 rapid notifications within the throttle window (1ms apart)
|
|
270
|
-
for (let i = 2; i <= 11; i++) {
|
|
271
|
-
await act(async () => {
|
|
272
|
-
vi.advanceTimersByTime(1);
|
|
273
|
-
mockAgent.messages = createMessages(i);
|
|
274
|
-
notifyMessagesChanged(mockAgent);
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Should still be at leading-edge render count (burst was coalesced)
|
|
279
|
-
expect(renderCount.current).toBe(rendersAfterMount + 1);
|
|
280
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
281
|
-
|
|
282
|
-
// Advance past the throttle window — trailing edge fires once
|
|
283
|
-
await act(async () => {
|
|
284
|
-
vi.advanceTimersByTime(100);
|
|
285
|
-
});
|
|
286
|
-
expect(renderCount.current).toBe(rendersAfterMount + 2);
|
|
287
|
-
expect(screen.getByTestId("count").textContent).toBe("11");
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("with throttleMs, new notification after trailing edge fires immediately (new cycle)", async () => {
|
|
291
|
-
const TestComponent = createTestComponent({ throttleMs: 100 });
|
|
292
|
-
|
|
293
|
-
render(<TestComponent />);
|
|
294
|
-
|
|
295
|
-
// Leading edge
|
|
296
|
-
await act(async () => {
|
|
297
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
298
|
-
notifyMessagesChanged(mockAgent);
|
|
299
|
-
});
|
|
300
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
301
|
-
|
|
302
|
-
// Second notification — deferred
|
|
303
|
-
await act(async () => {
|
|
304
|
-
vi.advanceTimersByTime(10);
|
|
305
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
306
|
-
notifyMessagesChanged(mockAgent);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// Trailing edge fires
|
|
310
|
-
await act(async () => {
|
|
311
|
-
vi.advanceTimersByTime(100);
|
|
312
|
-
});
|
|
313
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
314
|
-
|
|
315
|
-
// New notification well after the window — should fire immediately as a new leading edge
|
|
316
|
-
await act(async () => {
|
|
317
|
-
vi.advanceTimersByTime(200);
|
|
318
|
-
mockAgent.messages = [
|
|
319
|
-
userMsg("1", "a"),
|
|
320
|
-
assistantMsg("2", "b"),
|
|
321
|
-
userMsg("3", "c"),
|
|
322
|
-
];
|
|
323
|
-
notifyMessagesChanged(mockAgent);
|
|
324
|
-
});
|
|
325
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("with throttleMs, onStateChanged is also throttled (shared window)", async () => {
|
|
329
|
-
const TestComponent = createTestComponent({
|
|
330
|
-
updates: [
|
|
331
|
-
UseAgentUpdate.OnMessagesChanged,
|
|
332
|
-
UseAgentUpdate.OnStateChanged,
|
|
333
|
-
],
|
|
334
|
-
throttleMs: 100,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
render(<TestComponent />);
|
|
338
|
-
|
|
339
|
-
// Fire onMessagesChanged to start the throttle window (leading edge)
|
|
340
|
-
await act(async () => {
|
|
341
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
342
|
-
notifyMessagesChanged(mockAgent);
|
|
343
|
-
});
|
|
344
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
345
|
-
|
|
346
|
-
// Fire onStateChanged 10ms later — should be deferred (within throttle window)
|
|
347
|
-
await act(async () => {
|
|
348
|
-
vi.advanceTimersByTime(10);
|
|
349
|
-
mockAgent.state = { count: 42 };
|
|
350
|
-
notifyStateChanged(mockAgent);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// State update is pending, not yet rendered
|
|
354
|
-
expect(screen.getByTestId("state").textContent).toBe("{}");
|
|
355
|
-
|
|
356
|
-
// Trailing edge fires after the window — await so microtask from
|
|
357
|
-
// batchedForceUpdate flushes and triggers the React re-render.
|
|
358
|
-
await act(async () => {
|
|
359
|
-
vi.advanceTimersByTime(100);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
expect(screen.getByTestId("state").textContent).toBe('{"count":42}');
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it("with throttleMs, pending trailing timer does not fire after unmount", async () => {
|
|
366
|
-
const renderCount = { current: 0 };
|
|
367
|
-
const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
|
|
368
|
-
|
|
369
|
-
const { unmount } = render(<TestComponent />);
|
|
370
|
-
|
|
371
|
-
// Leading edge — fires immediately
|
|
372
|
-
await act(async () => {
|
|
373
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
374
|
-
notifyMessagesChanged(mockAgent);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// Second notification — schedules trailing timer
|
|
378
|
-
await act(async () => {
|
|
379
|
-
vi.advanceTimersByTime(10);
|
|
380
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
381
|
-
notifyMessagesChanged(mockAgent);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
const countBeforeUnmount = renderCount.current;
|
|
385
|
-
|
|
386
|
-
// Unmount before trailing fires
|
|
387
|
-
unmount();
|
|
388
|
-
|
|
389
|
-
// Advancing past the window should NOT cause additional renders
|
|
390
|
-
await act(async () => {
|
|
391
|
-
vi.advanceTimersByTime(100);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
expect(renderCount.current).toBe(countBeforeUnmount);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it("with throttleMs and only OnStateChanged subscribed, first state fires on leading edge", async () => {
|
|
398
|
-
const TestComponent = createTestComponent({
|
|
399
|
-
updates: [UseAgentUpdate.OnStateChanged],
|
|
400
|
-
throttleMs: 100,
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
render(<TestComponent />);
|
|
404
|
-
|
|
405
|
-
// First onStateChanged fires immediately (leading edge) — await so
|
|
406
|
-
// microtask from batchedForceUpdate flushes.
|
|
407
|
-
await act(async () => {
|
|
408
|
-
mockAgent.state = { value: "test" };
|
|
409
|
-
notifyStateChanged(mockAgent);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
expect(screen.getByTestId("state").textContent).toBe('{"value":"test"}');
|
|
413
|
-
|
|
414
|
-
// No onMessagesChanged subscription should exist — messages notification
|
|
415
|
-
// does nothing because the handler was never registered.
|
|
416
|
-
act(() => {
|
|
417
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
418
|
-
notifyMessagesChanged(mockAgent);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
expect(screen.getByTestId("state").textContent).toBe('{"value":"test"}');
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it.each([
|
|
425
|
-
{ label: "NaN", value: NaN },
|
|
426
|
-
{ label: "Infinity", value: Infinity },
|
|
427
|
-
{ label: "-1", value: -1 },
|
|
428
|
-
{ label: "-Infinity", value: -Infinity },
|
|
429
|
-
])(
|
|
430
|
-
"with invalid throttleMs ($label), falls back to unthrottled and warns",
|
|
431
|
-
async ({ value }) => {
|
|
432
|
-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
433
|
-
const TestComponent = createTestComponent({ throttleMs: value });
|
|
434
|
-
|
|
435
|
-
render(<TestComponent />);
|
|
436
|
-
|
|
437
|
-
// Should warn about the invalid value
|
|
438
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
439
|
-
expect.stringContaining(
|
|
440
|
-
"throttleMs must be a non-negative finite number",
|
|
441
|
-
),
|
|
442
|
-
expect.any(Error),
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
// Should behave as unthrottled — every notification fires immediately
|
|
446
|
-
await act(async () => {
|
|
447
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
448
|
-
notifyMessagesChanged(mockAgent);
|
|
449
|
-
});
|
|
450
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
451
|
-
|
|
452
|
-
await act(async () => {
|
|
453
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
454
|
-
notifyMessagesChanged(mockAgent);
|
|
455
|
-
});
|
|
456
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
457
|
-
},
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
it("trailing-edge render reflects the latest messages, not stale data", async () => {
|
|
461
|
-
const TestComponent = createTestComponent({ throttleMs: 100 });
|
|
462
|
-
render(<TestComponent />);
|
|
463
|
-
|
|
464
|
-
// Leading edge
|
|
465
|
-
await act(async () => {
|
|
466
|
-
mockAgent.messages = [userMsg("1", "A")];
|
|
467
|
-
notifyMessagesChanged(mockAgent);
|
|
468
|
-
});
|
|
469
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
470
|
-
|
|
471
|
-
// Multiple deferred notifications with increasing messages
|
|
472
|
-
await act(async () => {
|
|
473
|
-
vi.advanceTimersByTime(20);
|
|
474
|
-
mockAgent.messages = [userMsg("1", "A"), assistantMsg("2", "B")];
|
|
475
|
-
notifyMessagesChanged(mockAgent);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
await act(async () => {
|
|
479
|
-
vi.advanceTimersByTime(20);
|
|
480
|
-
mockAgent.messages = [
|
|
481
|
-
userMsg("1", "A"),
|
|
482
|
-
assistantMsg("2", "B"),
|
|
483
|
-
assistantMsg("3", "C"),
|
|
484
|
-
];
|
|
485
|
-
notifyMessagesChanged(mockAgent);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// Still deferred
|
|
489
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
490
|
-
|
|
491
|
-
// Trailing edge fires — must show all 3 messages (latest state)
|
|
492
|
-
await act(async () => {
|
|
493
|
-
vi.advanceTimersByTime(100);
|
|
494
|
-
});
|
|
495
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it("trailing edge fires at exactly throttleMs after the leading edge", async () => {
|
|
499
|
-
const TestComponent = createTestComponent({ throttleMs: 100 });
|
|
500
|
-
render(<TestComponent />);
|
|
501
|
-
|
|
502
|
-
// Leading edge at T=0
|
|
503
|
-
await act(async () => {
|
|
504
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
505
|
-
notifyMessagesChanged(mockAgent);
|
|
506
|
-
});
|
|
507
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
508
|
-
|
|
509
|
-
// Deferred notification at T=40
|
|
510
|
-
await act(async () => {
|
|
511
|
-
vi.advanceTimersByTime(40);
|
|
512
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
513
|
-
notifyMessagesChanged(mockAgent);
|
|
514
|
-
});
|
|
515
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
516
|
-
|
|
517
|
-
// At T=99, trailing has NOT fired yet
|
|
518
|
-
await act(async () => {
|
|
519
|
-
vi.advanceTimersByTime(59);
|
|
520
|
-
});
|
|
521
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
522
|
-
|
|
523
|
-
// At T=100, trailing fires
|
|
524
|
-
await act(async () => {
|
|
525
|
-
vi.advanceTimersByTime(1);
|
|
526
|
-
});
|
|
527
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it("changing throttleMs cleans up pending timers from the previous configuration", async () => {
|
|
531
|
-
function DynamicThrottleComponent({ throttleMs }: { throttleMs: number }) {
|
|
532
|
-
const { agent } = useAgent({
|
|
533
|
-
agentId: "test-agent",
|
|
534
|
-
updates: [UseAgentUpdate.OnMessagesChanged],
|
|
535
|
-
throttleMs,
|
|
536
|
-
});
|
|
537
|
-
return <div data-testid="count">{agent.messages.length}</div>;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const { rerender } = render(<DynamicThrottleComponent throttleMs={200} />);
|
|
541
|
-
|
|
542
|
-
// Leading edge
|
|
543
|
-
await act(async () => {
|
|
544
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
545
|
-
notifyMessagesChanged(mockAgent);
|
|
546
|
-
});
|
|
547
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
548
|
-
|
|
549
|
-
// Deferred notification — pending timer set for 200ms
|
|
550
|
-
await act(async () => {
|
|
551
|
-
vi.advanceTimersByTime(50);
|
|
552
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
553
|
-
notifyMessagesChanged(mockAgent);
|
|
554
|
-
});
|
|
555
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
556
|
-
|
|
557
|
-
// Change throttleMs — effect re-runs, old 200ms timer should be cleaned up
|
|
558
|
-
rerender(<DynamicThrottleComponent throttleMs={50} />);
|
|
559
|
-
|
|
560
|
-
// New notification fires as leading edge under the new 50ms throttle
|
|
561
|
-
await act(async () => {
|
|
562
|
-
mockAgent.messages = [
|
|
563
|
-
userMsg("1", "a"),
|
|
564
|
-
assistantMsg("2", "b"),
|
|
565
|
-
userMsg("3", "c"),
|
|
566
|
-
];
|
|
567
|
-
notifyMessagesChanged(mockAgent);
|
|
568
|
-
});
|
|
569
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
570
|
-
|
|
571
|
-
// Advance past what would have been the old 200ms trailing edge —
|
|
572
|
-
// no ghost render should occur from the old timer
|
|
573
|
-
await act(async () => {
|
|
574
|
-
vi.advanceTimersByTime(200);
|
|
575
|
-
});
|
|
576
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("notification immediately after trailing edge is throttled (trailing restarts the window)", async () => {
|
|
580
|
-
const renderCount = { current: 0 };
|
|
581
|
-
const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
|
|
582
|
-
|
|
583
|
-
render(<TestComponent />);
|
|
584
|
-
const rendersAfterMount = renderCount.current;
|
|
585
|
-
|
|
586
|
-
// T=0: Leading edge fires immediately
|
|
587
|
-
await act(async () => {
|
|
588
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
589
|
-
notifyMessagesChanged(mockAgent);
|
|
590
|
-
});
|
|
591
|
-
expect(renderCount.current).toBe(rendersAfterMount + 1);
|
|
592
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
593
|
-
|
|
594
|
-
// T=10: Deferred notification — schedules trailing
|
|
595
|
-
await act(async () => {
|
|
596
|
-
vi.advanceTimersByTime(10);
|
|
597
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
598
|
-
notifyMessagesChanged(mockAgent);
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
// T=100: Trailing fires (render #2) and restarts window
|
|
602
|
-
await act(async () => {
|
|
603
|
-
vi.advanceTimersByTime(90);
|
|
604
|
-
});
|
|
605
|
-
expect(renderCount.current).toBe(rendersAfterMount + 2);
|
|
606
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
607
|
-
|
|
608
|
-
// T=101: Notification 1ms after trailing — should be DEFERRED (within new window), not immediate
|
|
609
|
-
await act(async () => {
|
|
610
|
-
vi.advanceTimersByTime(1);
|
|
611
|
-
mockAgent.messages = [
|
|
612
|
-
userMsg("1", "a"),
|
|
613
|
-
assistantMsg("2", "b"),
|
|
614
|
-
userMsg("3", "c"),
|
|
615
|
-
];
|
|
616
|
-
notifyMessagesChanged(mockAgent);
|
|
617
|
-
});
|
|
618
|
-
// Still 2 — the notification was deferred, not a new leading edge
|
|
619
|
-
expect(renderCount.current).toBe(rendersAfterMount + 2);
|
|
620
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
621
|
-
|
|
622
|
-
// T=200: New trailing fires (render #3)
|
|
623
|
-
await act(async () => {
|
|
624
|
-
vi.advanceTimersByTime(99);
|
|
625
|
-
});
|
|
626
|
-
expect(renderCount.current).toBe(rendersAfterMount + 3);
|
|
627
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
it("cleans up all subscriptions after unmount", () => {
|
|
631
|
-
const TestComponent = createTestComponent({
|
|
632
|
-
updates: [
|
|
633
|
-
UseAgentUpdate.OnMessagesChanged,
|
|
634
|
-
UseAgentUpdate.OnStateChanged,
|
|
635
|
-
],
|
|
636
|
-
throttleMs: 100,
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
const subscriberCountBefore = mockAgent.subscribers.length;
|
|
640
|
-
const { unmount } = render(<TestComponent />);
|
|
641
|
-
|
|
642
|
-
// Should have added subscriber(s)
|
|
643
|
-
expect(mockAgent.subscribers.length).toBeGreaterThan(subscriberCountBefore);
|
|
644
|
-
|
|
645
|
-
unmount();
|
|
646
|
-
|
|
647
|
-
// All subscriptions should be cleaned up
|
|
648
|
-
expect(mockAgent.subscribers.length).toBe(subscriberCountBefore);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it("single notification within window does not trigger a trailing re-render", async () => {
|
|
652
|
-
const renderCount = { current: 0 };
|
|
653
|
-
const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
|
|
654
|
-
|
|
655
|
-
render(<TestComponent />);
|
|
656
|
-
const rendersAfterMount = renderCount.current;
|
|
657
|
-
|
|
658
|
-
// Leading edge — fires immediately
|
|
659
|
-
await act(async () => {
|
|
660
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
661
|
-
notifyMessagesChanged(mockAgent);
|
|
662
|
-
});
|
|
663
|
-
expect(renderCount.current).toBe(rendersAfterMount + 1);
|
|
664
|
-
|
|
665
|
-
// Advance well past the throttle window — no trailing should fire
|
|
666
|
-
await act(async () => {
|
|
667
|
-
vi.advanceTimersByTime(200);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
// No additional render since there was no second notification
|
|
671
|
-
expect(renderCount.current).toBe(rendersAfterMount + 1);
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
it("with throttleMs, onRunInitialized still fires immediately during throttle window", async () => {
|
|
675
|
-
const renderCount = { current: 0 };
|
|
676
|
-
const TestComponent = createTestComponent({
|
|
677
|
-
updates: [
|
|
678
|
-
UseAgentUpdate.OnMessagesChanged,
|
|
679
|
-
UseAgentUpdate.OnRunStatusChanged,
|
|
680
|
-
],
|
|
681
|
-
throttleMs: 100,
|
|
682
|
-
renderCount,
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
render(<TestComponent />);
|
|
686
|
-
const rendersAfterMount = renderCount.current;
|
|
687
|
-
|
|
688
|
-
// Fire onMessagesChanged to start the throttle window
|
|
689
|
-
await act(async () => {
|
|
690
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
691
|
-
notifyMessagesChanged(mockAgent);
|
|
692
|
-
});
|
|
693
|
-
expect(renderCount.current).toBe(rendersAfterMount + 1);
|
|
694
|
-
|
|
695
|
-
// Fire onRunInitialized 10ms later — fires via microtask batch
|
|
696
|
-
await act(async () => {
|
|
697
|
-
vi.advanceTimersByTime(10);
|
|
698
|
-
notifyRunInitialized(mockAgent);
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
// Run status notification fires via microtask batch
|
|
702
|
-
expect(renderCount.current).toBe(rendersAfterMount + 2);
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
it("changing throttleMs from positive to 0 disables throttling immediately", async () => {
|
|
706
|
-
function DynamicThrottleComponent({ throttleMs }: { throttleMs: number }) {
|
|
707
|
-
const { agent } = useAgent({
|
|
708
|
-
agentId: "test-agent",
|
|
709
|
-
updates: [UseAgentUpdate.OnMessagesChanged],
|
|
710
|
-
throttleMs,
|
|
711
|
-
});
|
|
712
|
-
return <div data-testid="count">{agent.messages.length}</div>;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const { rerender } = render(<DynamicThrottleComponent throttleMs={200} />);
|
|
716
|
-
|
|
717
|
-
// Leading edge with throttle active
|
|
718
|
-
await act(async () => {
|
|
719
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
720
|
-
notifyMessagesChanged(mockAgent);
|
|
721
|
-
});
|
|
722
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
723
|
-
|
|
724
|
-
// Deferred notification — within throttle window
|
|
725
|
-
await act(async () => {
|
|
726
|
-
vi.advanceTimersByTime(50);
|
|
727
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
728
|
-
notifyMessagesChanged(mockAgent);
|
|
729
|
-
});
|
|
730
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
731
|
-
|
|
732
|
-
// Switch to unthrottled
|
|
733
|
-
rerender(<DynamicThrottleComponent throttleMs={0} />);
|
|
734
|
-
|
|
735
|
-
// Both notifications should fire immediately now
|
|
736
|
-
await act(async () => {
|
|
737
|
-
mockAgent.messages = [
|
|
738
|
-
userMsg("1", "a"),
|
|
739
|
-
assistantMsg("2", "b"),
|
|
740
|
-
userMsg("3", "c"),
|
|
741
|
-
];
|
|
742
|
-
notifyMessagesChanged(mockAgent);
|
|
743
|
-
});
|
|
744
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
745
|
-
|
|
746
|
-
// Second immediate notification also fires (no coalescing)
|
|
747
|
-
await act(async () => {
|
|
748
|
-
mockAgent.messages = [
|
|
749
|
-
userMsg("1", "a"),
|
|
750
|
-
assistantMsg("2", "b"),
|
|
751
|
-
userMsg("3", "c"),
|
|
752
|
-
assistantMsg("4", "d"),
|
|
753
|
-
];
|
|
754
|
-
notifyMessagesChanged(mockAgent);
|
|
755
|
-
});
|
|
756
|
-
expect(screen.getByTestId("count").textContent).toBe("4");
|
|
757
|
-
});
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
describe("useAgent defaultThrottleMs from provider", () => {
|
|
761
|
-
let mockAgent: MockStepwiseAgent;
|
|
762
|
-
|
|
763
|
-
beforeEach(() => {
|
|
764
|
-
vi.useFakeTimers();
|
|
765
|
-
mockAgent = new MockStepwiseAgent();
|
|
766
|
-
mockAgent.agentId = "test-agent";
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
afterEach(() => {
|
|
770
|
-
vi.useRealTimers();
|
|
771
|
-
vi.restoreAllMocks();
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
it("uses provider defaultThrottleMs when no explicit throttleMs is passed", async () => {
|
|
775
|
-
mockUseCopilotKit.mockReturnValue(
|
|
776
|
-
createMockContext(mockAgent, { defaultThrottleMs: 100 }),
|
|
777
|
-
);
|
|
778
|
-
|
|
779
|
-
const TestComponent = createTestComponent({ throttleMs: undefined });
|
|
780
|
-
|
|
781
|
-
render(<TestComponent />);
|
|
782
|
-
|
|
783
|
-
// Leading edge — fires immediately
|
|
784
|
-
await act(async () => {
|
|
785
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
786
|
-
notifyMessagesChanged(mockAgent);
|
|
787
|
-
});
|
|
788
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
789
|
-
|
|
790
|
-
// Second notification within 100ms window — should be deferred (throttled)
|
|
791
|
-
await act(async () => {
|
|
792
|
-
vi.advanceTimersByTime(10);
|
|
793
|
-
mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
|
|
794
|
-
notifyMessagesChanged(mockAgent);
|
|
795
|
-
});
|
|
796
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
797
|
-
|
|
798
|
-
// Trailing edge fires after 100ms
|
|
799
|
-
await act(async () => {
|
|
800
|
-
vi.advanceTimersByTime(100);
|
|
801
|
-
});
|
|
802
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
it("explicit throttleMs overrides provider defaultThrottleMs", async () => {
|
|
806
|
-
mockUseCopilotKit.mockReturnValue(
|
|
807
|
-
createMockContext(mockAgent, { defaultThrottleMs: 5000 }),
|
|
808
|
-
);
|
|
809
|
-
|
|
810
|
-
// Explicit throttleMs=100 should override provider's 5000
|
|
811
|
-
const TestComponent = createTestComponent({ throttleMs: 100 });
|
|
812
|
-
|
|
813
|
-
render(<TestComponent />);
|
|
814
|
-
|
|
815
|
-
// Leading edge
|
|
816
|
-
await act(async () => {
|
|
817
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
818
|
-
notifyMessagesChanged(mockAgent);
|
|
819
|
-
});
|
|
820
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
821
|
-
|
|
822
|
-
// Deferred within 100ms window
|
|
823
|
-
await act(async () => {
|
|
824
|
-
vi.advanceTimersByTime(10);
|
|
825
|
-
mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
|
|
826
|
-
notifyMessagesChanged(mockAgent);
|
|
827
|
-
});
|
|
828
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
829
|
-
|
|
830
|
-
// At 100ms trailing fires (not waiting for provider's 5000ms)
|
|
831
|
-
await act(async () => {
|
|
832
|
-
vi.advanceTimersByTime(100);
|
|
833
|
-
});
|
|
834
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it("without provider defaultThrottleMs or explicit throttleMs, behaves unthrottled", async () => {
|
|
838
|
-
mockUseCopilotKit.mockReturnValue(createMockContext(mockAgent));
|
|
839
|
-
|
|
840
|
-
const TestComponent = createTestComponent({});
|
|
841
|
-
|
|
842
|
-
render(<TestComponent />);
|
|
843
|
-
|
|
844
|
-
await act(async () => {
|
|
845
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
846
|
-
notifyMessagesChanged(mockAgent);
|
|
847
|
-
});
|
|
848
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
849
|
-
|
|
850
|
-
// Immediately fires — no throttle
|
|
851
|
-
await act(async () => {
|
|
852
|
-
mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
|
|
853
|
-
notifyMessagesChanged(mockAgent);
|
|
854
|
-
});
|
|
855
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
it("explicit throttleMs: 0 overrides non-zero provider defaultThrottleMs (opt-out)", async () => {
|
|
859
|
-
mockUseCopilotKit.mockReturnValue(
|
|
860
|
-
createMockContext(mockAgent, { defaultThrottleMs: 500 }),
|
|
861
|
-
);
|
|
862
|
-
|
|
863
|
-
const TestComponent = createTestComponent({ throttleMs: 0 });
|
|
864
|
-
|
|
865
|
-
render(<TestComponent />);
|
|
866
|
-
|
|
867
|
-
// Both notifications fire immediately — throttleMs: 0 means no throttle
|
|
868
|
-
await act(async () => {
|
|
869
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
870
|
-
notifyMessagesChanged(mockAgent);
|
|
871
|
-
});
|
|
872
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
873
|
-
|
|
874
|
-
await act(async () => {
|
|
875
|
-
mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
|
|
876
|
-
notifyMessagesChanged(mockAgent);
|
|
877
|
-
});
|
|
878
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
it.each([
|
|
882
|
-
{ label: "NaN", value: NaN },
|
|
883
|
-
{ label: "Infinity", value: Infinity },
|
|
884
|
-
{ label: "-1", value: -1 },
|
|
885
|
-
{ label: "-Infinity", value: -Infinity },
|
|
886
|
-
])(
|
|
887
|
-
"with invalid provider defaultThrottleMs ($label), falls back to unthrottled and warns",
|
|
888
|
-
async ({ value }) => {
|
|
889
|
-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
890
|
-
|
|
891
|
-
mockUseCopilotKit.mockReturnValue(
|
|
892
|
-
createMockContext(mockAgent, { defaultThrottleMs: value }),
|
|
893
|
-
);
|
|
894
|
-
|
|
895
|
-
const TestComponent = createTestComponent({ throttleMs: undefined });
|
|
896
|
-
|
|
897
|
-
render(<TestComponent />);
|
|
898
|
-
|
|
899
|
-
// The core setter rejects invalid values and logs an error
|
|
900
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
901
|
-
expect.stringContaining("must be a non-negative finite number"),
|
|
902
|
-
expect.any(Error),
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
// Should behave as unthrottled (setter rejected the value)
|
|
906
|
-
await act(async () => {
|
|
907
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
908
|
-
notifyMessagesChanged(mockAgent);
|
|
909
|
-
});
|
|
910
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
911
|
-
|
|
912
|
-
await act(async () => {
|
|
913
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
914
|
-
notifyMessagesChanged(mockAgent);
|
|
915
|
-
});
|
|
916
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
917
|
-
},
|
|
918
|
-
);
|
|
919
|
-
|
|
920
|
-
it("dynamically changing provider defaultThrottleMs updates throttle behavior", async () => {
|
|
921
|
-
// Start with 200ms throttle from provider
|
|
922
|
-
mockUseCopilotKit.mockReturnValue(
|
|
923
|
-
createMockContext(mockAgent, { defaultThrottleMs: 200 }),
|
|
924
|
-
);
|
|
925
|
-
|
|
926
|
-
const TestComponent = createTestComponent({ throttleMs: undefined });
|
|
927
|
-
const { rerender } = render(<TestComponent />);
|
|
928
|
-
|
|
929
|
-
// Leading edge fires immediately
|
|
930
|
-
await act(async () => {
|
|
931
|
-
mockAgent.messages = [userMsg("1", "hello")];
|
|
932
|
-
notifyMessagesChanged(mockAgent);
|
|
933
|
-
});
|
|
934
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
935
|
-
|
|
936
|
-
// Deferred within 200ms window
|
|
937
|
-
await act(async () => {
|
|
938
|
-
vi.advanceTimersByTime(10);
|
|
939
|
-
mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
|
|
940
|
-
notifyMessagesChanged(mockAgent);
|
|
941
|
-
});
|
|
942
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
943
|
-
|
|
944
|
-
// Flush trailing edge
|
|
945
|
-
await act(async () => {
|
|
946
|
-
vi.advanceTimersByTime(200);
|
|
947
|
-
});
|
|
948
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
949
|
-
|
|
950
|
-
// Change provider default to 50ms
|
|
951
|
-
mockUseCopilotKit.mockReturnValue(
|
|
952
|
-
createMockContext(mockAgent, { defaultThrottleMs: 50 }),
|
|
953
|
-
);
|
|
954
|
-
rerender(<TestComponent />);
|
|
955
|
-
|
|
956
|
-
// Leading edge fires immediately
|
|
957
|
-
await act(async () => {
|
|
958
|
-
mockAgent.messages = [
|
|
959
|
-
userMsg("1", "hello"),
|
|
960
|
-
assistantMsg("2", "world"),
|
|
961
|
-
userMsg("3", "new"),
|
|
962
|
-
];
|
|
963
|
-
notifyMessagesChanged(mockAgent);
|
|
964
|
-
});
|
|
965
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
966
|
-
|
|
967
|
-
// Deferred within 50ms window
|
|
968
|
-
await act(async () => {
|
|
969
|
-
vi.advanceTimersByTime(10);
|
|
970
|
-
mockAgent.messages = [
|
|
971
|
-
userMsg("1", "hello"),
|
|
972
|
-
assistantMsg("2", "world"),
|
|
973
|
-
userMsg("3", "new"),
|
|
974
|
-
assistantMsg("4", "reply"),
|
|
975
|
-
];
|
|
976
|
-
notifyMessagesChanged(mockAgent);
|
|
977
|
-
});
|
|
978
|
-
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
979
|
-
|
|
980
|
-
// Trailing fires after only 50ms (not 200ms)
|
|
981
|
-
await act(async () => {
|
|
982
|
-
vi.advanceTimersByTime(50);
|
|
983
|
-
});
|
|
984
|
-
expect(screen.getByTestId("count").textContent).toBe("4");
|
|
985
|
-
});
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
describe("CopilotKitCore.setDefaultThrottleMs", () => {
|
|
989
|
-
it("stores valid values", () => {
|
|
990
|
-
const core = new CopilotKitCore({});
|
|
991
|
-
core.setDefaultThrottleMs(100);
|
|
992
|
-
expect(core.defaultThrottleMs).toBe(100);
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
it("stores 0", () => {
|
|
996
|
-
const core = new CopilotKitCore({});
|
|
997
|
-
core.setDefaultThrottleMs(100);
|
|
998
|
-
core.setDefaultThrottleMs(0);
|
|
999
|
-
expect(core.defaultThrottleMs).toBe(0);
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
it("stores undefined", () => {
|
|
1003
|
-
const core = new CopilotKitCore({});
|
|
1004
|
-
core.setDefaultThrottleMs(100);
|
|
1005
|
-
core.setDefaultThrottleMs(undefined);
|
|
1006
|
-
expect(core.defaultThrottleMs).toBeUndefined();
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
it.each([
|
|
1010
|
-
{ label: "NaN", value: NaN },
|
|
1011
|
-
{ label: "Infinity", value: Infinity },
|
|
1012
|
-
{ label: "-1", value: -1 },
|
|
1013
|
-
{ label: "-Infinity", value: -Infinity },
|
|
1014
|
-
])(
|
|
1015
|
-
"rejects invalid value ($label) and preserves previous value",
|
|
1016
|
-
({ value }) => {
|
|
1017
|
-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1018
|
-
const core = new CopilotKitCore({});
|
|
1019
|
-
core.setDefaultThrottleMs(200);
|
|
1020
|
-
core.setDefaultThrottleMs(value);
|
|
1021
|
-
expect(core.defaultThrottleMs).toBe(200);
|
|
1022
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
1023
|
-
expect.stringContaining("must be a non-negative finite number"),
|
|
1024
|
-
expect.any(Error),
|
|
1025
|
-
);
|
|
1026
|
-
errorSpy.mockRestore();
|
|
1027
|
-
},
|
|
1028
|
-
);
|
|
1029
|
-
});
|