Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentTryoutRawRounds.test.ts
陈子默 b08eb009bb perf: 优化 Agent 流式会话处理
- 合并流式通知与持久化写入,限制浏览器会话缓存

- 增量投影轮次事件并稳定关联异步工具任务
2026-07-27 19:41:11 +08:00

399 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {beforeEach, describe, expect, it} from 'vitest';
import {useAgentTryoutRawRounds} from './useAgentTryoutRawRounds';
describe('useAgentTryoutRawRounds', () => {
beforeEach(() => {
sessionStorage.clear();
});
it('按原始事件顺序生成 timeline', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-1',
});
const roundId = store.createRound('上一轮问题');
store.recordEvent(roundId, {
domain: 'LLM',
payload: { reasoning: '先思考' },
type: 'THINKING',
});
store.recordEvent(roundId, {
domain: 'LLM',
payload: { delta: '上一轮回答' },
type: 'MESSAGE',
});
store.completeRound(roundId);
expect(store.buildTimelineItems().map((item) => item.type)).toEqual([
'message',
'message',
]);
});
it('业务引用只用于展示', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-2',
});
const roundId = store.createRound('查知识库');
store.recordEvent(roundId, {
domain: 'BUSINESS',
payload: {
items: [
{
chunkContent: '知识库原文',
chunkId: 'chunk-1',
documentId: 'doc-1',
knowledgeId: 'kb-1',
},
],
},
type: 'CITATIONS',
});
store.recordEvent(roundId, {
domain: 'LLM',
payload: { delta: '引用后的回答' },
type: 'MESSAGE',
});
store.completeRound(roundId);
expect(store.buildTimelineItems().map((item) => item.type)).toEqual([
'message',
'knowledge',
'message',
]);
});
it('AgentScope fragment 工具事件不进入页面时间线', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-fragment',
});
const roundId = store.createRound('调用内部片段');
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
input: { text: 'fragment' },
toolCallId: 'fragment-1',
toolName: '__fragment__',
},
type: 'TOOL_CALL',
});
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
output: 'internal',
toolCallId: 'fragment-1',
toolName: '__fragment__',
},
type: 'TOOL_RESULT',
});
expect(store.buildTimelineItems().map((item) => item.type)).toEqual([
'message',
]);
});
it('AgentScope context_reload 工具事件不进入页面时间线', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-context-reload',
});
const roundId = store.createRound('展开第一层');
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
input: { working_context_offload_uuid: 'context-id' },
toolCallId: 'context-reload-1',
toolName: 'context_reload',
},
type: 'TOOL_CALL',
});
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
output: 'context',
toolCallId: 'context-reload-1',
toolName: 'context_reload',
},
type: 'TOOL_RESULT',
});
expect(store.buildTimelineItems().map((item) => item.type)).toEqual([
'message',
]);
});
it('刷新后能从 raw rounds 恢复 timeline', () => {
const first = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-5',
});
const roundId = first.createRound('问题');
first.recordEvent(roundId, {
domain: 'LLM',
payload: { delta: '回答' },
type: 'MESSAGE',
});
first.completeRound(roundId);
const restored = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-5',
});
expect(restored.buildTimelineItems().map((item) => item.type)).toEqual([
'message',
'message',
]);
});
it('错误轮次不会被 completeRound 覆盖为成功状态', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-error',
});
const roundId = store.createRound('会失败的问题');
store.recordEvent(roundId, {
domain: 'LLM',
payload: { delta: '半截回答' },
type: 'MESSAGE',
});
store.recordEvent(roundId, {
domain: 'SYSTEM',
payload: { message: '调用失败' },
type: 'ERROR',
});
store.completeRound(roundId);
const assistant = store
.buildTimelineItems()
.find((item) => item.type === 'message' && item.role === 'assistant');
expect(assistant).toMatchObject({ status: 'error' });
});
it('流式重建 timeline 时保持稳定 item id', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-stable-id',
});
const roundId = store.createRound('问题');
store.recordEvent(roundId, {
domain: 'LLM',
payload: { delta: '你' },
type: 'MESSAGE',
});
const firstIds = store.buildTimelineItems().map((item) => item.id);
store.recordEvent(roundId, {
domain: 'LLM',
payload: { delta: '好' },
type: 'MESSAGE',
});
const secondIds = store.buildTimelineItems().map((item) => item.id);
expect(secondIds).toEqual(firstIds);
});
it('审批状态作为展示事件缓存并可刷新恢复', () => {
const first = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-approval',
});
const roundId = first.createRound('审批工具');
first.recordEvent(roundId, {
domain: 'TOOL',
payload: {
requestId: 'req-1',
resumeToken: 'resume-1',
toolCallId: 'call-approval',
toolName: 'dangerous_tool',
},
type: 'FORM_REQUEST',
});
first.recordEvent(roundId, {
domain: 'TOOL',
payload: {
requestId: 'req-1',
resumeToken: 'resume-1',
toolCallId: 'call-approval',
},
type: 'FORM_APPROVING',
});
first.flush();
const restored = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-approval',
});
const tool = restored
.buildTimelineItems()
.find((item) => item.type === 'tool');
expect(tool).toMatchObject({
status: 'approving',
toolCallId: 'call-approval',
});
});
it('异步工作流轮询事件始终归并到首张审批卡', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-async-tool',
});
const roundId = store.createRound('生成文档');
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
input: { user_input: '写一篇小作文' },
requestId: 'req-async',
resumeToken: 'resume-async',
toolCallId: 'submit-call-1',
toolName: '文档生成',
},
type: 'FORM_REQUEST',
});
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
asyncTool: true,
phase: 'submit',
sourceToolCallId: 'submit-call-1',
status: 'RUNNING',
taskId: 'task-1',
toolCallId: 'task-1',
toolName: '文档生成',
},
type: 'TOOL_RESULT',
});
for (const sourceToolCallId of ['observe-call-1', 'observe-call-2']) {
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
asyncTool: true,
input: { taskId: 'task-1' },
phase: 'observe',
sourceToolCallId,
status: 'RUNNING',
taskId: 'task-1',
toolCallId: 'task-1',
toolName: '文档生成',
},
type: 'TOOL_CALL',
});
}
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
asyncTool: true,
phase: 'result',
sourceToolCallId: 'result-call-1',
status: 'SUCCEEDED',
taskId: 'task-1',
toolCallId: 'task-1',
toolName: '文档生成',
},
type: 'TOOL_RESULT',
});
const tools = store
.buildTimelineItems()
.filter((item) => item.type === 'tool');
expect(tools).toHaveLength(1);
expect(tools[0]).toMatchObject({
mode: 'approval',
status: 'success',
taskId: 'task-1',
toolCallId: 'task-1',
toolName: '文档生成',
});
});
it('连续文本增量压缩后刷新内容保持一致', () => {
const sessionId = 'stream-compaction';
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId,
});
const roundId = store.createRound('你好');
const liveItems = store.buildTimelineItems();
const assistantText = (
items: ReturnType<typeof store.buildTimelineItems>,
) =>
items
.flatMap((item) =>
item.type === 'message' && item.role === 'assistant'
? item.parts
.filter((part) => part.type === 'text')
.map((part) => part.content)
: [],
)
.join('');
for (const delta of ['你', '好', '', '世界']) {
const event = store.recordEvent(roundId, {
domain: 'LLM',
payload: { delta },
type: 'MESSAGE',
});
if (!event) {
throw new Error('流式事件记录失败');
}
store.projectEvent(liveItems, roundId, event);
}
expect(store.currentVariant(roundId)?.runtimeEvents).toHaveLength(1);
expect(store.currentVariant(roundId)?.runtimeEvents[0]?.payload.delta).toBe(
'你好,世界',
);
expect(assistantText(liveItems)).toBe('你好,世界');
store.flush();
const restored = useAgentTryoutRawRounds({
mode: 'draft',
sessionId,
});
expect(assistantText(restored.buildTimelineItems())).toBe('你好,世界');
});
it('存在待持久化增量时结束事件仍立即落盘', () => {
const sessionId = 'terminal-persist';
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId,
});
const roundId = store.createRound('结束测试');
store.recordEvent(roundId, {
domain: 'LLM',
payload: { reasoning: '思考中' },
type: 'THINKING',
});
store.recordEvent(roundId, {
domain: 'SYSTEM',
payload: {},
type: 'DONE',
});
const restored = useAgentTryoutRawRounds({
mode: 'draft',
sessionId,
});
expect(restored.currentVariant(roundId)?.status).toBe('completed');
expect(
restored
.currentVariant(roundId)
?.runtimeEvents.some(
(event) => event.domain === 'SYSTEM' && event.type === 'DONE',
),
).toBe(true);
});
});