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, }) }) })