import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { callRoute } from '../integration/api/helpers/call-route' import { installAuthMocks, mockAuthenticated, resetAuthMockState } from '../helpers/auth' import { resetSystemState } from '../helpers/db-reset' import { prisma } from '../helpers/prisma' import { seedMinimalDomainState } from './helpers/seed' import { expectLifecycleEvents, listTaskEventTypes, waitForTaskTerminalState } from './helpers/tasks' import { startSystemWorkers, stopSystemWorkers, type SystemWorkers } from './helpers/workers' type PollState = { status: 'processing' | 'completed' resultUrl?: string } const videoState = vi.hoisted(() => ({ pollResponses: new Map(), uploadedCosKey: 'video/system-video.mp4', })) vi.mock('@/lib/generator-api', async () => { const actual = await vi.importActual('@/lib/generator-api') return { ...actual, generateVideo: vi.fn(async () => ({ success: true, async: true, externalId: 'video-ext-1', })), } }) vi.mock('@/lib/async-poll', async () => { const actual = await vi.importActual('@/lib/async-poll') return { ...actual, pollAsyncTask: vi.fn(async (externalId: string) => { const queue = videoState.pollResponses.get(externalId) || [] const next = queue.shift() if (!next) { return { status: 'completed', resultUrl: 'https://provider.example/video-final.mp4' } } videoState.pollResponses.set(externalId, queue) return next }), } }) vi.mock('@/lib/media/outbound-image', async () => { const actual = await vi.importActual('@/lib/media/outbound-image') return { ...actual, normalizeToBase64ForGeneration: vi.fn(async (input: string) => input), } }) vi.mock('@/lib/workers/utils', async () => { const actual = await vi.importActual('@/lib/workers/utils') return { ...actual, uploadVideoSourceToCos: vi.fn(async () => videoState.uploadedCosKey), } }) describe('system - generate video', () => { let workers: SystemWorkers = {} beforeEach(async () => { vi.resetModules() vi.clearAllMocks() videoState.uploadedCosKey = 'video/system-video.mp4' videoState.pollResponses.clear() videoState.pollResponses.set('video-ext-1', [ { status: 'processing' }, { status: 'completed', resultUrl: 'https://provider.example/video-final.mp4' }, ]) await resetSystemState() installAuthMocks() }) afterEach(async () => { await stopSystemWorkers(workers) workers = {} resetAuthMockState() }) it('queued external generation -> polling -> videoUrl persisted', async () => { const seeded = await seedMinimalDomainState() mockAuthenticated(seeded.user.id) workers = await startSystemWorkers(['video']) const mod = await import('@/app/api/novel-promotion/[projectId]/generate-video/route') const response = await callRoute( mod.POST, 'POST', { locale: 'zh', storyboardId: seeded.storyboard.id, panelIndex: 0, videoModel: 'fal::seedance/video', }, { params: { projectId: seeded.project.id } }, ) expect(response.status).toBe(200) const json = await response.json() as { async: boolean; taskId: string } const task = await waitForTaskTerminalState(json.taskId) expect(task.status).toBe('completed') expect(task.type).toBe('video_panel') expect(task.externalId).toBe('video-ext-1') const panel = await prisma.novelPromotionPanel.findUnique({ where: { id: seeded.panel.id }, select: { videoUrl: true }, }) expect(panel?.videoUrl).toBe(videoState.uploadedCosKey) const eventTypes = await listTaskEventTypes(json.taskId) expectLifecycleEvents(eventTypes, 'completed') }) })