需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
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',
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|