feat:Strengthen the testing framework

This commit is contained in:
saturn
2026-03-15 18:15:25 +08:00
parent eec27fbabf
commit ecbd183a77
31 changed files with 2326 additions and 85 deletions

View File

@@ -0,0 +1,154 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { queryFalStatus, submitFalTask } from '@/lib/async-submit'
import { startScenarioServer } from '../../helpers/fakes/scenario-server'
describe('provider contract - fal queue', () => {
let server: Awaited<ReturnType<typeof startScenarioServer>> | null = null
beforeEach(async () => {
server = await startScenarioServer()
process.env.FAL_QUEUE_BASE_URL = `${server.baseUrl}/fal`
})
afterEach(async () => {
delete process.env.FAL_QUEUE_BASE_URL
await server?.close()
server = null
})
it('submits the expected auth header and json payload', async () => {
server!.defineScenario({
method: 'POST',
path: '/fal/fal-ai/nano-banana-pro',
mode: 'success',
submitResponse: {
status: 200,
body: { request_id: 'req_image_1' },
},
})
const requestId = await submitFalTask(
'fal-ai/nano-banana-pro',
{
prompt: 'generate image',
image_urls: ['data:image/png;base64,AAAA'],
},
'fal-key-1',
)
expect(requestId).toBe('req_image_1')
const requests = server!.getRequests('POST', '/fal/fal-ai/nano-banana-pro')
expect(requests).toHaveLength(1)
expect(requests[0]?.headers.authorization).toBe('Key fal-key-1')
expect(JSON.parse(requests[0]?.bodyText || '{}')).toEqual({
prompt: 'generate image',
image_urls: ['data:image/png;base64,AAAA'],
})
})
it('treats transient status failure as pending and completes after retry', async () => {
server!.defineScenario({
method: 'GET',
path: '/fal/fal-ai/veo3.1/requests/req_video_1/status',
mode: 'retryable_error_then_success',
pollSequence: [
{ status: 503, body: { error: 'upstream unavailable' } },
{ status: 200, body: { status: 'COMPLETED' } },
],
})
server!.defineScenario({
method: 'GET',
path: '/fal/fal-ai/veo3.1/fast/image-to-video/requests/req_video_1',
mode: 'success',
submitResponse: {
status: 200,
body: {
video: { url: 'https://cdn.local/video.mp4' },
},
},
})
const first = await queryFalStatus('fal-ai/veo3.1/fast/image-to-video', 'req_video_1', 'fal-key-2')
const second = await queryFalStatus('fal-ai/veo3.1/fast/image-to-video', 'req_video_1', 'fal-key-2')
expect(first).toEqual({
status: 'IN_PROGRESS',
completed: false,
failed: false,
})
expect(second).toEqual({
status: 'COMPLETED',
completed: true,
failed: false,
resultUrl: 'https://cdn.local/video.mp4',
})
})
it('marks a failed status response as failed with explicit provider error', async () => {
server!.defineScenario({
method: 'GET',
path: '/fal/fal-ai/veo3.1/requests/req_failed/status',
mode: 'fatal_error',
submitResponse: {
status: 200,
body: {
status: 'FAILED',
error: 'content moderation failed',
},
},
})
const result = await queryFalStatus('fal-ai/veo3.1/fast/image-to-video', 'req_failed', 'fal-key-3')
expect(result).toEqual({
status: 'FAILED',
completed: false,
failed: true,
error: 'content moderation failed',
})
})
it('fails explicitly when submit response is malformed', async () => {
server!.defineScenario({
method: 'POST',
path: '/fal/fal-ai/nano-banana-pro',
mode: 'malformed_response',
submitResponse: {
status: 200,
body: { ok: true },
},
})
await expect(
submitFalTask('fal-ai/nano-banana-pro', { prompt: 'bad response' }, 'fal-key-4'),
).rejects.toThrow('FAL未返回request_id')
})
it('treats completed result without media url as failed', async () => {
server!.defineScenario({
method: 'GET',
path: '/fal/fal-ai/nano-banana-pro/requests/req_no_media/status',
mode: 'queued_then_success',
submitResponse: {
status: 200,
body: { status: 'COMPLETED' },
},
})
server!.defineScenario({
method: 'GET',
path: '/fal/fal-ai/nano-banana-pro/requests/req_no_media',
mode: 'malformed_response',
submitResponse: {
status: 200,
body: { images: [] },
},
})
const result = await queryFalStatus('fal-ai/nano-banana-pro', 'req_no_media', 'fal-key-5')
expect(result).toEqual({
status: 'COMPLETED',
completed: true,
failed: false,
resultUrl: undefined,
})
})
})