import React, { useCallback, useMemo, useRef, useState } from "react"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import { renderWithCopilotKit, MockStepwiseAgent, runStartedEvent, runFinishedEvent, } from "../../__tests__/utils/test-helpers"; import { useConfigureSuggestions } from "../use-configure-suggestions"; import { useSuggestions } from "../use-suggestions"; import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared"; import { Suggestion } from "@copilotkit/core"; import { AbstractAgent, AgentSubscriber, Message, RunAgentParameters, RunAgentResult, } from "@ag-ui/client"; import { useCopilotKit } from "../../providers/CopilotKitProvider"; class ImmediateSuggestionsProviderAgent extends AbstractAgent { constructor(private responses: Suggestion[]) { super({ agentId: DEFAULT_AGENT_ID }); } run(): never { throw new Error("SuggestionsProviderAgent should not use stream run"); } setResponses(next: Suggestion[]) { this.responses = next; } override clone(): ImmediateSuggestionsProviderAgent { const cloned = new ImmediateSuggestionsProviderAgent(this.responses); cloned.threadId = this.threadId; cloned.description = this.description; cloned.messages = JSON.parse(JSON.stringify(this.messages)); cloned.state = JSON.parse(JSON.stringify(this.state)); return cloned; } override async runAgent( parameters: RunAgentParameters = {}, subscriber?: AgentSubscriber, ): Promise { const input = this.prepareRunAgentInput(parameters); this.isRunning = true; if (subscriber?.onRunInitialized) { await subscriber.onRunInitialized({ agent: this, messages: this.messages, state: this.state, input, }); } const suggestionMessage: Message = { id: randomUUID(), role: "assistant", content: "", toolCalls: [ { id: randomUUID(), type: "function", function: { name: "copilotkitSuggest", arguments: JSON.stringify({ suggestions: this.responses }), }, }, ], } as Message; this.addMessage(suggestionMessage); if (subscriber?.onMessagesChanged) { await subscriber.onMessagesChanged({ agent: this, messages: this.messages, state: this.state, input, }); } if (subscriber?.onRunFinalized) { await subscriber.onRunFinalized({ agent: this, messages: this.messages, state: this.state, input, }); } this.isRunning = false; return { newMessages: [suggestionMessage], result: undefined, }; } } const TestHarness: React.FC = () => { useConfigureSuggestions({ instructions: "Return deterministic suggestions", providerAgentId: DEFAULT_AGENT_ID, available: "always", }); const { suggestions, isLoading, reloadSuggestions } = useSuggestions(); const handleReload = useCallback(() => { reloadSuggestions(); }, [reloadSuggestions]); return (
{suggestions.length}
{JSON.stringify(suggestions)}
{isLoading ? "loading" : "idle"}
); }; const StaticSuggestionsHarness: React.FC = () => { useConfigureSuggestions({ suggestions: [{ title: "Static A", message: "First static" }], }); const { suggestions, reloadSuggestions } = useSuggestions(); const { copilotkit } = useCopilotKit(); const handleReload = useCallback(() => { reloadSuggestions(); }, [reloadSuggestions]); const handleAddMessage = useCallback(() => { const agent = copilotkit.getAgent(DEFAULT_AGENT_ID); agent?.addMessage({ id: randomUUID(), role: "user", content: "User message", }); reloadSuggestions(); }, [copilotkit, reloadSuggestions]); return (
{suggestions.length}
{JSON.stringify(suggestions)}
); }; const RunClearsSuggestionsHarness: React.FC = () => { const [label, setLabel] = useState("First static"); useConfigureSuggestions({ suggestions: [{ title: label, message: label }], }); const { suggestions, reloadSuggestions } = useSuggestions(); const { copilotkit } = useCopilotKit(); const handleReload = useCallback(() => { reloadSuggestions(); }, [reloadSuggestions]); const handleRun = useCallback(() => { const agent = copilotkit.getAgent(DEFAULT_AGENT_ID); if (!agent) { return; } agent.addMessage({ id: randomUUID(), role: "user", content: "Initiating run", }); void copilotkit.runAgent({ agent }).catch(() => {}); }, [copilotkit]); const handleComplete = useCallback(() => { const agent = copilotkit.getAgent(DEFAULT_AGENT_ID); if (agent instanceof MockStepwiseAgent) { agent.complete(); } }, [copilotkit]); const handleUpdateSuggestions = useCallback(() => { setLabel("Updated static"); }, []); return (
{suggestions.length}
{JSON.stringify(suggestions)}
); }; const DynamicSuggestionsHarness: React.FC = () => { const [label, setLabel] = useState("First dynamic"); const config = useMemo( () => ({ suggestions: [{ title: label, message: label }], }), [label], ); useConfigureSuggestions(config); const { suggestions } = useSuggestions(); const handleUpdate = useCallback(() => { setLabel("Second dynamic"); }, []); return (
{JSON.stringify(suggestions)}
); }; const DependencyDrivenHarness: React.FC = () => { const configRef = useRef({ suggestions: [{ title: "Version 0", message: "Version 0" }], }); const [version, setVersion] = useState(0); useConfigureSuggestions(configRef.current, [version]); const { suggestions } = useSuggestions(); const handleBump = useCallback(() => { setVersion((prev) => { const next = prev + 1; configRef.current.suggestions = [ { title: `Version ${next}`, message: `Version ${next}` }, ]; return next; }); }, []); return (
{JSON.stringify(suggestions)}
); }; const DeferredReloadHarness: React.FC = () => { const [label, setLabel] = useState("Initial"); const config = useMemo( () => ({ suggestions: [{ title: label, message: label }], }), [label], ); useConfigureSuggestions(config); const { copilotkit } = useCopilotKit(); const { suggestions } = useSuggestions(); const handleStartRun = useCallback(() => { const agent = copilotkit.getAgent(DEFAULT_AGENT_ID); if (agent) { void copilotkit.runAgent({ agent }); } }, [copilotkit]); const handleEmitStart = useCallback(() => { const agent = copilotkit.getAgent(DEFAULT_AGENT_ID); if (agent instanceof MockStepwiseAgent) { agent.emit(runStartedEvent()); } }, [copilotkit]); const handleEmitFinish = useCallback(() => { const agent = copilotkit.getAgent(DEFAULT_AGENT_ID); if (agent instanceof MockStepwiseAgent) { agent.emit(runFinishedEvent()); agent.complete(); } }, [copilotkit]); const handleUpdate = useCallback(() => { setLabel("Deferred"); }, []); return (
{JSON.stringify(suggestions)}
); }; const DualAgentGlobalHarness: React.FC<{ consumer?: "*" }> = ({ consumer }) => { const [label, setLabel] = useState("Global v1"); const config = useMemo(() => { const base = { suggestions: [{ title: label, message: label }], }; return consumer ? { ...base, consumerAgentId: consumer } : base; }, [label, consumer]); useConfigureSuggestions(config as any); const { suggestions: alphaSuggestions } = useSuggestions({ agentId: "alpha", }); const { suggestions: betaSuggestions } = useSuggestions({ agentId: "beta" }); const handleUpdate = useCallback(() => { setLabel((prev) => (prev === "Global v1" ? "Global v2" : "Global v3")); }, []); return (
{JSON.stringify(alphaSuggestions)}
{JSON.stringify(betaSuggestions)}
); }; const DynamicStreamingHarness: React.FC = () => { const [topic, setTopic] = useState("Alpha"); useConfigureSuggestions( { instructions: `Offer choices about ${topic}`, providerAgentId: "provider", consumerAgentId: "consumer", available: "always", }, [topic], ); const { suggestions, reloadSuggestions } = useSuggestions({ agentId: "consumer", }); const handleReload = useCallback(() => { reloadSuggestions(); }, [reloadSuggestions]); const handleNextTopic = useCallback(() => { setTopic((prev) => (prev === "Alpha" ? "Beta" : "Gamma")); }, []); return (
{JSON.stringify(suggestions)}
); }; describe("useConfigureSuggestions", () => { it("registers suggestions config and surfaces generated suggestions", async () => { const agent = new ImmediateSuggestionsProviderAgent([ { title: "Option A", message: "Take path A", isLoading: false }, { title: "Option B", message: "Take path B", isLoading: false }, ]); renderWithCopilotKit({ agent, children: , }); await waitFor(() => { expect(screen.getByTestId("suggestions-count").textContent).toBe("2"); expect(screen.getByTestId("suggestions-loading").textContent).toBe( "idle", ); }); const json = screen.getByTestId("suggestions-json").textContent; expect(json).toContain("Option A"); expect(json).toContain("Option B"); }); }); describe("global suggestions coverage", () => { it("applies updates across all agents when consumerAgentId is undefined", async () => { const agents = { alpha: new MockStepwiseAgent(), beta: new MockStepwiseAgent(), }; renderWithCopilotKit({ agents, children: , }); await waitFor(() => { expect(screen.getByTestId("alpha-json").textContent).toContain( "Global v1", ); expect(screen.getByTestId("beta-json").textContent).toContain( "Global v1", ); }); fireEvent.click(screen.getByTestId("update-global")); await waitFor(() => { expect(screen.getByTestId("alpha-json").textContent).toContain( "Global v2", ); expect(screen.getByTestId("beta-json").textContent).toContain( "Global v2", ); }); }); it("applies updates across all agents when consumerAgentId is '*'", async () => { const agents = { alpha: new MockStepwiseAgent(), beta: new MockStepwiseAgent(), }; renderWithCopilotKit({ agents, children: , }); await waitFor(() => { expect(screen.getByTestId("alpha-json").textContent).toContain( "Global v1", ); expect(screen.getByTestId("beta-json").textContent).toContain( "Global v1", ); }); fireEvent.click(screen.getByTestId("update-global")); await waitFor(() => { expect(screen.getByTestId("alpha-json").textContent).toContain( "Global v2", ); expect(screen.getByTestId("beta-json").textContent).toContain( "Global v2", ); }); }); }); describe("dynamic suggestions with MockAgent", () => { it("reloads streaming suggestions when instructions change", async () => { const provider = new ImmediateSuggestionsProviderAgent([ { title: "Alpha Choice", message: "Pick Alpha", isLoading: false }, { title: "Alpha Backup", message: "Alpha again", isLoading: false }, ]); provider.agentId = "provider"; const consumer = new MockStepwiseAgent(); consumer.agentId = "consumer"; renderWithCopilotKit({ agents: { provider, consumer }, children: , }); fireEvent.click(screen.getByTestId("dynamic-reload")); await new Promise((resolve) => setTimeout(resolve, 0)); await waitFor(() => { expect(screen.getByTestId("dynamic-json").textContent).toContain( "Alpha Choice", ); expect(screen.getByTestId("dynamic-json").textContent).toContain( "Alpha Backup", ); }); provider.setResponses([ { title: "Beta Pick", message: "Choose Beta", isLoading: false }, { title: "Beta Extra", message: "More Beta", isLoading: false }, ]); fireEvent.click(screen.getByTestId("dynamic-topic")); fireEvent.click(screen.getByTestId("dynamic-reload")); await new Promise((resolve) => setTimeout(resolve, 0)); await waitFor(() => { expect(screen.getByTestId("dynamic-json").textContent).toContain( "Beta Pick", ); expect(screen.getByTestId("dynamic-json").textContent).toContain( "Beta Extra", ); }); }); }); describe("static suggestions defaults", () => { it("shows static suggestions only before the first message", async () => { const agent = new MockStepwiseAgent(); renderWithCopilotKit({ agent, children: , }); await waitFor(() => { expect(screen.getByTestId("suggestions-count").textContent).toBe("1"); expect(screen.getByTestId("suggestions-json").textContent).toContain( '"isLoading":false', ); }); fireEvent.click(screen.getByTestId("add-message")); await waitFor(() => { expect(screen.getByTestId("suggestions-count").textContent).toBe("0"); }); }); }); describe("suggestions lifecycle during runs", () => { it("clears suggestions immediately when the agent run starts", async () => { const agent = new MockStepwiseAgent(); renderWithCopilotKit({ agent, children: , }); await waitFor(() => { expect(screen.getByTestId("suggestions-count").textContent).toBe("1"); expect(screen.getByTestId("suggestions-json").textContent).toContain( '"isLoading":false', ); }); fireEvent.click(screen.getByTestId("update-suggestions")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Updated static", ); }); fireEvent.click(screen.getByTestId("run-agent")); await waitFor(() => { expect(screen.getByTestId("suggestions-count").textContent).toBe("0"); }); agent.emit(runStartedEvent()); agent.emit(runFinishedEvent()); fireEvent.click(screen.getByTestId("complete-agent")); await waitFor(() => { expect(agent.isRunning).toBe(false); }); expect(screen.getByTestId("suggestions-count").textContent).toBe("0"); }); }); describe("useConfigureSuggestions dependencies", () => { it("reloads suggestions when the provided config changes", async () => { const agent = new MockStepwiseAgent(); renderWithCopilotKit({ agent, children: , }); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "First dynamic", ); }); fireEvent.click(screen.getByTestId("update")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Second dynamic", ); }); }); it("reloads suggestions when optional dependencies change", async () => { const agent = new MockStepwiseAgent(); renderWithCopilotKit({ agent, children: , }); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Version 0", ); }); fireEvent.click(screen.getByTestId("bump")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Version 1", ); }); }); it("defers reloads while a run is in progress and applies them afterward", async () => { const agent = new MockStepwiseAgent(); renderWithCopilotKit({ agent, children: , }); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Initial", ); }); fireEvent.click(screen.getByTestId("start-run")); fireEvent.click(screen.getByTestId("emit-start")); fireEvent.click(screen.getByTestId("update-label")); expect(screen.getByTestId("suggestions-json").textContent).toContain("[]"); fireEvent.click(screen.getByTestId("emit-finish")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Deferred", ); }); }); });