feat:Strengthen the testing framework
This commit is contained in:
50
tests/regression/panel-variant-cross-storyboard.test.ts
Normal file
50
tests/regression/panel-variant-cross-storyboard.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { beforeEach, describe, expect, it } 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 '../system/helpers/seed'
|
||||
|
||||
describe('regression - panel variant cross storyboard safety', () => {
|
||||
beforeEach(async () => {
|
||||
await resetSystemState()
|
||||
installAuthMocks()
|
||||
})
|
||||
|
||||
it('sourcePanelId from another storyboard -> explicit invalid params and no dirty panel', async () => {
|
||||
const seeded = await seedMinimalDomainState()
|
||||
mockAuthenticated(seeded.user.id)
|
||||
|
||||
const beforeCount = await prisma.novelPromotionPanel.count({
|
||||
where: { storyboardId: seeded.storyboard.id },
|
||||
})
|
||||
|
||||
const mod = await import('@/app/api/novel-promotion/[projectId]/panel-variant/route')
|
||||
const response = await callRoute(
|
||||
mod.POST,
|
||||
'POST',
|
||||
{
|
||||
locale: 'zh',
|
||||
storyboardId: seeded.storyboard.id,
|
||||
insertAfterPanelId: seeded.panel.id,
|
||||
sourcePanelId: seeded.foreignPanel.id,
|
||||
variant: {
|
||||
video_prompt: 'variant prompt',
|
||||
description: 'variant description',
|
||||
},
|
||||
},
|
||||
{ params: { projectId: seeded.project.id } },
|
||||
)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
const json = await response.json() as { error?: { code?: string } }
|
||||
expect(json.error?.code).toBe('INVALID_PARAMS')
|
||||
|
||||
const afterCount = await prisma.novelPromotionPanel.count({
|
||||
where: { storyboardId: seeded.storyboard.id },
|
||||
})
|
||||
expect(afterCount).toBe(beforeCount)
|
||||
|
||||
resetAuthMockState()
|
||||
})
|
||||
})
|
||||
108
tests/regression/task-dedupe-recovery.test.ts
Normal file
108
tests/regression/task-dedupe-recovery.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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('regression - task dedupe recovery', () => {
|
||||
beforeEach(async () => {
|
||||
await resetBillingState()
|
||||
vi.clearAllMocks()
|
||||
reconcileMock.isJobAlive.mockResolvedValue(true)
|
||||
})
|
||||
|
||||
it('replaces locale-less queued task instead of deduping forever', async () => {
|
||||
const user = await createTestUser()
|
||||
const project = await createTestProject(user.id)
|
||||
const stale = await prisma.task.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
projectId: project.id,
|
||||
type: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
|
||||
targetType: 'NovelPromotionEpisode',
|
||||
targetId: 'episode-regression-1',
|
||||
status: TASK_STATUS.QUEUED,
|
||||
payload: { episodeId: 'episode-regression-1' },
|
||||
dedupeKey: 'script_to_storyboard_run:episode-regression-1',
|
||||
queuedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
const replacement = await createTask({
|
||||
userId: user.id,
|
||||
projectId: project.id,
|
||||
type: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
|
||||
targetType: 'NovelPromotionEpisode',
|
||||
targetId: 'episode-regression-1',
|
||||
payload: {
|
||||
episodeId: 'episode-regression-1',
|
||||
meta: { locale: 'zh' },
|
||||
},
|
||||
dedupeKey: 'script_to_storyboard_run:episode-regression-1',
|
||||
})
|
||||
|
||||
expect(replacement.deduped).toBe(false)
|
||||
expect(replacement.task.id).not.toBe(stale.id)
|
||||
|
||||
const failedStale = await prisma.task.findUnique({ where: { id: stale.id } })
|
||||
expect(failedStale).toMatchObject({
|
||||
status: TASK_STATUS.FAILED,
|
||||
errorCode: 'TASK_LOCALE_REQUIRED',
|
||||
dedupeKey: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('replaces orphaned queued task when queue job is gone', async () => {
|
||||
const user = await createTestUser()
|
||||
const project = await createTestProject(user.id)
|
||||
const orphan = await prisma.task.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
projectId: project.id,
|
||||
type: TASK_TYPE.VIDEO_PANEL,
|
||||
targetType: 'NovelPromotionPanel',
|
||||
targetId: 'panel-regression-1',
|
||||
status: TASK_STATUS.QUEUED,
|
||||
payload: {
|
||||
storyboardId: 'storyboard-regression-1',
|
||||
panelIndex: 1,
|
||||
meta: { locale: 'zh' },
|
||||
},
|
||||
dedupeKey: 'video_panel:panel-regression-1',
|
||||
queuedAt: new Date(),
|
||||
},
|
||||
})
|
||||
reconcileMock.isJobAlive.mockResolvedValue(false)
|
||||
|
||||
const replacement = await createTask({
|
||||
userId: user.id,
|
||||
projectId: project.id,
|
||||
type: TASK_TYPE.VIDEO_PANEL,
|
||||
targetType: 'NovelPromotionPanel',
|
||||
targetId: 'panel-regression-1',
|
||||
payload: {
|
||||
storyboardId: 'storyboard-regression-1',
|
||||
panelIndex: 1,
|
||||
meta: { locale: 'zh' },
|
||||
},
|
||||
dedupeKey: 'video_panel:panel-regression-1',
|
||||
})
|
||||
|
||||
expect(replacement.deduped).toBe(false)
|
||||
expect(replacement.task.id).not.toBe(orphan.id)
|
||||
|
||||
const failedOrphan = await prisma.task.findUnique({ where: { id: orphan.id } })
|
||||
expect(failedOrphan).toMatchObject({
|
||||
status: TASK_STATUS.FAILED,
|
||||
errorCode: 'RECONCILE_ORPHAN',
|
||||
dedupeKey: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
68
tests/regression/task-enqueue-billing-rollback.test.ts
Normal file
68
tests/regression/task-enqueue-billing-rollback.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { submitTask } from '@/lib/task/submitter'
|
||||
import { TASK_TYPE } from '@/lib/task/types'
|
||||
import { prisma } from '../helpers/prisma'
|
||||
import { resetBillingState } from '../helpers/db-reset'
|
||||
import { createTestUser, seedBalance } from '../helpers/billing-fixtures'
|
||||
|
||||
const queueState = vi.hoisted(() => ({
|
||||
message: 'queue add failed',
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/task/queues', () => ({
|
||||
addTaskJob: vi.fn(async () => {
|
||||
throw new Error(queueState.message)
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/task/publisher', () => ({
|
||||
publishTaskEvent: vi.fn(async () => ({})),
|
||||
}))
|
||||
|
||||
describe('regression - enqueue compensation', () => {
|
||||
beforeEach(async () => {
|
||||
await resetBillingState()
|
||||
vi.clearAllMocks()
|
||||
process.env.BILLING_MODE = 'ENFORCE'
|
||||
queueState.message = 'queue unavailable'
|
||||
})
|
||||
|
||||
it('rolls back frozen balance when queue submission fails', async () => {
|
||||
const user = await createTestUser()
|
||||
await seedBalance(user.id, 10)
|
||||
|
||||
await expect(
|
||||
submitTask({
|
||||
userId: user.id,
|
||||
locale: 'en',
|
||||
projectId: 'project-regression-enqueue',
|
||||
type: TASK_TYPE.VOICE_LINE,
|
||||
targetType: 'VoiceLine',
|
||||
targetId: 'line-regression-enqueue',
|
||||
payload: { maxSeconds: 6 },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: 'EXTERNAL_ERROR' })
|
||||
|
||||
const task = await prisma.task.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: TASK_TYPE.VOICE_LINE,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
const balance = await prisma.userBalance.findUnique({ where: { userId: user.id } })
|
||||
const freeze = await prisma.balanceFreeze.findFirst({ orderBy: { createdAt: 'desc' } })
|
||||
|
||||
expect(task).toMatchObject({
|
||||
status: 'failed',
|
||||
errorCode: 'ENQUEUE_FAILED',
|
||||
})
|
||||
expect(task?.billingInfo).toMatchObject({
|
||||
billable: true,
|
||||
status: 'rolled_back',
|
||||
})
|
||||
expect(balance?.balance).toBeCloseTo(10, 8)
|
||||
expect(balance?.frozenAmount).toBeCloseTo(0, 8)
|
||||
expect(freeze?.status).toBe('rolled_back')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user