@copilotkit/react-core 1.54.1 → 1.55.0-next.8
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/CHANGELOG.md +127 -116
- package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
- package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
- package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
- package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
- package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
- package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
- package/dist/index.cjs +27 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -5
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1941 -35
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +77 -7
- package/dist/v2/index.css +1 -2
- package/dist/v2/index.d.cts +6 -4
- package/dist/v2/index.d.mts +6 -4
- package/dist/v2/index.mjs +7 -4
- package/dist/v2/index.umd.js +5725 -24
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +37 -9
- package/scripts/scope-preflight.mjs +101 -0
- package/src/components/CopilotListeners.tsx +2 -6
- package/src/components/copilot-provider/copilot-messages.tsx +1 -1
- package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
- package/src/components/copilot-provider/copilotkit.tsx +4 -4
- package/src/context/copilot-messages-context.tsx +1 -1
- package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
- package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
- package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
- package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
- package/src/hooks/use-agent-nodename.ts +1 -1
- package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
- package/src/hooks/use-coagent.ts +1 -1
- package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat_internal.ts +2 -2
- package/src/hooks/use-copilot-readable.ts +1 -1
- package/src/hooks/use-frontend-tool.ts +2 -2
- package/src/hooks/use-human-in-the-loop.ts +2 -2
- package/src/hooks/use-langgraph-interrupt.ts +2 -5
- package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
- package/src/hooks/use-render-tool-call.ts +1 -1
- package/src/lib/copilot-task.ts +1 -1
- package/src/setupTests.ts +18 -14
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
- package/src/v2/__tests__/globalSetup.ts +14 -0
- package/src/v2/__tests__/setup.ts +93 -0
- package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
- package/src/v2/components/CopilotKitInspector.tsx +50 -0
- package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
- package/src/v2/components/WildcardToolCallRender.tsx +86 -0
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
- package/src/v2/components/chat/CopilotChat.tsx +431 -0
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
- package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
- package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
- package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
- package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
- package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
- package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
- package/src/v2/components/chat/CopilotChatView.tsx +598 -0
- package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
- package/src/v2/components/chat/CopilotPopup.tsx +81 -0
- package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
- package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
- package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
- package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
- package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
- package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
- package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
- package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
- package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
- package/src/v2/components/chat/__tests__/setup.ts +1 -0
- package/src/v2/components/chat/index.ts +79 -0
- package/src/v2/components/index.ts +7 -0
- package/src/v2/components/license-warning-banner.tsx +198 -0
- package/src/v2/components/ui/button.tsx +123 -0
- package/src/v2/components/ui/dropdown-menu.tsx +258 -0
- package/src/v2/components/ui/tooltip.tsx +60 -0
- package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
- package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
- package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
- package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
- package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
- package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
- package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
- package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
- package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
- package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
- package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
- package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
- package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
- package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
- package/src/v2/hooks/index.ts +18 -0
- package/src/v2/hooks/use-agent-context.tsx +45 -0
- package/src/v2/hooks/use-agent.tsx +155 -0
- package/src/v2/hooks/use-component.tsx +89 -0
- package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
- package/src/v2/hooks/use-default-render-tool.tsx +254 -0
- package/src/v2/hooks/use-frontend-tool.tsx +43 -0
- package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
- package/src/v2/hooks/use-interrupt.tsx +305 -0
- package/src/v2/hooks/use-keyboard-height.tsx +67 -0
- package/src/v2/hooks/use-render-activity-message.tsx +73 -0
- package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
- package/src/v2/hooks/use-render-tool-call.tsx +175 -0
- package/src/v2/hooks/use-render-tool.tsx +181 -0
- package/src/v2/hooks/use-suggestions.tsx +91 -0
- package/src/v2/hooks/use-threads.tsx +256 -0
- package/src/v2/hooks/useKatexStyles.ts +27 -0
- package/src/v2/index.css +1 -1
- package/src/v2/index.ts +18 -2
- package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
- package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
- package/src/v2/lib/react-core.ts +156 -0
- package/src/v2/lib/slots.tsx +143 -0
- package/src/v2/lib/transcription-client.ts +184 -0
- package/src/v2/lib/utils.ts +8 -0
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
- package/src/v2/providers/CopilotKitProvider.tsx +600 -0
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
- package/src/v2/providers/index.ts +14 -0
- package/src/v2/styles/globals.css +230 -0
- package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
- package/src/v2/types/defineToolCallRenderer.ts +65 -0
- package/src/v2/types/frontend-tool.ts +8 -0
- package/src/v2/types/human-in-the-loop.ts +33 -0
- package/src/v2/types/index.ts +7 -0
- package/src/v2/types/interrupt.ts +15 -0
- package/src/v2/types/react-activity-message-renderer.ts +27 -0
- package/src/v2/types/react-custom-message-renderer.ts +17 -0
- package/src/v2/types/react-tool-call-renderer.ts +32 -0
- package/tsdown.config.ts +34 -10
- package/vitest.config.mjs +4 -3
- package/LICENSE +0 -21
- package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
- package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
- package/dist/copilotkit-C94ayZbs.cjs +0 -2161
- package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
- package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
- package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
- package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
- package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
- package/dist/v2/index.css.map +0 -1
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { ReactFrontendTool } from "../../types/frontend-tool";
|
|
6
|
+
import type { ReactToolCallRenderer } from "../../types";
|
|
7
|
+
import {
|
|
8
|
+
CopilotKitProvider,
|
|
9
|
+
useCopilotKit,
|
|
10
|
+
type CopilotKitContextValue,
|
|
11
|
+
} from "../CopilotKitProvider";
|
|
12
|
+
import { CopilotKitCoreReact } from "../../lib/react-core";
|
|
13
|
+
import { useFrontendTool } from "../../hooks/use-frontend-tool";
|
|
14
|
+
|
|
15
|
+
// Mock console methods to suppress expected warnings
|
|
16
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
17
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
+
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
consoleErrorSpy.mockRestore();
|
|
26
|
+
consoleWarnSpy.mockRestore();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("CopilotKitProvider stability", () => {
|
|
30
|
+
describe("instance stability", () => {
|
|
31
|
+
it("returns the same copilotkit instance after re-render with new renderToolCalls array", () => {
|
|
32
|
+
const instances: CopilotKitCoreReact[] = [];
|
|
33
|
+
|
|
34
|
+
function Collector({ children }: { children?: React.ReactNode }) {
|
|
35
|
+
const { copilotkit } = useCopilotKit();
|
|
36
|
+
instances.push(copilotkit);
|
|
37
|
+
return <>{children}</>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const renderToolCalls1: ReactToolCallRenderer<any>[] = [
|
|
41
|
+
{
|
|
42
|
+
name: "tool1",
|
|
43
|
+
args: z.object({ a: z.string() }),
|
|
44
|
+
render: () => <div>Tool 1</div>,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const renderToolCalls2: ReactToolCallRenderer<any>[] = [
|
|
49
|
+
{
|
|
50
|
+
name: "tool1",
|
|
51
|
+
args: z.object({ a: z.string() }),
|
|
52
|
+
render: () => <div>Tool 1 updated</div>,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const { rerender } = render(
|
|
57
|
+
<CopilotKitProvider renderToolCalls={renderToolCalls1}>
|
|
58
|
+
<Collector />
|
|
59
|
+
</CopilotKitProvider>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
rerender(
|
|
63
|
+
<CopilotKitProvider renderToolCalls={renderToolCalls2}>
|
|
64
|
+
<Collector />
|
|
65
|
+
</CopilotKitProvider>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(instances.length).toBeGreaterThanOrEqual(2);
|
|
69
|
+
const first = instances[0];
|
|
70
|
+
for (const instance of instances) {
|
|
71
|
+
expect(instance).toBe(first);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns the same copilotkit instance after re-render with new frontendTools array", () => {
|
|
76
|
+
const instances: CopilotKitCoreReact[] = [];
|
|
77
|
+
|
|
78
|
+
function Collector() {
|
|
79
|
+
const { copilotkit } = useCopilotKit();
|
|
80
|
+
instances.push(copilotkit);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tools1: ReactFrontendTool[] = [
|
|
85
|
+
{ name: "toolA", description: "Tool A", handler: vi.fn() },
|
|
86
|
+
];
|
|
87
|
+
const tools2: ReactFrontendTool[] = [
|
|
88
|
+
{ name: "toolB", description: "Tool B", handler: vi.fn() },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const { rerender } = render(
|
|
92
|
+
<CopilotKitProvider frontendTools={tools1}>
|
|
93
|
+
<Collector />
|
|
94
|
+
</CopilotKitProvider>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
rerender(
|
|
98
|
+
<CopilotKitProvider frontendTools={tools2}>
|
|
99
|
+
<Collector />
|
|
100
|
+
</CopilotKitProvider>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(instances.length).toBeGreaterThanOrEqual(2);
|
|
104
|
+
const first = instances[0];
|
|
105
|
+
for (const instance of instances) {
|
|
106
|
+
expect(instance).toBe(first);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("context value stability", () => {
|
|
112
|
+
it("does not change context value reference when only tools change", () => {
|
|
113
|
+
const contextValues: CopilotKitContextValue[] = [];
|
|
114
|
+
|
|
115
|
+
function Collector() {
|
|
116
|
+
const context = useCopilotKit();
|
|
117
|
+
contextValues.push(context);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tools1: ReactFrontendTool[] = [
|
|
122
|
+
{ name: "toolA", description: "Tool A" },
|
|
123
|
+
];
|
|
124
|
+
const tools2: ReactFrontendTool[] = [
|
|
125
|
+
{ name: "toolB", description: "Tool B" },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const { rerender } = render(
|
|
129
|
+
<CopilotKitProvider frontendTools={tools1}>
|
|
130
|
+
<Collector />
|
|
131
|
+
</CopilotKitProvider>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const initialContext = contextValues[contextValues.length - 1];
|
|
135
|
+
|
|
136
|
+
rerender(
|
|
137
|
+
<CopilotKitProvider frontendTools={tools2}>
|
|
138
|
+
<Collector />
|
|
139
|
+
</CopilotKitProvider>,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const afterRerender = contextValues[contextValues.length - 1];
|
|
143
|
+
|
|
144
|
+
expect(afterRerender?.copilotkit).toBe(initialContext?.copilotkit);
|
|
145
|
+
expect(afterRerender?.executingToolCallIds).toBe(
|
|
146
|
+
initialContext?.executingToolCallIds,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("setter calls on prop changes", () => {
|
|
152
|
+
it("calls setTools when frontendTools change instead of recreating instance", () => {
|
|
153
|
+
const setToolsSpy = vi.fn();
|
|
154
|
+
let spyAttached = false;
|
|
155
|
+
|
|
156
|
+
function SpyAttacher() {
|
|
157
|
+
const { copilotkit } = useCopilotKit();
|
|
158
|
+
if (!spyAttached) {
|
|
159
|
+
const originalSetTools = copilotkit.setTools.bind(copilotkit);
|
|
160
|
+
copilotkit.setTools = (tools) => {
|
|
161
|
+
setToolsSpy(tools);
|
|
162
|
+
return originalSetTools(tools);
|
|
163
|
+
};
|
|
164
|
+
spyAttached = true;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tools1: ReactFrontendTool[] = [
|
|
170
|
+
{ name: "toolA", description: "Tool A", handler: vi.fn() },
|
|
171
|
+
];
|
|
172
|
+
const tools2: ReactFrontendTool[] = [
|
|
173
|
+
{ name: "toolB", description: "Tool B", handler: vi.fn() },
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const { rerender } = render(
|
|
177
|
+
<CopilotKitProvider frontendTools={tools1}>
|
|
178
|
+
<SpyAttacher />
|
|
179
|
+
</CopilotKitProvider>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
setToolsSpy.mockClear();
|
|
183
|
+
|
|
184
|
+
rerender(
|
|
185
|
+
<CopilotKitProvider frontendTools={tools2}>
|
|
186
|
+
<SpyAttacher />
|
|
187
|
+
</CopilotKitProvider>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(setToolsSpy).toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("calls setRenderToolCalls when renderToolCalls change", () => {
|
|
194
|
+
const setRenderToolCallsSpy = vi.fn();
|
|
195
|
+
let spyAttached = false;
|
|
196
|
+
|
|
197
|
+
function SpyAttacher() {
|
|
198
|
+
const { copilotkit } = useCopilotKit();
|
|
199
|
+
if (!spyAttached) {
|
|
200
|
+
const original = copilotkit.setRenderToolCalls.bind(copilotkit);
|
|
201
|
+
copilotkit.setRenderToolCalls = (renderToolCalls) => {
|
|
202
|
+
setRenderToolCallsSpy(renderToolCalls);
|
|
203
|
+
return original(renderToolCalls);
|
|
204
|
+
};
|
|
205
|
+
spyAttached = true;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rtc1: ReactToolCallRenderer<any>[] = [
|
|
211
|
+
{
|
|
212
|
+
name: "render1",
|
|
213
|
+
args: z.object({ x: z.string() }),
|
|
214
|
+
render: () => <div>R1</div>,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
const rtc2: ReactToolCallRenderer<any>[] = [
|
|
218
|
+
{
|
|
219
|
+
name: "render2",
|
|
220
|
+
args: z.object({ y: z.string() }),
|
|
221
|
+
render: () => <div>R2</div>,
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const { rerender } = render(
|
|
226
|
+
<CopilotKitProvider renderToolCalls={rtc1}>
|
|
227
|
+
<SpyAttacher />
|
|
228
|
+
</CopilotKitProvider>,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
setRenderToolCallsSpy.mockClear();
|
|
232
|
+
|
|
233
|
+
rerender(
|
|
234
|
+
<CopilotKitProvider renderToolCalls={rtc2}>
|
|
235
|
+
<SpyAttacher />
|
|
236
|
+
</CopilotKitProvider>,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(setRenderToolCallsSpy).toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("no unnecessary re-renders from stable props", () => {
|
|
244
|
+
it("does not re-render children when provider re-renders with same stable props", () => {
|
|
245
|
+
let childRenderCount = 0;
|
|
246
|
+
|
|
247
|
+
function Child() {
|
|
248
|
+
childRenderCount++;
|
|
249
|
+
useCopilotKit();
|
|
250
|
+
return <div>child</div>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const stableTools: ReactFrontendTool[] = [
|
|
254
|
+
{ name: "tool1", description: "Tool 1" },
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
const { rerender } = render(
|
|
258
|
+
<CopilotKitProvider frontendTools={stableTools}>
|
|
259
|
+
<Child />
|
|
260
|
+
</CopilotKitProvider>,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const initialCount = childRenderCount;
|
|
264
|
+
|
|
265
|
+
rerender(
|
|
266
|
+
<CopilotKitProvider frontendTools={stableTools}>
|
|
267
|
+
<Child />
|
|
268
|
+
</CopilotKitProvider>,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(childRenderCount - initialCount).toBeLessThanOrEqual(1);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("setter effects skip initial mount (didMountRef guard)", () => {
|
|
276
|
+
it("does not call setTools on initial mount (constructor handles it)", () => {
|
|
277
|
+
const setToolsCalls: unknown[][] = [];
|
|
278
|
+
let spyAttached = false;
|
|
279
|
+
|
|
280
|
+
function SpyAttacher() {
|
|
281
|
+
const { copilotkit } = useCopilotKit();
|
|
282
|
+
if (!spyAttached) {
|
|
283
|
+
const originalSetTools = copilotkit.setTools.bind(copilotkit);
|
|
284
|
+
copilotkit.setTools = (tools) => {
|
|
285
|
+
setToolsCalls.push([tools]);
|
|
286
|
+
return originalSetTools(tools);
|
|
287
|
+
};
|
|
288
|
+
spyAttached = true;
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const tools: ReactFrontendTool[] = [
|
|
294
|
+
{ name: "toolA", description: "Tool A", handler: vi.fn() },
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
render(
|
|
298
|
+
<CopilotKitProvider frontendTools={tools}>
|
|
299
|
+
<SpyAttacher />
|
|
300
|
+
</CopilotKitProvider>,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// setTools should NOT have been called on initial mount
|
|
304
|
+
// because the constructor already sets the initial tools
|
|
305
|
+
// and the didMountRef guard skips the first effect invocation.
|
|
306
|
+
expect(setToolsCalls).toHaveLength(0);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("does not call setRenderToolCalls on initial mount", () => {
|
|
310
|
+
const calls: unknown[][] = [];
|
|
311
|
+
let spyAttached = false;
|
|
312
|
+
|
|
313
|
+
function SpyAttacher() {
|
|
314
|
+
const { copilotkit } = useCopilotKit();
|
|
315
|
+
if (!spyAttached) {
|
|
316
|
+
const original = copilotkit.setRenderToolCalls.bind(copilotkit);
|
|
317
|
+
copilotkit.setRenderToolCalls = (renderToolCalls) => {
|
|
318
|
+
calls.push([renderToolCalls]);
|
|
319
|
+
return original(renderToolCalls);
|
|
320
|
+
};
|
|
321
|
+
spyAttached = true;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const rtc: ReactToolCallRenderer<any>[] = [
|
|
327
|
+
{
|
|
328
|
+
name: "render1",
|
|
329
|
+
args: z.object({ x: z.string() }),
|
|
330
|
+
render: () => <div>R1</div>,
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
render(
|
|
335
|
+
<CopilotKitProvider renderToolCalls={rtc}>
|
|
336
|
+
<SpyAttacher />
|
|
337
|
+
</CopilotKitProvider>,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Provider setter effects are skipped on mount;
|
|
341
|
+
// only the constructor sets the initial render tool calls.
|
|
342
|
+
expect(calls).toHaveLength(0);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("dynamic tool preservation on mount", () => {
|
|
347
|
+
it("preserves dynamically registered tools from child hooks after provider mounts", () => {
|
|
348
|
+
let capturedInstance: CopilotKitCoreReact | null = null;
|
|
349
|
+
|
|
350
|
+
function DynamicToolChild() {
|
|
351
|
+
const { copilotkit } = useCopilotKit();
|
|
352
|
+
capturedInstance = copilotkit;
|
|
353
|
+
|
|
354
|
+
// Register a tool dynamically via the hook
|
|
355
|
+
useFrontendTool({
|
|
356
|
+
name: "dynamicTool",
|
|
357
|
+
description: "A dynamically registered tool",
|
|
358
|
+
handler: async () => "result",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Provider has its own tool via props
|
|
365
|
+
const providerTools: ReactFrontendTool[] = [
|
|
366
|
+
{
|
|
367
|
+
name: "providerTool",
|
|
368
|
+
description: "From provider props",
|
|
369
|
+
handler: vi.fn(),
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
render(
|
|
374
|
+
<CopilotKitProvider frontendTools={providerTools}>
|
|
375
|
+
<DynamicToolChild />
|
|
376
|
+
</CopilotKitProvider>,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// Both the provider tool (from constructor) and the dynamic tool
|
|
380
|
+
// (from useFrontendTool hook) should exist on the instance.
|
|
381
|
+
// If the provider's setter effects ran on mount and called setTools(),
|
|
382
|
+
// the dynamic tool would be wiped out.
|
|
383
|
+
expect(capturedInstance).not.toBeNull();
|
|
384
|
+
const dynamicTool = capturedInstance!.getTool({
|
|
385
|
+
toolName: "dynamicTool",
|
|
386
|
+
});
|
|
387
|
+
const providerTool = capturedInstance!.getTool({
|
|
388
|
+
toolName: "providerTool",
|
|
389
|
+
});
|
|
390
|
+
expect(dynamicTool).toBeDefined();
|
|
391
|
+
expect(providerTool).toBeDefined();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("preserves dynamically registered render tool calls from child hooks after provider mounts", () => {
|
|
395
|
+
let capturedInstance: CopilotKitCoreReact | null = null;
|
|
396
|
+
|
|
397
|
+
function DynamicRenderChild() {
|
|
398
|
+
const { copilotkit } = useCopilotKit();
|
|
399
|
+
capturedInstance = copilotkit;
|
|
400
|
+
|
|
401
|
+
useFrontendTool({
|
|
402
|
+
name: "renderableTool",
|
|
403
|
+
description: "Has a render function",
|
|
404
|
+
parameters: z.object({ msg: z.string() }),
|
|
405
|
+
handler: async () => "ok",
|
|
406
|
+
render: () => <div>Rendered!</div>,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const providerRtc: ReactToolCallRenderer<any>[] = [
|
|
413
|
+
{
|
|
414
|
+
name: "providerRenderer",
|
|
415
|
+
args: z.object({ x: z.string() }),
|
|
416
|
+
render: () => <div>Provider Render</div>,
|
|
417
|
+
},
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
render(
|
|
421
|
+
<CopilotKitProvider renderToolCalls={providerRtc}>
|
|
422
|
+
<DynamicRenderChild />
|
|
423
|
+
</CopilotKitProvider>,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(capturedInstance).not.toBeNull();
|
|
427
|
+
const renderToolCalls = capturedInstance!.renderToolCalls;
|
|
428
|
+
|
|
429
|
+
// Both the provider-level renderer and the hook-registered renderer
|
|
430
|
+
// should exist. If setter effects ran on mount, only the provider
|
|
431
|
+
// renderer would remain.
|
|
432
|
+
const providerRenderer = renderToolCalls.find(
|
|
433
|
+
(r) => r.name === "providerRenderer",
|
|
434
|
+
);
|
|
435
|
+
const hookRenderer = renderToolCalls.find(
|
|
436
|
+
(r) => r.name === "renderableTool",
|
|
437
|
+
);
|
|
438
|
+
expect(providerRenderer).toBeDefined();
|
|
439
|
+
expect(hookRenderer).toBeDefined();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("React.StrictMode", () => {
|
|
444
|
+
it("returns the same copilotkit instance in StrictMode", () => {
|
|
445
|
+
const instances: CopilotKitCoreReact[] = [];
|
|
446
|
+
|
|
447
|
+
function Collector() {
|
|
448
|
+
const { copilotkit } = useCopilotKit();
|
|
449
|
+
instances.push(copilotkit);
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
render(
|
|
454
|
+
<React.StrictMode>
|
|
455
|
+
<CopilotKitProvider>
|
|
456
|
+
<Collector />
|
|
457
|
+
</CopilotKitProvider>
|
|
458
|
+
</React.StrictMode>,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// StrictMode double-renders in dev, so we expect multiple captures
|
|
462
|
+
expect(instances.length).toBeGreaterThanOrEqual(2);
|
|
463
|
+
const first = instances[0];
|
|
464
|
+
for (const instance of instances) {
|
|
465
|
+
expect(instance).toBe(first);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("calls setTools at most once during StrictMode mount cycle", () => {
|
|
470
|
+
const setToolsCalls: unknown[][] = [];
|
|
471
|
+
let spyAttached = false;
|
|
472
|
+
|
|
473
|
+
function SpyAttacher() {
|
|
474
|
+
const { copilotkit } = useCopilotKit();
|
|
475
|
+
if (!spyAttached) {
|
|
476
|
+
const originalSetTools = copilotkit.setTools.bind(copilotkit);
|
|
477
|
+
copilotkit.setTools = (tools) => {
|
|
478
|
+
setToolsCalls.push([tools]);
|
|
479
|
+
return originalSetTools(tools);
|
|
480
|
+
};
|
|
481
|
+
spyAttached = true;
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const tools: ReactFrontendTool[] = [
|
|
487
|
+
{ name: "toolA", description: "Tool A", handler: vi.fn() },
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
render(
|
|
491
|
+
<React.StrictMode>
|
|
492
|
+
<CopilotKitProvider frontendTools={tools}>
|
|
493
|
+
<SpyAttacher />
|
|
494
|
+
</CopilotKitProvider>
|
|
495
|
+
</React.StrictMode>,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// StrictMode fires effects twice (mount → cleanup → remount).
|
|
499
|
+
// The didMountRef guard skips the initial mount. After cleanup,
|
|
500
|
+
// didMountRef.current stays true (refs persist), so the remount
|
|
501
|
+
// call fires setTools once. This is expected and harmless — it
|
|
502
|
+
// sets the same tools the constructor already established.
|
|
503
|
+
// The critical invariant (dynamic tools not overwritten) is
|
|
504
|
+
// verified by the separate "preserves dynamically registered
|
|
505
|
+
// tools through StrictMode remount cycle" test.
|
|
506
|
+
expect(setToolsCalls.length).toBeLessThanOrEqual(1);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("preserves dynamically registered tools through StrictMode remount cycle", () => {
|
|
510
|
+
let capturedInstance: CopilotKitCoreReact | null = null;
|
|
511
|
+
|
|
512
|
+
function DynamicToolChild() {
|
|
513
|
+
const { copilotkit } = useCopilotKit();
|
|
514
|
+
capturedInstance = copilotkit;
|
|
515
|
+
|
|
516
|
+
useFrontendTool({
|
|
517
|
+
name: "strictModeTool",
|
|
518
|
+
description: "Survives StrictMode remount",
|
|
519
|
+
handler: async () => "ok",
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
render(
|
|
526
|
+
<React.StrictMode>
|
|
527
|
+
<CopilotKitProvider>
|
|
528
|
+
<DynamicToolChild />
|
|
529
|
+
</CopilotKitProvider>
|
|
530
|
+
</React.StrictMode>,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
expect(capturedInstance).not.toBeNull();
|
|
534
|
+
const tool = capturedInstance!.getTool({ toolName: "strictModeTool" });
|
|
535
|
+
expect(tool).toBeDefined();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("preserves dynamically registered render tool calls through StrictMode remount cycle", () => {
|
|
539
|
+
let capturedInstance: CopilotKitCoreReact | null = null;
|
|
540
|
+
|
|
541
|
+
function DynamicRenderChild() {
|
|
542
|
+
const { copilotkit } = useCopilotKit();
|
|
543
|
+
capturedInstance = copilotkit;
|
|
544
|
+
|
|
545
|
+
useFrontendTool({
|
|
546
|
+
name: "strictModeRenderTool",
|
|
547
|
+
description: "Has render, survives StrictMode remount",
|
|
548
|
+
parameters: z.object({ topic: z.string() }),
|
|
549
|
+
handler: async () => "ok",
|
|
550
|
+
render: () => <div>Rendered!</div>,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
render(
|
|
557
|
+
<React.StrictMode>
|
|
558
|
+
<CopilotKitProvider>
|
|
559
|
+
<DynamicRenderChild />
|
|
560
|
+
</CopilotKitProvider>
|
|
561
|
+
</React.StrictMode>,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
expect(capturedInstance).not.toBeNull();
|
|
565
|
+
const renderToolCalls = capturedInstance!.renderToolCalls;
|
|
566
|
+
const hookRenderer = renderToolCalls.find(
|
|
567
|
+
(r) => r.name === "strictModeRenderTool",
|
|
568
|
+
);
|
|
569
|
+
expect(hookRenderer).toBeDefined();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("hook render entries coexist with prop render entries through StrictMode remount", () => {
|
|
573
|
+
let capturedInstance: CopilotKitCoreReact | null = null;
|
|
574
|
+
|
|
575
|
+
function DynamicRenderChild() {
|
|
576
|
+
const { copilotkit } = useCopilotKit();
|
|
577
|
+
capturedInstance = copilotkit;
|
|
578
|
+
|
|
579
|
+
useFrontendTool({
|
|
580
|
+
name: "hookTool",
|
|
581
|
+
description: "Registered via hook",
|
|
582
|
+
parameters: z.object({ x: z.string() }),
|
|
583
|
+
handler: async () => "ok",
|
|
584
|
+
render: () => <div>Hook render</div>,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const propRtc: ReactToolCallRenderer<any>[] = [
|
|
591
|
+
{
|
|
592
|
+
name: "propTool",
|
|
593
|
+
args: z.object({ y: z.string() }),
|
|
594
|
+
render: () => <div>Prop render</div>,
|
|
595
|
+
},
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
render(
|
|
599
|
+
<React.StrictMode>
|
|
600
|
+
<CopilotKitProvider renderToolCalls={propRtc}>
|
|
601
|
+
<DynamicRenderChild />
|
|
602
|
+
</CopilotKitProvider>
|
|
603
|
+
</React.StrictMode>,
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
expect(capturedInstance).not.toBeNull();
|
|
607
|
+
const renderToolCalls = capturedInstance!.renderToolCalls;
|
|
608
|
+
expect(renderToolCalls.find((r) => r.name === "propTool")).toBeDefined();
|
|
609
|
+
expect(renderToolCalls.find((r) => r.name === "hookTool")).toBeDefined();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("context value is stable through StrictMode remount", () => {
|
|
613
|
+
const contextValues: CopilotKitContextValue[] = [];
|
|
614
|
+
|
|
615
|
+
function Collector() {
|
|
616
|
+
const context = useCopilotKit();
|
|
617
|
+
contextValues.push(context);
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
render(
|
|
622
|
+
<React.StrictMode>
|
|
623
|
+
<CopilotKitProvider>
|
|
624
|
+
<Collector />
|
|
625
|
+
</CopilotKitProvider>
|
|
626
|
+
</React.StrictMode>,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
expect(contextValues.length).toBeGreaterThanOrEqual(2);
|
|
630
|
+
const first = contextValues[0]!;
|
|
631
|
+
for (const ctx of contextValues) {
|
|
632
|
+
expect(ctx.copilotkit).toBe(first.copilotkit);
|
|
633
|
+
expect(ctx.executingToolCallIds).toBe(first.executingToolCallIds);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe("hook render entries survive prop changes", () => {
|
|
639
|
+
it("preserves hook-registered render entries when provider renderToolCalls prop changes", () => {
|
|
640
|
+
let capturedInstance: CopilotKitCoreReact | null = null;
|
|
641
|
+
|
|
642
|
+
function DynamicRenderChild() {
|
|
643
|
+
const { copilotkit } = useCopilotKit();
|
|
644
|
+
capturedInstance = copilotkit;
|
|
645
|
+
|
|
646
|
+
useFrontendTool({
|
|
647
|
+
name: "hookTool",
|
|
648
|
+
description: "Registered via hook",
|
|
649
|
+
parameters: z.object({ x: z.string() }),
|
|
650
|
+
handler: async () => "ok",
|
|
651
|
+
render: () => <div>Hook render</div>,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const rtc1: ReactToolCallRenderer<any>[] = [
|
|
658
|
+
{
|
|
659
|
+
name: "propToolA",
|
|
660
|
+
args: z.object({ a: z.string() }),
|
|
661
|
+
render: () => <div>A</div>,
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
const rtc2: ReactToolCallRenderer<any>[] = [
|
|
665
|
+
{
|
|
666
|
+
name: "propToolB",
|
|
667
|
+
args: z.object({ b: z.string() }),
|
|
668
|
+
render: () => <div>B</div>,
|
|
669
|
+
},
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
const { rerender } = render(
|
|
673
|
+
<CopilotKitProvider renderToolCalls={rtc1}>
|
|
674
|
+
<DynamicRenderChild />
|
|
675
|
+
</CopilotKitProvider>,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
// Rerender with new prop render tool calls
|
|
679
|
+
rerender(
|
|
680
|
+
<CopilotKitProvider renderToolCalls={rtc2}>
|
|
681
|
+
<DynamicRenderChild />
|
|
682
|
+
</CopilotKitProvider>,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
expect(capturedInstance).not.toBeNull();
|
|
686
|
+
const renderToolCalls = capturedInstance!.renderToolCalls;
|
|
687
|
+
// propToolA should be gone (replaced by propToolB)
|
|
688
|
+
expect(
|
|
689
|
+
renderToolCalls.find((r) => r.name === "propToolA"),
|
|
690
|
+
).toBeUndefined();
|
|
691
|
+
// propToolB should exist (from new props)
|
|
692
|
+
expect(renderToolCalls.find((r) => r.name === "propToolB")).toBeDefined();
|
|
693
|
+
// hookTool should survive the prop change
|
|
694
|
+
expect(renderToolCalls.find((r) => r.name === "hookTool")).toBeDefined();
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe("runtimeUrl deduplication", () => {
|
|
699
|
+
it("always calls setRuntimeUrl with the same URL on re-render (AgentRegistry deduplicates)", () => {
|
|
700
|
+
const setRuntimeUrlCalls: unknown[] = [];
|
|
701
|
+
let spyAttached = false;
|
|
702
|
+
|
|
703
|
+
function SpyAttacher() {
|
|
704
|
+
const { copilotkit } = useCopilotKit();
|
|
705
|
+
if (!spyAttached) {
|
|
706
|
+
const original = copilotkit.setRuntimeUrl.bind(copilotkit);
|
|
707
|
+
copilotkit.setRuntimeUrl = (...args: [string | undefined]) => {
|
|
708
|
+
setRuntimeUrlCalls.push(args[0]);
|
|
709
|
+
return original(...args);
|
|
710
|
+
};
|
|
711
|
+
spyAttached = true;
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const { rerender } = render(
|
|
717
|
+
<CopilotKitProvider runtimeUrl="http://localhost:3000/api">
|
|
718
|
+
<SpyAttacher />
|
|
719
|
+
</CopilotKitProvider>,
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
// Re-render with the SAME runtimeUrl
|
|
723
|
+
rerender(
|
|
724
|
+
<CopilotKitProvider runtimeUrl="http://localhost:3000/api">
|
|
725
|
+
<SpyAttacher />
|
|
726
|
+
</CopilotKitProvider>,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// The config effect may re-fire if other deps (mergedHeaders, etc.)
|
|
730
|
+
// change reference on rerender. The actual deduplication of /info
|
|
731
|
+
// fetches happens inside AgentRegistry.setRuntimeUrl(), which has
|
|
732
|
+
// a guard: `if (this._runtimeUrl === normalizedRuntimeUrl) return`.
|
|
733
|
+
// Here we verify every call receives the same URL.
|
|
734
|
+
expect(setRuntimeUrlCalls.length).toBeGreaterThanOrEqual(1);
|
|
735
|
+
for (const url of setRuntimeUrlCalls) {
|
|
736
|
+
expect(url).toBe("http://localhost:3000/api");
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|