Files
makelore/tests/unit/works-square-design-workspace.test.ts

603 lines
20 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { WorksSquareDesignWorkspace } from '@electron/image-workspace/works-square-workspace';
import { DesignWorkspaceModuleError } from '@electron/image-workspace/module';
import { getValidWorksSquareAccessToken } from '@electron/services/works-square-session';
import { designValuesFixture } from '../fixtures/design-workspace-v2';
vi.mock('@electron/services/works-square-session', () => ({
getValidWorksSquareAccessToken: vi.fn(),
}));
const getTokenMock = vi.mocked(getValidWorksSquareAccessToken);
function jsonResponse(payload: unknown, status = 200): Response {
return new Response(JSON.stringify(payload), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
const serverSummary = {
workspace_id: 'workspace-1',
client_workspace_id: 'client-workspace-1',
title: '咖啡机新品主视觉',
direction_id: 'direction-1',
session_id: 'session-1',
direction_revision: 4,
specification_revision: 3,
workspace_view_revision: 6,
updated_at: '2026-08-30T10:00:00.000Z',
};
const serverForm = {
workspace_id: 'workspace-1',
direction_id: 'direction-1',
direction_revision: 4,
raw_turn_sequence: 1,
specification_revision: 3,
workspace_view_revision: 6,
specification_revision_id: 'specification-revision-3',
specification_digest: 'a'.repeat(64),
specification: {
schema_version: 1,
values: designValuesFixture,
field_decisions: {},
},
decision_prompts: [],
active_quotes: [],
recent_change_set: { interaction_id: 'interaction-1', changes: [] },
};
const serverWorkspace = {
workspace: serverSummary,
form: serverForm,
turns: [{
turn_id: 'turn-1',
raw_turn_sequence: 1,
user_message: '做一张新品主视觉',
assistant_message: '已写入设计表单。',
}],
tasks: [],
assets: [],
};
type MockSocket = {
readyState: number;
onopen: (() => void) | null;
onmessage: ((event: { data: unknown }) => void) | null;
onerror: ((event: unknown) => void) | null;
onclose: ((event: { code: number; reason: string }) => void) | null;
send: ReturnType<typeof vi.fn>;
close: ReturnType<typeof vi.fn>;
};
function createSocket(): MockSocket {
const socket: MockSocket = {
readyState: 1,
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
send: vi.fn(),
close: vi.fn((code = 1000, reason = '') => socket.onclose?.({ code, reason })),
};
return socket;
}
describe('Works Square V2 Design Workspace adapter', () => {
beforeEach(() => {
vi.clearAllMocks();
getTokenMock.mockResolvedValue('access-token');
});
it('maps the V2 bootstrap and canonical Living Form detail', async () => {
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/api/design/capabilities')) {
return jsonResponse({ conversation: true, generation: true, image: true, video: true });
}
if (url.includes('/api/design/workspaces?')) return jsonResponse([serverSummary]);
if (url.endsWith('/api/design/workspaces/workspace-1')) return jsonResponse(serverWorkspace);
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
});
await expect(module.bootstrap()).resolves.toMatchObject({
workspaces: [{
workspaceId: 'workspace-1',
sessionId: 'session-1',
directionRevision: 4,
specificationRevision: 3,
}],
});
await expect(module.getWorkspace('workspace-1')).resolves.toMatchObject({
workspace: { title: '咖啡机新品主视觉' },
form: {
workspaceId: 'workspace-1',
specification: { values: { output: { aspect_ratio: '3:4' } } },
},
turns: [{ userMessage: '做一张新品主视觉' }],
});
});
it.each([
{
command: {
kind: 'apply_input' as const,
workspaceId: 'workspace-1',
sessionId: 'session-1',
expectedDirectionRevision: 4,
clientOperationId: 'operation-edit-1',
input: {
kind: 'direct_edit' as const,
operations: [{ kind: 'set' as const, path: 'output.aspect_ratio', value: '16:9' }],
},
},
expected: {
client_command_id: 'operation-edit-1',
name: 'design.input.apply',
input: {
expected_direction_revision: 4,
client_operation_id: 'operation-edit-1',
input: {
kind: 'direct_edit',
operations: [{ kind: 'set', path: 'output.aspect_ratio', value: '16:9' }],
},
},
},
},
{
command: {
kind: 'request_quote' as const,
workspaceId: 'workspace-1',
sessionId: 'session-1',
expectedDirectionRevision: 4,
specificationRevision: 3,
clientOperationId: 'operation-quote-1',
},
expected: {
client_command_id: 'operation-quote-1',
name: 'design.quote.request',
input: {
expected_direction_revision: 4,
specification_revision: 3,
client_operation_id: 'operation-quote-1',
},
},
},
{
command: {
kind: 'confirm_generation' as const,
workspaceId: 'workspace-1',
sessionId: 'session-1',
quoteId: 'quote-1',
expectedDirectionRevision: 4,
clientOperationId: 'operation-confirm-1',
},
expected: {
client_command_id: 'operation-confirm-1',
name: 'design.generation.confirm',
input: {
quote_id: 'quote-1',
expected_direction_revision: 4,
client_operation_id: 'operation-confirm-1',
},
},
},
])('submits $expected.name with one stable operation identity', async ({ command, expected }) => {
const requests: Array<{ url: string; init?: RequestInit }> = [];
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
const url = String(input);
requests.push({ url, init });
if (url.endsWith('/commands')) {
return jsonResponse({ run_id: 'run-1', status: 'queued', error: null });
}
if (url.endsWith('/runs/run-1')) {
return jsonResponse({ run_id: 'run-1', status: 'succeeded', error: null });
}
if (url.endsWith('/api/design/workspaces/workspace-1')) return jsonResponse(serverWorkspace);
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
});
const result = await module.submitCommand(command);
expect(result).toMatchObject({
clientOperationId: command.clientOperationId,
runId: 'run-1',
workspace: { workspace: { workspaceId: 'workspace-1' } },
});
const submitted = requests.find((request) => request.url.endsWith('/commands'));
expect(JSON.parse(String(submitted?.init?.body))).toEqual(expected);
});
it('surfaces a terminal Gateway failure without fetching a false-success Workspace', async () => {
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/commands')) {
return jsonResponse({ run_id: 'run-1', status: 'queued', error: null });
}
if (url.endsWith('/runs/run-1')) {
return jsonResponse({
run_id: 'run-1',
status: 'failed',
error: { code: 'design_quote_blocked', message: 'blocked', retryable: false },
});
}
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
});
await expect(module.submitCommand({
kind: 'request_quote',
workspaceId: 'workspace-1',
sessionId: 'session-1',
expectedDirectionRevision: 4,
specificationRevision: 3,
clientOperationId: 'operation-quote-1',
})).rejects.toMatchObject({
status: 422,
code: 'design_quote_blocked',
} satisfies Partial<DesignWorkspaceModuleError>);
expect(fetchMock).not.toHaveBeenCalledWith(
expect.stringContaining('/api/design/workspaces/workspace-1'),
expect.anything(),
);
});
it('projects an invalid reasoner result as a safe actionable message', async () => {
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/commands')) {
return jsonResponse({ run_id: 'run-1', status: 'queued', error: null });
}
if (url.endsWith('/runs/run-1')) {
return jsonResponse({
run_id: 'run-1',
status: 'failed',
error: {
code: 'design_reasoner_invalid',
message: 'Design guidance returned an invalid result',
retryable: false,
},
});
}
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
});
await expect(module.submitCommand({
kind: 'apply_input',
workspaceId: 'workspace-1',
sessionId: 'session-1',
expectedDirectionRevision: 4,
clientOperationId: 'operation-chat-1',
input: { kind: 'chat', message: '画一只会做饭的机器人' },
})).rejects.toMatchObject({
status: 502,
code: 'design_reasoner_invalid',
message: 'AI 没有整理好这次想法,请再试一次',
commandOutcome: 'definitive_failure',
} satisfies Partial<DesignWorkspaceModuleError>);
});
it('marks polling auth failure as unknown after the command was accepted', async () => {
getTokenMock
.mockResolvedValueOnce('access-token')
.mockResolvedValueOnce('access-token')
.mockResolvedValueOnce(null);
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/commands')) {
return jsonResponse({ run_id: 'run-1', status: 'queued', error: null });
}
if (url.endsWith('/runs/run-1')) return jsonResponse({ detail: 'expired' }, 401);
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
});
await expect(module.submitCommand({
kind: 'apply_input',
workspaceId: 'workspace-1',
sessionId: 'session-1',
expectedDirectionRevision: 4,
clientOperationId: 'operation-chat-1',
input: { kind: 'chat', message: '画一只会做饭的机器人' },
})).rejects.toMatchObject({
status: 401,
code: 'AUTH_EXPIRED',
commandOutcome: 'unknown',
} satisfies Partial<DesignWorkspaceModuleError>);
});
it('resumes the V2 event stream from the supplied sequence and normalizes deltas', async () => {
const socket = createSocket();
let streamUrl = '';
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/stream-tickets')) {
return jsonResponse({ stream_url: 'https://works.example/api/agents/stream/ticket-1' });
}
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
webSocketFactory: async (url) => {
streamUrl = url;
setTimeout(() => socket.onopen?.(), 0);
return { socket };
},
});
const subscription = await module.openWorkspaceEvents({
workspaceId: 'workspace-1',
sessionId: 'session-1',
afterEventId: 'session-1:7',
});
const nextEvent = subscription.events[Symbol.asyncIterator]().next();
socket.onmessage?.({
data: JSON.stringify({
type: 'event',
event: {
session_id: 'session-1',
sequence: 8,
runtime: 'design',
type: 'design.assistant.delta',
schema_version: 1,
payload: {
workspace_id: 'workspace-1',
direction_id: 'direction-1',
client_operation_id: 'operation-chat-1',
direction_revision: 4,
chunk_index: 0,
delta: '正在整理设计方向',
},
},
}),
});
await expect(nextEvent).resolves.toEqual({
done: false,
value: {
id: 'session-1:8',
type: 'design.assistant.delta',
workspaceId: 'workspace-1',
directionId: 'direction-1',
clientOperationId: 'operation-chat-1',
directionRevision: 4,
chunkIndex: 0,
delta: '正在整理设计方向',
},
});
expect(streamUrl).toContain('after_sequence=7');
expect(streamUrl).toMatch(/^wss:/);
subscription.close();
});
it('normalizes bounded public AI activity without exposing provider details', async () => {
const socket = createSocket();
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/stream-tickets')) {
return jsonResponse({ stream_url: 'https://works.example/api/agents/stream/ticket-1' });
}
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
webSocketFactory: async () => {
setTimeout(() => socket.onopen?.(), 0);
return { socket };
},
});
const subscription = await module.openWorkspaceEvents({
workspaceId: 'workspace-1',
sessionId: 'session-1',
});
const nextEvent = subscription.events[Symbol.asyncIterator]().next();
socket.onmessage?.({
data: JSON.stringify({
type: 'event',
event: {
session_id: 'session-1',
sequence: 8,
runtime: 'design',
type: 'design.assistant.progress',
schema_version: 1,
payload: {
workspace_id: 'workspace-1',
direction_id: 'direction-1',
client_operation_id: 'operation-chat-1',
stage: 'reviewing_context',
message: 'RAW provider chain-of-thought must not cross Main',
},
},
}),
});
await expect(nextEvent).resolves.toEqual({
done: false,
value: {
id: 'session-1:8',
type: 'design.assistant.progress',
workspaceId: 'workspace-1',
directionId: 'direction-1',
clientOperationId: 'operation-chat-1',
stage: 'reviewing_context',
message: '正在把它和前面的设计想法放在一起看…',
},
});
subscription.close();
});
it('carries the Gateway operation identity with the canonical Design turn', async () => {
const socket = createSocket();
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/stream-tickets')) {
return jsonResponse({ stream_url: 'https://works.example/api/agents/stream/ticket-1' });
}
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
webSocketFactory: async () => {
setTimeout(() => socket.onopen?.(), 0);
return { socket };
},
});
const subscription = await module.openWorkspaceEvents({
workspaceId: 'workspace-1',
sessionId: 'session-1',
});
const nextEvent = subscription.events[Symbol.asyncIterator]().next();
socket.onmessage?.({
data: JSON.stringify({
type: 'event',
event: {
session_id: 'session-1',
sequence: 9,
runtime: 'design',
type: 'design.direction.updated',
client_command_id: 'operation-chat-1',
schema_version: 1,
payload: {
replayed: false,
operation: {
operation_kind: 'chat',
interaction_id: 'interaction-2',
base_direction_revision: 4,
new_direction_revision: 5,
raw_turn_sequence: 2,
specification_revision: 3,
specification_revision_id: 'specification-revision-3',
workspace_view_revision: 7,
specification_revision_created: false,
meaning_changed: false,
change_set: { interaction_id: 'interaction-2', changes: [] },
turn_id: 'turn-2',
assistant_message: '你希望它更像漫画还是电影?',
created_decision_prompt_ids: [],
resolved_decision_prompt_id: null,
superseded_decision_prompt_ids: [],
superseded_quote_count: 0,
quote_id: null,
generation_task_id: null,
},
form: { ...serverForm, raw_turn_sequence: 2, direction_revision: 5 },
},
},
}),
});
await expect(nextEvent).resolves.toMatchObject({
done: false,
value: {
id: 'session-1:9',
type: 'design.direction.updated',
clientOperationId: 'operation-chat-1',
operation: { turnId: 'turn-2' },
},
});
subscription.close();
});
it('normalizes Gateway command terminal events by their stable operation identity', async () => {
const socket = createSocket();
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.endsWith('/stream-tickets')) {
return jsonResponse({ stream_url: 'https://works.example/api/agents/stream/ticket-1' });
}
throw new Error(`Unexpected URL: ${url}`);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
webSocketFactory: async () => {
setTimeout(() => socket.onopen?.(), 0);
return { socket };
},
});
const subscription = await module.openWorkspaceEvents({
workspaceId: 'workspace-1',
sessionId: 'session-1',
});
const iterator = subscription.events[Symbol.asyncIterator]();
const nextEvent = iterator.next();
socket.onmessage?.({
data: JSON.stringify({
type: 'event',
event: {
session_id: 'session-1',
sequence: 9,
runtime: 'design',
type: 'command.failed',
client_command_id: 'operation-chat-1',
schema_version: 1,
terminal: true,
payload: {
error: {
code: 'design_reasoner_invalid',
message: 'private technical detail',
retryable: false,
},
},
},
}),
});
await expect(nextEvent).resolves.toEqual({
done: false,
value: {
id: 'session-1:9',
type: 'command.failed',
workspaceId: 'workspace-1',
clientOperationId: 'operation-chat-1',
outcome: 'failed',
errorCode: 'design_reasoner_invalid',
},
});
subscription.close();
});
it('refreshes authentication once after a 401', async () => {
getTokenMock.mockResolvedValueOnce('expired-token').mockResolvedValueOnce('fresh-token');
const fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
const authorization = new Headers(init?.headers).get('Authorization');
if (authorization === 'Bearer expired-token') return jsonResponse({ detail: 'expired' }, 401);
return jsonResponse(serverWorkspace);
});
const module = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://works.example',
fetchImpl: fetchMock as unknown as typeof fetch,
});
await expect(module.getWorkspace('workspace-1')).resolves.toMatchObject({
workspace: { workspaceId: 'workspace-1' },
});
expect(getTokenMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ forceRefresh: true }));
});
});