import { afterEach, describe, expect, it, vi } from 'vitest'; import { createEditorAgentConversation, deleteEditorAgentConversation, getEditorAgentConversation, listEditorAgentConversations, streamEditorAgentMessage, } from './editorAgentClient'; const requestJsonMock = vi.hoisted(() => vi.fn()); const fetchWithApiAuthMock = vi.hoisted(() => vi.fn()); vi.mock('../apiClient', () => ({ requestJson: requestJsonMock, fetchWithApiAuth: fetchWithApiAuthMock, })); function createSseResponse(payload: string) { const encoder = new TextEncoder(); return new Response( new ReadableStream({ start(controller) { controller.enqueue(encoder.encode(payload)); controller.close(); }, }), { status: 200, headers: { 'Content-Type': 'text/event-stream; charset=utf-8' }, }, ); } describe('editorAgentClient', () => { afterEach(() => { requestJsonMock.mockReset(); fetchWithApiAuthMock.mockReset(); }); it('uses the planned editor agent conversation CRUD routes', async () => { requestJsonMock .mockResolvedValueOnce({ conversations: [ { conversationId: 'conversation-1', projectId: 'project-1', title: '角色参考', createdAt: '2026-07-03T00:00:00.000Z', updatedAt: '2026-07-03T00:00:00.000Z', }, ], }) .mockResolvedValueOnce({ conversation: { conversationId: 'conversation-2', projectId: 'project-1', title: '新对话', messages: [], createdAt: '2026-07-03T00:00:00.000Z', updatedAt: '2026-07-03T00:01:00.000Z', }, }) .mockResolvedValueOnce({ conversation: { conversationId: 'conversation-1', projectId: 'project-1', title: '角色参考', messages: [], createdAt: '2026-07-03T00:00:00.000Z', updatedAt: '2026-07-03T00:02:00.000Z', }, }) .mockResolvedValueOnce({ conversation: { conversationId: 'conversation-1', projectId: 'project-1', title: '角色参考', messages: [], createdAt: '2026-07-03T00:00:00.000Z', updatedAt: '2026-07-03T00:02:00.000Z', }, }); await listEditorAgentConversations('project-1'); await createEditorAgentConversation('project-1', { title: '新对话' }); await getEditorAgentConversation('conversation-1'); await deleteEditorAgentConversation('conversation-1'); expect(requestJsonMock).toHaveBeenNthCalledWith( 1, '/api/editor/projects/project-1/agent-conversations', { method: 'GET' }, '读取画布 Agent 会话列表失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 2, '/api/editor/projects/project-1/agent-conversations', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: '新对话' }), }), '创建画布 Agent 会话失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 3, '/api/editor/agent-conversations/conversation-1', { method: 'GET' }, '读取画布 Agent 会话失败', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 4, '/api/editor/agent-conversations/conversation-1', { method: 'DELETE' }, '删除画布 Agent 会话失败', ); }); it('posts editor agent message streams and emits typed SSE events', async () => { fetchWithApiAuthMock.mockResolvedValueOnce( createSseResponse( [ 'event: stage', 'data: {"conversationId":"conversation-1","stage":"thinking"}', '', 'event: message_delta', 'data: {"conversationId":"conversation-1","messageId":"assistant-1","role":"assistant","kind":"chat","textDelta":"我先看一下"}', '', 'event: done', 'data: {"conversationId":"conversation-1","title":"生成角色"}', '', '', ].join('\n'), ), ); const events: string[] = []; await streamEditorAgentMessage( 'conversation-1', { clientMessageId: 'client-message-1', text: '帮我把角色改成像素风', attachments: [], }, { onEvent: (event) => events.push(event.event), }, ); expect(events).toEqual(['stage', 'message_delta', 'done']); expect(fetchWithApiAuthMock).toHaveBeenCalledWith( '/api/editor/agent-conversations/conversation-1/messages/stream', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientMessageId: 'client-message-1', text: '帮我把角色改成像素风', attachments: [], }), }), expect.objectContaining({ timeoutMs: 1_200_000, authImpact: 'local', }), ); }); });