/**
* Integration test for the context timing race condition (CPK-7060).
*
* When a useFrontendTool handler calls setState(), the corresponding
* useAgentContext value is updated asynchronously (React defers useEffect to a
* later scheduler task). Without yielding before the follow-up agent run,
* runAgent reads stale context from the store.
*
* CopilotKitCoreReact.waitForPendingFrameworkUpdates() fixes this by awaiting a
* zero-delay timeout, which yields to React's scheduler before reading context.
*
* This test uses real React rendering in jsdom so that actual useState +
* useAgentContext + useFrontendTool lifecycle interactions are exercised.
*/
import React, { useState } from "react";
import { screen, fireEvent, waitFor } from "@testing-library/react";
import { z } from "zod";
import { useFrontendTool } from "../use-frontend-tool";
import { useAgentContext } from "../use-agent-context";
import { CopilotChat } from "../../components/chat/CopilotChat";
import {
type AgentSubscriber,
type BaseEvent,
type RunAgentParameters,
} from "@ag-ui/client";
import type { Context } from "@ag-ui/client";
import {
MockStepwiseAgent,
renderWithCopilotKit,
runStartedEvent,
runFinishedEvent,
toolCallChunkEvent,
testId,
} from "../../__tests__/utils/test-helpers";
describe("useAgentContext timing - follow-up run sees updated context", () => {
it("follow-up agent run receives context updated by useFrontendTool handler", async () => {
/**
* Agent subclass that records the context parameter on every runAgent call.
* After complete() is called on the subject the Observable completes
* immediately for subsequent subscriptions, so the follow-up run resolves
* with no new messages — which is fine; we only need to capture context.
*/
class ContextCapturingAgent extends MockStepwiseAgent {
// Shared so the clone and original both see the captured contexts
public contextPerRun: Context[][] = [];
clone(): this {
const cloned = super.clone();
(cloned as unknown as ContextCapturingAgent).contextPerRun =
this.contextPerRun;
return cloned;
}
async runAgent(
parameters?: RunAgentParameters,
subscriber?: AgentSubscriber,
) {
this.contextPerRun.push(parameters?.context ?? []);
return super.runAgent(parameters, subscriber);
}
}
const agent = new ContextCapturingAgent();
/**
* Component that wires React state into useAgentContext and exposes a
* frontend tool that updates that state.
*/
const TestComponent: React.FC = () => {
const [prefs, setPrefs] = useState<{ spicy: boolean }>({ spicy: true });
useAgentContext({
description: "user preferences",
value: prefs,
});
useFrontendTool({
name: "updatePrefs",
parameters: z.object({}),
followUp: true,
handler: async () => {
setPrefs({ spicy: false });
},
});
return null;
};
renderWithCopilotKit({
agent,
children: (
<>