agentChatEvents.test.js
5.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* 回归测试:9b1c3f1 修的"多聊天串台"不变式(40 号文档方案三)。
*
* 不变式:handleEvent 收到的每一条事件都要无条件写进它自己的 st(每个 agent 一份),
* 但只有在事件所属的 requestAgentId 仍等于"用户当前选中的 agent"时,才允许触碰
* 全局副作用(路由跳转 / 对话列表 / DOM 滚动)。这条测试不需要挂载整个 Vue 组件——
* handleEvent 已经被抽成不依赖组件实例的纯函数(agentChatEvents.js),直接喂事件、
* 断言 st 内容和副作用回调的调用次数即可。
*/
import { describe, it, expect, vi } from 'vitest';
import { createAgentChatEventHandler, createTypewriterQueue, flushTypewriter } from './agentChatEvents.js';
function makeState() {
return {
messages: [], activeConvId: null,
sending: false, streaming: false, streamContent: '',
subAgents: {}, subLogList: [], acpLog: [],
thinkingText: '', thinkingContent: '', showThinking: true,
};
}
describe('agentChatEvents · 串台加固', () => {
it('切走之后,旧 agent 的事件继续更新它自己的 st,但不再触发全局副作用', () => {
let current = 'agent-A';
const scrollToBottom = vi.fn();
const scrollAcpLog = vi.fn();
const onNewConversation = vi.fn();
const handleEvent = createAgentChatEventHandler({
getCurrentAgentId: () => current,
onNewConversation,
scrollToBottom,
scrollAcpLog,
scrollSubLogs: vi.fn(),
pushChunk: (st, text) => { st.streamContent += text; }, // 跳过打字机定时器,测同步内容
});
const stA = makeState();
handleEvent(stA, 'agent-A', 'session', { conversationId: 'conv-A' });
handleEvent(stA, 'agent-A', 'agent_output', { message: '你好' });
expect(scrollToBottom).toHaveBeenCalledTimes(1); // 只有 agent_output 会调 scrollToBottom
expect(onNewConversation).toHaveBeenCalledTimes(1); // session 走的是 onNewConversation
// 用户切到另一个 agent
current = 'agent-B';
scrollToBottom.mockClear();
scrollAcpLog.mockClear();
handleEvent(stA, 'agent-A', 'agent_output', { message: ',世界' });
handleEvent(stA, 'agent-A', 'tool_call', { tool: 'bash' });
// st 照常被写:内容没丢
expect(stA.streamContent).toBe('你好,世界');
expect(stA.acpLog).toHaveLength(1);
// 但不再是当前 agent,全局副作用一次都不该触发
expect(scrollToBottom).not.toHaveBeenCalled();
expect(scrollAcpLog).not.toHaveBeenCalled();
// 切回来之后,新事件恢复触发副作用,且历史内容完好
current = 'agent-A';
handleEvent(stA, 'agent-A', 'tool_call', { tool: 'grep' });
expect(scrollAcpLog).toHaveBeenCalledTimes(1);
expect(stA.acpLog).toHaveLength(2);
});
it('两个 agent 同时在跑,事件不会互相串台到对方的 st 里', () => {
let current = 'agent-A';
const handleEvent = createAgentChatEventHandler({
getCurrentAgentId: () => current,
onNewConversation: vi.fn(),
scrollToBottom: vi.fn(),
scrollAcpLog: vi.fn(),
scrollSubLogs: vi.fn(),
pushChunk: (st, text) => { st.streamContent += text; },
});
const stA = makeState();
const stB = makeState();
// 交替喂 A、B 两条流的事件,模拟后台并发
handleEvent(stA, 'agent-A', 'agent_output', { message: 'A1' });
handleEvent(stB, 'agent-B', 'agent_output', { message: 'B1' });
current = 'agent-B'; // 用户切到 B
handleEvent(stA, 'agent-A', 'agent_output', { message: 'A2' });
handleEvent(stB, 'agent-B', 'agent_output', { message: 'B2' });
expect(stA.streamContent).toBe('A1A2');
expect(stB.streamContent).toBe('B1B2');
});
});
describe('agentChatEvents · 打字机渲染缓冲', () => {
it('文本按固定节奏匀速吐出,而不是一次性整段落地', () => {
vi.useFakeTimers();
try {
const st = makeState();
const pushChunk = createTypewriterQueue({ tickMs: 10, charsPerTick: 2 });
pushChunk(st, 'abcdef');
expect(st.streamContent).toBe(''); // 入队即刻不落地,等定时器吐
vi.advanceTimersByTime(10);
expect(st.streamContent).toBe('ab');
vi.advanceTimersByTime(10);
expect(st.streamContent).toBe('abcd');
vi.advanceTimersByTime(10);
expect(st.streamContent).toBe('abcdef');
vi.advanceTimersByTime(10); // 队列空了,定时器该自己停
expect(st._typeTimer).toBeNull();
} finally {
vi.useRealTimers();
}
});
it('flushTypewriter 立刻吐光剩余队列(打断/结束时用)', () => {
vi.useFakeTimers();
try {
const st = makeState();
const pushChunk = createTypewriterQueue({ tickMs: 10, charsPerTick: 1 });
pushChunk(st, 'hello');
vi.advanceTimersByTime(10); // 只吐了 1 个字符
expect(st.streamContent).toBe('h');
flushTypewriter(st);
expect(st.streamContent).toBe('hello');
expect(st._typeTimer).toBeNull();
expect(st._typeQueue).toBe('');
} finally {
vi.useRealTimers();
}
});
});