/* @vitest-environment jsdom */ import { act, render, screen, waitFor } from '@testing-library/react'; import { useEffect, useRef, useState } from 'react'; import { expect, test, vi } from 'vitest'; import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController'; type TestSession = { sessionId: string; messages: Array<{ id: string; role: string; kind?: string; text: string; createdAt?: string; }>; }; type TestMessagePayload = { clientMessageId: string; text: string; }; function TestHarness({ streamMessage, }: { streamMessage: ( sessionId: string, payload: TestMessagePayload, options?: { onUpdate?: (text: string) => void }, ) => Promise; }) { const hasOpenedRef = useRef(false); const flow = usePlatformCreationAgentFlowController< TestSession, Record, { session: TestSession }, TestMessagePayload, { action: string }, { session: TestSession } >({ client: { createSession: async () => ({ session: { sessionId: 'session-1', messages: [], }, }), getSession: async () => ({ session: { sessionId: 'session-1', messages: [], }, }), streamMessage, executeAction: async () => ({ session: { sessionId: 'session-1', messages: [], }, }), selectSession: (response) => response.session, }, createPayload: {}, workspaceStage: 'match3d-agent-workspace', resultStage: 'match3d-result', platformStage: 'platform', isCompileAction: () => false, resolveErrorMessage: (error, fallback) => error instanceof Error ? error.message : fallback, errorMessages: { open: '打开失败', restoreMissingSession: '缺少会话', restore: '恢复失败', submit: '发送失败', execute: '执行失败', }, enterCreateTab: () => {}, setSelectionStage: () => {}, }); useEffect(() => { if (hasOpenedRef.current) { return; } hasOpenedRef.current = true; void flow.openWorkspace({}); }, [flow]); return (
{flow.session?.messages.map((message) => (
{`${message.role}:${message.kind}:${message.text}`}
))}
{flow.error ?
{flow.error}
: null}
); } type ActionTestSession = TestSession & { draft?: { profileId: string } | null; }; function ActionErrorHarness({ onActionError, }: { onActionError: (params: { session: ActionTestSession; setSession: (session: ActionTestSession) => void; }) => void; }) { const [stage, setStage] = useState('platform'); const hasOpenedRef = useRef(false); const flow = usePlatformCreationAgentFlowController< ActionTestSession, Record, { session: ActionTestSession }, TestMessagePayload, { action: string }, { session: ActionTestSession } >({ client: { createSession: async () => ({ session: { sessionId: 'session-1', messages: [], draft: { profileId: 'profile-draft-1' }, }, }), getSession: async () => ({ session: { sessionId: 'session-1', messages: [], draft: { profileId: 'profile-draft-1' }, }, }), streamMessage: async () => ({ sessionId: 'session-1', messages: [], draft: { profileId: 'profile-draft-1' }, }), executeAction: async () => { throw new Error('模型生成失败'); }, selectSession: (response) => response.session, }, createPayload: {}, workspaceStage: 'match3d-agent-workspace', resultStage: 'match3d-result', platformStage: 'platform', isCompileAction: () => true, resolveErrorMessage: (error, fallback) => error instanceof Error ? error.message : fallback, errorMessages: { open: '打开失败', restoreMissingSession: '缺少会话', restore: '恢复失败', submit: '发送失败', execute: '执行失败', }, enterCreateTab: () => {}, setSelectionStage: setStage, onActionError: ({ session, setSession }) => { onActionError({ session, setSession }); }, }); useEffect(() => { if (hasOpenedRef.current) { return; } hasOpenedRef.current = true; void flow.openWorkspace({}); }, [flow]); return (
{stage}
{flow.session?.draft?.profileId ?? 'missing'}
{flow.error ?
{flow.error}
: null}
); } function ActionCompleteHarness({ onActionComplete, }: { onActionComplete: () => { openResult?: boolean } | void; }) { const [stage, setStage] = useState('platform'); const hasOpenedRef = useRef(false); const flow = usePlatformCreationAgentFlowController< ActionTestSession, Record, { session: ActionTestSession }, TestMessagePayload, { action: string }, { session: ActionTestSession } >({ client: { createSession: async () => ({ session: { sessionId: 'session-1', messages: [], draft: { profileId: 'profile-draft-1' }, }, }), getSession: async () => ({ session: { sessionId: 'session-1', messages: [], draft: { profileId: 'profile-draft-1' }, }, }), streamMessage: async () => ({ sessionId: 'session-1', messages: [], draft: { profileId: 'profile-draft-1' }, }), executeAction: async () => ({ session: { sessionId: 'session-1', messages: [], draft: { profileId: 'profile-ready-1' }, }, }), selectSession: (response) => response.session, }, createPayload: {}, workspaceStage: 'match3d-agent-workspace', resultStage: 'match3d-result', platformStage: 'platform', isCompileAction: () => true, resolveErrorMessage: (error, fallback) => error instanceof Error ? error.message : fallback, errorMessages: { open: '打开失败', restoreMissingSession: '缺少会话', restore: '恢复失败', submit: '发送失败', execute: '执行失败', }, enterCreateTab: () => {}, setSelectionStage: setStage, onActionComplete: ({ setSession, response }) => { setSession(response.session); return onActionComplete(); }, }); useEffect(() => { if (hasOpenedRef.current) { return; } hasOpenedRef.current = true; void flow.openWorkspace({}); }, [flow]); return (
{stage}
{flow.session?.draft?.profileId ?? 'missing'}
); } function SessionChangeHarness({ onSessionChanged, }: { onSessionChanged: (session: TestSession | null) => void; }) { const flow = usePlatformCreationAgentFlowController< TestSession, Record, { session: TestSession }, TestMessagePayload, { action: string }, { session: TestSession } >({ client: { createSession: async () => ({ session: { sessionId: 'session-open', messages: [], }, }), getSession: async () => ({ session: { sessionId: 'session-restore', messages: [], }, }), streamMessage: async () => ({ sessionId: 'session-open', messages: [], }), executeAction: async () => ({ session: { sessionId: 'session-compile', messages: [], }, }), selectSession: (response) => response.session, }, createPayload: {}, workspaceStage: 'match3d-agent-workspace', resultStage: 'match3d-result', platformStage: 'platform', isCompileAction: (payload) => payload.action === 'match3d_compile_draft', resolveErrorMessage: (error, fallback) => error instanceof Error ? error.message : fallback, errorMessages: { open: '打开失败', restoreMissingSession: '缺少会话', restore: '恢复失败', submit: '发送失败', execute: '执行失败', }, enterCreateTab: () => {}, setSelectionStage: () => {}, onSessionChanged, onActionComplete: ({ response, setSession }) => { setSession(response.session); }, }); return (
); } function SessionSetterIdentityHarness({ onSetterIdentity, }: { onSetterIdentity: (setter: unknown) => void; }) { const [renderCount, setRenderCount] = useState(0); const flow = usePlatformCreationAgentFlowController< TestSession, Record, { session: TestSession }, TestMessagePayload, { action: string }, { session: TestSession } >({ client: { createSession: async () => ({ session: { sessionId: 'session-open', messages: [], }, }), getSession: async () => ({ session: { sessionId: 'session-restore', messages: [], }, }), streamMessage: async () => ({ sessionId: 'session-open', messages: [], }), executeAction: async () => ({ session: { sessionId: 'session-compile', messages: [], }, }), selectSession: (response) => response.session, }, createPayload: {}, workspaceStage: 'match3d-agent-workspace', resultStage: 'match3d-result', platformStage: 'platform', isCompileAction: () => false, resolveErrorMessage: (error, fallback) => error instanceof Error ? error.message : fallback, errorMessages: { open: '打开失败', restoreMissingSession: '缺少会话', restore: '恢复失败', submit: '发送失败', execute: '执行失败', }, enterCreateTab: () => {}, setSelectionStage: () => {}, onSessionChanged: () => {}, }); useEffect(() => { onSetterIdentity(flow.setSession); }); return ( ); } test('creation agent flow preserves streamed assistant text when stream fails', async () => { const streamMessage = vi.fn(async (_sessionId, _payload, options) => { options?.onUpdate?.('先把方洞万能的反差定住。'); throw new Error('方洞挑战聊天生成失败:LLM 请求超时'); }); render(); await waitFor(() => { expect(screen.getByRole('button', { name: '发送' })).toBeTruthy(); }); await act(async () => { screen.getByRole('button', { name: '发送' }).click(); }); await waitFor(() => { expect(screen.getByText('方洞挑战聊天生成失败:LLM 请求超时')).toBeTruthy(); }); expect(screen.getByTestId('messages').textContent).toContain( 'user:chat:做一个办公室文具方洞挑战', ); expect(screen.getByTestId('messages').textContent).toContain( 'assistant:warning:先把方洞万能的反差定住。', ); }); test('creation agent flow exposes session setter after compile action fails', async () => { const onActionError = vi.fn( ({ setSession, }: { session: ActionTestSession; setSession: (session: ActionTestSession) => void; }) => { setSession({ sessionId: 'session-1', messages: [], draft: { profileId: 'profile-after-failure' }, }); }, ); render(); await waitFor(() => { expect(screen.getByRole('button', { name: '执行' })).toBeTruthy(); }); await act(async () => { screen.getByRole('button', { name: '执行' }).click(); }); await waitFor(() => { expect(screen.getByText('模型生成失败')).toBeTruthy(); }); expect(onActionError).toHaveBeenCalledTimes(1); expect(screen.getByTestId('profile').textContent).toBe( 'profile-after-failure', ); expect(screen.getByTestId('stage').textContent).toBe( 'match3d-agent-workspace', ); }); test('creation agent flow suppresses compile result stage for background completion', async () => { const onActionComplete = vi.fn(() => ({ openResult: false })); render(); await waitFor(() => { expect(screen.getByTestId('stage').textContent).toBe( 'match3d-agent-workspace', ); }); await act(async () => { screen.getByRole('button', { name: '执行' }).click(); }); await waitFor(() => { expect(onActionComplete).toHaveBeenCalledTimes(1); }); expect(screen.getByTestId('profile').textContent).toBe('profile-ready-1'); expect(screen.getByTestId('stage').textContent).toBe( 'match3d-agent-workspace', ); }); test('creation agent flow notifies session changes after open restore and compile', async () => { const onSessionChanged = vi.fn(); render(); await act(async () => { screen.getByRole('button', { name: '打开' }).click(); }); await act(async () => { screen.getByRole('button', { name: '恢复' }).click(); }); await act(async () => { screen.getByRole('button', { name: '编译' }).click(); }); await waitFor(() => { expect(onSessionChanged).toHaveBeenCalledTimes(3); }); expect( onSessionChanged.mock.calls.map(([session]) => session?.sessionId), ).toEqual(['session-open', 'session-restore', 'session-compile']); }); test('creation agent flow keeps session setter stable across parent rerenders', async () => { const onSetterIdentity = vi.fn(); render(); await waitFor(() => { expect(onSetterIdentity).toHaveBeenCalledTimes(1); }); const initialSetter = onSetterIdentity.mock.calls[0]?.[0]; await act(async () => { screen.getByRole('button', { name: /重渲染/u }).click(); }); await waitFor(() => { expect(onSetterIdentity).toHaveBeenCalledTimes(2); }); expect(onSetterIdentity.mock.calls[1]?.[0]).toBe(initialSetter); });