@copilotkit/react-core 1.56.2 → 1.56.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{copilotkit-Cj2ZIxVr.mjs → copilotkit-Bd0m5HFp.mjs} +266 -81
- package/dist/copilotkit-Bd0m5HFp.mjs.map +1 -0
- package/dist/{copilotkit-CCbxm6JM.d.mts → copilotkit-DFaI4j2r.d.mts} +64 -18
- package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -0
- package/dist/{copilotkit-BtP7w7cT.d.cts → copilotkit-Dg4r4Gi_.d.cts} +64 -18
- package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -0
- package/dist/{copilotkit-CSJw5BG8.cjs → copilotkit-tb4zqaMK.cjs} +265 -80
- package/dist/copilotkit-tb4zqaMK.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +31 -44
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +264 -83
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx +107 -0
- package/src/components/copilot-provider/copilotkit.tsx +6 -1
- package/src/context/__tests__/threads-context.test.tsx +116 -3
- package/src/context/threads-context.tsx +18 -1
- package/src/v2/components/chat/CopilotChat.tsx +91 -4
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +4 -1
- package/src/v2/components/chat/CopilotChatInput.tsx +22 -0
- package/src/v2/components/chat/CopilotChatView.tsx +207 -44
- package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +66 -0
- package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +186 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +438 -4
- package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +56 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +172 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +94 -0
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -1
- package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +37 -0
- package/src/v2/components/chat/index.ts +2 -0
- package/src/v2/components/chat/last-user-message-context.ts +21 -0
- package/src/v2/components/chat/normalize-auto-scroll.ts +17 -0
- package/src/v2/components/license-warning-banner.tsx +20 -1
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +6 -0
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +6 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +76 -50
- package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +219 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +68 -0
- package/src/v2/hooks/use-agent.tsx +34 -77
- package/src/v2/hooks/use-pin-to-send.ts +94 -0
- package/src/v2/hooks/use-threads.tsx +55 -12
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +29 -1
- package/src/v2/providers/CopilotKitProvider.tsx +2 -11
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +106 -0
- package/dist/copilotkit-BtP7w7cT.d.cts.map +0 -1
- package/dist/copilotkit-CCbxm6JM.d.mts.map +0 -1
- package/dist/copilotkit-CSJw5BG8.cjs.map +0 -1
- package/dist/copilotkit-Cj2ZIxVr.mjs.map +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
|
|
5
|
+
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
|
|
6
|
+
import { CopilotChatView } from "../CopilotChatView";
|
|
7
|
+
import { LastUserMessageContext } from "../last-user-message-context";
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
HTMLElement.prototype.scrollTo = vi.fn();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Wrapper to provide required context (same pattern as CopilotChatView.slots.e2e.test.tsx)
|
|
14
|
+
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
15
|
+
<CopilotKitProvider>
|
|
16
|
+
<CopilotChatConfigurationProvider threadId="test-thread">
|
|
17
|
+
<div style={{ height: 400 }}>{children}</div>
|
|
18
|
+
</CopilotChatConfigurationProvider>
|
|
19
|
+
</CopilotKitProvider>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const sampleMessages = [
|
|
23
|
+
{ id: "1", role: "user" as const, content: "Hello" },
|
|
24
|
+
{ id: "2", role: "assistant" as const, content: "Hi there!" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Wait for the ScrollView's `hasMounted` useEffect to flip — the pre-mount
|
|
28
|
+
// fallback render does not include the message list, so a findBy on the
|
|
29
|
+
// message list is a reliable "mount is done" signal. Without this gate,
|
|
30
|
+
// absence assertions pass vacuously against the pre-mount render.
|
|
31
|
+
async function waitForMount(screen: {
|
|
32
|
+
findByTestId: (id: string) => Promise<HTMLElement>;
|
|
33
|
+
}) {
|
|
34
|
+
await screen.findByTestId("copilot-message-list");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("CopilotChatView pin-to-send mode", () => {
|
|
38
|
+
it("renders the pin-to-send spacer element when autoScroll='pin-to-send'", async () => {
|
|
39
|
+
const screen = render(
|
|
40
|
+
<TestWrapper>
|
|
41
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
42
|
+
<CopilotChatView autoScroll="pin-to-send" messages={sampleMessages} />
|
|
43
|
+
</LastUserMessageContext.Provider>
|
|
44
|
+
</TestWrapper>,
|
|
45
|
+
);
|
|
46
|
+
await waitForMount(screen);
|
|
47
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
48
|
+
expect(spacer).not.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not render the spacer when autoScroll='pin-to-bottom'", async () => {
|
|
52
|
+
const screen = render(
|
|
53
|
+
<TestWrapper>
|
|
54
|
+
<CopilotChatView autoScroll="pin-to-bottom" messages={sampleMessages} />
|
|
55
|
+
</TestWrapper>,
|
|
56
|
+
);
|
|
57
|
+
await waitForMount(screen);
|
|
58
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
59
|
+
expect(spacer).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not render the spacer when autoScroll='none'", async () => {
|
|
63
|
+
const screen = render(
|
|
64
|
+
<TestWrapper>
|
|
65
|
+
<CopilotChatView autoScroll="none" messages={sampleMessages} />
|
|
66
|
+
</TestWrapper>,
|
|
67
|
+
);
|
|
68
|
+
await waitForMount(screen);
|
|
69
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
70
|
+
expect(spacer).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("boolean true still maps to pin-to-bottom (back-compat)", async () => {
|
|
74
|
+
const screen = render(
|
|
75
|
+
<TestWrapper>
|
|
76
|
+
<CopilotChatView autoScroll={true} messages={sampleMessages} />
|
|
77
|
+
</TestWrapper>,
|
|
78
|
+
);
|
|
79
|
+
await waitForMount(screen);
|
|
80
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
81
|
+
expect(spacer).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("boolean false still maps to none (back-compat)", async () => {
|
|
85
|
+
const screen = render(
|
|
86
|
+
<TestWrapper>
|
|
87
|
+
<CopilotChatView autoScroll={false} messages={sampleMessages} />
|
|
88
|
+
</TestWrapper>,
|
|
89
|
+
);
|
|
90
|
+
await waitForMount(screen);
|
|
91
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
92
|
+
expect(spacer).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -4,7 +4,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
4
4
|
import { CopilotChat } from "../CopilotChat";
|
|
5
5
|
import { useAgent } from "../../../hooks/use-agent";
|
|
6
6
|
import { useCopilotKit } from "../../../providers/CopilotKitProvider";
|
|
7
|
-
import { useCopilotChatConfiguration } from "../../../providers/CopilotChatConfigurationProvider";
|
|
8
7
|
import { MockStepwiseAgent } from "../../../__tests__/utils/test-helpers";
|
|
9
8
|
import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
|
|
10
9
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
normalizeAutoScroll,
|
|
4
|
+
type AutoScrollMode,
|
|
5
|
+
} from "../normalize-auto-scroll";
|
|
6
|
+
|
|
7
|
+
describe("normalizeAutoScroll", () => {
|
|
8
|
+
it("returns 'pin-to-bottom' for undefined (default)", () => {
|
|
9
|
+
expect(normalizeAutoScroll(undefined)).toBe("pin-to-bottom");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("maps true -> 'pin-to-bottom'", () => {
|
|
13
|
+
expect(normalizeAutoScroll(true)).toBe("pin-to-bottom");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("maps false -> 'none'", () => {
|
|
17
|
+
expect(normalizeAutoScroll(false)).toBe("none");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("passes 'pin-to-bottom' through", () => {
|
|
21
|
+
expect(normalizeAutoScroll("pin-to-bottom")).toBe("pin-to-bottom");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("passes 'pin-to-send' through", () => {
|
|
25
|
+
expect(normalizeAutoScroll("pin-to-send")).toBe("pin-to-send");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("passes 'none' through", () => {
|
|
29
|
+
expect(normalizeAutoScroll("none")).toBe("none");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("falls back to 'pin-to-bottom' for unknown strings", () => {
|
|
33
|
+
expect(normalizeAutoScroll("bogus" as AutoScrollMode)).toBe(
|
|
34
|
+
"pin-to-bottom",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context used by `CopilotChatView` to announce the latest user message
|
|
5
|
+
* to descendants (notably `usePinToSend`), so scroll logic can anchor
|
|
6
|
+
* the viewport to the most recent user turn in "pin-to-send" mode.
|
|
7
|
+
*
|
|
8
|
+
* `sendNonce` increments on each new send so repeated IDs (e.g., message
|
|
9
|
+
* edits that preserve the ID) still trigger dependent effects.
|
|
10
|
+
*/
|
|
11
|
+
export type LastUserMessageState = {
|
|
12
|
+
id: string | null;
|
|
13
|
+
sendNonce: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const LastUserMessageContext = React.createContext<LastUserMessageState>(
|
|
17
|
+
{
|
|
18
|
+
id: null,
|
|
19
|
+
sendNonce: 0,
|
|
20
|
+
},
|
|
21
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AutoScrollMode = "pin-to-bottom" | "pin-to-send" | "none";
|
|
2
|
+
|
|
3
|
+
const VALID: readonly AutoScrollMode[] = [
|
|
4
|
+
"pin-to-bottom",
|
|
5
|
+
"pin-to-send",
|
|
6
|
+
"none",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export function normalizeAutoScroll(
|
|
10
|
+
value: AutoScrollMode | boolean | undefined,
|
|
11
|
+
): AutoScrollMode {
|
|
12
|
+
if (value === undefined) return "pin-to-bottom";
|
|
13
|
+
if (value === true) return "pin-to-bottom";
|
|
14
|
+
if (value === false) return "none";
|
|
15
|
+
if ((VALID as readonly string[]).includes(value)) return value;
|
|
16
|
+
return "pin-to-bottom";
|
|
17
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
// Total reserved vertical space for the fixed license banner: banner height
|
|
4
|
+
// (~36px) + bottom offset (8px) + visual gap above the chat input (~8px).
|
|
5
|
+
const LICENSE_BANNER_OFFSET_PX = 52;
|
|
6
|
+
const LICENSE_BANNER_OFFSET_VAR = "--copilotkit-license-banner-offset";
|
|
2
7
|
|
|
3
8
|
interface LicenseWarningBannerProps {
|
|
4
9
|
type:
|
|
@@ -72,6 +77,20 @@ function BannerShell({
|
|
|
72
77
|
actionUrl: string;
|
|
73
78
|
onDismiss?: () => void;
|
|
74
79
|
}) {
|
|
80
|
+
// Publish the banner's reserved bottom offset so the chat input can lift
|
|
81
|
+
// itself above it via padding-bottom: var(--copilotkit-license-banner-offset).
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (typeof document === "undefined") return;
|
|
84
|
+
const root = document.documentElement;
|
|
85
|
+
root.style.setProperty(
|
|
86
|
+
LICENSE_BANNER_OFFSET_VAR,
|
|
87
|
+
`${LICENSE_BANNER_OFFSET_PX}px`,
|
|
88
|
+
);
|
|
89
|
+
return () => {
|
|
90
|
+
root.style.removeProperty(LICENSE_BANNER_OFFSET_VAR);
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
75
94
|
return (
|
|
76
95
|
<div style={{ ...BANNER_STYLES.base, ...getSeverityStyle(severity) }}>
|
|
77
96
|
<span>{message}</span>
|
|
@@ -29,6 +29,10 @@ describe("useAgent stability during runtime connection", () => {
|
|
|
29
29
|
runtimeTransport: string;
|
|
30
30
|
headers: Record<string, string>;
|
|
31
31
|
agents: Record<string, AbstractAgent>;
|
|
32
|
+
subscribeToAgentWithOptions: (
|
|
33
|
+
agent: AbstractAgent,
|
|
34
|
+
subscriber: any,
|
|
35
|
+
) => { unsubscribe: () => void };
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
beforeEach(() => {
|
|
@@ -40,6 +44,8 @@ describe("useAgent stability during runtime connection", () => {
|
|
|
40
44
|
runtimeTransport: "rest",
|
|
41
45
|
headers: {},
|
|
42
46
|
agents: {},
|
|
47
|
+
subscribeToAgentWithOptions: (agent, subscriber) =>
|
|
48
|
+
agent.subscribe(subscriber),
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
mockUseCopilotKit.mockReturnValue({
|
|
@@ -47,6 +47,10 @@ describe("useAgent thread isolation", () => {
|
|
|
47
47
|
runtimeTransport: string;
|
|
48
48
|
headers: Record<string, string>;
|
|
49
49
|
agents: Record<string, AbstractAgent>;
|
|
50
|
+
subscribeToAgentWithOptions: (
|
|
51
|
+
agent: AbstractAgent,
|
|
52
|
+
subscriber: any,
|
|
53
|
+
) => { unsubscribe: () => void };
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
let registeredAgent: CloneableAgent;
|
|
@@ -64,6 +68,8 @@ describe("useAgent thread isolation", () => {
|
|
|
64
68
|
runtimeTransport: "rest",
|
|
65
69
|
headers: {},
|
|
66
70
|
agents: { "my-agent": registeredAgent },
|
|
71
|
+
subscribeToAgentWithOptions: (agent, subscriber) =>
|
|
72
|
+
agent.subscribe(subscriber),
|
|
67
73
|
};
|
|
68
74
|
|
|
69
75
|
mockUseCopilotKit.mockReturnValue({
|
|
@@ -4,7 +4,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
4
4
|
import { useAgent, UseAgentUpdate } from "../use-agent";
|
|
5
5
|
import { useCopilotKit } from "../../providers/CopilotKitProvider";
|
|
6
6
|
import { MockStepwiseAgent } from "../../__tests__/utils/test-helpers";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CopilotKitCore,
|
|
9
|
+
CopilotKitCoreRuntimeConnectionStatus,
|
|
10
|
+
} from "@copilotkit/core";
|
|
8
11
|
import type { Message } from "@ag-ui/core";
|
|
9
12
|
import type { RunAgentInput } from "@ag-ui/client";
|
|
10
13
|
|
|
@@ -126,11 +129,19 @@ function createTestComponent(
|
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
/** Factory for the mock return value of useCopilotKit
|
|
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. */
|
|
130
135
|
function createMockContext(
|
|
131
136
|
agent: MockStepwiseAgent,
|
|
132
137
|
overrides: { defaultThrottleMs?: number } = {},
|
|
133
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
|
+
}
|
|
134
145
|
return {
|
|
135
146
|
copilotkit: {
|
|
136
147
|
getAgent: () => agent,
|
|
@@ -139,7 +150,8 @@ function createMockContext(
|
|
|
139
150
|
runtimeTransport: "rest",
|
|
140
151
|
headers: {},
|
|
141
152
|
agents: { [String(agent.agentId)]: agent },
|
|
142
|
-
defaultThrottleMs:
|
|
153
|
+
defaultThrottleMs: core.defaultThrottleMs,
|
|
154
|
+
subscribeToAgentWithOptions: core.subscribeToAgentWithOptions.bind(core),
|
|
143
155
|
},
|
|
144
156
|
executingToolCallIds: new Set(),
|
|
145
157
|
};
|
|
@@ -313,7 +325,7 @@ describe("useAgent throttleMs", () => {
|
|
|
313
325
|
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
314
326
|
});
|
|
315
327
|
|
|
316
|
-
it("with throttleMs, onStateChanged
|
|
328
|
+
it("with throttleMs, onStateChanged is also throttled (shared window)", async () => {
|
|
317
329
|
const TestComponent = createTestComponent({
|
|
318
330
|
updates: [
|
|
319
331
|
UseAgentUpdate.OnMessagesChanged,
|
|
@@ -324,19 +336,29 @@ describe("useAgent throttleMs", () => {
|
|
|
324
336
|
|
|
325
337
|
render(<TestComponent />);
|
|
326
338
|
|
|
327
|
-
// Fire onMessagesChanged to start the throttle window
|
|
339
|
+
// Fire onMessagesChanged to start the throttle window (leading edge)
|
|
328
340
|
act(() => {
|
|
329
341
|
mockAgent.messages = [userMsg("1", "a")];
|
|
330
342
|
notifyMessagesChanged(mockAgent);
|
|
331
343
|
});
|
|
344
|
+
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
332
345
|
|
|
333
|
-
// Fire onStateChanged 10ms later —
|
|
334
|
-
|
|
346
|
+
// Fire onStateChanged 10ms later — should be deferred (within throttle window)
|
|
347
|
+
act(() => {
|
|
335
348
|
vi.advanceTimersByTime(10);
|
|
336
349
|
mockAgent.state = { count: 42 };
|
|
337
350
|
notifyStateChanged(mockAgent);
|
|
338
351
|
});
|
|
339
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
|
+
|
|
340
362
|
expect(screen.getByTestId("state").textContent).toBe('{"count":42}');
|
|
341
363
|
});
|
|
342
364
|
|
|
@@ -372,7 +394,7 @@ describe("useAgent throttleMs", () => {
|
|
|
372
394
|
expect(renderCount.current).toBe(countBeforeUnmount);
|
|
373
395
|
});
|
|
374
396
|
|
|
375
|
-
it("with throttleMs and
|
|
397
|
+
it("with throttleMs and only OnStateChanged subscribed, first state fires on leading edge", async () => {
|
|
376
398
|
const TestComponent = createTestComponent({
|
|
377
399
|
updates: [UseAgentUpdate.OnStateChanged],
|
|
378
400
|
throttleMs: 100,
|
|
@@ -380,7 +402,8 @@ describe("useAgent throttleMs", () => {
|
|
|
380
402
|
|
|
381
403
|
render(<TestComponent />);
|
|
382
404
|
|
|
383
|
-
//
|
|
405
|
+
// First onStateChanged fires immediately (leading edge) — await so
|
|
406
|
+
// microtask from batchedForceUpdate flushes.
|
|
384
407
|
await act(async () => {
|
|
385
408
|
mockAgent.state = { value: "test" };
|
|
386
409
|
notifyStateChanged(mockAgent);
|
|
@@ -388,15 +411,13 @@ describe("useAgent throttleMs", () => {
|
|
|
388
411
|
|
|
389
412
|
expect(screen.getByTestId("state").textContent).toBe('{"value":"test"}');
|
|
390
413
|
|
|
391
|
-
// No onMessagesChanged subscription should exist
|
|
414
|
+
// No onMessagesChanged subscription should exist — messages notification
|
|
415
|
+
// does nothing because the handler was never registered.
|
|
392
416
|
act(() => {
|
|
393
417
|
mockAgent.messages = [userMsg("1", "a")];
|
|
394
418
|
notifyMessagesChanged(mockAgent);
|
|
395
419
|
});
|
|
396
420
|
|
|
397
|
-
// onMessagesChanged was sent but no handler is subscribed, so no
|
|
398
|
-
// re-render is triggered. We verify by checking state still shows the
|
|
399
|
-
// last rendered value.
|
|
400
421
|
expect(screen.getByTestId("state").textContent).toBe('{"value":"test"}');
|
|
401
422
|
});
|
|
402
423
|
|
|
@@ -418,6 +439,7 @@ describe("useAgent throttleMs", () => {
|
|
|
418
439
|
expect.stringContaining(
|
|
419
440
|
"throttleMs must be a non-negative finite number",
|
|
420
441
|
),
|
|
442
|
+
expect.any(Error),
|
|
421
443
|
);
|
|
422
444
|
|
|
423
445
|
// Should behave as unthrottled — every notification fires immediately
|
|
@@ -864,23 +886,23 @@ describe("useAgent defaultThrottleMs from provider", () => {
|
|
|
864
886
|
])(
|
|
865
887
|
"with invalid provider defaultThrottleMs ($label), falls back to unthrottled and warns",
|
|
866
888
|
({ value }) => {
|
|
889
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
890
|
+
|
|
867
891
|
mockUseCopilotKit.mockReturnValue(
|
|
868
892
|
createMockContext(mockAgent, { defaultThrottleMs: value }),
|
|
869
893
|
);
|
|
870
894
|
|
|
871
|
-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
872
895
|
const TestComponent = createTestComponent({ throttleMs: undefined });
|
|
873
896
|
|
|
874
897
|
render(<TestComponent />);
|
|
875
898
|
|
|
876
|
-
|
|
877
|
-
expect.stringContaining("provider-level defaultThrottleMs"),
|
|
878
|
-
);
|
|
899
|
+
// The core setter rejects invalid values and logs an error
|
|
879
900
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
880
901
|
expect.stringContaining("must be a non-negative finite number"),
|
|
902
|
+
expect.any(Error),
|
|
881
903
|
);
|
|
882
904
|
|
|
883
|
-
// Should behave as unthrottled
|
|
905
|
+
// Should behave as unthrottled (setter rejected the value)
|
|
884
906
|
act(() => {
|
|
885
907
|
mockAgent.messages = [userMsg("1", "a")];
|
|
886
908
|
notifyMessagesChanged(mockAgent);
|
|
@@ -963,41 +985,45 @@ describe("useAgent defaultThrottleMs from provider", () => {
|
|
|
963
985
|
});
|
|
964
986
|
});
|
|
965
987
|
|
|
966
|
-
describe("CopilotKitCore.setDefaultThrottleMs
|
|
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
|
+
|
|
967
1009
|
it.each([
|
|
968
1010
|
{ label: "NaN", value: NaN },
|
|
969
1011
|
{ label: "Infinity", value: Infinity },
|
|
970
1012
|
{ label: "-1", value: -1 },
|
|
971
1013
|
{ label: "-Infinity", value: -Infinity },
|
|
972
|
-
])(
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
render(<TestComponent />);
|
|
988
|
-
|
|
989
|
-
// Should behave as unthrottled (no provider default in effect)
|
|
990
|
-
act(() => {
|
|
991
|
-
mockAgent.messages = [userMsg("1", "a")];
|
|
992
|
-
notifyMessagesChanged(mockAgent);
|
|
993
|
-
});
|
|
994
|
-
expect(screen.getByTestId("count").textContent).toBe("1");
|
|
995
|
-
|
|
996
|
-
act(() => {
|
|
997
|
-
mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
|
|
998
|
-
notifyMessagesChanged(mockAgent);
|
|
999
|
-
});
|
|
1000
|
-
expect(screen.getByTestId("count").textContent).toBe("2");
|
|
1001
|
-
vi.useRealTimers();
|
|
1002
|
-
});
|
|
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
|
+
);
|
|
1003
1029
|
});
|