845 lines
25 KiB
TypeScript
845 lines
25 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import type {
|
|
EasyFlowAguiResumeOptions,
|
|
EasyFlowAguiRunOptions,
|
|
} from '../shared/agent-agui/client';
|
|
|
|
import { useUserStore } from '@easyflow/stores';
|
|
|
|
import { EventType } from '@ag-ui/client';
|
|
import { createPinia, setActivePinia } from 'pinia';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { clearAgentChatBrowserCache } from '#/utils/agent-chat-cache';
|
|
|
|
import { easyFlowAguiCustomEvent } from '../shared/agent-agui/custom-events';
|
|
import { agentChatRuntimeManager } from './agentChatRuntimeManager';
|
|
|
|
const aguiMocks = vi.hoisted(() => ({
|
|
abort: vi.fn(),
|
|
cancel: vi.fn(async () => undefined),
|
|
detach: vi.fn(),
|
|
resume: vi.fn(),
|
|
run: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../shared/agent-agui/client', () => ({
|
|
createAguiRunId: () => 'run-test',
|
|
EasyFlowAguiHttpError: class extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly status: number,
|
|
) {
|
|
super(message);
|
|
}
|
|
},
|
|
EasyFlowAguiProjectionError: class extends Error {
|
|
constructor(public readonly projectionCause: unknown) {
|
|
super('Agent 事件投影失败');
|
|
}
|
|
},
|
|
EasyFlowAguiClient: class {
|
|
abort = aguiMocks.abort;
|
|
cancel = aguiMocks.cancel;
|
|
detach = aguiMocks.detach;
|
|
resume = aguiMocks.resume;
|
|
run = aguiMocks.run;
|
|
},
|
|
}));
|
|
|
|
vi.mock('./api', () => ({
|
|
generateAgentSessionId: vi.fn(),
|
|
}));
|
|
|
|
describe('agentChatRuntimeManager', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
sessionStorage.clear();
|
|
vi.clearAllMocks();
|
|
aguiMocks.cancel.mockResolvedValue(undefined);
|
|
aguiMocks.resume.mockImplementation(() => new Promise(() => {}));
|
|
aguiMocks.run.mockImplementation(() => new Promise(() => {}));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('发起请求后立即创建带起始时间的助手轮次', async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(12_000);
|
|
aguiMocks.run.mockImplementation(() => new Promise(() => {}));
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'timer-user',
|
|
loginName: 'timer-user',
|
|
nickname: '计时用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '计时测试',
|
|
sessionId: 'timer-session',
|
|
});
|
|
|
|
const assistant = agentChatRuntimeManager
|
|
.getSnapshot('timer-session')
|
|
?.items.find(
|
|
(item) => item.type === 'message' && item.role === 'assistant',
|
|
);
|
|
expect(assistant).toEqual(
|
|
expect.objectContaining({
|
|
roundId: expect.any(String),
|
|
status: 'streaming',
|
|
turnStartedAt: 12_000,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('投影 AG-UI 输入确认并按账号隔离快照', async () => {
|
|
let runOptions: EasyFlowAguiRunOptions | undefined;
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runOptions = options;
|
|
return new Promise(() => {});
|
|
});
|
|
const userStore = useUserStore();
|
|
const firstAccount = {
|
|
avatar: '',
|
|
id: 'user-1',
|
|
loginName: 'admin',
|
|
nickname: '管理员',
|
|
tenantId: 'tenant-1',
|
|
};
|
|
userStore.setUserInfo(firstAccount);
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
documentUploadIds: ['document-upload-1'],
|
|
documents: [{ name: 'draft.docx', status: 'ready' }],
|
|
images: [
|
|
{
|
|
name: 'draft.png',
|
|
previewUrl: '/draft.png',
|
|
status: 'ready',
|
|
},
|
|
],
|
|
prompt: '识别图片',
|
|
sessionId: '101',
|
|
});
|
|
runOptions?.onEvent({
|
|
name: easyFlowAguiCustomEvent.inputAccepted,
|
|
type: EventType.CUSTOM,
|
|
value: {
|
|
attachments: [
|
|
{
|
|
attachmentRef: 'formal:101:201:document:0',
|
|
name: 'draft.docx',
|
|
readSnapshotId: 'snapshot-1',
|
|
},
|
|
],
|
|
images: [
|
|
{
|
|
imageRef: 'formal:101:201:0:png',
|
|
name: 'draft.png',
|
|
previewUrl: '/formal.png',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const accepted = agentChatRuntimeManager.getSnapshot('101');
|
|
const userMessage = accepted?.items.find(
|
|
(item) => item.type === 'message' && item.role === 'user',
|
|
);
|
|
expect(userMessage?.type === 'message' && userMessage.images?.[0]).toEqual(
|
|
expect.objectContaining({ imageRef: 'formal:101:201:0:png' }),
|
|
);
|
|
expect(
|
|
userMessage?.type === 'message' && userMessage.documents?.[0],
|
|
).toEqual(expect.objectContaining({ readSnapshotId: 'snapshot-1' }));
|
|
expect(runOptions?.forwardedProps).toEqual(
|
|
expect.objectContaining({
|
|
easyflow: expect.objectContaining({
|
|
input: expect.objectContaining({
|
|
documentUploadIds: ['document-upload-1'],
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
|
|
userStore.setUserInfo({
|
|
...firstAccount,
|
|
id: 'user-2',
|
|
loginName: 'other',
|
|
});
|
|
expect(agentChatRuntimeManager.getSnapshot('101')).toBeUndefined();
|
|
|
|
clearAgentChatBrowserCache(firstAccount);
|
|
userStore.setUserInfo(firstAccount);
|
|
expect(agentChatRuntimeManager.getSnapshot('101')).toBeUndefined();
|
|
});
|
|
|
|
it('输入已确认后将模型错误转换为可重试的用户提示', async () => {
|
|
let resolveRun: (() => void) | undefined;
|
|
let runOptions: EasyFlowAguiRunOptions | undefined;
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runOptions = options;
|
|
return new Promise<void>((resolve) => {
|
|
resolveRun = resolve;
|
|
});
|
|
});
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'retry-user',
|
|
loginName: 'retry-user',
|
|
nickname: '重试用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '介绍文档',
|
|
sessionId: 'retry-session',
|
|
});
|
|
runOptions?.onEvent({
|
|
name: easyFlowAguiCustomEvent.inputAccepted,
|
|
type: EventType.CUSTOM,
|
|
value: {},
|
|
});
|
|
runOptions?.onEvent({
|
|
code: 'MODEL_ERROR',
|
|
message: 'Retries exhausted: 2/2',
|
|
runId: 'run-test',
|
|
threadId: 'retry-session',
|
|
type: EventType.RUN_ERROR,
|
|
});
|
|
resolveRun?.();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
const snapshot = agentChatRuntimeManager.getSnapshot('retry-session');
|
|
expect(snapshot).toEqual(
|
|
expect.objectContaining({
|
|
error: '模型连接异常',
|
|
retryContextReady: true,
|
|
terminalOutcome: 'failed',
|
|
}),
|
|
);
|
|
expect(snapshot?.items.find((item) => item.type === 'error')).toEqual(
|
|
expect.objectContaining({ message: '模型连接异常' }),
|
|
);
|
|
});
|
|
|
|
it('刷新恢复已确认输入时重新触发草稿清理回调', async () => {
|
|
const account = {
|
|
avatar: '',
|
|
id: 'accepted-restore-user',
|
|
loginName: 'accepted-restore-user',
|
|
nickname: '输入恢复用户',
|
|
tenantId: 'tenant-1',
|
|
};
|
|
useUserStore().setUserInfo(account);
|
|
const sessionId = 'accepted-restore-session';
|
|
sessionStorage.setItem(
|
|
`easyflow:agent-chat-runtime:tenant-1:accepted-restore-user:${sessionId}`,
|
|
JSON.stringify({
|
|
agentId: 'agent-1',
|
|
completed: false,
|
|
inputAccepted: true,
|
|
items: [],
|
|
prompt: '刷新前输入',
|
|
runId: 'run-accepted-restore',
|
|
roundId: 'round-accepted-restore',
|
|
sending: true,
|
|
sessionId,
|
|
startedAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
version: 4,
|
|
}),
|
|
);
|
|
aguiMocks.resume.mockImplementation(() => new Promise(() => {}));
|
|
const onInputAccepted = vi.fn();
|
|
|
|
agentChatRuntimeManager.resume(sessionId, onInputAccepted);
|
|
|
|
expect(onInputAccepted).toHaveBeenCalledTimes(1);
|
|
expect(aguiMocks.resume).toHaveBeenCalledWith(
|
|
expect.objectContaining({ runId: 'run-accepted-restore' }),
|
|
);
|
|
clearAgentChatBrowserCache(account);
|
|
});
|
|
|
|
it('硬刷新后从持久化游标续传且保留已投影正文', async () => {
|
|
const account = {
|
|
avatar: '',
|
|
id: 'cursor-restore-user',
|
|
loginName: 'cursor-restore-user',
|
|
nickname: '游标恢复用户',
|
|
tenantId: 'tenant-1',
|
|
};
|
|
useUserStore().setUserInfo(account);
|
|
const sessionId = 'cursor-restore-session';
|
|
sessionStorage.setItem(
|
|
`easyflow:agent-chat-runtime:tenant-1:cursor-restore-user:${sessionId}`,
|
|
JSON.stringify({
|
|
agentId: 'agent-1',
|
|
completed: false,
|
|
inputAccepted: false,
|
|
items: [
|
|
{
|
|
id: 'assistant-cursor',
|
|
parts: [
|
|
{
|
|
content: '刷新前正文',
|
|
id: 'text-cursor',
|
|
type: 'text',
|
|
},
|
|
],
|
|
role: 'assistant',
|
|
roundId: 'round-cursor',
|
|
status: 'streaming',
|
|
type: 'message',
|
|
},
|
|
],
|
|
lastCursor: 9,
|
|
prompt: '游标恢复',
|
|
projectionToolArgs: {},
|
|
projectionToolNames: {},
|
|
runId: 'run-cursor-restore',
|
|
roundId: 'round-cursor',
|
|
sending: true,
|
|
sessionId,
|
|
startedAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
version: 5,
|
|
}),
|
|
);
|
|
aguiMocks.resume.mockImplementationOnce(
|
|
async (options: EasyFlowAguiResumeOptions) => {
|
|
expect(options.afterCursor).toBe(9);
|
|
options.onEvent({
|
|
delta: '续传正文',
|
|
messageId: 'assistant-cursor',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
options.onCursor?.(10);
|
|
options.onEvent({
|
|
runId: 'run-cursor-restore',
|
|
threadId: sessionId,
|
|
type: EventType.RUN_FINISHED,
|
|
});
|
|
options.onCursor?.(11);
|
|
},
|
|
);
|
|
|
|
agentChatRuntimeManager.resume(sessionId);
|
|
|
|
expect(aguiMocks.resume).toHaveBeenCalledTimes(1);
|
|
expect(aguiMocks.resume).toHaveBeenCalledWith(
|
|
expect.objectContaining({ afterCursor: 9 }),
|
|
);
|
|
|
|
await vi.waitFor(() => {
|
|
expect(agentChatRuntimeManager.getSnapshot(sessionId)).toEqual(
|
|
expect.objectContaining({ completed: true, lastCursor: 11 }),
|
|
);
|
|
});
|
|
const timeline = JSON.stringify(
|
|
agentChatRuntimeManager.getSnapshot(sessionId)?.items,
|
|
);
|
|
expect(timeline).toContain('刷新前正文续传正文');
|
|
clearAgentChatBrowserCache(account);
|
|
});
|
|
|
|
it('短暂断网超过旧重试窗口后仍按游标恢复到服务端终态', async () => {
|
|
vi.useFakeTimers();
|
|
const account = {
|
|
avatar: '',
|
|
id: 'reconnect-user',
|
|
loginName: 'reconnect-user',
|
|
nickname: '重连用户',
|
|
tenantId: 'tenant-1',
|
|
};
|
|
useUserStore().setUserInfo(account);
|
|
aguiMocks.run.mockRejectedValueOnce(new TypeError('Failed to fetch'));
|
|
aguiMocks.resume
|
|
.mockRejectedValueOnce(new TypeError('Network unavailable'))
|
|
.mockRejectedValueOnce(new TypeError('Network unavailable'))
|
|
.mockImplementationOnce(async (options: EasyFlowAguiResumeOptions) => {
|
|
await options.onEvent({
|
|
runId: 'run-test',
|
|
threadId: 'reconnect-session',
|
|
type: EventType.RUN_FINISHED,
|
|
});
|
|
});
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '断网恢复',
|
|
sessionId: 'reconnect-session',
|
|
});
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
|
|
expect(aguiMocks.resume).toHaveBeenCalledTimes(3);
|
|
expect(agentChatRuntimeManager.getSnapshot('reconnect-session')).toEqual(
|
|
expect.objectContaining({ completed: true, sending: false }),
|
|
);
|
|
clearAgentChatBrowserCache(account);
|
|
});
|
|
|
|
it('合并流式通知并在 AG-UI 终态立即持久化', async () => {
|
|
vi.useFakeTimers();
|
|
let runOptions: EasyFlowAguiRunOptions | undefined;
|
|
let resolveRun: (() => void) | undefined;
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runOptions = options;
|
|
return new Promise<void>((resolve) => {
|
|
resolveRun = resolve;
|
|
});
|
|
});
|
|
const account = {
|
|
avatar: '',
|
|
id: 'stream-user',
|
|
loginName: 'stream-user',
|
|
nickname: '流式用户',
|
|
tenantId: 'tenant-1',
|
|
};
|
|
useUserStore().setUserInfo(account);
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '流式测试',
|
|
sessionId: 'stream-session',
|
|
});
|
|
const listener = vi.fn();
|
|
const unsubscribe = agentChatRuntimeManager.subscribe(listener);
|
|
const storageSpy = vi.spyOn(sessionStorage, 'setItem');
|
|
|
|
for (const delta of ['A', 'B', 'C']) {
|
|
runOptions?.onEvent({
|
|
delta,
|
|
messageId: 'assistant-1',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
}
|
|
expect(listener).not.toHaveBeenCalled();
|
|
expect(
|
|
JSON.stringify(
|
|
agentChatRuntimeManager.getSnapshot('stream-session')?.items,
|
|
),
|
|
).toContain('ABC');
|
|
|
|
await vi.advanceTimersByTimeAsync(50);
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
await vi.advanceTimersByTimeAsync(250);
|
|
expect(storageSpy).toHaveBeenCalledTimes(2);
|
|
|
|
listener.mockClear();
|
|
storageSpy.mockClear();
|
|
runOptions?.onEvent({
|
|
runId: 'run-1',
|
|
threadId: 'stream-session',
|
|
type: EventType.RUN_FINISHED,
|
|
});
|
|
resolveRun?.();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(storageSpy).toHaveBeenCalledTimes(2);
|
|
expect(agentChatRuntimeManager.getSnapshot('stream-session')).toEqual(
|
|
expect.objectContaining({ completed: true, sending: false }),
|
|
);
|
|
|
|
unsubscribe();
|
|
storageSpy.mockRestore();
|
|
clearAgentChatBrowserCache(account);
|
|
});
|
|
|
|
it('工具开始事件立即通知页面并保留调用中快照', async () => {
|
|
vi.useFakeTimers();
|
|
let runOptions: EasyFlowAguiRunOptions | undefined;
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runOptions = options;
|
|
return new Promise(() => {});
|
|
});
|
|
const account = {
|
|
avatar: '',
|
|
id: 'tool-status-user',
|
|
loginName: 'tool-status-user',
|
|
nickname: '工具状态用户',
|
|
tenantId: 'tenant-1',
|
|
};
|
|
useUserStore().setUserInfo(account);
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '生成文件',
|
|
sessionId: 'tool-status-session',
|
|
});
|
|
const listener = vi.fn();
|
|
const unsubscribe = agentChatRuntimeManager.subscribe(listener);
|
|
|
|
runOptions?.onEvent({
|
|
toolCallId: 'write-call-1',
|
|
toolCallName: 'write_text_file',
|
|
type: EventType.TOOL_CALL_START,
|
|
});
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(
|
|
agentChatRuntimeManager
|
|
.getSnapshot('tool-status-session')
|
|
?.items.find(
|
|
(item) => item.type === 'tool' && item.toolCallId === 'write-call-1',
|
|
),
|
|
).toEqual(expect.objectContaining({ status: 'running' }));
|
|
|
|
unsubscribe();
|
|
clearAgentChatBrowserCache(account);
|
|
});
|
|
|
|
it('停止后同会话重发不会接收旧运行的迟到事件', async () => {
|
|
const runs: EasyFlowAguiRunOptions[] = [];
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runs.push(options);
|
|
return new Promise(() => {});
|
|
});
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'race-user',
|
|
loginName: 'race-user',
|
|
nickname: '竞态用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
aguiMocks.resume.mockImplementationOnce(
|
|
async (options: EasyFlowAguiResumeOptions) => {
|
|
options.onEvent({
|
|
code: 'RUN_CANCELLED',
|
|
message: '用户已停止生成',
|
|
runId: 'run-test',
|
|
threadId: 'race-session',
|
|
type: EventType.RUN_ERROR,
|
|
});
|
|
},
|
|
);
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '旧问题',
|
|
sessionId: 'race-session',
|
|
});
|
|
await agentChatRuntimeManager.stop('race-session');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '新问题',
|
|
sessionId: 'race-session',
|
|
});
|
|
|
|
runs[0]?.onEvent({
|
|
delta: '旧流迟到正文',
|
|
messageId: 'old-assistant',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
runs[1]?.onEvent({
|
|
delta: '新流正文',
|
|
messageId: 'new-assistant',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
|
|
const snapshot = JSON.stringify(
|
|
agentChatRuntimeManager.getSnapshot('race-session')?.items,
|
|
);
|
|
expect(snapshot).toContain('新流正文');
|
|
expect(snapshot).not.toContain('旧流迟到正文');
|
|
});
|
|
|
|
it('用户取消的 AG-UI 终态不会被异步完成回调改写为成功', async () => {
|
|
let runOptions: EasyFlowAguiRunOptions | undefined;
|
|
let resolveRun: (() => void) | undefined;
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runOptions = options;
|
|
return new Promise<void>((resolve) => {
|
|
resolveRun = resolve;
|
|
});
|
|
});
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'cancel-user',
|
|
loginName: 'cancel-user',
|
|
nickname: '取消用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '需要审批的任务',
|
|
sessionId: 'cancel-session',
|
|
});
|
|
runOptions?.onEvent({
|
|
delta: '准备执行',
|
|
messageId: 'assistant-1',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
runOptions?.onEvent({
|
|
code: 'RUN_CANCELLED',
|
|
message: '用户拒绝执行',
|
|
runId: 'run-1',
|
|
threadId: 'cancel-session',
|
|
type: EventType.RUN_ERROR,
|
|
});
|
|
resolveRun?.();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
const assistant = agentChatRuntimeManager
|
|
.getSnapshot('cancel-session')
|
|
?.items.find(
|
|
(item) => item.type === 'message' && item.role === 'assistant',
|
|
);
|
|
expect(assistant).toEqual(
|
|
expect.objectContaining({
|
|
turnSucceeded: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('取消请求失败且原流无终态时保留等待期间事件并立即恢复订阅', async () => {
|
|
let rejectCancel: ((error: Error) => void) | undefined;
|
|
let resolveRun: (() => void) | undefined;
|
|
let runOptions: EasyFlowAguiRunOptions | undefined;
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runOptions = options;
|
|
return new Promise<void>((resolve) => {
|
|
resolveRun = resolve;
|
|
});
|
|
});
|
|
aguiMocks.detach.mockImplementationOnce(() => resolveRun?.());
|
|
aguiMocks.cancel.mockImplementation(
|
|
() =>
|
|
new Promise<undefined>((_resolve, reject) => {
|
|
rejectCancel = reject;
|
|
}),
|
|
);
|
|
aguiMocks.resume.mockImplementation(() => new Promise(() => {}));
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'cancel-failed-user',
|
|
loginName: 'cancel-failed-user',
|
|
nickname: '取消失败用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '取消失败恢复',
|
|
sessionId: 'cancel-failed-session',
|
|
});
|
|
|
|
const stopPromise = agentChatRuntimeManager.stop('cancel-failed-session');
|
|
runOptions?.onEvent({
|
|
delta: '取消等待期间正文',
|
|
messageId: 'assistant-cancel-failed',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
runOptions?.onCursor?.(1);
|
|
rejectCancel?.(new Error('取消接口暂时不可用'));
|
|
|
|
await expect(stopPromise).rejects.toThrow('取消接口暂时不可用');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
expect(
|
|
JSON.stringify(
|
|
agentChatRuntimeManager.getSnapshot('cancel-failed-session')?.items,
|
|
),
|
|
).toContain('取消等待期间正文');
|
|
expect(
|
|
agentChatRuntimeManager.getSnapshot('cancel-failed-session'),
|
|
).toEqual(expect.objectContaining({ sending: true }));
|
|
expect(aguiMocks.resume).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('取消成功后重放服务端终态且不把尚未投影的自然完成改成取消', async () => {
|
|
aguiMocks.resume.mockImplementationOnce(
|
|
async (options: EasyFlowAguiResumeOptions) => {
|
|
options.onEvent({
|
|
runId: 'run-test',
|
|
threadId: 'cancel-authoritative-session',
|
|
type: EventType.RUN_FINISHED,
|
|
});
|
|
options.onCursor?.(8);
|
|
},
|
|
);
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'cancel-authoritative-user',
|
|
loginName: 'cancel-authoritative-user',
|
|
nickname: '取消终态仲裁用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '取消时服务端已经自然完成',
|
|
sessionId: 'cancel-authoritative-session',
|
|
});
|
|
|
|
await agentChatRuntimeManager.stop('cancel-authoritative-session');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
expect(
|
|
agentChatRuntimeManager.getSnapshot('cancel-authoritative-session'),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
completed: true,
|
|
lastCursor: 8,
|
|
sending: false,
|
|
terminalOutcome: 'success',
|
|
}),
|
|
);
|
|
expect(
|
|
JSON.stringify(
|
|
agentChatRuntimeManager.getSnapshot('cancel-authoritative-session')
|
|
?.items,
|
|
),
|
|
).not.toContain('"turnSucceeded":false');
|
|
});
|
|
|
|
it('取消请求失败但原流已到终态时直接展示权威完成态', async () => {
|
|
let rejectCancel: ((error: Error) => void) | undefined;
|
|
let resolveRun: (() => void) | undefined;
|
|
let runOptions: EasyFlowAguiRunOptions | undefined;
|
|
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
|
runOptions = options;
|
|
return new Promise<void>((resolve) => {
|
|
resolveRun = resolve;
|
|
});
|
|
});
|
|
aguiMocks.cancel.mockImplementation(
|
|
() =>
|
|
new Promise<undefined>((_resolve, reject) => {
|
|
rejectCancel = reject;
|
|
}),
|
|
);
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'cancel-terminal-user',
|
|
loginName: 'cancel-terminal-user',
|
|
nickname: '取消终态用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '取消与完成竞态',
|
|
sessionId: 'cancel-terminal-session',
|
|
});
|
|
|
|
const stopPromise = agentChatRuntimeManager.stop('cancel-terminal-session');
|
|
runOptions?.onEvent({
|
|
runId: 'run-test',
|
|
threadId: 'cancel-terminal-session',
|
|
type: EventType.RUN_FINISHED,
|
|
});
|
|
runOptions?.onCursor?.(1);
|
|
resolveRun?.();
|
|
await Promise.resolve();
|
|
rejectCancel?.(new Error('取消接口晚到失败'));
|
|
|
|
await expect(stopPromise).rejects.toThrow('取消接口晚到失败');
|
|
expect(
|
|
agentChatRuntimeManager.getSnapshot('cancel-terminal-session'),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
completed: true,
|
|
sending: false,
|
|
terminalOutcome: 'success',
|
|
}),
|
|
);
|
|
expect(aguiMocks.resume).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('首条传输闪断后按最后游标恢复且不会重复投影', async () => {
|
|
aguiMocks.run.mockImplementation(
|
|
async (options: EasyFlowAguiRunOptions) => {
|
|
options.onEvent({
|
|
delta: '前半段',
|
|
messageId: 'assistant-1',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
options.onCursor?.(7);
|
|
throw new Error('network reset');
|
|
},
|
|
);
|
|
aguiMocks.resume.mockImplementation(
|
|
async (options: EasyFlowAguiResumeOptions) => {
|
|
expect(options.afterCursor).toBe(7);
|
|
options.onEvent({
|
|
delta: '后半段',
|
|
messageId: 'assistant-1',
|
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
});
|
|
options.onEvent({
|
|
runId: 'run-test',
|
|
threadId: 'recover-session',
|
|
type: EventType.RUN_FINISHED,
|
|
});
|
|
},
|
|
);
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'recover-user',
|
|
loginName: 'recover-user',
|
|
nickname: '恢复用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '恢复测试',
|
|
sessionId: 'recover-session',
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(aguiMocks.resume).toHaveBeenCalledTimes(1);
|
|
expect(agentChatRuntimeManager.getSnapshot('recover-session')).toEqual(
|
|
expect.objectContaining({ completed: true, sending: false }),
|
|
);
|
|
});
|
|
|
|
const timeline = JSON.stringify(
|
|
agentChatRuntimeManager.getSnapshot('recover-session')?.items,
|
|
);
|
|
expect(timeline).toContain('前半段后半段');
|
|
expect(timeline.match(/前半段/g)).toHaveLength(1);
|
|
});
|
|
|
|
it('pagehide 只断开页面传输并保留可恢复运行态', async () => {
|
|
aguiMocks.run.mockImplementation(() => new Promise(() => {}));
|
|
aguiMocks.resume.mockImplementation(() => new Promise(() => {}));
|
|
useUserStore().setUserInfo({
|
|
avatar: '',
|
|
id: 'reload-user',
|
|
loginName: 'reload-user',
|
|
nickname: '刷新用户',
|
|
tenantId: 'tenant-1',
|
|
});
|
|
await agentChatRuntimeManager.start({
|
|
agentId: 'agent-1',
|
|
prompt: '刷新测试',
|
|
sessionId: 'reload-session',
|
|
});
|
|
|
|
globalThis.dispatchEvent(new Event('pagehide'));
|
|
|
|
expect(aguiMocks.detach).toHaveBeenCalled();
|
|
expect(agentChatRuntimeManager.getSnapshot('reload-session')).toEqual(
|
|
expect.objectContaining({ completed: false, sending: true }),
|
|
);
|
|
|
|
globalThis.dispatchEvent(new Event('pageshow'));
|
|
expect(aguiMocks.resume).toHaveBeenCalled();
|
|
});
|
|
});
|