import { defineComponent, reactive, ref } from "vue"; import { fireEvent, screen, waitFor, cleanup } from "@testing-library/vue"; import { describe, it, expect, afterEach } from "vitest"; import { DEFAULT_AGENT_ID } from "@copilotkit/shared"; import type { AbstractAgent } from "@ag-ui/client"; import { useConfigureSuggestions } from "../use-configure-suggestions"; import { useSuggestions } from "../use-suggestions"; import { useCopilotKit } from "../../providers/useCopilotKit"; import { renderWithCopilotKit, MockStepwiseAgent, runStartedEvent, runFinishedEvent, } from "../../__tests__/utils/test-helpers"; import { SuggestionsProviderAgent, StateCapturingAgent, } from "../../__tests__/utils/agents"; afterEach(() => { cleanup(); }); class ImmediateSuggestionsProviderAgent extends SuggestionsProviderAgent { constructor(responses: any[]) { super(responses, DEFAULT_AGENT_ID); } } 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 }, ]); const TestHarness = defineComponent({ setup() { useConfigureSuggestions({ instructions: "Return deterministic suggestions", providerAgentId: DEFAULT_AGENT_ID, available: "always", }); const { suggestions, isLoading, reloadSuggestions } = useSuggestions(); return { suggestions, isLoading, reloadSuggestions }; }, template: `
{{ suggestions.length }}
{{ JSON.stringify(suggestions) }}
{{ isLoading ? "loading" : "idle" }}
`, }); renderWithCopilotKit({ agent, children: TestHarness }); await waitFor(() => { expect(screen.getByTestId("suggestions-count").textContent).toBe("2"); expect(screen.getByTestId("suggestions-loading").textContent).toBe( "idle", ); }); }); }); describe("global suggestions coverage", () => { it("applies updates across all agents when consumerAgentId is undefined", async () => { const alpha = new StateCapturingAgent([{ newMessages: [] }], "alpha"); const beta = new StateCapturingAgent([{ newMessages: [] }], "beta"); const config = reactive({ suggestions: [{ title: "Global v1", message: "Global v1" }], consumerAgentId: undefined, }); const Harness = defineComponent({ setup() { useConfigureSuggestions(config); const { suggestions: alphaSuggestions } = useSuggestions({ agentId: "alpha", }); const { suggestions: betaSuggestions } = useSuggestions({ agentId: "beta", }); const updateGlobal = () => { config.suggestions = [{ title: "Global v2", message: "Global v2" }]; }; return { alphaSuggestions, betaSuggestions, updateGlobal }; }, template: `
{{ JSON.stringify(alphaSuggestions) }}
{{ JSON.stringify(betaSuggestions) }}
`, }); renderWithCopilotKit({ agents: { alpha: alpha as unknown as AbstractAgent, beta: beta as unknown as AbstractAgent, }, agentId: "alpha", children: Harness, }); expect(screen.getByTestId("alpha-json").textContent).toContain("Global v1"); expect(screen.getByTestId("beta-json").textContent).toContain("Global v1"); await 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 alpha = new StateCapturingAgent([{ newMessages: [] }], "alpha"); const beta = new StateCapturingAgent([{ newMessages: [] }], "beta"); const config = reactive({ suggestions: [{ title: "Global v1", message: "Global v1" }], consumerAgentId: "*", }); const Harness = defineComponent({ setup() { useConfigureSuggestions(config); const { suggestions: alphaSuggestions } = useSuggestions({ agentId: "alpha", }); const { suggestions: betaSuggestions } = useSuggestions({ agentId: "beta", }); const updateGlobal = () => { config.suggestions = [{ title: "Global v2", message: "Global v2" }]; }; return { alphaSuggestions, betaSuggestions, updateGlobal }; }, template: `
{{ JSON.stringify(alphaSuggestions) }}
{{ JSON.stringify(betaSuggestions) }}
`, }); renderWithCopilotKit({ agents: { alpha: alpha as unknown as AbstractAgent, beta: beta as unknown as AbstractAgent, }, agentId: "alpha", children: Harness, }); await 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", message: "Alpha", isLoading: false }, ]); const topic = ref("Alpha"); const Harness = defineComponent({ setup() { useConfigureSuggestions( { instructions: `Offer choices about ${topic.value}`, providerAgentId: DEFAULT_AGENT_ID, consumerAgentId: DEFAULT_AGENT_ID, available: "always", }, [topic], ); const { suggestions, reloadSuggestions } = useSuggestions(); const nextTopic = () => { topic.value = "Beta"; provider.setResponses([ { title: "Beta", message: "Beta", isLoading: false }, ]); }; return { suggestions, reloadSuggestions, nextTopic }; }, template: `
{{ JSON.stringify(suggestions) }}
`, }); renderWithCopilotKit({ agent: provider, children: Harness }); await fireEvent.click(screen.getByTestId("dynamic-reload")); await waitFor(() => { expect(screen.getByTestId("dynamic-json").textContent).toContain("Alpha"); }); await fireEvent.click(screen.getByTestId("dynamic-topic")); await fireEvent.click(screen.getByTestId("dynamic-reload")); await waitFor(() => { expect(screen.getByTestId("dynamic-json").textContent).toContain("Beta"); }); }); }); describe("static suggestions defaults", () => { it("shows static suggestions only before the first message", async () => { const agent = new MockStepwiseAgent(); const Harness = defineComponent({ setup() { useConfigureSuggestions({ suggestions: [{ title: "Static A", message: "First static" }], }); const { suggestions, reloadSuggestions } = useSuggestions(); const { copilotkit } = useCopilotKit(); const addMessage = () => { const current = copilotkit.value.getAgent(DEFAULT_AGENT_ID); current?.addMessage({ id: "u1", role: "user", content: "User message", } as any); reloadSuggestions(); }; return { suggestions, reloadSuggestions, addMessage }; }, template: `
{{ JSON.stringify(suggestions) }}
`, }); renderWithCopilotKit({ agent, children: Harness }); await fireEvent.click(screen.getByTestId("reload-suggestions")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Static A", ); }); await fireEvent.click(screen.getByTestId("add-message")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "[]", ); }); }); }); describe("suggestions lifecycle during runs", () => { it("clears suggestions immediately when the agent run starts", async () => { const agent = new MockStepwiseAgent(); const Harness = defineComponent({ setup() { useConfigureSuggestions({ suggestions: [{ title: "First static", message: "First static" }], }); const { suggestions, reloadSuggestions } = useSuggestions(); const { copilotkit } = useCopilotKit(); const runAgent = () => { const current = copilotkit.value.getAgent(DEFAULT_AGENT_ID); if (current) { current.addMessage({ id: "u-run", role: "user", content: "Initiating run", } as any); void copilotkit.value.runAgent({ agent: current }); } }; return { suggestions, reloadSuggestions, runAgent }; }, template: `
{{ JSON.stringify(suggestions) }}
`, }); renderWithCopilotKit({ agent, children: Harness }); await fireEvent.click(screen.getByTestId("reload-suggestions")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "First static", ); }); await fireEvent.click(screen.getByTestId("run-agent")); agent.emit(runStartedEvent()); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "[]", ); }); agent.emit(runFinishedEvent()); agent.complete(); }); }); describe("useConfigureSuggestions dependencies", () => { it("reloads suggestions when the provided config changes", async () => { const provider = new ImmediateSuggestionsProviderAgent([ { title: "Version 0", message: "Version 0", isLoading: false }, ]); const version = ref(0); const Harness = defineComponent({ setup() { const config = reactive({ instructions: "Versioned suggestions", providerAgentId: DEFAULT_AGENT_ID, consumerAgentId: DEFAULT_AGENT_ID, available: "always" as const, version, }); useConfigureSuggestions(config, [version]); const { suggestions } = useSuggestions(); const bump = () => { version.value += 1; provider.setResponses([ { title: `Version ${version.value}`, message: `Version ${version.value}`, isLoading: false, }, ]); }; return { suggestions, bump }; }, template: `
{{ JSON.stringify(suggestions) }}
`, }); renderWithCopilotKit({ agent: provider, children: Harness }); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Version 0", ); }); await fireEvent.click(screen.getByTestId("bump")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Version 1", ); }); }); it("reloads suggestions when optional dependencies change", async () => { const provider = new ImmediateSuggestionsProviderAgent([ { title: "Initial", message: "Initial", isLoading: false }, ]); const version = ref(0); const Harness = defineComponent({ setup() { const configRef = ref({ suggestions: [{ title: "Version 0", message: "Version 0" }], }); useConfigureSuggestions(configRef.value, [version]); const { suggestions } = useSuggestions(); const bump = () => { version.value += 1; configRef.value.suggestions = [ { title: `Version ${version.value}`, message: `Version ${version.value}`, }, ]; provider.setResponses([ { title: `Version ${version.value}`, message: `Version ${version.value}`, isLoading: false, }, ]); }; return { suggestions, bump }; }, template: `
{{ JSON.stringify(suggestions) }}
`, }); renderWithCopilotKit({ agent: provider, children: Harness }); await 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(); const label = ref("Initial"); const Harness = defineComponent({ setup() { const config = reactive({ suggestions: [{ title: label.value, message: label.value }], }); useConfigureSuggestions(config, [label]); const { copilotkit } = useCopilotKit(); const { suggestions } = useSuggestions(); const startRun = () => { const current = copilotkit.value.getAgent(DEFAULT_AGENT_ID); if (current) { void copilotkit.value.runAgent({ agent: current }); } }; const emitStart = async () => { const current = copilotkit.value.getAgent(DEFAULT_AGENT_ID); if (current instanceof MockStepwiseAgent) { await current.emit(runStartedEvent()); } }; const emitFinish = async () => { const current = copilotkit.value.getAgent(DEFAULT_AGENT_ID); if (current instanceof MockStepwiseAgent) { await current.emit(runFinishedEvent()); await current.complete(); } }; const updateLabel = () => { label.value = "Deferred"; config.suggestions = [{ title: label.value, message: label.value }]; }; return { suggestions, startRun, emitStart, emitFinish, updateLabel }; }, template: `
{{ JSON.stringify(suggestions) }}
`, }); renderWithCopilotKit({ agent, children: Harness }); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Initial", ); }); await fireEvent.click(screen.getByTestId("start-run")); await fireEvent.click(screen.getByTestId("emit-start")); await fireEvent.click(screen.getByTestId("update-label")); expect(screen.getByTestId("suggestions-json").textContent).toContain("[]"); await fireEvent.click(screen.getByTestId("emit-finish")); await waitFor(() => { expect(screen.getByTestId("suggestions-json").textContent).toContain( "Deferred", ); }); }); });