fix(design): bound requests and prevent mutation replay

This commit is contained in:
2026-08-19 12:51:46 +08:00
parent 1ba68a9e41
commit 87e4140f8a
7 changed files with 447 additions and 55 deletions

View File

@@ -627,6 +627,95 @@ describe('Works Square AI design adapter', () => {
);
});
it('aborts a stuck Quote request at the design request deadline', async () => {
vi.useFakeTimers();
try {
let requestSignal: AbortSignal | null = null;
const fetchMock = vi.fn<typeof fetch>((_input, init) => {
requestSignal = init?.signal ?? null;
return new Promise<Response>(() => undefined);
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
requestTimeoutMs: 25,
});
const outcome = adapter.updateGenerationQuote({
workspaceId: 'workspace-one',
quoteId: 'quote-one',
finalPrompt: 'updated prompt',
generationParameters: quoteConfirmationInput.generationParameters,
}).then(
() => ({ state: 'resolved' as const }),
(error: unknown) => ({ state: 'rejected' as const, error }),
);
await vi.advanceTimersByTimeAsync(25);
await expect(Promise.race([
outcome,
Promise.resolve({ state: 'pending' as const }),
])).resolves.toMatchObject({
state: 'rejected',
error: {
status: 504,
code: 'DESIGN_WORKSPACE_REQUEST_TIMEOUT',
message: 'AI 设计服务响应超时,请重试',
},
});
expect(requestSignal?.aborted).toBe(true);
expect(fetchMock).toHaveBeenCalledOnce();
} finally {
vi.useRealTimers();
}
});
it('also bounds a Quote response whose JSON body never finishes', async () => {
vi.useFakeTimers();
try {
let requestSignal: AbortSignal | null = null;
const fetchMock = vi.fn<typeof fetch>((_input, init) => {
requestSignal = init?.signal ?? null;
return Promise.resolve(new Response(new ReadableStream<Uint8Array>({
start: () => undefined,
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
requestTimeoutMs: 25,
});
const outcome = adapter.updateGenerationQuote({
workspaceId: 'workspace-one',
quoteId: 'quote-one',
finalPrompt: 'updated prompt',
generationParameters: quoteConfirmationInput.generationParameters,
}).then(
() => ({ state: 'resolved' as const }),
(error: unknown) => ({ state: 'rejected' as const, error }),
);
await vi.advanceTimersByTimeAsync(25);
await expect(Promise.race([
outcome,
Promise.resolve({ state: 'pending' as const }),
])).resolves.toMatchObject({
state: 'rejected',
error: {
status: 504,
code: 'DESIGN_WORKSPACE_REQUEST_TIMEOUT',
},
});
expect(requestSignal?.aborted).toBe(true);
} finally {
vi.useRealTimers();
}
});
it('normalizes the server video option shape so the duration picker stays available', async () => {
const videoConversation = {
...serverConversation,
@@ -1731,6 +1820,7 @@ describe('Works Square AI design adapter', () => {
expect(getTokenMock).toHaveBeenNthCalledWith(2, {
fetchImpl: fetchMock,
forceRefresh: true,
requestTimeoutMs: 30_000,
});
expect(fetchMock).toHaveBeenNthCalledWith(
2,