合并 AI 设计图生图客户端
This commit is contained in:
@@ -711,11 +711,152 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the historical video picker behavior when the brief medium is not decided yet', async () => {
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
|
||||
...conversationFixture(),
|
||||
phase: 'shaping',
|
||||
brief: {
|
||||
version: 2,
|
||||
status: 'draft',
|
||||
medium: null,
|
||||
summary: '',
|
||||
ready: false,
|
||||
missingDecision: '请选择首帧来源',
|
||||
},
|
||||
messages: [
|
||||
...conversationFixture().messages,
|
||||
{
|
||||
id: 'message-undecided-source',
|
||||
role: 'assistant',
|
||||
kind: 'choice',
|
||||
text: '请选择视频首帧来源。',
|
||||
quickReplies: ['从作品列表选择图片'],
|
||||
generationQuote: null,
|
||||
turnRevision: 2,
|
||||
createdAt: '2026-08-12T09:00:00Z',
|
||||
},
|
||||
],
|
||||
turnRevision: 2,
|
||||
} satisfies DesignConversation);
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));
|
||||
const picker = screen.getByRole('dialog', { name: '选择视频首帧' });
|
||||
const selectButton = within(picker).getByRole('button', { name: '选择此图片' });
|
||||
await waitFor(() => expect(selectButton).toBeEnabled());
|
||||
fireEvent.click(selectButton);
|
||||
|
||||
await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
|
||||
'workspace-cloud',
|
||||
'conversation-cloud',
|
||||
2,
|
||||
'使用作品图片作为视频首帧',
|
||||
expect.stringMatching(/^turn-/),
|
||||
['asset-one'],
|
||||
));
|
||||
expect(screen.queryByRole('dialog', { name: '选择视频首帧' }))
|
||||
.not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the image reference picker and submits a successful work Asset id', async () => {
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
|
||||
...conversationFixture(),
|
||||
phase: 'shaping',
|
||||
brief: {
|
||||
version: 2,
|
||||
status: 'draft',
|
||||
medium: 'image',
|
||||
summary: '参考海洋公益海报生成新图',
|
||||
ready: false,
|
||||
missingDecision: '请选择参考图来源',
|
||||
},
|
||||
messages: [
|
||||
...conversationFixture().messages,
|
||||
{
|
||||
id: 'message-image-source',
|
||||
role: 'assistant',
|
||||
kind: 'choice',
|
||||
text: '请选择图生图参考图。',
|
||||
quickReplies: ['从作品列表选择图片'],
|
||||
generationQuote: null,
|
||||
turnRevision: 2,
|
||||
createdAt: '2026-08-12T09:00:00Z',
|
||||
},
|
||||
],
|
||||
turnRevision: 2,
|
||||
} satisfies DesignConversation);
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));
|
||||
const picker = screen.getByRole('dialog', { name: '选择图生图参考图' });
|
||||
expect(within(picker).getByText(/作为图生图参考/)).toBeInTheDocument();
|
||||
const selectButton = within(picker).getByRole('button', { name: '选择此图片' });
|
||||
await waitFor(() => expect(selectButton).toBeEnabled());
|
||||
fireEvent.click(selectButton);
|
||||
|
||||
await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
|
||||
'workspace-cloud',
|
||||
'conversation-cloud',
|
||||
2,
|
||||
'使用作品图片作为图生图参考图',
|
||||
expect.stringMatching(/^turn-/),
|
||||
['asset-one'],
|
||||
));
|
||||
expect(screen.queryByRole('dialog', { name: '选择图生图参考图' }))
|
||||
.not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uploads a local image reference and submits its Asset id to the Agent', async () => {
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
|
||||
...conversationFixture(),
|
||||
messages: [
|
||||
...conversationFixture().messages,
|
||||
{
|
||||
id: 'message-image-source',
|
||||
role: 'assistant',
|
||||
kind: 'choice',
|
||||
text: '请选择图生图参考图。',
|
||||
quickReplies: ['从作品列表选择图片'],
|
||||
generationQuote: null,
|
||||
turnRevision: 2,
|
||||
createdAt: '2026-08-12T09:00:00Z',
|
||||
},
|
||||
],
|
||||
turnRevision: 2,
|
||||
} satisfies DesignConversation);
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));
|
||||
const picker = screen.getByRole('dialog', { name: '选择图生图参考图' });
|
||||
const file = new File(['image-bytes'], 'reference.png', { type: 'image/png' });
|
||||
|
||||
fireEvent.change(within(picker).getByLabelText('上传本地图片'), {
|
||||
target: { files: [file] },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(uploadImageWorkspaceAssetMock)
|
||||
.toHaveBeenCalledWith('workspace-cloud', file));
|
||||
await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
|
||||
'workspace-cloud',
|
||||
'conversation-cloud',
|
||||
2,
|
||||
'使用上传图片作为图生图参考图',
|
||||
expect.stringMatching(/^turn-/),
|
||||
['asset-uploaded'],
|
||||
));
|
||||
await waitFor(() => expect(
|
||||
screen.queryByRole('dialog', { name: '选择图生图参考图' }),
|
||||
).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('submits a successful work Asset id when it is selected as the video first frame', async () => {
|
||||
const pendingSelection = deferred<DesignConversation>();
|
||||
sendImageWorkspaceMessageMock.mockReturnValueOnce(pendingSelection.promise);
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
|
||||
...conversationFixture(),
|
||||
brief: {
|
||||
...conversationFixture().brief,
|
||||
medium: 'video',
|
||||
},
|
||||
turnRevision: 2,
|
||||
messages: [
|
||||
...conversationFixture().messages,
|
||||
@@ -759,6 +900,10 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
it('uploads a local first-frame image and submits its Asset id to the Agent', async () => {
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
|
||||
...conversationFixture(),
|
||||
brief: {
|
||||
...conversationFixture().brief,
|
||||
medium: 'video',
|
||||
},
|
||||
messages: [
|
||||
...conversationFixture().messages,
|
||||
{
|
||||
@@ -793,6 +938,9 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
expect.stringMatching(/^turn-/),
|
||||
['asset-uploaded'],
|
||||
));
|
||||
await waitFor(() => expect(
|
||||
screen.queryByRole('dialog', { name: '选择视频首帧' }),
|
||||
).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders policy-blocked generation tasks with a user-facing design message', async () => {
|
||||
|
||||
@@ -232,6 +232,30 @@ describe('Works Square AI design adapter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes a missing Brief medium to null and rejects invalid non-null values', async () => {
|
||||
const { medium: _medium, ...legacyBrief } = serverConversation.brief;
|
||||
const legacyConversation = {
|
||||
...serverConversation,
|
||||
brief: legacyBrief,
|
||||
};
|
||||
const invalidConversation = {
|
||||
...serverConversation,
|
||||
brief: { ...serverConversation.brief, medium: 'audio' },
|
||||
};
|
||||
const fetchMock = vi.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(jsonResponse(legacyConversation))
|
||||
.mockResolvedValueOnce(jsonResponse(invalidConversation));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
await expect(adapter.getConversation('workspace-one', 'conversation-one'))
|
||||
.resolves.toMatchObject({ brief: { medium: null } });
|
||||
await expect(adapter.getConversation('workspace-one', 'conversation-one'))
|
||||
.rejects.toMatchObject({ code: 'DESIGN_WORKSPACE_RESPONSE_INVALID' });
|
||||
});
|
||||
|
||||
it('submits a conversation turn through the persistent Agent Gateway Session', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(jsonResponse(serverConversation))
|
||||
@@ -940,6 +964,11 @@ describe('Works Square AI design adapter', () => {
|
||||
});
|
||||
|
||||
it('reuses one design Agent Session and normalizes matching task events from fresh WebSocket tickets', async () => {
|
||||
const { medium: _medium, ...legacyBrief } = serverConversation.brief;
|
||||
const legacyConversation = {
|
||||
...serverConversation,
|
||||
brief: legacyBrief,
|
||||
};
|
||||
const snapshotTask = {
|
||||
task_id: 'task-snapshot',
|
||||
workspace_id: 'workspace-one',
|
||||
@@ -964,10 +993,21 @@ describe('Works Square AI design adapter', () => {
|
||||
workspace_id: 'workspace-one',
|
||||
conversation_id: 'conversation-one',
|
||||
workspace_view_revision: 2,
|
||||
conversation: serverConversation,
|
||||
conversation: legacyConversation,
|
||||
generation_tasks: [snapshotTask],
|
||||
},
|
||||
};
|
||||
const invalidSnapshotEvent = {
|
||||
...snapshotEvent,
|
||||
sequence: 98,
|
||||
payload: {
|
||||
...snapshotEvent.payload,
|
||||
conversation: {
|
||||
...serverConversation,
|
||||
brief: { ...serverConversation.brief, medium: 'audio' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const assistantDeltaEvent = {
|
||||
session_id: 'session-one',
|
||||
sequence: 2,
|
||||
@@ -1038,6 +1078,7 @@ describe('Works Square AI design adapter', () => {
|
||||
const { sockets, webSocketFactory } = scriptedSockets([
|
||||
{
|
||||
frames: [
|
||||
{ type: 'event', event: invalidSnapshotEvent },
|
||||
{ type: 'event', event: snapshotEvent },
|
||||
{ type: 'event', event: malformedDeltaEvent },
|
||||
{ type: 'event', event: assistantDeltaEvent },
|
||||
@@ -1077,6 +1118,7 @@ describe('Works Square AI design adapter', () => {
|
||||
conversation: expect.objectContaining({
|
||||
conversationId: 'conversation-one',
|
||||
title: serverConversation.title,
|
||||
brief: expect.objectContaining({ medium: null }),
|
||||
messages: [expect.objectContaining({
|
||||
text: serverConversation.messages[0].text,
|
||||
})],
|
||||
|
||||
Reference in New Issue
Block a user