import "@testing-library/jest-dom";
import React from "react";
import { act, render, screen } from "@testing-library/react";
import { GlobalChatbox } from "./GlobalChatbox";
const createSession = jest.fn();
const mockFetchAgentRuntimeHealth = jest.fn();
const mockFetchAgentModels = jest.fn();
let mockCurrentProjectId = "project-1";
jest.mock("@refinedev/core", () => ({
useNotification: () => ({ open: jest.fn() }),
}));
jest.mock("@/lib/chatModels", () => ({
fetchAgentModels: (...args: unknown[]) => mockFetchAgentModels(...args),
}));
jest.mock("@/lib/agentRuntime", () => ({
fetchAgentRuntimeHealth: (...args: unknown[]) =>
mockFetchAgentRuntimeHealth(...args),
}));
jest.mock("@/store/projectStore", () => ({
useProjectStore: (selector: (state: { currentProjectId: string }) => unknown) =>
selector({ currentProjectId: mockCurrentProjectId }),
}));
jest.mock("./globalChatboxVoice", () => ({
useSpeechSynthesis: () => ({
speechState: "idle",
speakingMessageId: null,
speak: jest.fn(),
pause: jest.fn(),
resume: jest.fn(),
stop: jest.fn(),
isSupported: true,
}),
useSpeechRecognition: () => ({
isListening: false,
start: jest.fn(),
stop: jest.fn(),
isSupported: true,
}),
}));
jest.mock("./hooks/useAgentToolActions", () => ({
useAgentToolActions: () => jest.fn(),
}));
jest.mock("./hooks/useAgentChatSession", () => ({
useAgentChatSession: () => ({
messages: [],
chatSessions: [],
activeSessionId: undefined,
isHydrating: false,
loadingSessionId: null,
isStreaming: false,
sessionTitle: "新会话",
sendPrompt: jest.fn(),
createBranch: jest.fn(),
abort: jest.fn(),
replyPermission: jest.fn(),
replyQuestion: jest.fn(),
rejectQuestion: jest.fn(),
createSession,
renameSession: jest.fn(),
removeSession: jest.fn(),
switchSession: jest.fn(),
}),
}));
jest.mock("./AgentHeader", () => ({
AgentHeader: () =>
Agent header
,
}));
jest.mock("./AgentHistoryPanel", () => ({
AgentHistoryPanel: () => History
,
}));
jest.mock("./AgentWorkspace", () => ({
AgentWorkspace: ({ runtimeState }: { runtimeState: string }) => (
Workspace state: {runtimeState}
),
}));
jest.mock("./AgentComposer", () => ({
AgentComposer: React.forwardRef(function MockAgentComposer(props: { approvalMode: string }, _ref) {
return Composer mode: {props.approvalMode}
;
}),
}));
jest.mock("./GlobalChatboxParts", () => ({
Blob: () => null,
}));
describe("GlobalChatbox lifecycle", () => {
beforeEach(() => {
jest.useFakeTimers();
createSession.mockClear();
mockFetchAgentRuntimeHealth.mockReset();
mockFetchAgentRuntimeHealth.mockImplementation(() => new Promise(() => {}));
mockFetchAgentModels.mockReset();
mockFetchAgentModels.mockImplementation(() => new Promise(() => {}));
mockCurrentProjectId = "project-1";
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
jest.restoreAllMocks();
});
it("keeps content mounted and preserves the session across close and reopen", async () => {
const { rerender } = render();
act(() => jest.runOnlyPendingTimers());
expect(createSession).toHaveBeenCalledTimes(1);
expect(screen.getByTestId("agent-workspace")).toBeInTheDocument();
rerender();
act(() => jest.advanceTimersByTime(300));
expect(screen.getByTestId("agent-workspace")).toBeInTheDocument();
rerender();
act(() => jest.runOnlyPendingTimers());
expect(createSession).toHaveBeenCalledTimes(1);
mockCurrentProjectId = "project-2";
rerender();
act(() => jest.runOnlyPendingTimers());
expect(createSession).toHaveBeenCalledTimes(2);
});
it("defaults the composer to automatic permission approval", () => {
render();
expect(screen.getByText("Composer mode: auto")).toBeInTheDocument();
});
it("passes backend startup failures into the existing workspace empty state", async () => {
mockFetchAgentRuntimeHealth.mockResolvedValueOnce(false);
render();
await act(async () => Promise.resolve());
expect(screen.getByText("Workspace state: unavailable")).toBeInTheDocument();
});
it("reserves models unavailable for an empty model configuration", async () => {
mockFetchAgentRuntimeHealth.mockResolvedValueOnce(true);
mockFetchAgentModels.mockResolvedValueOnce({ models: [] });
render();
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(screen.getByText("Workspace state: models_unavailable")).toBeInTheDocument();
});
it("reports an auth dependency failure as unavailable and retries quickly", async () => {
jest.spyOn(console, "error").mockImplementation(() => undefined);
mockFetchAgentRuntimeHealth.mockResolvedValue(true);
mockFetchAgentModels
.mockRejectedValueOnce(new Error("authentication service unavailable"))
.mockResolvedValueOnce({
defaultModel: "provider/model",
models: [{ id: "provider/model", label: "Model" }],
});
render();
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(screen.getByText("Workspace state: unavailable")).toBeInTheDocument();
expect(mockFetchAgentModels).toHaveBeenCalledTimes(1);
await act(async () => {
jest.advanceTimersByTime(5_000);
await Promise.resolve();
await Promise.resolve();
});
expect(screen.getByText("Workspace state: ready")).toBeInTheDocument();
expect(mockFetchAgentModels).toHaveBeenCalledTimes(2);
});
});