Files
Genarrative/src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx
2026-05-11 20:27:41 +08:00

394 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* @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<TestSession>;
}) {
const hasOpenedRef = useRef(false);
const flow = usePlatformCreationAgentFlowController<
TestSession,
Record<string, never>,
{ 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 (
<div>
<button
type="button"
onClick={() => {
void flow.submitMessage({
clientMessageId: 'client-message-1',
text: '做一个办公室文具方洞挑战',
});
}}
>
</button>
<div data-testid="messages">
{flow.session?.messages.map((message) => (
<div
key={message.id}
>{`${message.role}:${message.kind}:${message.text}`}</div>
))}
</div>
{flow.error ? <div>{flow.error}</div> : null}
</div>
);
}
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<string, never>,
{ 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 (
<div>
<button
type="button"
onClick={() => {
void flow.executeAction({ action: 'match3d_compile_draft' });
}}
>
</button>
<div data-testid="stage">{stage}</div>
<div data-testid="profile">
{flow.session?.draft?.profileId ?? 'missing'}
</div>
{flow.error ? <div>{flow.error}</div> : null}
</div>
);
}
function ActionCompleteHarness({
onActionComplete,
}: {
onActionComplete: () => { openResult?: boolean } | void;
}) {
const [stage, setStage] = useState('platform');
const hasOpenedRef = useRef(false);
const flow = usePlatformCreationAgentFlowController<
ActionTestSession,
Record<string, never>,
{ 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 (
<div>
<button
type="button"
onClick={() => {
void flow.executeAction({ action: 'match3d_compile_draft' });
}}
>
</button>
<div data-testid="stage">{stage}</div>
<div data-testid="profile">
{flow.session?.draft?.profileId ?? 'missing'}
</div>
</div>
);
}
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(<TestHarness streamMessage={streamMessage} />);
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(<ActionErrorHarness onActionError={onActionError} />);
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(<ActionCompleteHarness onActionComplete={onActionComplete} />);
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',
);
});