增加流式信息中断处理机制
This commit is contained in:
@@ -3,12 +3,13 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
|
||||
import { useAgentChatSession } from "./useAgentChatSession";
|
||||
import { streamAgentChat } from "@/lib/chatStream";
|
||||
import { abortAgentChat, resumeAgentChatStream, streamAgentChat } from "@/lib/chatStream";
|
||||
import type { StreamEvent } from "@/lib/chatStream";
|
||||
|
||||
jest.mock("@/lib/chatStream", () => ({
|
||||
abortAgentChat: jest.fn(async () => undefined),
|
||||
forkAgentChat: jest.fn(async () => "forked-session"),
|
||||
resumeAgentChatStream: jest.fn(async () => undefined),
|
||||
streamAgentChat: jest.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
@@ -42,7 +43,12 @@ describe("useAgentChatSession", () => {
|
||||
listChatSessions.mockReset();
|
||||
saveActiveChatState.mockReset();
|
||||
updateChatSessionTitle.mockReset();
|
||||
jest.mocked(abortAgentChat).mockReset();
|
||||
jest.mocked(resumeAgentChatStream).mockReset();
|
||||
jest.mocked(streamAgentChat).mockReset();
|
||||
jest.mocked(abortAgentChat).mockImplementation(async () => undefined);
|
||||
jest.mocked(resumeAgentChatStream).mockImplementation(async () => undefined);
|
||||
jest.mocked(streamAgentChat).mockImplementation(async () => undefined);
|
||||
saveActiveChatState.mockImplementation(async (state) => state.sessionId);
|
||||
});
|
||||
|
||||
@@ -103,7 +109,7 @@ describe("useAgentChatSession", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("waits for the stream session id before persisting a new streaming conversation", async () => {
|
||||
it("persists a new conversation only after the stream is done", async () => {
|
||||
listChatSessions.mockResolvedValue([]);
|
||||
let emitStreamEvent: ((event: StreamEvent) => void) | undefined;
|
||||
jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
|
||||
@@ -147,15 +153,140 @@ describe("useAgentChatSession", () => {
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(saveActiveChatState).toHaveBeenCalledTimes(1);
|
||||
expect(saveActiveChatState).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
emitStreamEvent?.({
|
||||
type: "done",
|
||||
sessionId: "chat-stream-1",
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(saveActiveChatState).toHaveBeenCalledTimes(1));
|
||||
expect(saveActiveChatState.mock.calls[0][0]).toMatchObject({
|
||||
sessionId: "chat-stream-1",
|
||||
messages: [
|
||||
expect.objectContaining({ role: "user", content: "第一条消息" }),
|
||||
expect.objectContaining({ role: "assistant", content: "收到" }),
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("hydrates a backend streaming session and resumes its stream", async () => {
|
||||
listChatSessions.mockResolvedValue([
|
||||
{
|
||||
id: "session-streaming",
|
||||
title: "运行中",
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
isStreaming: true,
|
||||
runStatus: "running",
|
||||
},
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAgentChatSession({
|
||||
projectId: "project-1",
|
||||
onToolCall: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.isHydrating).toBe(false));
|
||||
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
expect(result.current.activeSessionId).toBe("session-loaded");
|
||||
expect(resumeAgentChatStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: "session-loaded",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("updates resumed messages from state, token, and done events", async () => {
|
||||
listChatSessions.mockResolvedValue([
|
||||
{
|
||||
id: "session-streaming",
|
||||
title: "运行中",
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
isStreaming: true,
|
||||
},
|
||||
]);
|
||||
jest.mocked(resumeAgentChatStream).mockImplementationOnce(async ({ onEvent }) => {
|
||||
onEvent({
|
||||
type: "state",
|
||||
sessionId: "session-loaded",
|
||||
messages: [
|
||||
{ id: "u1", role: "user", content: "继续分析" },
|
||||
{ id: "a1", role: "assistant", content: "已有" },
|
||||
],
|
||||
isStreaming: true,
|
||||
runStatus: "running",
|
||||
});
|
||||
onEvent({
|
||||
type: "token",
|
||||
sessionId: "session-loaded",
|
||||
content: "输出",
|
||||
});
|
||||
onEvent({
|
||||
type: "done",
|
||||
sessionId: "session-loaded",
|
||||
});
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAgentChatSession({
|
||||
projectId: "project-1",
|
||||
onToolCall: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.isHydrating).toBe(false));
|
||||
await waitFor(() => expect(result.current.isStreaming).toBe(false));
|
||||
|
||||
expect(result.current.messages).toEqual([
|
||||
expect.objectContaining({ id: "u1", role: "user", content: "继续分析" }),
|
||||
expect.objectContaining({ id: "a1", role: "assistant", content: "已有输出" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("aborts a resumed streaming session through the backend abort endpoint", async () => {
|
||||
listChatSessions.mockResolvedValue([
|
||||
{
|
||||
id: "session-streaming",
|
||||
title: "运行中",
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
isStreaming: true,
|
||||
},
|
||||
]);
|
||||
jest.mocked(resumeAgentChatStream).mockImplementationOnce(async () => {
|
||||
await new Promise<void>(() => undefined);
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAgentChatSession({
|
||||
projectId: "project-1",
|
||||
onToolCall: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.isStreaming).toBe(true));
|
||||
|
||||
act(() => {
|
||||
result.current.abort();
|
||||
});
|
||||
|
||||
expect(abortAgentChat).toHaveBeenCalledWith("session-loaded");
|
||||
});
|
||||
|
||||
it("ignores generated session titles after the title was edited manually", async () => {
|
||||
listChatSessions.mockResolvedValue([]);
|
||||
jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
|
||||
|
||||
Reference in New Issue
Block a user