This commit is contained in:
2026-05-11 20:27:41 +08:00
parent e30b733b17
commit 481a27fc53
60 changed files with 6357 additions and 1100 deletions

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { act, render, screen, waitFor } from '@testing-library/react';
import { useEffect } from 'react';
import { useEffect, useRef, useState } from 'react';
import { expect, test, vi } from 'vitest';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
@@ -31,6 +31,7 @@ function TestHarness({
options?: { onUpdate?: (text: string) => void },
) => Promise<TestSession>;
}) {
const hasOpenedRef = useRef(false);
const flow = usePlatformCreationAgentFlowController<
TestSession,
Record<string, never>,
@@ -80,8 +81,12 @@ function TestHarness({
});
useEffect(() => {
if (hasOpenedRef.current) {
return;
}
hasOpenedRef.current = true;
void flow.openWorkspace({});
}, []);
}, [flow]);
return (
<div>
@@ -98,7 +103,9 @@ function TestHarness({
</button>
<div data-testid="messages">
{flow.session?.messages.map((message) => (
<div key={message.id}>{`${message.role}:${message.kind}:${message.text}`}</div>
<div
key={message.id}
>{`${message.role}:${message.kind}:${message.text}`}</div>
))}
</div>
{flow.error ? <div>{flow.error}</div> : null}
@@ -106,6 +113,193 @@ function TestHarness({
);
}
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?.('先把方洞万能的反差定住。');
@@ -123,9 +317,7 @@ test('creation agent flow preserves streamed assistant text when stream fails',
});
await waitFor(() => {
expect(
screen.getByText('方洞挑战聊天生成失败LLM 请求超时'),
).toBeTruthy();
expect(screen.getByText('方洞挑战聊天生成失败LLM 请求超时')).toBeTruthy();
});
expect(screen.getByTestId('messages').textContent).toContain(
@@ -135,3 +327,67 @@ test('creation agent flow preserves streamed assistant text when stream fails',
'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',
);
});