Files
makelore/tests/unit/cloud-chat-stream.test.ts
T

55 lines
4.3 KiB
TypeScript

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: '' });
});
});