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, ) => 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); }); });