| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- import { Message } from './api/models';
- import { isMergeableBody, mergeConsecutiveBodies } from './message-merge';
- function m(
- id: string,
- sender: 'me' | 'agent',
- text: string,
- extra: Partial<Message> = {},
- ): Message {
- return {
- id,
- conversationId: 'c',
- sender,
- kind: 'text',
- text,
- ts: new Date(Date.UTC(2026, 10, 6, 10, 0, Number(id.replace(/\D/g, '')) || 0)).toISOString(),
- state: 'sent',
- ...extra,
- };
- }
- describe('mergeConsecutiveBodies', () => {
- it('连续的 agent 正文合成一条', () => {
- const out = mergeConsecutiveBodies([m('a1', 'agent', '第一段'), m('a2', 'agent', '第二段')]);
- expect(out.length).toBe(1);
- expect(out[0].text).toBe('第一段\n\n第二段');
- expect(out[0].mergedIds).toEqual(['a1', 'a2']);
- });
- it('✅ Markdown 分段用空行分隔(不会糊成一句)', () => {
- const out = mergeConsecutiveBodies([m('a1', 'agent', '## 标题'), m('a2', 'agent', '- 列表项')]);
- expect(out[0].text).toBe('## 标题\n\n- 列表项');
- });
- it('用户消息断开合并', () => {
- const out = mergeConsecutiveBodies([
- m('a1', 'agent', '回答一'),
- m('u1', 'me', '追问'),
- m('a2', 'agent', '回答二'),
- ]);
- expect(out.map((x) => x.id)).toEqual(['a1', 'u1', 'a2']);
- });
- it('🔴 中间夹工具过程时断开', () => {
- const out = mergeConsecutiveBodies([
- m('a1', 'agent', '先说结论'),
- m('t1', 'agent', '💻 terminal: ls'),
- m('a2', 'agent', '再说细节'),
- ]);
- expect(out.length).toBe(3);
- expect(out[1].text).toContain('💻');
- });
- it('🔴 带附件的消息不并入正文', () => {
- const out = mergeConsecutiveBodies([
- m('a1', 'agent', '看这个文件'),
- m('a2', 'agent', '', {
- file: { kind: 'file', name: 'r.pdf', size: 1, mimeType: 'application/pdf', url: 'u' },
- }),
- m('a3', 'agent', '还有补充'),
- ]);
- expect(out.length).toBe(3);
- expect(out[1].file?.name).toBe('r.pdf');
- });
- it('🔴 失败消息不合并(保留重试与错误原因)', () => {
- const out = mergeConsecutiveBodies([
- m('a1', 'agent', 'ok'),
- m('a2', 'agent', '失败的内容', { state: 'failed', error: '超时' }),
- ]);
- expect(out.length).toBe(2);
- expect(out[1].error).toBe('超时');
- });
- it('🔴 群聊:不同发送者的相邻消息不合并', () => {
- const out = mergeConsecutiveBodies([
- m('a1', 'agent', 'A 说的', { senderId: '@a:x' }),
- m('a2', 'agent', 'B 说的', { senderId: '@b:x' }),
- ]);
- expect(out.length).toBe(2);
- });
- it('同一发送者(senderId 相同)仍合并', () => {
- const out = mergeConsecutiveBodies([
- m('a1', 'agent', '一段', { senderId: '@a:x' }),
- m('a2', 'agent', '二段', { senderId: '@a:x' }),
- ]);
- expect(out.length).toBe(1);
- });
- it('合并后时间取最后一段', () => {
- const out = mergeConsecutiveBodies([m('a1', 'agent', 'x'), m('a5', 'agent', 'y')]);
- expect(out[0].ts).toBe(m('a5', 'agent', 'y').ts);
- });
- it('合并后 @ 列表取并集去重', () => {
- const out = mergeConsecutiveBodies([
- m('a1', 'agent', 'x', { mentions: ['@p1:x', '@p2:x'] }),
- m('a2', 'agent', 'y', { mentions: ['@p2:x'] }),
- ]);
- expect(out[0].mentions).toEqual(['@p1:x', '@p2:x']);
- });
- it('单条消息原样返回(补 mergedIds)', () => {
- const out = mergeConsecutiveBodies([m('a1', 'agent', '只有一条')]);
- expect(out.length).toBe(1);
- expect(out[0].mergedIds).toEqual(['a1']);
- });
- it('空列表', () => {
- expect(mergeConsecutiveBodies([])).toEqual([]);
- });
- it('空白正文不参与合并', () => {
- const out = mergeConsecutiveBodies([m('a1', 'agent', '有内容'), m('a2', 'agent', ' ')]);
- expect(out.length).toBe(1);
- expect(out[0].text).toBe('有内容');
- });
- });
- describe('isMergeableBody', () => {
- it('普通 agent 正文可合并', () => {
- expect(isMergeableBody(m('a1', 'agent', 'x'))).toBe(true);
- });
- it('用户消息不可合并', () => {
- expect(isMergeableBody(m('u1', 'me', 'x'))).toBe(false);
- });
- it('空正文不可合并', () => {
- expect(isMergeableBody(m('a1', 'agent', ''))).toBe(false);
- });
- it('带任务 id 不可合并', () => {
- expect(isMergeableBody(m('a1', 'agent', 'x', { taskId: 't1' }))).toBe(false);
- });
- });
- /*
- * 🔴 2026-11-10 移动端实测:容器用多行 ✅/❌ 列表输出结论时,
- * 每条各自成块 → 一屏出现一堆小卡片。结果 emoji 应作为正文参与合并。
- */
- describe('结果 emoji 行参与合并', () => {
- const agent = (id: string, text: string, ts: number) => ({
- id,
- conversationId: 'c1',
- sender: 'agent' as const,
- kind: 'text' as const,
- text,
- ts: new Date(ts).toISOString(),
- state: 'sent' as const,
- });
- it('连续的 ✅/❌ 结论行合并成一段', () => {
- const out = mergeConsecutiveBodies([
- agent('a', '✅ 成果表有记录(05:34:19Z)', 1000),
- agent('b', '❌ 但回复正文没有任何 https', 2000),
- agent('c', '❌ 正文还有残留:「本地路径」', 3000),
- ]);
- expect(out.length).toBe(1);
- expect(out[0].text).toContain('成果表有记录');
- // 合并后用空行分隔(mergeBodies 的既定规则)
- expect(out[0].text).toContain('https');
- expect(out[0].text).toContain('本地路径');
- });
- it('纯工具 emoji 行仍然不参与合并(保持原行为)', () => {
- const out = mergeConsecutiveBodies([
- agent('a', '💻 terminal', 1000),
- agent('b', '🔎 Searching files', 2000),
- ]);
- // 不可合并的消息**原样保留**(不丢弃),所以仍是 2 条
- expect(out.length).toBe(2);
- expect(out[0].mergedIds).toEqual(['a']);
- expect(out[1].mergedIds).toEqual(['b']);
- });
- it('正文与结论混排也合并', () => {
- const out = mergeConsecutiveBodies([
- agent('a', '我来检查一下结果。', 1000),
- agent('b', '✅ 全部通过', 2000),
- ]);
- expect(out.length).toBe(1);
- });
- });
|