);
}
render(
,
);
expect(screen.getByTestId("isModalOpen").textContent).toBe("closed");
});
it("should inherit parent modal state when child has no isModalDefaultOpen", () => {
function ModalStateDisplay() {
const config = useCopilotChatConfiguration();
return (
{config?.isModalOpen ? "open" : "closed"}
{config?.setModalOpen ? "yes" : "no"}
);
}
render(
,
);
// Child should inherit parent's modal state (closed)
expect(screen.getByTestId("isModalOpen").textContent).toBe("closed");
expect(screen.getByTestId("hasSetModalOpen").textContent).toBe("yes");
});
it("should allow nested provider to override parent modal state with explicit isModalDefaultOpen", () => {
function ModalStateDisplay() {
const config = useCopilotChatConfiguration();
return (
{config?.isModalOpen ? "open" : "closed"}
);
}
render(
,
);
expect(screen.getByTestId("isModalOpen").textContent).toBe("closed");
});
});
/**
* CPK-7152: Bidirectional sync between nested providers.
*
* The fix must satisfy both:
* Behavior A — child provider respects its own isModalDefaultOpen even
* when a parent provider exists (covered by the existing
* "allow nested provider to override" test above).
* Behavior B — state changes in the inner provider propagate outward so
* that hooks reading from an outer provider stay in sync.
*
* Scenarios mirror the reproduction cases in:
* https://github.com/CopilotKit/deep-agent-cpk-experiments/tree/main/app/client/src/tickets/tkt-modal-default-open
*/
describe("Bidirectional sync (CPK-7152)", () => {
// Reusable probe/control component that reads the closest provider.
function ModalControls({ id }: { id: string }) {
const config = useCopilotChatConfiguration();
return (
<>
{String(config?.isModalOpen)}
>
);
}
it("scenario-sidebar-outer-hook: inner setModalOpen propagates to outer hook (Behavior B)", () => {
// Abe.Hu's layout: outer bare provider, inner provider owns explicit state.
// Toggling via the inner provider should update the outer hook.
render(
{/* OuterProbe sits outside the inner provider — reads outer context */}
,
);
expect(screen.getByTestId("outer-state").textContent).toBe("true");
expect(screen.getByTestId("inner-state").textContent).toBe("true");
act(() => {
fireEvent.click(screen.getByTestId("inner-close"));
});
// Inner closed — outer hook must reflect the change.
expect(screen.getByTestId("inner-state").textContent).toBe("false");
expect(screen.getByTestId("outer-state").textContent).toBe("false");
});
it("scenario-sidebar-outer-hook: outer setModalOpen propagates to inner (parent→child sync)", () => {
// If the user calls setModalOpen from the outer hook, the inner
// provider (and therefore the sidebar) must respond.
render(
,
);
expect(screen.getByTestId("outer-state").textContent).toBe("false");
expect(screen.getByTestId("inner-state").textContent).toBe("false");
act(() => {
fireEvent.click(screen.getByTestId("outer-open"));
});
// Outer opened — inner must follow.
expect(screen.getByTestId("outer-state").textContent).toBe("true");
expect(screen.getByTestId("inner-state").textContent).toBe("true");
});
it("scenario-nested-provider: three-level chain propagates through middle provider", () => {
// Mirrors the real provider stack:
// Provider 1 (user's outer, no isModalDefaultOpen)
// └── Provider 2 (CopilotChat's, no isModalDefaultOpen) — "middle"
// └── Provider 3 (CopilotSidebarView's, explicit isModalDefaultOpen)
//
// Toggling P3 must reach P1 even though P2 has no explicit default.
render(
{/* p2 has no isModalDefaultOpen — proxies p1's state */}
,
);
expect(screen.getByTestId("p1-state").textContent).toBe("true");
expect(screen.getByTestId("p3-state").textContent).toBe("true");
act(() => {
fireEvent.click(screen.getByTestId("p3-close"));
});
expect(screen.getByTestId("p3-state").textContent).toBe("false");
expect(screen.getByTestId("p1-state").textContent).toBe("false");
});
it("scenario-nested-provider: Behavior A still holds after sync fix (no regression)", () => {
// Explicit isModalDefaultOpen on a child must still override the
// parent's current value on initial render — the sync effect must
// not overwrite the child's own initial state.
render(
,
);
// Inner must start closed despite outer being open.
expect(screen.getByTestId("outer-state").textContent).toBe("true");
expect(screen.getByTestId("inner-state").textContent).toBe("false");
});
});
/**
* Regression coverage for the welcome-screen / /connect 404 bug
* (fix/welcome-not-showing-at-all). `hasExplicitThreadId` distinguishes a
* caller-chosen thread from a UUID auto-minted inside the provider chain —
* consumers that only make sense against a real backend thread (/connect,
* switch-flash suppression) must gate on this signal, not on !!threadId.
*/
describe("hasExplicitThreadId", () => {
function ExplicitProbe({ id = "probe" }: { id?: string } = {}) {
const config = useCopilotChatConfiguration();
return (
{String(config?.hasExplicitThreadId)}
);
}
it("infers true when threadId prop is supplied and hasExplicitThreadId is omitted", () => {
render(
,
);
expect(screen.getByTestId("probe-explicit").textContent).toBe("true");
});
it("infers false when no threadId prop is supplied and hasExplicitThreadId is omitted", () => {
render(
,
);
expect(screen.getByTestId("probe-explicit").textContent).toBe("false");
});
it("respects hasExplicitThreadId={false} even when a threadId prop is present (v1 bridge case)", () => {
// The v1 wrapper always pipes a UUID through as `threadId`
// (from ThreadsProvider). Without this override the provider would
// mis-infer the UUID as explicit, causing /connect to 404 and the
// welcome screen to stay hidden for fresh empty chats.
render(
,
);
expect(screen.getByTestId("probe-explicit").textContent).toBe("false");
});
it("parent=true overrides child's hasExplicitThreadId={false} via OR inheritance", () => {
// resolvedHasExplicitThreadId = ownHasExplicit || parentHasExplicit.
// Once an ancestor has marked the thread as caller-chosen, descendants
// cannot mask that — pinning the contract so "try to hide explicitness
// from a child" doesn't silently work.
render(
,
);
expect(screen.getByTestId("probe-explicit").textContent).toBe("true");
});
it("propagates through a three-level chain where the middle provider is bare", () => {
// Matches the real stack: outer layout provider (no threadId) →
// CopilotChat's own provider (no threadId) → inner feature provider
// (explicit threadId). Explicitness must cross the empty middle.
render(
,
);
expect(screen.getByTestId("probe-explicit").textContent).toBe("true");
});
it("non-explicit parent does not taint an explicit child", () => {
render(
,
);
expect(screen.getByTestId("probe-explicit").textContent).toBe("true");
});
});
describe("Nested providers", () => {
it("should handle multiple nested providers correctly", () => {
render(
,
);
// Innermost provider should win
expect(screen.getByTestId("agentId").textContent).toBe("inner-agent");
expect(screen.getByTestId("threadId").textContent).toBe("inner-thread");
expect(screen.getByTestId("placeholder").textContent).toBe("Inner");
});
});
});