@lobehub/chat 1.102.3 → 1.103.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/apps/desktop/README.md +322 -36
- package/apps/desktop/README.zh-CN.md +353 -0
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/resources/tray-dark.png +0 -0
- package/apps/desktop/resources/tray-light.png +0 -0
- package/apps/desktop/resources/tray.png +0 -0
- package/apps/desktop/src/main/const/env.ts +25 -0
- package/apps/desktop/src/main/core/TrayManager.ts +7 -1
- package/changelog/v1.json +18 -0
- package/locales/ar/subscription.json +24 -0
- package/locales/bg-BG/subscription.json +24 -0
- package/locales/de-DE/subscription.json +24 -0
- package/locales/en-US/subscription.json +24 -0
- package/locales/es-ES/subscription.json +24 -0
- package/locales/fa-IR/subscription.json +24 -0
- package/locales/fr-FR/subscription.json +24 -0
- package/locales/it-IT/subscription.json +24 -0
- package/locales/ja-JP/subscription.json +24 -0
- package/locales/ko-KR/subscription.json +24 -0
- package/locales/nl-NL/subscription.json +24 -0
- package/locales/pl-PL/subscription.json +24 -0
- package/locales/pt-BR/subscription.json +24 -0
- package/locales/ru-RU/subscription.json +24 -0
- package/locales/tr-TR/subscription.json +24 -0
- package/locales/vi-VN/subscription.json +24 -0
- package/locales/zh-CN/subscription.json +24 -0
- package/locales/zh-TW/subscription.json +24 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/README.md +55 -30
- package/packages/electron-client-ipc/README.zh-CN.md +73 -0
- package/packages/electron-server-ipc/README.md +42 -20
- package/packages/electron-server-ipc/README.zh-CN.md +76 -0
- package/packages/file-loaders/README.md +77 -51
- package/packages/file-loaders/README.zh-CN.md +89 -0
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +11 -8
- package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +11 -8
- package/src/app/[variants]/(main)/chat/(workspace)/features/ShareButton/index.tsx +3 -0
- package/src/app/[variants]/(main)/chat/@session/_layout/Desktop/SessionHeader.tsx +3 -0
- package/src/config/aiModels/qwen.ts +22 -2
- package/src/features/PlanIcon/index.tsx +126 -0
- package/src/features/User/PlanTag.tsx +33 -25
- package/src/libs/model-runtime/qwen/createImage.test.ts +613 -0
- package/src/libs/model-runtime/qwen/createImage.ts +218 -0
- package/src/libs/model-runtime/qwen/index.ts +2 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +19 -1
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/subscription.ts +24 -0
- package/src/types/subscription.ts +7 -0
- package/apps/desktop/resources/tray-icon.png +0 -0
@@ -0,0 +1,613 @@
|
|
1
|
+
// @vitest-environment edge-runtime
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { CreateImagePayload } from '../types/image';
|
5
|
+
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
|
6
|
+
import { createQwenImage } from './createImage';
|
7
|
+
|
8
|
+
// Mock the console.error to avoid polluting test output
|
9
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
10
|
+
|
11
|
+
const mockOptions: CreateImageOptions = {
|
12
|
+
apiKey: 'test-api-key',
|
13
|
+
provider: 'qwen',
|
14
|
+
};
|
15
|
+
|
16
|
+
beforeEach(() => {
|
17
|
+
// Reset all mocks before each test
|
18
|
+
vi.clearAllMocks();
|
19
|
+
});
|
20
|
+
|
21
|
+
afterEach(() => {
|
22
|
+
vi.clearAllMocks();
|
23
|
+
});
|
24
|
+
|
25
|
+
describe('createQwenImage', () => {
|
26
|
+
describe('Success scenarios', () => {
|
27
|
+
it('should successfully generate image with immediate success', async () => {
|
28
|
+
const mockTaskId = 'task-123456';
|
29
|
+
const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/test-image.jpg';
|
30
|
+
|
31
|
+
// Mock fetch for task creation and immediate success
|
32
|
+
global.fetch = vi
|
33
|
+
.fn()
|
34
|
+
.mockResolvedValueOnce({
|
35
|
+
ok: true,
|
36
|
+
json: async () => ({
|
37
|
+
output: { task_id: mockTaskId },
|
38
|
+
request_id: 'req-123',
|
39
|
+
}),
|
40
|
+
})
|
41
|
+
.mockResolvedValueOnce({
|
42
|
+
ok: true,
|
43
|
+
json: async () => ({
|
44
|
+
output: {
|
45
|
+
task_id: mockTaskId,
|
46
|
+
task_status: 'SUCCEEDED',
|
47
|
+
results: [{ url: mockImageUrl }],
|
48
|
+
},
|
49
|
+
request_id: 'req-124',
|
50
|
+
}),
|
51
|
+
});
|
52
|
+
|
53
|
+
const payload: CreateImagePayload = {
|
54
|
+
model: 'wanx2.1-t2i-turbo',
|
55
|
+
params: {
|
56
|
+
prompt: 'A beautiful sunset over the mountains',
|
57
|
+
},
|
58
|
+
};
|
59
|
+
|
60
|
+
const result = await createQwenImage(payload, mockOptions);
|
61
|
+
|
62
|
+
// Verify task creation request
|
63
|
+
expect(fetch).toHaveBeenCalledWith(
|
64
|
+
'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis',
|
65
|
+
{
|
66
|
+
method: 'POST',
|
67
|
+
headers: {
|
68
|
+
'Authorization': 'Bearer test-api-key',
|
69
|
+
'Content-Type': 'application/json',
|
70
|
+
'X-DashScope-Async': 'enable',
|
71
|
+
},
|
72
|
+
body: JSON.stringify({
|
73
|
+
input: {
|
74
|
+
prompt: 'A beautiful sunset over the mountains',
|
75
|
+
},
|
76
|
+
model: 'wanx2.1-t2i-turbo',
|
77
|
+
parameters: {
|
78
|
+
n: 1,
|
79
|
+
size: '1024*1024',
|
80
|
+
},
|
81
|
+
}),
|
82
|
+
},
|
83
|
+
);
|
84
|
+
|
85
|
+
// Verify status query request
|
86
|
+
expect(fetch).toHaveBeenCalledWith(
|
87
|
+
`https://dashscope.aliyuncs.com/api/v1/tasks/${mockTaskId}`,
|
88
|
+
{
|
89
|
+
headers: {
|
90
|
+
Authorization: 'Bearer test-api-key',
|
91
|
+
},
|
92
|
+
},
|
93
|
+
);
|
94
|
+
|
95
|
+
expect(result).toEqual({
|
96
|
+
imageUrl: mockImageUrl,
|
97
|
+
});
|
98
|
+
});
|
99
|
+
|
100
|
+
it('should handle task that needs polling before success', async () => {
|
101
|
+
const mockTaskId = 'task-polling';
|
102
|
+
const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/test-image-3.jpg';
|
103
|
+
|
104
|
+
global.fetch = vi
|
105
|
+
.fn()
|
106
|
+
.mockResolvedValueOnce({
|
107
|
+
ok: true,
|
108
|
+
json: async () => ({
|
109
|
+
output: { task_id: mockTaskId },
|
110
|
+
request_id: 'req-127',
|
111
|
+
}),
|
112
|
+
})
|
113
|
+
// First status query - still running
|
114
|
+
.mockResolvedValueOnce({
|
115
|
+
ok: true,
|
116
|
+
json: async () => ({
|
117
|
+
output: {
|
118
|
+
task_id: mockTaskId,
|
119
|
+
task_status: 'RUNNING',
|
120
|
+
},
|
121
|
+
request_id: 'req-128',
|
122
|
+
}),
|
123
|
+
})
|
124
|
+
// Second status query - succeeded
|
125
|
+
.mockResolvedValueOnce({
|
126
|
+
ok: true,
|
127
|
+
json: async () => ({
|
128
|
+
output: {
|
129
|
+
task_id: mockTaskId,
|
130
|
+
task_status: 'SUCCEEDED',
|
131
|
+
results: [{ url: mockImageUrl }],
|
132
|
+
},
|
133
|
+
request_id: 'req-129',
|
134
|
+
}),
|
135
|
+
});
|
136
|
+
|
137
|
+
const payload: CreateImagePayload = {
|
138
|
+
model: 'wanx2.1-t2i-turbo',
|
139
|
+
params: {
|
140
|
+
prompt: 'Abstract digital art',
|
141
|
+
},
|
142
|
+
};
|
143
|
+
|
144
|
+
const result = await createQwenImage(payload, mockOptions);
|
145
|
+
|
146
|
+
// Should have made 3 fetch calls: 1 create + 2 status checks
|
147
|
+
expect(fetch).toHaveBeenCalledTimes(3);
|
148
|
+
expect(result).toEqual({
|
149
|
+
imageUrl: mockImageUrl,
|
150
|
+
});
|
151
|
+
});
|
152
|
+
|
153
|
+
it('should handle custom image dimensions', async () => {
|
154
|
+
const mockTaskId = 'task-custom-size';
|
155
|
+
const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/custom-size.jpg';
|
156
|
+
|
157
|
+
global.fetch = vi
|
158
|
+
.fn()
|
159
|
+
.mockResolvedValueOnce({
|
160
|
+
ok: true,
|
161
|
+
json: async () => ({
|
162
|
+
output: { task_id: mockTaskId },
|
163
|
+
request_id: 'req-140',
|
164
|
+
}),
|
165
|
+
})
|
166
|
+
.mockResolvedValueOnce({
|
167
|
+
ok: true,
|
168
|
+
json: async () => ({
|
169
|
+
output: {
|
170
|
+
task_id: mockTaskId,
|
171
|
+
task_status: 'SUCCEEDED',
|
172
|
+
results: [{ url: mockImageUrl }],
|
173
|
+
},
|
174
|
+
request_id: 'req-141',
|
175
|
+
}),
|
176
|
+
});
|
177
|
+
|
178
|
+
const payload: CreateImagePayload = {
|
179
|
+
model: 'wanx2.1-t2i-turbo',
|
180
|
+
params: {
|
181
|
+
prompt: 'Custom size image',
|
182
|
+
width: 512,
|
183
|
+
height: 768,
|
184
|
+
},
|
185
|
+
};
|
186
|
+
|
187
|
+
await createQwenImage(payload, mockOptions);
|
188
|
+
|
189
|
+
expect(fetch).toHaveBeenCalledWith(
|
190
|
+
'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis',
|
191
|
+
expect.objectContaining({
|
192
|
+
body: JSON.stringify({
|
193
|
+
input: {
|
194
|
+
prompt: 'Custom size image',
|
195
|
+
},
|
196
|
+
model: 'wanx2.1-t2i-turbo',
|
197
|
+
parameters: {
|
198
|
+
n: 1,
|
199
|
+
size: '512*768',
|
200
|
+
},
|
201
|
+
}),
|
202
|
+
}),
|
203
|
+
);
|
204
|
+
});
|
205
|
+
|
206
|
+
it('should handle long running tasks with retries', async () => {
|
207
|
+
const mockTaskId = 'task-long-running';
|
208
|
+
|
209
|
+
// Mock status query that returns RUNNING a few times then succeeds
|
210
|
+
let statusCallCount = 0;
|
211
|
+
const statusQueryMock = vi.fn().mockImplementation(() => {
|
212
|
+
statusCallCount++;
|
213
|
+
if (statusCallCount <= 3) {
|
214
|
+
return Promise.resolve({
|
215
|
+
ok: true,
|
216
|
+
json: async () => ({
|
217
|
+
output: {
|
218
|
+
task_id: mockTaskId,
|
219
|
+
task_status: 'RUNNING',
|
220
|
+
},
|
221
|
+
request_id: `req-${133 + statusCallCount}`,
|
222
|
+
}),
|
223
|
+
});
|
224
|
+
} else {
|
225
|
+
return Promise.resolve({
|
226
|
+
ok: true,
|
227
|
+
json: async () => ({
|
228
|
+
output: {
|
229
|
+
task_id: mockTaskId,
|
230
|
+
task_status: 'SUCCEEDED',
|
231
|
+
results: [{ url: 'https://example.com/final-image.jpg' }],
|
232
|
+
},
|
233
|
+
request_id: 'req-137',
|
234
|
+
}),
|
235
|
+
});
|
236
|
+
}
|
237
|
+
});
|
238
|
+
|
239
|
+
global.fetch = vi
|
240
|
+
.fn()
|
241
|
+
.mockResolvedValueOnce({
|
242
|
+
ok: true,
|
243
|
+
json: async () => ({
|
244
|
+
output: { task_id: mockTaskId },
|
245
|
+
request_id: 'req-132',
|
246
|
+
}),
|
247
|
+
})
|
248
|
+
.mockImplementation(statusQueryMock);
|
249
|
+
|
250
|
+
const payload: CreateImagePayload = {
|
251
|
+
model: 'wanx2.1-t2i-turbo',
|
252
|
+
params: {
|
253
|
+
prompt: 'Long running task',
|
254
|
+
},
|
255
|
+
};
|
256
|
+
|
257
|
+
// Mock setTimeout to make test run faster but still allow controlled execution
|
258
|
+
vi.spyOn(global, 'setTimeout').mockImplementation((callback: any) => {
|
259
|
+
// Use setImmediate to avoid recursion issues
|
260
|
+
setImmediate(callback);
|
261
|
+
return 1 as any;
|
262
|
+
});
|
263
|
+
|
264
|
+
const result = await createQwenImage(payload, mockOptions);
|
265
|
+
|
266
|
+
expect(result).toEqual({
|
267
|
+
imageUrl: 'https://example.com/final-image.jpg',
|
268
|
+
});
|
269
|
+
|
270
|
+
// Should have made 1 create call + 4 status calls (3 RUNNING + 1 SUCCEEDED)
|
271
|
+
expect(fetch).toHaveBeenCalledTimes(5);
|
272
|
+
});
|
273
|
+
|
274
|
+
it('should handle seed value of 0 correctly', async () => {
|
275
|
+
const mockTaskId = 'task-with-zero-seed';
|
276
|
+
const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/seed-zero.jpg';
|
277
|
+
|
278
|
+
global.fetch = vi
|
279
|
+
.fn()
|
280
|
+
.mockResolvedValueOnce({
|
281
|
+
ok: true,
|
282
|
+
json: async () => ({
|
283
|
+
output: { task_id: mockTaskId },
|
284
|
+
request_id: 'req-seed-0',
|
285
|
+
}),
|
286
|
+
})
|
287
|
+
.mockResolvedValueOnce({
|
288
|
+
ok: true,
|
289
|
+
json: async () => ({
|
290
|
+
output: {
|
291
|
+
task_id: mockTaskId,
|
292
|
+
task_status: 'SUCCEEDED',
|
293
|
+
results: [{ url: mockImageUrl }],
|
294
|
+
},
|
295
|
+
request_id: 'req-seed-0-status',
|
296
|
+
}),
|
297
|
+
});
|
298
|
+
|
299
|
+
const payload: CreateImagePayload = {
|
300
|
+
model: 'wanx2.1-t2i-turbo',
|
301
|
+
params: {
|
302
|
+
prompt: 'Image with seed 0',
|
303
|
+
seed: 0,
|
304
|
+
},
|
305
|
+
};
|
306
|
+
|
307
|
+
await createQwenImage(payload, mockOptions);
|
308
|
+
|
309
|
+
// Verify that seed: 0 is included in the request
|
310
|
+
expect(fetch).toHaveBeenCalledWith(
|
311
|
+
'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis',
|
312
|
+
expect.objectContaining({
|
313
|
+
body: JSON.stringify({
|
314
|
+
input: {
|
315
|
+
prompt: 'Image with seed 0',
|
316
|
+
},
|
317
|
+
model: 'wanx2.1-t2i-turbo',
|
318
|
+
parameters: {
|
319
|
+
n: 1,
|
320
|
+
seed: 0,
|
321
|
+
size: '1024*1024',
|
322
|
+
},
|
323
|
+
}),
|
324
|
+
}),
|
325
|
+
);
|
326
|
+
});
|
327
|
+
});
|
328
|
+
|
329
|
+
describe('Error scenarios', () => {
|
330
|
+
it('should handle unsupported model', async () => {
|
331
|
+
const payload: CreateImagePayload = {
|
332
|
+
model: 'unsupported-model',
|
333
|
+
params: {
|
334
|
+
prompt: 'Test prompt',
|
335
|
+
},
|
336
|
+
};
|
337
|
+
|
338
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
339
|
+
expect.objectContaining({
|
340
|
+
errorType: 'ProviderBizError',
|
341
|
+
provider: 'qwen',
|
342
|
+
}),
|
343
|
+
);
|
344
|
+
|
345
|
+
// Should not make any fetch calls
|
346
|
+
expect(fetch).not.toHaveBeenCalled();
|
347
|
+
});
|
348
|
+
|
349
|
+
it('should handle task creation failure', async () => {
|
350
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
351
|
+
ok: false,
|
352
|
+
statusText: 'Bad Request',
|
353
|
+
json: async () => ({
|
354
|
+
message: 'Invalid model name',
|
355
|
+
}),
|
356
|
+
});
|
357
|
+
|
358
|
+
const payload: CreateImagePayload = {
|
359
|
+
model: 'invalid-model',
|
360
|
+
params: {
|
361
|
+
prompt: 'Test prompt',
|
362
|
+
},
|
363
|
+
};
|
364
|
+
|
365
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
366
|
+
expect.objectContaining({
|
367
|
+
errorType: 'ProviderBizError',
|
368
|
+
provider: 'qwen',
|
369
|
+
}),
|
370
|
+
);
|
371
|
+
});
|
372
|
+
|
373
|
+
it('should handle non-JSON error responses', async () => {
|
374
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
375
|
+
ok: false,
|
376
|
+
status: 500,
|
377
|
+
statusText: 'Internal Server Error',
|
378
|
+
json: async () => {
|
379
|
+
throw new Error('Failed to parse JSON');
|
380
|
+
},
|
381
|
+
});
|
382
|
+
|
383
|
+
const payload: CreateImagePayload = {
|
384
|
+
model: 'wanx2.1-t2i-turbo',
|
385
|
+
params: {
|
386
|
+
prompt: 'Test prompt',
|
387
|
+
},
|
388
|
+
};
|
389
|
+
|
390
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
391
|
+
expect.objectContaining({
|
392
|
+
errorType: 'ProviderBizError',
|
393
|
+
provider: 'qwen',
|
394
|
+
}),
|
395
|
+
);
|
396
|
+
});
|
397
|
+
|
398
|
+
it('should handle task failure status', async () => {
|
399
|
+
const mockTaskId = 'task-failed';
|
400
|
+
|
401
|
+
global.fetch = vi
|
402
|
+
.fn()
|
403
|
+
.mockResolvedValueOnce({
|
404
|
+
ok: true,
|
405
|
+
json: async () => ({
|
406
|
+
output: { task_id: mockTaskId },
|
407
|
+
request_id: 'req-130',
|
408
|
+
}),
|
409
|
+
})
|
410
|
+
.mockResolvedValueOnce({
|
411
|
+
ok: true,
|
412
|
+
json: async () => ({
|
413
|
+
output: {
|
414
|
+
task_id: mockTaskId,
|
415
|
+
task_status: 'FAILED',
|
416
|
+
error_message: 'Content policy violation',
|
417
|
+
},
|
418
|
+
request_id: 'req-131',
|
419
|
+
}),
|
420
|
+
});
|
421
|
+
|
422
|
+
const payload: CreateImagePayload = {
|
423
|
+
model: 'wanx2.1-t2i-turbo',
|
424
|
+
params: {
|
425
|
+
prompt: 'Invalid prompt that causes failure',
|
426
|
+
},
|
427
|
+
};
|
428
|
+
|
429
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
430
|
+
expect.objectContaining({
|
431
|
+
errorType: 'ProviderBizError',
|
432
|
+
provider: 'qwen',
|
433
|
+
}),
|
434
|
+
);
|
435
|
+
});
|
436
|
+
|
437
|
+
it('should handle task succeeded but no results', async () => {
|
438
|
+
const mockTaskId = 'task-no-results';
|
439
|
+
|
440
|
+
global.fetch = vi
|
441
|
+
.fn()
|
442
|
+
.mockResolvedValueOnce({
|
443
|
+
ok: true,
|
444
|
+
json: async () => ({
|
445
|
+
output: { task_id: mockTaskId },
|
446
|
+
request_id: 'req-134',
|
447
|
+
}),
|
448
|
+
})
|
449
|
+
.mockResolvedValueOnce({
|
450
|
+
ok: true,
|
451
|
+
json: async () => ({
|
452
|
+
output: {
|
453
|
+
task_id: mockTaskId,
|
454
|
+
task_status: 'SUCCEEDED',
|
455
|
+
results: [], // Empty results array
|
456
|
+
},
|
457
|
+
request_id: 'req-135',
|
458
|
+
}),
|
459
|
+
});
|
460
|
+
|
461
|
+
const payload: CreateImagePayload = {
|
462
|
+
model: 'wanx2.1-t2i-turbo',
|
463
|
+
params: {
|
464
|
+
prompt: 'Test prompt',
|
465
|
+
},
|
466
|
+
};
|
467
|
+
|
468
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
469
|
+
expect.objectContaining({
|
470
|
+
errorType: 'ProviderBizError',
|
471
|
+
provider: 'qwen',
|
472
|
+
}),
|
473
|
+
);
|
474
|
+
});
|
475
|
+
|
476
|
+
it('should handle status query failure', async () => {
|
477
|
+
const mockTaskId = 'task-query-fail';
|
478
|
+
|
479
|
+
global.fetch = vi
|
480
|
+
.fn()
|
481
|
+
.mockResolvedValueOnce({
|
482
|
+
ok: true,
|
483
|
+
json: async () => ({
|
484
|
+
output: { task_id: mockTaskId },
|
485
|
+
request_id: 'req-136',
|
486
|
+
}),
|
487
|
+
})
|
488
|
+
.mockResolvedValueOnce({
|
489
|
+
ok: false,
|
490
|
+
statusText: 'Unauthorized',
|
491
|
+
json: async () => ({
|
492
|
+
message: 'Invalid API key',
|
493
|
+
}),
|
494
|
+
});
|
495
|
+
|
496
|
+
const payload: CreateImagePayload = {
|
497
|
+
model: 'wanx2.1-t2i-turbo',
|
498
|
+
params: {
|
499
|
+
prompt: 'Test prompt',
|
500
|
+
},
|
501
|
+
};
|
502
|
+
|
503
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
504
|
+
expect.objectContaining({
|
505
|
+
errorType: 'ProviderBizError',
|
506
|
+
provider: 'qwen',
|
507
|
+
}),
|
508
|
+
);
|
509
|
+
});
|
510
|
+
|
511
|
+
it('should handle transient status query failures and retry', async () => {
|
512
|
+
const mockTaskId = 'task-transient-failure';
|
513
|
+
const mockImageUrl = 'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/retry-success.jpg';
|
514
|
+
|
515
|
+
let statusQueryCount = 0;
|
516
|
+
const statusQueryMock = vi.fn().mockImplementation(() => {
|
517
|
+
statusQueryCount++;
|
518
|
+
if (statusQueryCount === 1 || statusQueryCount === 2) {
|
519
|
+
// First two calls fail
|
520
|
+
return Promise.reject(new Error('Network timeout'));
|
521
|
+
} else {
|
522
|
+
// Third call succeeds
|
523
|
+
return Promise.resolve({
|
524
|
+
ok: true,
|
525
|
+
json: async () => ({
|
526
|
+
output: {
|
527
|
+
task_id: mockTaskId,
|
528
|
+
task_status: 'SUCCEEDED',
|
529
|
+
results: [{ url: mockImageUrl }],
|
530
|
+
},
|
531
|
+
request_id: 'req-retry-success',
|
532
|
+
}),
|
533
|
+
});
|
534
|
+
}
|
535
|
+
});
|
536
|
+
|
537
|
+
global.fetch = vi
|
538
|
+
.fn()
|
539
|
+
.mockResolvedValueOnce({
|
540
|
+
ok: true,
|
541
|
+
json: async () => ({
|
542
|
+
output: { task_id: mockTaskId },
|
543
|
+
request_id: 'req-transient',
|
544
|
+
}),
|
545
|
+
})
|
546
|
+
.mockImplementation(statusQueryMock);
|
547
|
+
|
548
|
+
const payload: CreateImagePayload = {
|
549
|
+
model: 'wanx2.1-t2i-turbo',
|
550
|
+
params: {
|
551
|
+
prompt: 'Test transient failure',
|
552
|
+
},
|
553
|
+
};
|
554
|
+
|
555
|
+
// Mock setTimeout to make test run faster
|
556
|
+
vi.spyOn(global, 'setTimeout').mockImplementation((callback: any) => {
|
557
|
+
setImmediate(callback);
|
558
|
+
return 1 as any;
|
559
|
+
});
|
560
|
+
|
561
|
+
const result = await createQwenImage(payload, mockOptions);
|
562
|
+
|
563
|
+
expect(result).toEqual({
|
564
|
+
imageUrl: mockImageUrl,
|
565
|
+
});
|
566
|
+
|
567
|
+
// Verify the mock was called the expected number of times
|
568
|
+
expect(statusQueryMock).toHaveBeenCalledTimes(3); // 2 failures + 1 success
|
569
|
+
|
570
|
+
// Should have made 1 create call + 3 status calls (2 failed + 1 succeeded)
|
571
|
+
expect(fetch).toHaveBeenCalledTimes(4);
|
572
|
+
});
|
573
|
+
|
574
|
+
it('should fail after consecutive query failures', async () => {
|
575
|
+
const mockTaskId = 'task-consecutive-failures';
|
576
|
+
|
577
|
+
global.fetch = vi
|
578
|
+
.fn()
|
579
|
+
.mockResolvedValueOnce({
|
580
|
+
ok: true,
|
581
|
+
json: async () => ({
|
582
|
+
output: { task_id: mockTaskId },
|
583
|
+
request_id: 'req-will-fail',
|
584
|
+
}),
|
585
|
+
})
|
586
|
+
// All subsequent calls fail
|
587
|
+
.mockRejectedValue(new Error('Persistent network error'));
|
588
|
+
|
589
|
+
const payload: CreateImagePayload = {
|
590
|
+
model: 'wanx2.1-t2i-turbo',
|
591
|
+
params: {
|
592
|
+
prompt: 'Test persistent failure',
|
593
|
+
},
|
594
|
+
};
|
595
|
+
|
596
|
+
// Mock setTimeout to make test run faster
|
597
|
+
vi.spyOn(global, 'setTimeout').mockImplementation((callback: any) => {
|
598
|
+
setImmediate(callback);
|
599
|
+
return 1 as any;
|
600
|
+
});
|
601
|
+
|
602
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
603
|
+
expect.objectContaining({
|
604
|
+
errorType: 'ProviderBizError',
|
605
|
+
provider: 'qwen',
|
606
|
+
}),
|
607
|
+
);
|
608
|
+
|
609
|
+
// Should have made 1 create call + 3 failed status calls (maxConsecutiveFailures)
|
610
|
+
expect(fetch).toHaveBeenCalledTimes(4);
|
611
|
+
});
|
612
|
+
});
|
613
|
+
});
|