Runtime model checks previously mapped every authenticated endpoint failure to models_unavailable. Treat request failures as service outages and retry failed checks after five seconds; empty model configurations remain distinct.
195 lines
5.9 KiB
TypeScript
195 lines
5.9 KiB
TypeScript
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: () => <div>Agent header</div>,
|
|
}));
|
|
|
|
jest.mock("./AgentHistoryPanel", () => ({
|
|
AgentHistoryPanel: () => <div>History</div>,
|
|
}));
|
|
|
|
jest.mock("./AgentWorkspace", () => ({
|
|
AgentWorkspace: ({ runtimeState }: { runtimeState: string }) => (
|
|
<div data-testid="agent-workspace">Workspace state: {runtimeState}</div>
|
|
),
|
|
}));
|
|
|
|
jest.mock("./AgentComposer", () => ({
|
|
AgentComposer: React.forwardRef(function MockAgentComposer(props: { approvalMode: string }, _ref) {
|
|
return <div>Composer mode: {props.approvalMode}</div>;
|
|
}),
|
|
}));
|
|
|
|
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(<GlobalChatbox open onClose={jest.fn()} />);
|
|
|
|
act(() => jest.runOnlyPendingTimers());
|
|
expect(createSession).toHaveBeenCalledTimes(1);
|
|
expect(screen.getByTestId("agent-workspace")).toBeInTheDocument();
|
|
|
|
rerender(<GlobalChatbox open={false} onClose={jest.fn()} />);
|
|
act(() => jest.advanceTimersByTime(300));
|
|
|
|
expect(screen.getByTestId("agent-workspace")).toBeInTheDocument();
|
|
|
|
rerender(<GlobalChatbox open onClose={jest.fn()} />);
|
|
act(() => jest.runOnlyPendingTimers());
|
|
|
|
expect(createSession).toHaveBeenCalledTimes(1);
|
|
|
|
mockCurrentProjectId = "project-2";
|
|
rerender(<GlobalChatbox open onClose={jest.fn()} />);
|
|
act(() => jest.runOnlyPendingTimers());
|
|
|
|
expect(createSession).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("defaults the composer to automatic permission approval", () => {
|
|
render(<GlobalChatbox open onClose={jest.fn()} />);
|
|
|
|
expect(screen.getByText("Composer mode: auto")).toBeInTheDocument();
|
|
});
|
|
|
|
it("passes backend startup failures into the existing workspace empty state", async () => {
|
|
mockFetchAgentRuntimeHealth.mockResolvedValueOnce(false);
|
|
render(<GlobalChatbox open onClose={jest.fn()} />);
|
|
|
|
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(<GlobalChatbox open onClose={jest.fn()} />);
|
|
|
|
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(<GlobalChatbox open onClose={jest.fn()} />);
|
|
|
|
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);
|
|
});
|
|
});
|