feat: 统一 AI 设计 Workspace 与生成任务链路
需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
This commit is contained in:
196
tests/unit/works-square-design-workspace.test.ts
Normal file
196
tests/unit/works-square-design-workspace.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { WorksSquareDesignWorkspace } from '@electron/image-workspace/works-square-workspace';
|
||||
import { getValidWorksSquareAccessToken } from '@electron/services/works-square-session';
|
||||
|
||||
vi.mock('@electron/services/works-square-session', () => ({
|
||||
getValidWorksSquareAccessToken: vi.fn(),
|
||||
}));
|
||||
|
||||
const getTokenMock = vi.mocked(getValidWorksSquareAccessToken);
|
||||
|
||||
const serverWorkspace = {
|
||||
workspace_id: 'workspace-one',
|
||||
title: '海洋公益海报',
|
||||
turn_revision: 1,
|
||||
view_revision: 2,
|
||||
phase: 'awaiting_confirmation',
|
||||
brief: {
|
||||
version: 1,
|
||||
status: 'ready',
|
||||
medium: 'image',
|
||||
summary: '保护海洋的竖版公益海报',
|
||||
ready: true,
|
||||
missing_decision: null,
|
||||
},
|
||||
messages: [{
|
||||
role: 'assistant',
|
||||
kind: 'confirmation',
|
||||
text: '方向已经明确,是否开始生成?',
|
||||
quick_replies: ['确认生成'],
|
||||
generation_quote: {
|
||||
quote_id: 'quote-one',
|
||||
status: 'active',
|
||||
medium: 'image',
|
||||
brief_version: 1,
|
||||
brief_summary: '保护海洋的竖版公益海报',
|
||||
quoted_design_points: 1,
|
||||
expires_at: '2026-07-31T11:00:00Z',
|
||||
},
|
||||
turn_revision: 1,
|
||||
created_at: '2026-07-31T10:00:00Z',
|
||||
}],
|
||||
updated_at: '2026-07-31T10:00:00Z',
|
||||
};
|
||||
|
||||
function jsonResponse(payload: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
describe('Works Square AI design adapter', () => {
|
||||
beforeEach(() => {
|
||||
getTokenMock.mockReset();
|
||||
getTokenMock.mockResolvedValue('access-one');
|
||||
});
|
||||
|
||||
it('maps the server snake_case Workspace contract into the shared client model', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
conversation: true,
|
||||
generation: true,
|
||||
image: true,
|
||||
video: false,
|
||||
}))
|
||||
.mockResolvedValueOnce(jsonResponse([serverWorkspace]));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
await expect(adapter.bootstrap()).resolves.toMatchObject({
|
||||
capabilities: { conversation: true, image: true, video: false },
|
||||
workspaces: [{
|
||||
workspaceId: 'workspace-one',
|
||||
title: '海洋公益海报',
|
||||
turnRevision: 1,
|
||||
viewRevision: 2,
|
||||
brief: { missingDecision: null },
|
||||
}],
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(fetchMock.mock.calls.every(([, init]) => (
|
||||
(init?.headers as Record<string, string>).Authorization === 'Bearer access-one'
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps task creation behind structured Quote confirmation', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(serverWorkspace));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example/',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
const workspace = await adapter.confirmGeneration({
|
||||
workspaceId: 'workspace-one',
|
||||
clientTurnId: 'turn-two',
|
||||
expectedTurnRevision: 1,
|
||||
quoteId: 'quote-one',
|
||||
});
|
||||
|
||||
expect(workspace.workspaceId).toBe('workspace-one');
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://square.example/api/design/workspaces/workspace-one/turns',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_turn_id: 'turn-two',
|
||||
expected_turn_revision: 1,
|
||||
message: '确认生成',
|
||||
attachment_asset_ids: [],
|
||||
action: { type: 'confirm_generation', quote_id: 'quote-one' },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('maps stable generation tasks and private asset relay paths', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse([{
|
||||
task_id: 'task-one',
|
||||
workspace_id: 'workspace-one',
|
||||
medium: 'image',
|
||||
status: 'succeeded',
|
||||
brief_version: 1,
|
||||
brief_summary: '海洋公益海报',
|
||||
quote_id: 'quote-one',
|
||||
quoted_design_points: 1,
|
||||
failure_code: null,
|
||||
result_assets: [{
|
||||
asset_id: 'asset-one',
|
||||
media_type: 'image',
|
||||
mime_type: 'image/png',
|
||||
width: 1024,
|
||||
height: 1280,
|
||||
duration_milliseconds: null,
|
||||
created_at: '2026-07-31T10:01:00Z',
|
||||
}],
|
||||
created_at: '2026-07-31T10:00:00Z',
|
||||
updated_at: '2026-07-31T10:01:00Z',
|
||||
}]));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
await expect(adapter.listTasks('workspace-one')).resolves.toMatchObject([{
|
||||
taskId: 'task-one',
|
||||
status: 'succeeded',
|
||||
resultAssets: [{
|
||||
assetId: 'asset-one',
|
||||
contentPath: '/api/works/image-workspace/workspaces/workspace-one/assets/asset-one/content',
|
||||
}],
|
||||
}]);
|
||||
});
|
||||
|
||||
it('refreshes the Main-owned session once after an upstream 401 and preserves Range', async () => {
|
||||
getTokenMock
|
||||
.mockResolvedValueOnce('expired-token')
|
||||
.mockResolvedValueOnce('fresh-token');
|
||||
const fetchMock = vi.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(new Response(null, { status: 401 }))
|
||||
.mockResolvedValueOnce(new Response('partial', {
|
||||
status: 206,
|
||||
headers: {
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Range': 'bytes 0-6/100',
|
||||
},
|
||||
}));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
const response = await adapter.openAssetContent(
|
||||
'workspace-one',
|
||||
'asset-one',
|
||||
'bytes=0-6',
|
||||
);
|
||||
|
||||
expect(response.status).toBe(206);
|
||||
expect(getTokenMock).toHaveBeenNthCalledWith(2, {
|
||||
fetchImpl: fetchMock,
|
||||
forceRefresh: true,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.stringContaining('/assets/asset-one/content'),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Range: 'bytes=0-6',
|
||||
Authorization: 'Bearer fresh-token',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user