Files
waooplus/tests/unit/worker/video-generation-resume.test.ts
2026-03-08 17:10:06 +08:00

118 lines
3.7 KiB
TypeScript

import type { Job } from 'bullmq'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { TaskJobData } from '@/lib/task/types'
const prismaMock = vi.hoisted(() => ({
task: {
findUnique: vi.fn(),
},
}))
const taskServiceMock = vi.hoisted(() => ({
isTaskActive: vi.fn(async () => true),
trySetTaskExternalId: vi.fn(async () => true),
}))
const asyncPollMock = vi.hoisted(() => ({
pollAsyncTask: vi.fn(),
}))
const generatorApiMock = vi.hoisted(() => ({
generateImage: vi.fn(),
generateVideo: vi.fn(),
}))
vi.mock('@/lib/prisma', () => ({ prisma: prismaMock }))
vi.mock('@/lib/task/service', () => taskServiceMock)
vi.mock('@/lib/async-poll', () => asyncPollMock)
vi.mock('@/lib/generator-api', () => generatorApiMock)
vi.mock('@/lib/lipsync', () => ({ generateLipSync: vi.fn() }))
vi.mock('@/lib/storage', () => ({
getSignedUrl: vi.fn((value: string) => value),
toFetchableUrl: vi.fn((value: string) => value),
}))
vi.mock('@/lib/fonts', () => ({ initializeFonts: vi.fn(), createLabelSVG: vi.fn() }))
vi.mock('@/lib/media-process', () => ({ processMediaResult: vi.fn() }))
vi.mock('@/lib/config-service', () => ({
getProjectModelConfig: vi.fn(),
getUserModelConfig: vi.fn(),
resolveProjectModelCapabilityGenerationOptions: vi.fn(),
}))
import { resolveImageSourceFromGeneration, resolveVideoSourceFromGeneration } from '@/lib/workers/utils'
function buildJob(): Job<TaskJobData> {
return {
data: {
taskId: 'task-1',
type: 'VIDEO_PANEL',
locale: 'zh',
projectId: 'project-1',
episodeId: 'episode-1',
targetType: 'NovelPromotionPanel',
targetId: 'panel-1',
payload: {},
userId: 'user-1',
},
} as unknown as Job<TaskJobData>
}
describe('worker utils video generation resume', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('continues polling from existing externalId without re-submitting generation', async () => {
const externalId = 'OPENAI:VIDEO:b3BlbmFpLWNvbXBhdGlibGU6b2EtMQ:vid_123'
prismaMock.task.findUnique.mockResolvedValueOnce({ externalId })
asyncPollMock.pollAsyncTask.mockResolvedValueOnce({
status: 'completed',
resultUrl: 'https://oa.test/v1/videos/vid_123/content',
downloadHeaders: {
Authorization: 'Bearer oa-key',
},
})
const result = await resolveVideoSourceFromGeneration(buildJob(), {
userId: 'user-1',
modelId: 'openai-compatible:oa-1::sora-2',
imageUrl: 'data:image/png;base64,QQ==',
options: {
prompt: 'animate this frame',
},
})
expect(result).toEqual({
url: 'https://oa.test/v1/videos/vid_123/content',
downloadHeaders: {
Authorization: 'Bearer oa-key',
},
})
expect(asyncPollMock.pollAsyncTask).toHaveBeenCalledWith(externalId, 'user-1')
expect(generatorApiMock.generateVideo).not.toHaveBeenCalled()
})
it('prevents duplicate panel candidates by skipping task externalId resume when requested', async () => {
prismaMock.task.findUnique.mockResolvedValueOnce({ externalId: 'FAL:IMAGE:fal-ai/nano-banana-pro:req_1' })
generatorApiMock.generateImage.mockResolvedValueOnce({
success: true,
imageUrl: 'https://fal.test/new-image.png',
})
const result = await resolveImageSourceFromGeneration(buildJob(), {
userId: 'user-1',
modelId: 'fal::banana',
prompt: 'a cinematic portrait',
options: {
aspectRatio: '16:9',
},
allowTaskExternalIdResume: false,
})
expect(result).toBe('https://fal.test/new-image.png')
expect(prismaMock.task.findUnique).not.toHaveBeenCalled()
expect(asyncPollMock.pollAsyncTask).not.toHaveBeenCalled()
expect(generatorApiMock.generateImage).toHaveBeenCalledTimes(1)
})
})