feat(FN-3843): add chat stream reattach support
Adds the changeset for FN-3843, completing the chat stream reattach feature for the `@runfusion/fusion` CLI package. Fusion-Task-Id: FN-3843
This commit is contained in:
@@ -17,6 +17,7 @@ vi.mock("../../api", () => ({
|
||||
updateChatSession: vi.fn(),
|
||||
deleteChatSession: vi.fn(),
|
||||
streamChatResponse: vi.fn(),
|
||||
attachChatStream: vi.fn(),
|
||||
cancelChatResponse: vi.fn(),
|
||||
fetchAgents: vi.fn().mockResolvedValue([
|
||||
{ id: "agent-001", name: "Alpha", role: "executor", state: "idle", icon: undefined, createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z", metadata: {} },
|
||||
@@ -50,6 +51,7 @@ const mockFetchChatMessages = vi.mocked(apiModule.fetchChatMessages);
|
||||
const mockUpdateChatSession = vi.mocked(apiModule.updateChatSession);
|
||||
const mockDeleteChatSession = vi.mocked(apiModule.deleteChatSession);
|
||||
const mockStreamChatResponse = vi.mocked(apiModule.streamChatResponse);
|
||||
const mockAttachChatStream = vi.mocked(apiModule.attachChatStream);
|
||||
const mockCancelChatResponse = vi.mocked(apiModule.cancelChatResponse);
|
||||
const mockFetchAgents = vi.mocked(apiModule.fetchAgents);
|
||||
|
||||
@@ -100,6 +102,7 @@ describe("useChat", () => {
|
||||
});
|
||||
mockDeleteChatSession.mockResolvedValue({ success: true });
|
||||
mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
mockAttachChatStream.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
mockCancelChatResponse.mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
@@ -2066,18 +2069,23 @@ describe("useChat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears recovery streaming state when SSE delivers assistant message", async () => {
|
||||
let subscribeHandler: Record<string, (event: MessageEvent) => void> = {};
|
||||
mockSubscribeSse.mockImplementation((_url, options) => {
|
||||
if (options?.events) {
|
||||
subscribeHandler = options.events as typeof subscribeHandler;
|
||||
}
|
||||
return () => {};
|
||||
});
|
||||
|
||||
it("clears recovery streaming state when attach stream completes", async () => {
|
||||
const session = { ...makeSession({ id: "session-001", agentId: "agent-001" }), isGenerating: true };
|
||||
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
mockFetchChatMessages.mockResolvedValue({
|
||||
messages: [
|
||||
makeMessage({
|
||||
id: "msg-assistant-001",
|
||||
sessionId: "session-001",
|
||||
role: "assistant",
|
||||
content: "Generated response",
|
||||
}),
|
||||
],
|
||||
});
|
||||
mockAttachChatStream.mockImplementation((_sessionId, handlers) => {
|
||||
setTimeout(() => handlers.onDone?.({ messageId: "msg-assistant-001" }), 0);
|
||||
return { close: vi.fn(), isConnected: () => true };
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useChat("proj-123"));
|
||||
|
||||
@@ -2089,24 +2097,6 @@ describe("useChat", () => {
|
||||
result.current.selectSession("session-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
// Simulate SSE delivering the completed assistant message
|
||||
const assistantMessage = makeMessage({
|
||||
id: "msg-assistant-001",
|
||||
sessionId: "session-001",
|
||||
role: "assistant",
|
||||
content: "Generated response",
|
||||
});
|
||||
|
||||
act(() => {
|
||||
subscribeHandler["chat:message:added"](
|
||||
new MessageEvent("chat:message:added", { data: JSON.stringify(assistantMessage) }),
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isStreaming).toBe(false);
|
||||
expect(result.current.streamingText).toBe("");
|
||||
|
||||
@@ -11,6 +11,7 @@ vi.mock("../../api", () => ({
|
||||
createChatSession: vi.fn(),
|
||||
fetchChatMessages: vi.fn(),
|
||||
streamChatResponse: vi.fn(),
|
||||
attachChatStream: vi.fn(),
|
||||
cancelChatResponse: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -20,6 +21,7 @@ const mockFetchChatSession = vi.mocked(apiModule.fetchChatSession);
|
||||
const mockCreateChatSession = vi.mocked(apiModule.createChatSession);
|
||||
const mockFetchChatMessages = vi.mocked(apiModule.fetchChatMessages);
|
||||
const mockStreamChatResponse = vi.mocked(apiModule.streamChatResponse);
|
||||
const mockAttachChatStream = vi.mocked(apiModule.attachChatStream);
|
||||
const mockCancelChatResponse = vi.mocked(apiModule.cancelChatResponse);
|
||||
|
||||
function makeSession(overrides: Partial<ChatSession> & Pick<ChatSession, "id" | "agentId">): ChatSession {
|
||||
@@ -57,6 +59,7 @@ describe("useQuickChat", () => {
|
||||
session: { ...makeSession({ id: "session-001", agentId: "agent-001" }), isGenerating: false },
|
||||
});
|
||||
mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
mockAttachChatStream.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
mockCancelChatResponse.mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
@@ -1305,22 +1308,18 @@ describe("useQuickChat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears recovery streaming state when polling detects generation complete", async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
|
||||
it("clears recovery streaming state when attach stream completes", async () => {
|
||||
const session = { ...makeSession({ id: "session-001", agentId: "agent-001" }), isGenerating: true };
|
||||
mockFetchResumeChatSession.mockResolvedValue({ session });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
|
||||
// After first poll, server reports generation is done and has a new assistant message
|
||||
mockFetchChatSession.mockResolvedValue({
|
||||
session: { ...makeSession({ id: "session-001", agentId: "agent-001" }), isGenerating: false },
|
||||
});
|
||||
mockFetchChatMessages.mockResolvedValue({
|
||||
messages: [
|
||||
{ id: "msg-1", sessionId: "session-001", role: "assistant", content: "Done", thinkingOutput: null, metadata: null, createdAt: new Date().toISOString() },
|
||||
],
|
||||
});
|
||||
mockAttachChatStream.mockImplementation((_sessionId, handlers) => {
|
||||
setTimeout(() => handlers.onDone?.({ messageId: "msg-1" }), 0);
|
||||
return { close: vi.fn(), isConnected: () => true };
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuickChat("proj-123"));
|
||||
|
||||
@@ -1328,22 +1327,11 @@ describe("useQuickChat", () => {
|
||||
await result.current.switchSession("agent-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
// Advance time to trigger the polling interval (3s)
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(3500);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isStreaming).toBe(false);
|
||||
expect(result.current.streamingText).toBe("");
|
||||
expect(result.current.messages.some((m) => m.id === "msg-1")).toBe(true);
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user