Files
waooplus/tests/integration/task/create-task-dedupe.integration.test.ts
2026-03-15 18:15:25 +08:00

151 lines
4.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TASK_STATUS, TASK_TYPE } from '@/lib/task/types'
import { createTask } from '@/lib/task/service'
import { prisma } from '../../helpers/prisma'
import { resetBillingState } from '../../helpers/db-reset'
import { createTestProject, createTestUser } from '../../helpers/billing-fixtures'
const reconcileMock = vi.hoisted(() => ({
isJobAlive: vi.fn(async () => true),
}))
vi.mock('@/lib/task/reconcile', () => reconcileMock)
describe('task service dedupe + orphan recovery', () => {
beforeEach(async () => {
await resetBillingState()
vi.clearAllMocks()
reconcileMock.isJobAlive.mockResolvedValue(true)
})
it('dedupes to an active task when dedupeKey matches and queue job is alive', async () => {
const user = await createTestUser()
const project = await createTestProject(user.id)
const existing = await prisma.task.create({
data: {
userId: user.id,
projectId: project.id,
type: TASK_TYPE.VOICE_LINE,
targetType: 'NovelPromotionVoiceLine',
targetId: 'line-1',
status: TASK_STATUS.QUEUED,
payload: {
episodeId: 'episode-1',
lineId: 'line-1',
meta: { locale: 'zh' },
},
dedupeKey: 'voice_line:line-1',
queuedAt: new Date(),
},
})
const result = await createTask({
userId: user.id,
projectId: project.id,
type: TASK_TYPE.VOICE_LINE,
targetType: 'NovelPromotionVoiceLine',
targetId: 'line-1',
payload: {
episodeId: 'episode-1',
lineId: 'line-1',
meta: { locale: 'zh' },
},
dedupeKey: 'voice_line:line-1',
})
expect(result.deduped).toBe(true)
expect(result.task.id).toBe(existing.id)
expect(reconcileMock.isJobAlive).toHaveBeenCalledWith(existing.id)
})
it('fails orphaned active task and creates a replacement when queue job is missing', async () => {
const user = await createTestUser()
const project = await createTestProject(user.id)
const existing = await prisma.task.create({
data: {
userId: user.id,
projectId: project.id,
type: TASK_TYPE.VIDEO_PANEL,
targetType: 'NovelPromotionPanel',
targetId: 'panel-1',
status: TASK_STATUS.QUEUED,
payload: {
storyboardId: 'storyboard-1',
panelIndex: 1,
meta: { locale: 'zh' },
},
dedupeKey: 'video_panel:panel-1',
queuedAt: new Date(),
},
})
reconcileMock.isJobAlive.mockResolvedValue(false)
const result = await createTask({
userId: user.id,
projectId: project.id,
type: TASK_TYPE.VIDEO_PANEL,
targetType: 'NovelPromotionPanel',
targetId: 'panel-1',
payload: {
storyboardId: 'storyboard-1',
panelIndex: 1,
meta: { locale: 'zh' },
},
dedupeKey: 'video_panel:panel-1',
})
expect(result.deduped).toBe(false)
expect(result.task.id).not.toBe(existing.id)
const failedExisting = await prisma.task.findUnique({ where: { id: existing.id } })
expect(failedExisting).toMatchObject({
status: TASK_STATUS.FAILED,
errorCode: 'RECONCILE_ORPHAN',
dedupeKey: null,
})
})
it('fails locale-less active task and replaces it instead of deduping forever', async () => {
const user = await createTestUser()
const project = await createTestProject(user.id)
const existing = await prisma.task.create({
data: {
userId: user.id,
projectId: project.id,
type: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-1',
status: TASK_STATUS.QUEUED,
payload: {
episodeId: 'episode-1',
},
dedupeKey: 'script_to_storyboard_run:episode-1',
queuedAt: new Date(),
},
})
const result = await createTask({
userId: user.id,
projectId: project.id,
type: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-1',
payload: {
episodeId: 'episode-1',
meta: { locale: 'zh' },
},
dedupeKey: 'script_to_storyboard_run:episode-1',
})
expect(result.deduped).toBe(false)
expect(result.task.id).not.toBe(existing.id)
const failedExisting = await prisma.task.findUnique({ where: { id: existing.id } })
expect(failedExisting).toMatchObject({
status: TASK_STATUS.FAILED,
errorCode: 'TASK_LOCALE_REQUIRED',
dedupeKey: null,
})
})
})