399 lines
10 KiB
TypeScript
399 lines
10 KiB
TypeScript
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);
|
||
});
|
||
});
|