feat: 完善图像工作区与创作工具体验
This commit is contained in:
@@ -36,6 +36,49 @@ const serverConversation = {
|
||||
medium: 'image',
|
||||
brief_version: 1,
|
||||
brief_summary: '???????????',
|
||||
final_prompt: '??????????????',
|
||||
prompt_mode: 'guided',
|
||||
generation_parameters: {
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
aspect_ratio: '1:1',
|
||||
duration_seconds: null,
|
||||
},
|
||||
generation_options: {
|
||||
models: [{
|
||||
value: 'image-model-one',
|
||||
label: '??????',
|
||||
default: true,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}],
|
||||
resolutions: [{
|
||||
value: '1024x1024',
|
||||
label: '1024 × 1024',
|
||||
default: true,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}],
|
||||
durations: [],
|
||||
aspect_ratios: [{
|
||||
value: '1:1',
|
||||
label: '1:1',
|
||||
default: true,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}, {
|
||||
value: '16:9',
|
||||
label: '16:9',
|
||||
default: false,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}],
|
||||
},
|
||||
pricing: {
|
||||
schema: 'design-pricing-v2',
|
||||
amount: 1,
|
||||
rounding: 'ceil',
|
||||
},
|
||||
quoted_design_points: 1,
|
||||
expires_at: '2026-07-31T11:00:00Z',
|
||||
},
|
||||
@@ -55,6 +98,16 @@ const serverWorkspace = {
|
||||
updated_at: '2026-07-31T10:00:00Z',
|
||||
};
|
||||
|
||||
const quoteConfirmationInput = {
|
||||
finalPrompt: '??????????????',
|
||||
generationParameters: {
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
aspectRatio: '1:1',
|
||||
durationSeconds: null,
|
||||
},
|
||||
};
|
||||
|
||||
function jsonResponse(payload: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status,
|
||||
@@ -196,6 +249,156 @@ describe('Works Square AI design adapter', () => {
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns an explicit deletion result when the Workspace API responds with JSON', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse({
|
||||
workspace_id: 'workspace/one',
|
||||
deleted: true,
|
||||
}));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
await expect(adapter.deleteWorkspace('workspace/one')).resolves.toEqual({
|
||||
workspaceId: 'workspace/one',
|
||||
deleted: true,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://square.example/api/design/workspaces/workspace%2Fone',
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
headers: expect.objectContaining({
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer access-one',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a 204 deletion and clears only the deleted Workspace event/session caches', async () => {
|
||||
let deleted = false;
|
||||
let targetConversationReads = 0;
|
||||
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith('/api/design/workspaces/workspace-one') && init?.method === 'DELETE') {
|
||||
deleted = true;
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
if (url.endsWith('/workspaces/workspace-one/conversations/conversation-one')) {
|
||||
targetConversationReads += 1;
|
||||
if (deleted) {
|
||||
return jsonResponse({
|
||||
detail: { code: 'workspace_not_found', message: 'deleted' },
|
||||
}, 404);
|
||||
}
|
||||
return jsonResponse(serverConversation);
|
||||
}
|
||||
if (url.endsWith('/sessions/session-one/stream-tickets')) {
|
||||
return jsonResponse({
|
||||
stream_url: '/api/agents/sessions/session-one/ws?ticket=ticket-one',
|
||||
});
|
||||
}
|
||||
if (url.endsWith('/workspaces/workspace-two/conversations/conversation-two')) {
|
||||
return jsonResponse({
|
||||
...serverConversation,
|
||||
workspace_id: 'workspace-two',
|
||||
conversation_id: 'conversation-two',
|
||||
agent_session_id: 'session-two',
|
||||
});
|
||||
}
|
||||
if (url.endsWith('/sessions/session-two/stream-tickets')) {
|
||||
return jsonResponse({
|
||||
stream_url: '/api/agents/sessions/session-two/ws?ticket=ticket-two',
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected request: ${url}`);
|
||||
});
|
||||
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }, { open: true }]);
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
webSocketFactory,
|
||||
});
|
||||
const target = await adapter.openWorkspaceEvents({
|
||||
workspaceId: 'workspace-one',
|
||||
conversationId: 'conversation-one',
|
||||
});
|
||||
const keeper = await adapter.openWorkspaceEvents({
|
||||
workspaceId: 'workspace-two',
|
||||
conversationId: 'conversation-two',
|
||||
});
|
||||
|
||||
await expect(adapter.deleteWorkspace('workspace-one')).resolves.toEqual({
|
||||
workspaceId: 'workspace-one',
|
||||
deleted: true,
|
||||
});
|
||||
|
||||
expect(sockets.map((socket) => socket.readyState)).toEqual([3, 1]);
|
||||
await expect(adapter.openWorkspaceEvents({
|
||||
workspaceId: 'workspace-one',
|
||||
conversationId: 'conversation-one',
|
||||
})).rejects.toMatchObject({
|
||||
status: 404,
|
||||
code: 'workspace_not_found',
|
||||
message: '设计项目不存在或无权访问',
|
||||
});
|
||||
expect(targetConversationReads).toBe(2);
|
||||
expect(fetchMock.mock.calls.filter(([, init]) => init?.method === 'DELETE'))
|
||||
.toEqual([expect.arrayContaining([
|
||||
'https://square.example/api/design/workspaces/workspace-one',
|
||||
])]);
|
||||
target.close();
|
||||
keeper.close();
|
||||
});
|
||||
|
||||
it('preserves Workspace event/session caches when deletion fails', async () => {
|
||||
let ticketSequence = 0;
|
||||
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith('/workspaces/workspace-one/conversations/conversation-one')) {
|
||||
return jsonResponse(serverConversation);
|
||||
}
|
||||
if (url.endsWith('/sessions/session-one/stream-tickets')) {
|
||||
ticketSequence += 1;
|
||||
return jsonResponse({
|
||||
stream_url: `/api/agents/sessions/session-one/ws?ticket=ticket-${ticketSequence}`,
|
||||
});
|
||||
}
|
||||
if (url.endsWith('/api/design/workspaces/workspace-one') && init?.method === 'DELETE') {
|
||||
return jsonResponse({
|
||||
detail: { code: 'workspace_unavailable', message: 'private detail' },
|
||||
}, 503);
|
||||
}
|
||||
throw new Error(`Unexpected request: ${url}`);
|
||||
});
|
||||
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }, { open: true }]);
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
webSocketFactory,
|
||||
});
|
||||
const first = await adapter.openWorkspaceEvents({
|
||||
workspaceId: 'workspace-one',
|
||||
conversationId: 'conversation-one',
|
||||
});
|
||||
|
||||
await expect(adapter.deleteWorkspace('workspace-one')).rejects.toMatchObject({
|
||||
status: 503,
|
||||
code: 'workspace_unavailable',
|
||||
});
|
||||
const second = await adapter.openWorkspaceEvents({
|
||||
workspaceId: 'workspace-one',
|
||||
conversationId: 'conversation-one',
|
||||
});
|
||||
|
||||
expect(fetchMock.mock.calls.filter(([url]) => (
|
||||
String(url).endsWith('/workspaces/workspace-one/conversations/conversation-one')
|
||||
))).toHaveLength(1);
|
||||
expect(sockets.map((socket) => socket.readyState)).toEqual([1, 1]);
|
||||
first.close();
|
||||
second.close();
|
||||
});
|
||||
|
||||
it('lists, creates, and reads Conversations through the Workspace API', async () => {
|
||||
const secondConversation = {
|
||||
...serverConversation,
|
||||
@@ -348,6 +551,47 @@ describe('Works Square AI design adapter', () => {
|
||||
expect(commandBody.input).not.toHaveProperty('conversation_id');
|
||||
});
|
||||
|
||||
it('re-quotes a generation Quote through the cloud PATCH contract and maps every option', async () => {
|
||||
const quote = serverConversation.messages[0].generation_quote;
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse(quote));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
await expect(adapter.updateGenerationQuote({
|
||||
workspaceId: 'workspace-one',
|
||||
quoteId: 'quote-one',
|
||||
finalPrompt: '用户最新编辑后的完整提示词',
|
||||
generationParameters: {
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds: null,
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
quoteId: 'quote-one',
|
||||
finalPrompt: '??????????????',
|
||||
generationOptions: {
|
||||
models: [{ value: 'image-model-one', label: '??????', disabled: false }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://square.example/api/design/workspaces/workspace-one/generation-quotes/quote-one',
|
||||
expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
final_prompt: '用户最新编辑后的完整提示词',
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
aspect_ratio: '16:9',
|
||||
duration_seconds: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('uploads a local reference image as multipart data and maps the returned Asset', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse({
|
||||
asset_id: 'asset-uploaded',
|
||||
@@ -1008,6 +1252,7 @@ describe('Works Square AI design adapter', () => {
|
||||
clientTurnId: 'turn-confirm-live',
|
||||
expectedTurnRevision: 1,
|
||||
quoteId: 'quote-one',
|
||||
...quoteConfirmationInput,
|
||||
})).resolves.toMatchObject({ turnRevision: 2 });
|
||||
await consume;
|
||||
|
||||
@@ -1025,7 +1270,17 @@ describe('Works Square AI design adapter', () => {
|
||||
client_command_id: 'turn-confirm-live',
|
||||
name: 'turn.submit',
|
||||
input: {
|
||||
action: { type: 'confirm_generation', quote_id: 'quote-one' },
|
||||
action: {
|
||||
type: 'confirm_generation',
|
||||
quote_id: 'quote-one',
|
||||
final_prompt: quoteConfirmationInput.finalPrompt,
|
||||
aspect_ratio: '1:1',
|
||||
generation_parameters: {
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
duration_seconds: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1089,6 +1344,7 @@ describe('Works Square AI design adapter', () => {
|
||||
clientTurnId: 'turn-unavailable',
|
||||
expectedTurnRevision: 1,
|
||||
quoteId: 'quote-one',
|
||||
...quoteConfirmationInput,
|
||||
})).rejects.toMatchObject({
|
||||
status: 503,
|
||||
code: 'agent_runtime_unavailable',
|
||||
@@ -1256,6 +1512,7 @@ describe('Works Square AI design adapter', () => {
|
||||
clientTurnId: 'turn-two',
|
||||
expectedTurnRevision: 1,
|
||||
quoteId: 'quote-one',
|
||||
...quoteConfirmationInput,
|
||||
});
|
||||
|
||||
expect(workspace.workspaceId).toBe('workspace-one');
|
||||
@@ -1271,7 +1528,17 @@ describe('Works Square AI design adapter', () => {
|
||||
expected_turn_revision: 1,
|
||||
message: '确认生成',
|
||||
attachment_asset_ids: [],
|
||||
action: { type: 'confirm_generation', quote_id: 'quote-one' },
|
||||
action: {
|
||||
type: 'confirm_generation',
|
||||
quote_id: 'quote-one',
|
||||
final_prompt: quoteConfirmationInput.finalPrompt,
|
||||
aspect_ratio: '1:1',
|
||||
generation_parameters: {
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
duration_seconds: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user