import { describe, expect, it } from 'vitest'; import { consumeCloudChatEvent, type CloudChatStream } from '@/pages/CloudAgents/cloud-chat-stream'; const envelope = (items: unknown[], extra = {}) => ({ run_id: 'run-1', thread_id: 'thread-1', request_id: 'request-1', payload: { items }, ...extra }); const delta = (fields = {}) => ({ stream_event: { type: 'message_delta', message_id: 'message-1', ...fields } }); const consume = (state: CloudChatStream | null, value: unknown, id = '1-0', type = 'messages') => consumeCloudChatEvent(state, value, type, id, 'run-1', 'thread-1', 'request-1'); describe('personal Agent main-thread stream projection', () => { it('separates reasoning and answers in batched and single chunks with legacy fallback', () => { let state = consume(null, envelope([delta({ reasoning_content: '思考一', additional_reasoning_content: '不应重复', content: '回答一' })])); state = consume(state, { run_id: 'run-1', thread_id: 'thread-1', payload: { chunk: delta({ additional_reasoning_content: '思考二', content: '回答二' }) } }, '2-0'); expect(state).toMatchObject({ reasoning: '思考一思考二', content: '回答一回答二', cursor: '2-0' }); expect(JSON.stringify(state)).not.toContain('不应重复'); }); it('separates model messages across tool steps without treating tools as reasoning', () => { const state = consume(null, envelope([ delta({ reasoning_content: '先看资料', content: '第一段' }), { stream_event: { type: 'tool_call', name: 'search', arguments: 'private tool args' } }, delta({ message_id: 'message-2', reasoning_content: '核对资料', content: '第二段' }), ])); expect(state).toMatchObject({ reasoning: '先看资料\n\n核对资料', content: '第一段\n\n第二段', toolName: 'search' }); expect(JSON.stringify(state)).not.toContain('private tool args'); }); it('ignores other Runs/requests and excludes absent or child thread identities while advancing the cursor', () => { const state = consume(null, envelope([delta({ reasoning_content: '父线程' })])); expect(consume(state, envelope([delta({ reasoning_content: '另一个运行' })], { run_id: 'run-2' }), '2-0')).toBe(state); expect(consume(state, envelope([delta({ reasoning_content: '另一个请求' })], { request_id: 'request-2' }), '2-0')).toBe(state); const child = consume(state, envelope([delta({ reasoning_content: '子线程' })], { thread_id: 'child-thread' }), '2-0'); const missing = consume(child, envelope([delta({ content: '缺身份' })], { thread_id: undefined }), '3-0'); expect(missing).toMatchObject({ reasoning: '父线程', content: '', cursor: '3-0', ended: false }); expect(consume(missing, envelope([], { thread_id: 'child-thread' }), '4-0', 'end')).toMatchObject({ ended: false }); }); it('deduplicates replayed/out-of-order message IDs with exact large Redis IDs but accepts terminal reuse', () => { const state = consume(null, envelope([delta({ reasoning_content: '一次' })]), '99999999999999999999-1'); expect(consume(state, envelope([delta({ reasoning_content: '重放' })]), '99999999999999999999-1')).toBe(state); expect(consume(state, envelope([delta({ reasoning_content: '迟到' })]), '99999999999999999998-99')).toBe(state); const next = consume(state, envelope([delta({ reasoning_content: '二次' })]), '99999999999999999999-2'); expect(consume(next, envelope([]), '99999999999999999999-2', 'end')).toMatchObject({ reasoning: '一次二次', ended: true }); }); it('keeps missing-ID compatibility and retains same-request reasoning over a resumed Run only', () => { const first = consume(null, envelope([delta({ reasoning_content: '之前', content: '旧预览' })]), ''); const next = consumeCloudChatEvent(first, envelope([delta({ message_id: 'message-2', reasoning_content: '继续', content: '新预览' })], { run_id: 'run-2' }), 'messages', '1-0', 'run-2', 'thread-1', 'request-1'); expect(next).toMatchObject({ cursor: '1-0', reasoning: '之前\n\n继续', content: '新预览', ended: false }); const other = consumeCloudChatEvent(next, envelope([delta({ reasoning_content: '新问题' })], { run_id: 'run-3', request_id: 'request-3' }), 'messages', '1-0', 'run-3', 'thread-1', 'request-3'); expect(other).toMatchObject({ reasoning: '新问题', content: '' }); }); });