diff --git a/web/src/hooks/useChat.test.ts b/web/src/hooks/useChat.test.ts index 3384bf3..a7dab48 100644 --- a/web/src/hooks/useChat.test.ts +++ b/web/src/hooks/useChat.test.ts @@ -523,4 +523,56 @@ describe('useChat - handleServerMessage characterization', () => { const s1 = result.current.messages.find((m) => m.id === 's1'); expect(s1?.content).toBe('part1part2'); }); + + it('18. stream_end arriving after onDisconnect is idempotent (no crash, no side effect)', () => { + const { result } = renderUseChat(); + act(() => { + result.current.handleServerMessage({ type: 'stream_delta', id: 'e1', delta: 'partial' }); + }); + expect(result.current.messages[0].content).toBe('partial'); + + // 断连:finishStreaming 重置 streamingRef + act(() => result.current.finishStreaming()); + + // 重连后 stream_end 到达(可能在断连前已发出,重连后才收到) + expect(() => { + act(() => result.current.handleServerMessage({ type: 'stream_end', id: 'e1' })); + }).not.toThrow(); + // stream_end 不应改变已 flush 的内容 + const e1 = result.current.messages.find((m) => m.id === 'e1'); + expect(e1?.content).toBe('partial'); + + // 紧接着 assistant_response 到达,应正常按 id 替换(断连后的正常恢复路径) + act(() => + result.current.handleServerMessage({ + type: 'assistant_response', + id: 'e1', + content: 'final', + role: 'assistant', + }), + ); + expect(result.current.messages.find((m) => m.id === 'e1')?.content).toBe('final'); + }); + + it('19. multiple disconnect-reconnect cycles: finishStreaming is idempotent across cycles', () => { + const { result } = renderUseChat(); + + // 周期 1 + act(() => result.current.handleServerMessage({ type: 'stream_delta', id: 'c1', delta: 'a' })); + act(() => result.current.finishStreaming()); + expect(result.current.messages.find((m) => m.id === 'c1')?.content).toBe('a'); + + // 周期 2:新流式消息 + act(() => result.current.handleServerMessage({ type: 'stream_delta', id: 'c2', delta: 'b' })); + act(() => result.current.finishStreaming()); + expect(result.current.messages.find((m) => m.id === 'c2')?.content).toBe('b'); + // c1 不受影响 + expect(result.current.messages.find((m) => m.id === 'c1')?.content).toBe('a'); + + // 周期 3:连续两次 finishStreaming(无 delta 间隔),验证幂等 + act(() => result.current.finishStreaming()); + act(() => result.current.finishStreaming()); + // 不崩溃,消息状态稳定 + expect(result.current.messages).toHaveLength(2); + }); });