merge: integrate remote main

This commit is contained in:
inman
2026-09-04 14:14:02 +08:00
11 changed files with 258 additions and 46 deletions

View File

@@ -93,11 +93,12 @@ describe('youth AI Design Canvas page', () => {
expect(screen.queryByText(/规格版本|编译器|生成策略|字段决策/)).not.toBeInTheDocument();
});
it('presents unfinished AI output as progress instead of a chat reply', () => {
prepareWorkspace();
const unfinishedDraft = '收到,我们要收到,我们要制作一张社团活动海报。';
it('shows one unfinished assistant reply incrementally and replaces it with the canonical turn', async () => {
const { workspace } = prepareWorkspace();
const firstChunk = '收到,我们要制作';
const unfinishedDraft = `${firstChunk}一张社团活动海报。`;
useImageWorkspaceStore.setState({
assistantStreams: { 'operation-chat-1': unfinishedDraft },
assistantStreams: { 'operation-chat-1': firstChunk },
pendingOperations: {
'operation-chat-1': {
id: 'operation-chat-1',
@@ -121,9 +122,69 @@ describe('youth AI Design Canvas page', () => {
render(<ImageCanvas />);
const conversation = screen.getByTestId('image-workspace-conversation');
expect(within(conversation).getByRole('status')).toHaveTextContent('AI 正在整理你的想法');
const pendingMessage = within(conversation).getByTestId('pending-design-chat-operation-chat-1');
expect(within(pendingMessage).getByText('做一张社团活动海报')).toBeInTheDocument();
expect(within(pendingMessage).getByText('发送中')).toBeInTheDocument();
expect(within(conversation).queryByRole('button', { name: '下一步:看看制作方案' }))
.not.toBeInTheDocument();
expect(within(conversation).queryByTestId('design-assistant-progress')).not.toBeInTheDocument();
expect(within(conversation).queryByText('AI 正在整理你的想法')).not.toBeInTheDocument();
expect(within(conversation).getByText('我已经整理了用途、受众和初步概念,请确认右侧建议。')).toBeInTheDocument();
expect(within(conversation).queryByText(unfinishedDraft)).not.toBeInTheDocument();
const streamingReply = within(conversation)
.getByTestId('streaming-design-assistant-operation-chat-1');
expect(streamingReply).toHaveTextContent(firstChunk);
act(() => {
useImageWorkspaceStore.setState({
assistantStreams: { 'operation-chat-1': unfinishedDraft },
});
});
expect(streamingReply).toHaveTextContent(unfinishedDraft);
act(() => {
useImageWorkspaceStore.setState({
workspace: {
...workspace,
turns: [...workspace.turns, {
turnId: 'turn-2',
rawTurnSequence: 2,
userMessage: '做一张社团活动海报',
assistantMessage: unfinishedDraft,
}],
},
pendingOperations: {},
});
});
await waitFor(() => {
expect(within(conversation).queryByTestId('streaming-design-assistant-operation-chat-1'))
.not.toBeInTheDocument();
});
expect(within(conversation).getAllByText(unfinishedDraft)).toHaveLength(1);
expect(within(conversation).getByRole('button', { name: '下一步:看看制作方案' }))
.toBeInTheDocument();
});
it('opens the mobile quote confirmation from the ready conversation without starting a paid task', async () => {
const { actions } = prepareWorkspace();
const quotedWorkspace = designWorkspaceFixture({
form: designFormFixture({ activeQuotes: [designQuoteFixture()] }),
});
actions.requestQuote.mockImplementationOnce(async () => {
useImageWorkspaceStore.setState({ workspace: quotedWorkspace });
return quotedWorkspace;
});
render(<ImageCanvas />);
const conversation = screen.getByTestId('image-workspace-conversation');
fireEvent.click(within(conversation).getByRole('button', { name: '下一步:看看制作方案' }));
expect(actions.requestQuote).toHaveBeenCalledOnce();
expect(actions.confirmGeneration).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByRole('heading', { name: '制作方案准备好了' })).toBeInTheDocument();
});
expect(screen.getByTestId('design-offered-quote')).toBeInTheDocument();
});
it('shows a submitted message immediately and replaces it with the canonical turn', async () => {
@@ -334,9 +395,13 @@ describe('youth AI Design Canvas page', () => {
});
render(<ImageCanvas />);
expect(within(screen.getByTestId('image-workspace-conversation'))
.getByRole('button', { name: '正在准备方案' })).toBeDisabled();
openMobileCreationCard();
expect(screen.getByText('还在确认刚才的操作')).toBeInTheDocument();
expect(screen.getByText(/不会重复制作或重复扣费/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: '看看制作方案', exact: true })).toBeDisabled();
expect(actions.requestQuote).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '查看原来的结果' }));
expect(actions.retryOperation).toHaveBeenCalledWith('operation-1');
});

View File

@@ -523,6 +523,25 @@ describe('V2 Living Form store', () => {
const source = await loadedStore();
useImageWorkspaceStore.getState().setFieldDraft('intent.purpose', '本地草稿');
useImageWorkspaceStore.setState({
assistantStreams: { 'operation-chat-unknown': '这段回复还没确认' },
pendingOperations: {
'operation-chat-unknown': {
id: 'operation-chat-unknown',
label: '发送创作想法',
command: {
kind: 'apply_input',
workspaceId: 'workspace-1',
sessionId: 'session-1',
expectedDirectionRevision: 4,
clientOperationId: 'operation-chat-unknown',
input: { kind: 'chat', message: '保留这条未知结果' },
},
status: 'unknown',
error: '结果尚未确认',
clearDraftPaths: [],
clearChatDraft: true,
},
},
quoteBlockers: [{
code: 'missing_choice',
message: '还需要选择作品形状',
@@ -573,9 +592,12 @@ describe('V2 Living Form store', () => {
}),
});
expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({
'operation-chat-unknown': '这段回复还没确认',
'operation-chat-1': '正在整理',
});
expect(useImageWorkspaceStore.getState()).toMatchObject({
lastEventId: 'session-1:8',
assistantStreams: { 'operation-chat-1': '正在整理' },
fieldDrafts: { 'intent.purpose': '本地草稿' },
quoteBlockers: [],
workspace: { form: { directionRevision: 5, specificationRevision: 4 } },

View File

@@ -1,4 +1,4 @@
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { YouthCreationCard } from '@/pages/ImageCanvas/YouthCreationCard';
import { DesignTaskPanel } from '@/pages/ImageCanvas/DesignProductionPanel';
@@ -21,12 +21,14 @@ function renderCard(
quoteBlockers?: DesignCompilationIssue[];
onOpenFineTune?: () => void;
onFocusComposer?: () => void;
onQuoteOffered?: () => void;
} = {},
) {
const requestQuote = vi.fn().mockResolvedValue(workspace);
const resolveDecisionPrompt = vi.fn().mockResolvedValue(workspace);
const onOpenFineTune = options.onOpenFineTune ?? vi.fn();
const onFocusComposer = options.onFocusComposer ?? vi.fn();
const onQuoteOffered = options.onQuoteOffered ?? vi.fn();
useImageWorkspaceStore.setState({
workspace,
quoteBlockers: [],
@@ -41,6 +43,7 @@ function renderCard(
generationAvailable={options.generationAvailable ?? true}
onOpenFineTune={onOpenFineTune}
onFocusComposer={onFocusComposer}
onQuoteOffered={onQuoteOffered}
/>,
);
return {
@@ -49,6 +52,7 @@ function renderCard(
resolveDecisionPrompt,
onOpenFineTune,
onFocusComposer,
onQuoteOffered,
};
}
@@ -97,8 +101,8 @@ describe('YouthCreationCard', () => {
vi.clearAllMocks();
});
it('shows a plain creation summary and offers a youth-facing ready action', () => {
const { requestQuote } = renderCard();
it('shows a plain creation summary and reveals the offered quote after requesting it', async () => {
const { onQuoteOffered, requestQuote } = renderCard();
expect(screen.getByRole('heading', { name: 'AI 听懂的想法' })).toBeInTheDocument();
expect(screen.getByText('会跟着聊天自动更新,不需要逐项填写。')).toBeInTheDocument();
@@ -112,6 +116,7 @@ describe('YouthCreationCard', () => {
fireEvent.click(screen.getByRole('button', { name: '看看制作方案' }));
expect(requestQuote).toHaveBeenCalledOnce();
await waitFor(() => expect(onQuoteOffered).toHaveBeenCalledOnce());
});
it('does not surface legacy decision prompts as a form', () => {