import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import { describe, it, expect, beforeEach } from "vitest"; import { CopilotKitProvider } from "../../../providers/CopilotKitProvider"; import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider"; import { CopilotChat } from "../CopilotChat"; import { MockStepwiseAgent } from "../../../__tests__/utils/test-helpers"; import { DEFAULT_AGENT_ID } from "@copilotkit/shared"; /** * Mock agent that records every connectAgent() invocation and resolves * immediately with an empty run result. Tracking lives on the class so * per-thread clones (from useAgent's WeakMap) share the counter. */ class TrackingAgent extends MockStepwiseAgent { static connectCalls: Array<{ threadId: string | undefined; agentId: string | undefined; }> = []; static reset() { TrackingAgent.connectCalls = []; } async connectAgent( _params: unknown, _subscriber: unknown, ): Promise<{ result: unknown; newMessages: [] }> { TrackingAgent.connectCalls.push({ threadId: this.threadId, agentId: this.agentId, }); return { result: undefined, newMessages: [] }; } } function renderWithKit(ui: React.ReactNode, agent: TrackingAgent) { return render(
{ui}
, ); } /** * Regression coverage for fix/welcome-not-showing-at-all. * * The underlying bug: the v1 wrapper pipes a ThreadsProvider- * minted UUID through to CopilotChatConfigurationProvider as `threadId`. * CopilotChat previously treated any non-empty providedThreadId as "caller * supplied a real backend thread" and (a) fired /connect (→ 404 for an * auto-minted UUID) and (b) suppressed the welcome screen forever. The * fix threads an `hasExplicitThreadId` signal through the provider chain; * these tests pin the contract that /connect and welcome-screen gating * now follow that signal rather than `!!threadId`. */ describe("CopilotChat welcome / connect integration", () => { beforeEach(() => { TrackingAgent.reset(); }); describe("v1 bridge scenario (config provider marks threadId as non-explicit)", () => { it("does not call connectAgent and shows the welcome screen", async () => { const agent = new TrackingAgent(); agent.agentId = DEFAULT_AGENT_ID; renderWithKit( , agent, ); // Give the connect-effect a chance to misfire. await new Promise((r) => setTimeout(r, 50)); expect(TrackingAgent.connectCalls).toHaveLength(0); expect(screen.getByTestId("copilot-welcome-screen")).toBeDefined(); }); }); describe("plain CopilotChat (no threadId anywhere)", () => { it("does not call connectAgent and shows the welcome screen", async () => { const agent = new TrackingAgent(); agent.agentId = DEFAULT_AGENT_ID; renderWithKit(, agent); await new Promise((r) => setTimeout(r, 50)); expect(TrackingAgent.connectCalls).toHaveLength(0); expect(screen.getByTestId("copilot-welcome-screen")).toBeDefined(); }); }); describe("explicit threadId via CopilotChat prop", () => { it("calls connectAgent with that threadId and suppresses the welcome screen", async () => { const agent = new TrackingAgent(); agent.agentId = DEFAULT_AGENT_ID; renderWithKit(, agent); await waitFor(() => { expect(TrackingAgent.connectCalls.length).toBeGreaterThan(0); }); // The per-thread clone carries threadId; agentId is the default. expect( TrackingAgent.connectCalls.some((c) => c.threadId === "real-thread"), ).toBe(true); // Welcome screen is suppressed even after connect resolves, because the // thread was caller-picked (hasExplicitThreadId=true). expect(screen.queryByTestId("copilot-welcome-screen")).toBeNull(); }); }); describe("explicit threadId via wrapping CopilotChatConfigurationProvider", () => { it("inherits explicitness from the provider and connects", async () => { const agent = new TrackingAgent(); agent.agentId = DEFAULT_AGENT_ID; renderWithKit( , agent, ); await waitFor(() => { expect(TrackingAgent.connectCalls.length).toBeGreaterThan(0); }); expect( TrackingAgent.connectCalls.some((c) => c.threadId === "from-config"), ).toBe(true); expect(screen.queryByTestId("copilot-welcome-screen")).toBeNull(); }); }); describe("thread switch between two explicit threads", () => { it("keeps the welcome screen hidden across the switch", async () => { const agent = new TrackingAgent(); agent.agentId = DEFAULT_AGENT_ID; const { rerender } = renderWithKit( , agent, ); await waitFor(() => { expect( TrackingAgent.connectCalls.some((c) => c.threadId === "thread-a"), ).toBe(true); }); // After thread-a's connect resolves, welcome must still be hidden // because the thread is caller-picked. expect(screen.queryByTestId("copilot-welcome-screen")).toBeNull(); rerender(
, ); // During the switch (lastConnected="thread-a" !== "thread-b") isConnecting // is true — welcome must not flash. expect(screen.queryByTestId("copilot-welcome-screen")).toBeNull(); await waitFor(() => { expect( TrackingAgent.connectCalls.some((c) => c.threadId === "thread-b"), ).toBe(true); }); // And after thread-b's connect resolves. expect(screen.queryByTestId("copilot-welcome-screen")).toBeNull(); }); }); });