Files
waoowaoo/tests/unit/worker/voice-worker.test.ts

173 lines
4.7 KiB
TypeScript

import type { Job } from 'bullmq'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TASK_TYPE, type TaskJobData } from '@/lib/task/types'
type WorkerProcessor = (job: Job<TaskJobData>) => Promise<unknown>
const workerState = vi.hoisted(() => ({
processor: null as WorkerProcessor | null,
}))
const generateVoiceLineMock = vi.hoisted(() => vi.fn())
const handleVoiceDesignTaskMock = vi.hoisted(() => vi.fn())
const reportTaskProgressMock = vi.hoisted(() => vi.fn(async () => undefined))
const withTaskLifecycleMock = vi.hoisted(() =>
vi.fn(async (job: Job<TaskJobData>, handler: WorkerProcessor) => await handler(job)),
)
vi.mock('bullmq', () => ({
Queue: class {
constructor(_name: string) {}
async add() {
return { id: 'job-1' }
}
async getJob() {
return null
}
},
Worker: class {
constructor(_name: string, processor: WorkerProcessor) {
workerState.processor = processor
}
},
}))
vi.mock('@/lib/redis', () => ({
queueRedis: {},
}))
vi.mock('@/lib/voice/generate-voice-line', () => ({
generateVoiceLine: generateVoiceLineMock,
}))
vi.mock('@/lib/workers/shared', () => ({
reportTaskProgress: reportTaskProgressMock,
withTaskLifecycle: withTaskLifecycleMock,
}))
vi.mock('@/lib/workers/handlers/voice-design', () => ({
handleVoiceDesignTask: handleVoiceDesignTaskMock,
}))
function buildJob(params: {
type: TaskJobData['type']
targetType?: string
targetId?: string
episodeId?: string | null
payload?: Record<string, unknown>
}): Job<TaskJobData> {
return {
data: {
taskId: 'task-1',
type: params.type,
locale: 'zh',
projectId: 'project-1',
episodeId: params.episodeId !== undefined ? params.episodeId : 'episode-1',
targetType: params.targetType ?? 'NovelPromotionVoiceLine',
targetId: params.targetId ?? 'line-1',
payload: params.payload ?? {},
userId: 'user-1',
},
} as unknown as Job<TaskJobData>
}
describe('worker voice processor behavior', () => {
beforeEach(async () => {
vi.clearAllMocks()
workerState.processor = null
generateVoiceLineMock.mockResolvedValue({
lineId: 'line-1',
audioUrl: 'cos/voice-line-1.mp3',
})
handleVoiceDesignTaskMock.mockResolvedValue({
presetId: 'preset-1',
previewAudioUrl: 'cos/preset-1.mp3',
})
const mod = await import('@/lib/workers/voice.worker')
mod.createVoiceWorker()
})
it('VOICE_LINE: lineId/episodeId 缺失时显式失败', async () => {
const processor = workerState.processor
expect(processor).toBeTruthy()
const missingLineJob = buildJob({
type: TASK_TYPE.VOICE_LINE,
targetId: '',
payload: { episodeId: 'episode-1' },
})
await expect(processor!(missingLineJob)).rejects.toThrow('VOICE_LINE task missing lineId')
const missingEpisodeJob = buildJob({
type: TASK_TYPE.VOICE_LINE,
episodeId: null,
targetId: 'line-1',
payload: {},
})
await expect(processor!(missingEpisodeJob)).rejects.toThrow('VOICE_LINE task missing episodeId')
})
it('VOICE_LINE: 正常生成时把核心参数传给 generateVoiceLine', async () => {
const processor = workerState.processor
expect(processor).toBeTruthy()
const job = buildJob({
type: TASK_TYPE.VOICE_LINE,
payload: {
lineId: 'line-9',
episodeId: 'episode-9',
audioModel: 'fal::voice-model',
},
})
const result = await processor!(job)
expect(result).toEqual({ lineId: 'line-1', audioUrl: 'cos/voice-line-1.mp3' })
expect(generateVoiceLineMock).toHaveBeenCalledWith({
projectId: 'project-1',
episodeId: 'episode-9',
lineId: 'line-9',
userId: 'user-1',
audioModel: 'fal::voice-model',
})
})
it('VOICE_DESIGN / ASSET_HUB_VOICE_DESIGN: 路由到 voice design handler', async () => {
const processor = workerState.processor
expect(processor).toBeTruthy()
const designJob = buildJob({
type: TASK_TYPE.VOICE_DESIGN,
targetType: 'NovelPromotionVoiceDesign',
targetId: 'voice-design-1',
})
const assetHubJob = buildJob({
type: TASK_TYPE.ASSET_HUB_VOICE_DESIGN,
targetType: 'GlobalAssetHubVoiceDesign',
targetId: 'asset-hub-voice-design-1',
})
await processor!(designJob)
await processor!(assetHubJob)
expect(handleVoiceDesignTaskMock).toHaveBeenCalledTimes(2)
expect(generateVoiceLineMock).not.toHaveBeenCalled()
})
it('未知任务类型: 显式报错', async () => {
const processor = workerState.processor
expect(processor).toBeTruthy()
const unsupportedJob = buildJob({
type: TASK_TYPE.AI_CREATE_CHARACTER,
targetId: 'character-1',
})
await expect(processor!(unsupportedJob)).rejects.toThrow('Unsupported voice task type')
})
})