import { EventType } from '@ag-ui/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { EasyFlowAguiClient } from './client'; vi.mock('#/api/request', () => ({ createEventStreamHeaders: () => ({ 'easyflow-token': 'test-token' }), resolveApiUrl: (url: string) => `http://localhost${url}`, })); function sse(events: unknown[]) { return new Response( events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(''), { headers: { 'Content-Type': 'text/event-stream' }, status: 200 }, ); } describe('easyFlowAguiClient', () => { afterEach(() => { vi.unstubAllGlobals(); }); it('trims outbound history, tools, context and state at the transport boundary', async () => { let requestBody: Record | undefined; const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { requestBody = JSON.parse(String(init?.body)); return sse([ { runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED }, { runId: 'run-1', threadId: '101', type: EventType.RUN_FINISHED, }, ]); }); vi.stubGlobal('fetch', fetchMock); const received: string[] = []; await new EasyFlowAguiClient().run({ forwardedProps: { easyflow: { input: { imageUploadIds: ['image-1'] } } }, onEvent: (event) => received.push(event.type), threadId: '101', url: '/api/v1/agent/1/agui/run', userMessage: { content: '你好', id: 'user-1', role: 'user' }, }); expect(received).toEqual([EventType.RUN_STARTED, EventType.RUN_FINISHED]); expect(requestBody).toEqual( expect.objectContaining({ context: [], messages: [{ content: '你好', id: 'user-1', role: 'user' }], state: {}, tools: [], }), ); }); it('converts proxied forwardedProps to transport JSON before the SDK clones input', async () => { let requestBody: Record | undefined; vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { requestBody = JSON.parse(String(init?.body)); return sse([ { runId: 'run-1', threadId: 'draft-1', type: EventType.RUN_STARTED }, { runId: 'run-1', threadId: 'draft-1', type: EventType.RUN_FINISHED, }, ]); }), ); const forwardedProps = new Proxy( { easyflow: { draft: { agent: { id: 'agent-1' } } } }, {}, ); await expect( new EasyFlowAguiClient().run({ forwardedProps, onEvent: () => undefined, threadId: 'draft-1', url: '/api/v1/agent/agui/run/draft', userMessage: { content: '你好', id: 'user-1', role: 'user' }, }), ).resolves.toBeUndefined(); expect(requestBody?.forwardedProps).toEqual(forwardedProps); }); it('rejects a clean EOF without RUN_FINISHED or RUN_ERROR', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => sse([{ runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED }]), ), ); await expect( new EasyFlowAguiClient().run({ onEvent: () => undefined, threadId: '101', url: '/api/v1/agent/1/agui/run', userMessage: { content: '你好', id: 'user-1', role: 'user' }, }), ).rejects.toThrow('缺少终态'); }); it('treats a standard cancelled terminal as an accepted stop', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => sse([ { runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED }, { code: 'RUN_CANCELLED', message: '用户拒绝执行', runId: 'run-1', threadId: '101', type: EventType.RUN_ERROR, }, ]), ), ); await expect( new EasyFlowAguiClient().run({ onEvent: () => undefined, threadId: '101', url: '/api/v1/agent/1/agui/run', userMessage: { content: '你好', id: 'user-1', role: 'user' }, }), ).resolves.toBeUndefined(); }); });