Files
makelore/tests/unit/works-square-design-workspace.test.ts
brother7 03dae62cf3 feat: 支持 AI 设计项目多会话交互
需求:用户可在同一设计项目中新建和切换会话,任务列表保持项目级共享。

实现:
- 增加会话选择、新建入口及本地数据迁移
- Conversation 独立消息、Brief、Quote 与流式状态
- 保留项目任务并修复 Task revision 与 ABA 异步竞态
- 保持 Enter 发送、Shift+Enter 换行及 IME 保护
2026-08-06 18:22:15 +08:00

1247 lines
42 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';
import type { DesignWorkspaceEvent } from '../../shared/image-workspace';
vi.mock('@electron/services/works-square-session', () => ({
getValidWorksSquareAccessToken: vi.fn(),
}));
const getTokenMock = vi.mocked(getValidWorksSquareAccessToken);
const serverConversation = {
conversation_id: 'conversation-one',
workspace_id: 'workspace-one',
agent_session_id: 'session-one',
title: '????',
turn_revision: 1,
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',
}],
created_at: '2026-07-31T10:00:00Z',
updated_at: '2026-07-31T10:00:00Z',
};
const serverWorkspace = {
workspace_id: 'workspace-one',
title: '??????',
view_revision: 2,
conversation_count: 1,
phase: 'awaiting_confirmation',
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' },
});
}
function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
let resolve!: (value: T) => void;
const promise = new Promise<T>((accept) => {
resolve = accept;
});
return { promise, resolve };
}
type MockSocketScript = {
frames?: unknown[];
open?: boolean;
closeCode?: number;
closeReason?: string;
};
class MockAgentWebSocket {
readonly url: string;
readonly sent: string[] = [];
readyState = 0;
onopen: (() => void) | null = null;
onmessage: ((event: { data: unknown }) => void) | null = null;
onerror: ((event: unknown) => void) | null = null;
onclose: ((event: { code: number; reason: string }) => void) | null = null;
private closed = false;
constructor(url: string, private readonly script: MockSocketScript) {
this.url = url;
setTimeout(() => this.runScript(), 0);
}
send(data: string): void {
this.sent.push(data);
}
emitFrame(frame: unknown): void {
this.onmessage?.({ data: JSON.stringify(frame) });
}
close(code = 1000, reason = ''): void {
this.emitClose(code, reason);
}
private runScript(): void {
if (this.closed) return;
if (this.script.open !== false) {
this.readyState = 1;
this.onopen?.();
for (const frame of this.script.frames ?? []) {
this.onmessage?.({ data: JSON.stringify(frame) });
}
}
if (this.script.closeCode !== undefined) {
this.emitClose(this.script.closeCode, this.script.closeReason ?? '');
}
}
private emitClose(code: number, reason: string): void {
if (this.closed) return;
this.closed = true;
this.readyState = 3;
this.onclose?.({ code, reason });
}
}
function scriptedSockets(scripts: MockSocketScript[]) {
const sockets: MockAgentWebSocket[] = [];
const webSocketFactory = vi.fn((url: string) => {
const socket = new MockAgentWebSocket(url, scripts[sockets.length] ?? { closeCode: 1000 });
sockets.push(socket);
return { socket };
});
return { sockets, webSocketFactory };
}
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: '??????',
viewRevision: 2,
conversationCount: 1,
}],
});
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls.every(([, init]) => (
(init?.headers as Record<string, string>).Authorization === 'Bearer access-one'
))).toBe(true);
});
it('submits a conversation turn through the persistent Agent Gateway Session', async () => {
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse(serverConversation))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-one',
status: 'queued',
error: null,
}, 202))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-one',
status: 'succeeded',
error: null,
}))
.mockResolvedValueOnce(jsonResponse(serverConversation));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
clientInstanceId: 'installation-one',
});
await expect(adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-two',
expectedTurnRevision: 1,
message: '????????????',
attachmentAssetIds: ['asset-reference'],
})).resolves.toMatchObject({
workspaceId: 'workspace-one',
turnRevision: 1,
});
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'https://square.example/api/agents/sessions/session-one/commands',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
client_command_id: 'turn-two',
name: 'turn.submit',
input: {
expected_turn_revision: 1,
message: '????????????',
attachment_asset_ids: ['asset-reference'],
action: null,
},
}),
}),
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'https://square.example/api/agents/sessions/session-one/runs/run-one',
expect.objectContaining({ headers: expect.any(Object) }),
);
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'https://square.example/api/design/workspaces/workspace-one/conversations/conversation-one',
expect.objectContaining({ headers: expect.any(Object) }),
);
const commandBody = JSON.parse(String(fetchMock.mock.calls[1][1]?.body));
expect(commandBody.input).not.toHaveProperty('conversation_id');
});
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',
media_type: 'image',
mime_type: 'image/png',
width: 1200,
height: 800,
duration_milliseconds: null,
created_at: '2026-08-06T10:00:00Z',
}));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
await expect(adapter.uploadAsset({
workspaceId: 'workspace-one',
fileName: 'poster.webp',
mimeType: 'image/webp',
bytes: new Uint8Array([1, 2, 3]),
})).resolves.toMatchObject({
assetId: 'asset-uploaded',
workspaceId: 'workspace-one',
mimeType: 'image/png',
contentPath: '/api/works/image-workspace/workspaces/workspace-one/assets/asset-uploaded/content',
});
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe('https://square.example/api/design/workspaces/workspace-one/assets');
expect(init?.method).toBe('POST');
expect((init?.headers as Record<string, string>).Authorization).toBe('Bearer access-one');
expect((init?.headers as Record<string, string>)['Content-Type']).toBeUndefined();
const form = init?.body as FormData;
expect(form.get('file')).toBeInstanceOf(File);
expect((form.get('file') as File).name).toBe('poster.webp');
expect(await (form.get('file') as File).arrayBuffer()).toEqual(
Uint8Array.from([1, 2, 3]).buffer,
);
});
it('completes a design turn from the connected WebSocket without polling the run endpoint', async () => {
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }]);
const fetchMock = vi.fn<typeof fetch>(async (input) => {
const url = String(input);
if (url.endsWith('/conversations/conversation-one')) {
return jsonResponse({ ...serverConversation, agent_session_id: 'session-live' });
}
if (url.endsWith('/stream-tickets')) {
return jsonResponse({
ticket: 'ticket-live',
transport: 'websocket',
stream_url: '/api/agents/sessions/session-live/ws?ticket=ticket-live',
expires_at: '2026-08-03T03:00:00Z',
});
}
if (url.endsWith('/api/agents/sessions/session-live/commands')) {
queueMicrotask(() => sockets[0]?.emitFrame({
type: 'event',
event: {
session_id: 'session-live',
sequence: 6,
runtime: 'design',
type: 'run.completed',
command_id: 'command-live',
run_id: 'run-live',
client_command_id: 'turn-live',
schema_version: 1,
terminal: true,
occurred_at: '2026-08-03T02:00:05Z',
payload: { status: 'succeeded' },
},
}));
return jsonResponse({
command_id: 'command-live',
run_id: 'run-live',
status: 'accepted',
}, 202);
}
if (url.endsWith('/conversations/conversation-one')) {
return jsonResponse(serverConversation);
}
throw new Error(`Unexpected request: ${url}`);
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const subscription = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-one', conversationId: 'conversation-one' });
try {
await expect(adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-live',
expectedTurnRevision: 1,
message: '????????????',
})).resolves.toMatchObject({ workspaceId: 'workspace-one' });
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/runs/'))).toHaveLength(0);
} finally {
subscription.close();
}
});
it('does not let terminal Run completion overtake streamed design events', async () => {
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }]);
const streamedConversation = {
...serverConversation,
agent_session_id: 'session-ordered',
turn_revision: 2,
};
const fetchMock = vi.fn<typeof fetch>(async (input) => {
const url = String(input);
if (url.endsWith('/conversations/conversation-one')) {
return jsonResponse(streamedConversation);
}
if (url.endsWith('/stream-tickets')) {
return jsonResponse({
stream_url: '/api/agents/sessions/session-ordered/ws?ticket=ticket-ordered',
});
}
if (url.endsWith('/api/agents/sessions/session-ordered/commands')) {
queueMicrotask(() => {
sockets[0]?.emitFrame({
type: 'event',
event: {
session_id: 'session-ordered',
sequence: 4,
runtime: 'design',
type: 'design.assistant.delta',
run_id: 'run-ordered',
schema_version: 1,
payload: {
workspace_id: 'workspace-one',
conversation_id: 'conversation-one',
client_turn_id: 'turn-ordered',
turn_revision: 2,
chunk_index: 0,
delta: '??????',
},
},
});
sockets[0]?.emitFrame({
type: 'event',
event: {
session_id: 'session-ordered',
sequence: 5,
runtime: 'design',
type: 'design.conversation.updated',
run_id: 'run-ordered',
schema_version: 1,
payload: {
workspace_id: 'workspace-one',
conversation_id: 'conversation-one',
workspace_view_revision: 3,
conversation: streamedConversation,
generation_tasks: [],
},
},
});
sockets[0]?.emitFrame({
type: 'event',
event: {
session_id: 'session-ordered',
sequence: 6,
runtime: 'design',
type: 'run.completed',
run_id: 'run-ordered',
schema_version: 1,
payload: { status: 'succeeded' },
},
});
});
return jsonResponse({ run_id: 'run-ordered', status: 'queued', error: null }, 202);
}
throw new Error(`Unexpected request: ${url}`);
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const subscription = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-one', conversationId: 'conversation-one' });
const releaseFirstWrite = deferred<void>();
const received: DesignWorkspaceEvent[] = [];
const consume = (async () => {
for await (const event of subscription.events) {
received.push(event);
if (received.length === 1) await releaseFirstWrite.promise;
if (received.length === 2) break;
}
})();
let resolved = false;
try {
const turn = adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-ordered',
expectedTurnRevision: 1,
message: '???????',
}).finally(() => {
resolved = true;
});
await vi.waitFor(() => expect(received).toHaveLength(1));
await new Promise<void>((resolve) => setTimeout(resolve, 0));
expect(received[0]).toMatchObject({
type: 'design.assistant.delta',
delta: '??????',
});
expect(resolved).toBe(false);
expect(fetchMock.mock.calls.filter(([url]) => (
String(url).endsWith('/conversations/conversation-one')
))).toHaveLength(1);
releaseFirstWrite.resolve();
await consume;
await expect(turn).resolves.toMatchObject({ turnRevision: 2 });
expect(received.map((event) => event.type)).toEqual([
'design.assistant.delta',
'design.conversation.snapshot',
]);
} finally {
releaseFirstWrite.resolve();
subscription.close();
}
});
it('bounds the delivery barrier when an opened stream has no consumer', async () => {
vi.useFakeTimers();
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }]);
const streamedConversation = {
...serverConversation,
agent_session_id: 'session-bounded',
turn_revision: 2,
};
const fetchMock = vi.fn<typeof fetch>(async (input) => {
const url = String(input);
if (url.endsWith('/conversations/conversation-one')) {
return jsonResponse(streamedConversation);
}
if (url.endsWith('/stream-tickets')) {
return jsonResponse({
stream_url: '/api/agents/sessions/session-bounded/ws?ticket=ticket-bounded',
});
}
if (url.endsWith('/api/agents/sessions/session-bounded/commands')) {
queueMicrotask(() => {
sockets[0]?.emitFrame({
type: 'event',
event: {
session_id: 'session-bounded',
sequence: 5,
runtime: 'design',
type: 'design.conversation.updated',
run_id: 'run-bounded',
schema_version: 1,
payload: {
workspace_id: 'workspace-one',
conversation_id: 'conversation-one',
workspace_view_revision: 3,
conversation: streamedConversation,
generation_tasks: [],
},
},
});
sockets[0]?.emitFrame({
type: 'event',
event: {
session_id: 'session-bounded',
sequence: 6,
runtime: 'design',
type: 'run.completed',
run_id: 'run-bounded',
schema_version: 1,
payload: { status: 'succeeded' },
},
});
});
return jsonResponse({ run_id: 'run-bounded', status: 'queued', error: null }, 202);
}
throw new Error(`Unexpected request: ${url}`);
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const subscriptionPromise = adapter.openWorkspaceEvents({ workspaceId: 'workspace-one', conversationId: 'conversation-one' });
await vi.advanceTimersByTimeAsync(0);
const subscription = await subscriptionPromise;
let resolved = false;
try {
const turn = adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-bounded',
expectedTurnRevision: 1,
message: '???????????????',
}).finally(() => {
resolved = true;
});
await vi.advanceTimersByTimeAsync(999);
expect(resolved).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(turn).resolves.toMatchObject({ turnRevision: 2 });
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/runs/')))
.toHaveLength(0);
} finally {
subscription.close();
vi.useRealTimers();
}
});
it('maps a failed Run received from the connected WebSocket without polling', async () => {
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }]);
const fetchMock = vi.fn<typeof fetch>(async (input) => {
const url = String(input);
if (url.endsWith('/conversations/conversation-one')) {
return jsonResponse({ ...serverConversation, agent_session_id: 'session-failed' });
}
if (url.endsWith('/stream-tickets')) {
return jsonResponse({
stream_url: '/api/agents/sessions/session-failed/ws?ticket=ticket-failed',
});
}
if (url.endsWith('/api/agents/sessions/session-failed/commands')) {
queueMicrotask(() => sockets[0]?.emitFrame({
type: 'event',
event: {
session_id: 'session-failed',
sequence: 6,
runtime: 'design',
type: 'run.failed',
run_id: 'run-failed',
schema_version: 1,
payload: {
error: {
code: 'agent_command_invalid',
message: 'private validation detail',
retryable: false,
},
},
},
}));
return jsonResponse({ run_id: 'run-failed', status: 'queued', error: null }, 202);
}
throw new Error(`Unexpected request: ${url}`);
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const subscription = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-one', conversationId: 'conversation-one' });
try {
await expect(adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-failed',
expectedTurnRevision: 1,
message: 'invalid',
})).rejects.toMatchObject({
status: 422,
code: 'agent_command_invalid',
message: '设计请求内容无效,请检查后重试',
});
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/runs/'))).toHaveLength(0);
} finally {
subscription.close();
}
});
it('falls back to one Run request after the WebSocket disconnects', async () => {
const { webSocketFactory } = scriptedSockets([{ open: true, closeCode: 1006 }]);
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({
...serverConversation,
agent_session_id: 'session-fallback',
}))
.mockResolvedValueOnce(jsonResponse({
stream_url: '/api/agents/sessions/session-fallback/ws?ticket=ticket-fallback',
}))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-fallback',
status: 'queued',
error: null,
}, 202))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-fallback',
status: 'succeeded',
error: null,
}))
.mockResolvedValueOnce(jsonResponse(serverConversation));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const subscription = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-one', conversationId: 'conversation-one' });
try {
await expect(adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-fallback',
expectedTurnRevision: 1,
message: '???????',
})).resolves.toMatchObject({ workspaceId: 'workspace-one' });
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/runs/'))).toHaveLength(1);
} finally {
subscription.close();
}
});
it('waits for a slow design run without flooding the run endpoint', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-08-03T02:00:00Z'));
const startedAt = Date.now();
let runPolls = 0;
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
const url = String(input);
if (url.endsWith('/conversations/conversation-one')) {
return jsonResponse({ ...serverConversation, agent_session_id: 'session-slow' });
}
if (url.endsWith('/api/agents/sessions/session-slow/commands')) {
return jsonResponse({ run_id: 'run-slow', status: 'queued', error: null }, 202);
}
if (url.endsWith('/api/agents/sessions/session-slow/runs/run-slow')) {
runPolls += 1;
return jsonResponse({
run_id: 'run-slow',
status: Date.now() - startedAt >= 150_000 ? 'succeeded' : 'running',
error: null,
});
}
if (url.endsWith('/conversations/conversation-one')) {
return jsonResponse(serverConversation);
}
throw new Error(`Unexpected request: ${url} ${init?.method ?? 'GET'}`);
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
try {
const outcomePromise = adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-slow',
expectedTurnRevision: 1,
message: '?????????????',
}).then(
(value) => ({ value, error: null }),
(error: unknown) => ({ value: null, error }),
);
await vi.advanceTimersByTimeAsync(160_000);
const outcome = await outcomePromise;
expect(outcome.error).toBeNull();
expect(outcome.value).toMatchObject({ workspaceId: 'workspace-one' });
expect(runPolls).toBeLessThanOrEqual(40);
} finally {
vi.useRealTimers();
}
});
it('maps an invalid Runtime command to a user input error', async () => {
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse(serverConversation))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-invalid',
status: 'queued',
error: null,
}, 202))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-invalid',
status: 'failed',
error: {
code: 'agent_command_invalid',
message: 'private validation detail',
retryable: false,
},
}));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
await expect(adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-invalid',
expectedTurnRevision: 1,
message: 'invalid',
})).rejects.toMatchObject({
status: 422,
code: 'agent_command_invalid',
message: '设计请求内容无效,请检查后重试',
});
});
it('keeps task creation behind structured Quote confirmation', async () => {
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse(serverConversation))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-confirm',
status: 'queued',
error: null,
}, 202))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-confirm',
status: 'succeeded',
error: null,
}))
.mockResolvedValueOnce(jsonResponse(serverConversation));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example/',
fetchImpl: fetchMock,
});
const workspace = await adapter.confirmGeneration({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-two',
expectedTurnRevision: 1,
quoteId: 'quote-one',
});
expect(workspace.workspaceId).toBe('workspace-one');
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'https://square.example/api/agents/sessions/session-one/commands',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
client_command_id: 'turn-two',
name: 'turn.submit',
input: {
expected_turn_revision: 1,
message: '确认生成',
attachment_asset_ids: [],
action: { type: 'confirm_generation', quote_id: 'quote-one' },
},
}),
}),
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'https://square.example/api/agents/sessions/session-one/runs/run-confirm',
expect.objectContaining({ headers: expect.any(Object) }),
);
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'https://square.example/api/design/workspaces/workspace-one/conversations/conversation-one',
expect.objectContaining({ headers: expect.any(Object) }),
);
});
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',
},
}),
);
});
it('reuses one design Agent Session and normalizes matching task events from fresh WebSocket tickets', async () => {
const snapshotTask = {
task_id: 'task-snapshot',
workspace_id: 'workspace-one',
medium: 'image',
status: 'running',
brief_version: 1,
brief_summary: 'snapshot brief',
quote_id: 'quote-snapshot',
quoted_design_points: 1,
failure_code: null,
result_assets: [],
created_at: '2026-08-02T09:59:00Z',
updated_at: '2026-08-02T10:00:00Z',
};
const snapshotEvent = {
session_id: 'session-one',
sequence: 1,
runtime: 'design',
type: 'design.conversation.updated',
schema_version: 1,
payload: {
workspace_id: 'workspace-one',
conversation_id: 'conversation-one',
workspace_view_revision: 2,
conversation: serverConversation,
generation_tasks: [snapshotTask],
},
};
const assistantDeltaEvent = {
session_id: 'session-one',
sequence: 2,
runtime: 'design',
type: 'design.assistant.delta',
schema_version: 1,
payload: {
workspace_id: 'workspace-one',
conversation_id: 'conversation-one',
client_turn_id: 'turn-two',
turn_revision: 2,
chunk_index: 0,
delta: '??????',
},
};
const malformedDeltaEvent = {
...assistantDeltaEvent,
sequence: 99,
payload: {
...assistantDeltaEvent.payload,
chunk_index: -1,
},
};
const taskEvent = {
session_id: 'session-one',
sequence: 3,
runtime: 'design',
type: 'design.generation_task.updated',
command_id: null,
run_id: null,
client_command_id: null,
schema_version: 1,
terminal: false,
occurred_at: '2026-08-02T10:01:00Z',
payload: {
workspace_id: 'workspace-one',
workspace_view_revision: 4,
generation_task: {
task_id: 'task-live',
workspace_id: 'workspace-one',
medium: 'video',
status: 'running',
brief_version: 2,
brief_summary: '??????',
quote_id: 'quote-live',
quoted_design_points: 8,
failure_code: null,
result_assets: [],
created_at: '2026-08-02T10:00:00Z',
updated_at: '2026-08-02T10:01:00Z',
},
},
};
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse(serverConversation))
.mockResolvedValueOnce(jsonResponse({
ticket: 'secret-ticket-one',
transport: 'websocket',
stream_url: '/api/agents/sessions/session-one/ws?ticket=secret-ticket-one',
expires_at: '2026-08-02T10:02:00Z',
}))
.mockResolvedValueOnce(jsonResponse({
ticket: 'secret-ticket-two',
transport: 'websocket',
stream_url: '/api/agents/sessions/session-one/ws?ticket=secret-ticket-two',
expires_at: '2026-08-02T10:03:00Z',
}));
const { sockets, webSocketFactory } = scriptedSockets([
{
frames: [
{ type: 'event', event: snapshotEvent },
{ type: 'event', event: malformedDeltaEvent },
{ type: 'event', event: assistantDeltaEvent },
{ type: 'event', event: taskEvent },
],
closeCode: 1000,
},
{ closeCode: 1000 },
]);
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const first = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-one', conversationId: 'conversation-one' });
const received = [];
for await (const event of first.events) received.push(event);
first.close();
const second = await adapter.openWorkspaceEvents({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
afterEventId: 'session-one:3',
});
for await (const _event of second.events) {
// The second connection only proves Session reuse and a fresh ticket.
}
second.close();
expect(received).toEqual([
{
id: 'session-one:1',
type: 'design.conversation.snapshot',
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
workspaceViewRevision: 2,
conversation: expect.objectContaining({
conversationId: 'conversation-one',
title: serverConversation.title,
messages: [expect.objectContaining({
text: serverConversation.messages[0].text,
})],
}),
generationTasks: [expect.objectContaining({
taskId: 'task-snapshot',
medium: 'image',
status: 'running',
})],
},
{
id: 'session-one:2',
type: 'design.assistant.delta',
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-two',
turnRevision: 2,
chunkIndex: 0,
delta: '??????',
},
{
id: 'session-one:3',
type: 'design.generation_task.updated',
workspaceId: 'workspace-one',
workspaceViewRevision: 4,
generationTask: expect.objectContaining({
taskId: 'task-live',
medium: 'video',
status: 'running',
}),
},
]);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'https://square.example/api/design/workspaces/workspace-one/conversations/conversation-one',
expect.objectContaining({ headers: expect.any(Object) }),
);
expect(fetchMock.mock.calls.filter(([url]) => (
String(url).endsWith('/conversations/conversation-one')
)))
.toHaveLength(1);
expect(fetchMock.mock.calls.filter(([url]) => String(url).endsWith('/stream-tickets')))
.toHaveLength(2);
const ticketCalls = fetchMock.mock.calls.filter(([url]) => (
String(url).endsWith('/stream-tickets')
));
expect(ticketCalls.every(([, init]) => (
JSON.parse(String(init?.body)).transport === 'websocket'
))).toBe(true);
expect(sockets.map((socket) => socket.url)).toEqual([
'wss://square.example/api/agents/sessions/session-one/ws?ticket=secret-ticket-one&after_sequence=0',
'wss://square.example/api/agents/sessions/session-one/ws?ticket=secret-ticket-two&after_sequence=3',
]);
});
it('reloads the persistent Conversation Session after an upstream event cursor expires', async () => {
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({ ...serverConversation, agent_session_id: 'session-old' }))
.mockResolvedValueOnce(jsonResponse({
stream_url: '/api/agents/sessions/session-old/ws?ticket=ticket-old',
}))
.mockResolvedValueOnce(jsonResponse({ ...serverConversation, agent_session_id: 'session-new' }))
.mockResolvedValueOnce(jsonResponse({
stream_url: '/api/agents/sessions/session-new/ws?ticket=ticket-new',
}));
const { sockets, webSocketFactory } = scriptedSockets([
{ open: false, closeCode: 4409, closeReason: 'Agent event cursor expired' },
{ closeCode: 1000 },
]);
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
await expect(adapter.openWorkspaceEvents({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
afterEventId: 'session-old:99',
})).rejects.toMatchObject({ status: 410 });
const recovered = await adapter.openWorkspaceEvents({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
afterEventId: 'session-old:99',
});
for await (const _event of recovered.events) {
// Empty recovery stream.
}
expect(fetchMock.mock.calls.filter(([url]) => (
String(url).endsWith('/conversations/conversation-one')
))).toHaveLength(2);
expect(fetchMock.mock.calls.some(([, init]) => init?.method === 'DELETE')).toBe(false);
expect(sockets.map((socket) => socket.url)).toEqual([
'wss://square.example/api/agents/sessions/session-old/ws?ticket=ticket-old&after_sequence=99',
'wss://square.example/api/agents/sessions/session-new/ws?ticket=ticket-new&after_sequence=0',
]);
});
it('refreshes a closed persistent Session before opening the task stream', async () => {
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({ ...serverConversation, agent_session_id: 'session-old' }))
.mockResolvedValueOnce(jsonResponse({
code: 'agent_session_closed', message: 'closed',
}, 409))
.mockResolvedValueOnce(jsonResponse({ ...serverConversation, agent_session_id: 'session-new' }))
.mockResolvedValueOnce(jsonResponse({
stream_url: '/api/agents/sessions/session-new/ws?ticket=ticket-new',
}));
const { webSocketFactory } = scriptedSockets([{ closeCode: 1000 }]);
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const recovered = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-one', conversationId: 'conversation-one' });
for await (const _event of recovered.events) {
// Empty recovery stream.
}
expect(fetchMock.mock.calls.filter(([url]) => (
String(url).endsWith('/conversations/conversation-one')
))).toHaveLength(2);
expect(fetchMock.mock.calls.filter(([url]) => String(url).endsWith('/stream-tickets')))
.toHaveLength(2);
});
it('closes active local streams without deleting persistent Agent Sessions', async () => {
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse(serverConversation))
.mockResolvedValueOnce(jsonResponse({
stream_url: '/api/agents/sessions/session-one/ws?ticket=ticket-one',
}))
.mockResolvedValueOnce(jsonResponse({
...serverConversation,
workspace_id: 'workspace-two',
conversation_id: 'conversation-two',
agent_session_id: 'session-two',
}))
.mockResolvedValueOnce(jsonResponse({
stream_url: '/api/agents/sessions/session-two/ws?ticket=ticket-two',
}));
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' });
const second = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-two', conversationId: 'conversation-two' });
await adapter.closeEventSessions();
const closeCalls = fetchMock.mock.calls.filter(([, init]) => init?.method === 'DELETE');
expect(closeCalls).toHaveLength(0);
expect(sockets.map((socket) => socket.readyState)).toEqual([3, 3]);
first.close();
second.close();
});
it('reports an unavailable persistent Session instead of creating a client-owned Session', async () => {
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse({
...serverConversation,
agent_session_id: null,
}));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
await expect(adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-unavailable',
expectedTurnRevision: 1,
message: 'hello',
})).rejects.toMatchObject({
status: 503,
code: 'DESIGN_CONVERSATION_SESSION_UNAVAILABLE',
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('retries a turn once with the refreshed persistent Session when the old Session is stale', async () => {
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({ ...serverConversation, agent_session_id: 'session-old' }))
.mockResolvedValueOnce(jsonResponse({
code: 'agent_session_not_found',
message: 'stale',
}, 404))
.mockResolvedValueOnce(jsonResponse({ ...serverConversation, agent_session_id: 'session-new' }))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-new',
status: 'queued',
error: null,
}, 202))
.mockResolvedValueOnce(jsonResponse({
run_id: 'run-new',
status: 'succeeded',
error: null,
}))
.mockResolvedValueOnce(jsonResponse({ ...serverConversation, agent_session_id: 'session-new' }));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
await expect(adapter.submitMessage({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: 'turn-retry',
expectedTurnRevision: 1,
message: 'retry',
})).resolves.toMatchObject({ conversationId: 'conversation-one' });
expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([
'https://square.example/api/design/workspaces/workspace-one/conversations/conversation-one',
'https://square.example/api/agents/sessions/session-old/commands',
'https://square.example/api/design/workspaces/workspace-one/conversations/conversation-one',
'https://square.example/api/agents/sessions/session-new/commands',
'https://square.example/api/agents/sessions/session-new/runs/run-new',
'https://square.example/api/design/workspaces/workspace-one/conversations/conversation-one',
]);
});
});