import React from "react"; import { render } from "@testing-library/react"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; // Type-only (erased at runtime, so it does not defeat the vi.mock hoisting // below): shapes the importOriginal() spread in the @copilotkit/shared mock. import type * as CopilotKitShared from "@copilotkit/shared"; // ─── Mocks ──────────────────────────────────────────────────────────────────── // react-native uses Flow syntax that Vitest/Rollup can't parse outside of // Metro. Mock it so barrel imports from ../index don't trigger a parse error. vi.mock("react-native", () => { const _React = require("react"); const View = ({ children, style, testID }: any) => _React.createElement("div", { "data-testid": testID, style }, children); const Text = ({ children, style, testID }: any) => _React.createElement("span", { "data-testid": testID, style }, children); const Pressable = ({ children, onPress, testID, style }: any) => _React.createElement( "div", { "data-testid": testID, onClick: onPress, style }, children, ); const TouchableOpacity = ({ children, onPress, testID, style }: any) => _React.createElement( "button", { "data-testid": testID, onClick: onPress, style }, children, ); const Modal = ({ children, visible, testID }: any) => { if (!visible) return null; return _React.createElement("div", { "data-testid": testID }, children); }; return { View, Text, Pressable, TouchableOpacity, Modal, Animated: { View, Text, createAnimatedComponent: (comp: any) => comp, timing: () => ({ start: (cb?: any) => cb?.() }), Value: class { _value: number; constructor(v: number) { this._value = v; } setValue(v: number) { this._value = v; } }, }, Dimensions: { get: () => ({ width: 375, height: 812 }) }, StyleSheet: { create: (styles: any) => styles, hairlineWidth: 1 }, useWindowDimensions: () => ({ width: 375, height: 812 }), Platform: { OS: "ios" }, }; }); // vi.hoisted runs before vi.mock factories, making these available to both const hoisted = vi.hoisted(() => { const _React = require("react"); return { RealContext: _React.createContext(null), MockCoreConstructor: vi.fn(), }; }); let _capturedSubscriber: Record void>; let unsubscribeMock: ReturnType; function createMockCore() { return { subscribe: vi.fn((subscriber: any) => { _capturedSubscriber = subscriber; return { unsubscribe: unsubscribeMock }; }), subscribeToAgentWithOptions: vi.fn((_agent: any, _handlers: any) => ({ unsubscribe: vi.fn(), })), setRuntimeUrl: vi.fn(), setRuntimeTransport: vi.fn(), setHeaders: vi.fn(), setCredentials: vi.fn(), setProperties: vi.fn(), setDebug: vi.fn(), setDefaultThrottleMs: vi.fn(), getAgent: vi.fn(() => undefined), runtimeUrl: "https://api.test", runtimeTransport: "auto", runtimeConnectionStatus: "Disconnected", headers: {}, agents: {}, defaultThrottleMs: undefined, addTool: vi.fn(), removeTool: vi.fn(), getTool: vi.fn(() => undefined), addHookRenderToolCall: vi.fn(), registerThreadStore: vi.fn(), unregisterThreadStore: vi.fn(), intelligence: undefined, }; } let mockCoreInstance: ReturnType; vi.mock("@copilotkit/react-core/v2/headless", () => { // Regular function (not arrow) so it's new-able function CopilotKitCoreReact(this: any, ...args: any[]) { hoisted.MockCoreConstructor(...args); const instance = hoisted.MockCoreConstructor.mock.results.at(-1)?.value; if (instance) Object.assign(this, instance); } return { CopilotKitCoreReact, useAgent: () => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } return { agent: {} }; }, useFrontendTool: (_tool: any) => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } }, useComponent: () => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } }, useHumanInTheLoop: () => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } }, useInterrupt: () => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } }, useSuggestions: () => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } }, useConfigureSuggestions: () => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } }, useAgentContext: () => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } }, useThreads: (_input: any) => { const ctx = require("react").useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } return { threads: [], isLoading: false, error: null, hasMoreThreads: false, isFetchingMoreThreads: false, fetchMoreThreads: () => {}, renameThread: async () => {}, archiveThread: async () => {}, deleteThread: async () => {}, }; }, CopilotChatConfigurationProvider: ({ children }: any) => children, useCopilotChatConfiguration: () => null, CopilotChatDefaultLabels: {}, }; }); vi.mock("@copilotkit/react-core/v2/context", () => { const _React = require("react"); return { CopilotKitContext: hoisted.RealContext, LicenseContext: _React.createContext({ status: null, license: null, checkFeature: () => true, getLimit: () => null, }), useCopilotKit: () => { const ctx = _React.useContext(hoisted.RealContext); if (!ctx) { throw new Error("useCopilotKit must be used within CopilotKitProvider"); } return ctx; }, useLicenseContext: () => ({ status: null, license: null, checkFeature: () => true, getLimit: () => null, }), }; }); // Mock @gorhom/bottom-sheet to prevent its CommonJS require("react-native") // from bypassing the vite alias and loading the Flow-syntax react-native. vi.mock("@gorhom/bottom-sheet", () => { const React = require("react"); return { __esModule: true, default: React.forwardRef((props: any, ref: any) => React.createElement("mock-bottom-sheet", { ref }, props.children), ), BottomSheetBackdrop: () => null, BottomSheetFlatList: "FlatList", BottomSheetView: (props: any) => React.createElement("div", null, props.children), }; }); // Spread the real module rather than replacing it: `../headless` re-exports // @copilotkit/core's runtime enums (ToolCallStatus / CopilotKitCoreErrorCode / // CopilotKitCoreRuntimeConnectionStatus) as VALUES, so importing `../index` // evaluates real @copilotkit/core, which named-imports RUNTIME_MODE_SSE and // friends from @copilotkit/shared. A replace-everything factory has to restate // every one of those or the import throws; only createLicenseContextValue needs // stubbing here. vi.mock("@copilotkit/shared", async (importOriginal) => ({ ...(await importOriginal()), createLicenseContextValue: () => ({ status: null, license: null, checkFeature: () => true, getLimit: () => null, }), })); // Import after mocks import { CopilotKitProvider } from "../CopilotKitProvider"; import { useAgent, useFrontendTool, useHumanInTheLoop, useInterrupt, useThreads, } from "../index"; // ─── Tests ──────────────────────────────────────────────────────────────────── describe("Headless integration", () => { beforeEach(() => { unsubscribeMock = vi.fn(); mockCoreInstance = createMockCore(); hoisted.MockCoreConstructor.mockClear(); hoisted.MockCoreConstructor.mockReturnValue(mockCoreInstance); }); afterEach(() => { vi.clearAllMocks(); }); // ── Provider + hooks integration ────────────────────────────────────── describe("provider + hooks integration", () => { it("useAgent returns expected shape when called inside provider", () => { let result: any = null; function TestComponent() { result = useAgent(); return null; } render( , ); expect(result).not.toBeNull(); expect(result).toHaveProperty("agent"); expect(typeof result.agent).toBe("object"); }); it("useFrontendTool can register a tool without error", () => { function TestComponent() { useFrontendTool({ name: "test-tool", description: "A test tool", handler: async () => "done", }); return null; } // Should not throw expect(() => { render( , ); }).not.toThrow(); }); it("useThreads returns thread state when called inside provider", () => { let result: any = null; function TestComponent() { result = useThreads({ agentId: "test-agent" }); return null; } render( , ); expect(result).not.toBeNull(); expect(result).toHaveProperty("threads"); expect(result).toHaveProperty("isLoading"); expect(result).toHaveProperty("error"); expect(result).toHaveProperty("renameThread"); expect(result).toHaveProperty("archiveThread"); expect(result).toHaveProperty("deleteThread"); expect(result).toHaveProperty("hasMoreThreads"); expect(result).toHaveProperty("isFetchingMoreThreads"); expect(result).toHaveProperty("fetchMoreThreads"); expect(Array.isArray(result.threads)).toBe(true); expect(typeof result.renameThread).toBe("function"); expect(typeof result.archiveThread).toBe("function"); expect(typeof result.deleteThread).toBe("function"); }); it("multiple hooks can coexist in the same provider tree", () => { let agentResult: any = null; let threadsResult: any = null; function TestComponent() { agentResult = useAgent(); threadsResult = useThreads({ agentId: "test-agent" }); useFrontendTool({ name: "multi-tool", description: "Another tool", handler: async () => "ok", }); return null; } expect(() => { render( , ); }).not.toThrow(); expect(agentResult).toHaveProperty("agent"); expect(threadsResult).toHaveProperty("threads"); }); }); // ── Provider error boundary ─────────────────────────────────────────── describe("provider error boundary", () => { it("useAgent throws when called outside CopilotKitProvider", () => { function TestComponent() { useAgent(); return null; } // Suppress React error boundary console output const spy = vi.spyOn(console, "error").mockImplementation(() => {}); expect(() => { render(); }).toThrow("useCopilotKit must be used within CopilotKitProvider"); spy.mockRestore(); }); it("useFrontendTool throws when called outside CopilotKitProvider", () => { function TestComponent() { useFrontendTool({ name: "orphan-tool", description: "No provider", handler: async () => "fail", }); return null; } const spy = vi.spyOn(console, "error").mockImplementation(() => {}); expect(() => { render(); }).toThrow("useCopilotKit must be used within CopilotKitProvider"); spy.mockRestore(); }); it("useThreads throws when called outside CopilotKitProvider", () => { function TestComponent() { useThreads({ agentId: "test-agent" }); return null; } const spy = vi.spyOn(console, "error").mockImplementation(() => {}); expect(() => { render(); }).toThrow("useCopilotKit must be used within CopilotKitProvider"); spy.mockRestore(); }); it("useHumanInTheLoop throws when called outside CopilotKitProvider", () => { function TestComponent() { useHumanInTheLoop({ name: "orphan-hitl", render: () => null }); return null; } const spy = vi.spyOn(console, "error").mockImplementation(() => {}); expect(() => { render(); }).toThrow("useCopilotKit must be used within CopilotKitProvider"); spy.mockRestore(); }); it("useInterrupt throws when called outside CopilotKitProvider", () => { function TestComponent() { useInterrupt({ render: () => <> }); return null; } const spy = vi.spyOn(console, "error").mockImplementation(() => {}); expect(() => { render(); }).toThrow("useCopilotKit must be used within CopilotKitProvider"); spy.mockRestore(); }); }); });