gongxiangp2/lib/__tests__/transcode-queue-persistence.test.ts
chenpei 4e89b57798 feat(upload): optimize upload UI and performance
- Expand batch upload list area and optimize preview image layout
- Improve upload speed tracking and display (server-side & client-side)
- Reduce network detection log noise
- Optimize Docker build process (caching, env validation)
- Add shadow environment deployment support
2026-01-28 15:07:50 +08:00

362 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* R1修复:转码队列持久化测试
*
* 测试内容:
* - VideoTranscodeProgress 数据库模型
* - VideoTranscodeQueue 的数据库持久化功能
* - 启动恢复功能
* - 进度更新和清理功能
*/
// Mock Prisma
jest.mock('@/lib/prisma', () => ({
prisma: {
videoTranscodeProgress: {
upsert: jest.fn(),
findMany: jest.fn(),
deleteMany: jest.fn(),
findFirst: jest.fn(),
},
material: {
findFirst: jest.fn(),
},
},
}))
// Mock console methods to avoid clutter
global.console.log = jest.fn()
global.console.warn = jest.fn()
global.console.error = jest.fn()
describe('R1: 转码队列持久化', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('VideoTranscodeProgress 数据模型', () => {
it('应该定义正确的字段结构', () => {
const progress = {
id: 'test-id',
materialId: 'material-123',
stage: 'preview',
progress: 50,
status: 'processing',
priority: 0,
startedAt: new Date(),
completedAt: null,
errorMessage: null,
retryCount: 0,
lastHeartbeat: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
}
expect(progress).toHaveProperty('id')
expect(progress).toHaveProperty('materialId')
expect(progress).toHaveProperty('stage')
expect(progress).toHaveProperty('progress')
expect(progress).toHaveProperty('status')
expect(progress).toHaveProperty('lastHeartbeat')
})
it('应该支持唯一约束 (materialId, stage)', () => {
const uniqueKey = { materialId: 'material-123', stage: 'preview' }
expect(uniqueKey).toHaveProperty('materialId')
expect(uniqueKey).toHaveProperty('stage')
})
it('应该支持有效的状态值', () => {
const validStatuses = ['waiting', 'processing', 'completed', 'failed']
validStatuses.forEach(status => {
expect(['waiting', 'processing', 'completed', 'failed']).toContain(status)
})
})
it('应该支持有效的阶段值', () => {
const validStages = ['preview', 'clip', 'cover']
validStages.forEach(stage => {
expect(['preview', 'clip', 'cover']).toContain(stage)
})
})
})
describe('saveStageProgress 功能', () => {
it('应该使用 upsert 创建或更新进度记录', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.upsert = jest.fn().mockResolvedValue({
id: 'test-id',
materialId: 'material-123',
stage: 'preview',
progress: 25,
status: 'processing',
lastHeartbeat: new Date(),
})
// 模拟调用 saveStageProgress
await prisma.videoTranscodeProgress.upsert({
where: { materialId_stage: { materialId: 'material-123', stage: 'preview' } },
create: {
materialId: 'material-123',
stage: 'preview',
status: 'processing',
progress: 25,
errorMessage: null,
lastHeartbeat: new Date(),
},
update: {
status: 'processing',
progress: 25,
errorMessage: null,
lastHeartbeat: expect.any(Date),
},
})
expect(prisma.videoTranscodeProgress.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { materialId_stage: { materialId: 'material-123', stage: 'preview' } },
})
)
})
it('处理状态应该更新 lastHeartbeat', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.upsert = jest.fn().mockResolvedValue({})
await prisma.videoTranscodeProgress.upsert({
where: { materialId_stage: { materialId: 'material-123', stage: 'preview' } },
create: {
materialId: 'material-123',
stage: 'preview',
status: 'processing',
progress: 50,
errorMessage: null,
lastHeartbeat: new Date(),
},
update: {
status: 'processing',
progress: 50,
errorMessage: null,
lastHeartbeat: expect.any(Date),
},
})
expect(prisma.videoTranscodeProgress.upsert).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
lastHeartbeat: expect.any(Date),
}),
})
)
})
it('完成状态应该设置 completedAt', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.upsert = jest.fn().mockResolvedValue({})
await prisma.videoTranscodeProgress.upsert({
where: { materialId_stage: { materialId: 'material-123', stage: 'preview' } },
create: {
materialId: 'material-123',
stage: 'preview',
status: 'completed',
progress: 100,
errorMessage: null,
lastHeartbeat: new Date(),
},
update: {
status: 'completed',
progress: 100,
errorMessage: null,
completedAt: expect.any(Date),
},
})
expect(prisma.videoTranscodeProgress.upsert).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
completedAt: expect.any(Date),
}),
})
)
})
})
describe('loadWaitingTasks 功能', () => {
it('应该查询 waiting 状态的任务', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.findMany = jest.fn().mockResolvedValue([])
const timeoutThreshold = new Date(Date.now() - 10 * 60 * 1000)
await prisma.videoTranscodeProgress.findMany({
where: {
OR: [{ status: 'waiting' }],
},
include: {
material: {
select: {
videoSourceUrl: true,
videoCoverUrl: true,
},
},
},
orderBy: [{ priority: 'desc' }, { createdAt: 'asc' }],
})
expect(prisma.videoTranscodeProgress.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: expect.arrayContaining([
expect.objectContaining({ status: 'waiting' }),
]),
}),
})
)
})
it('应该查询心跳超时的 processing 任务', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.findMany = jest.fn().mockResolvedValue([])
const timeoutThreshold = new Date(Date.now() - 10 * 60 * 1000)
await prisma.videoTranscodeProgress.findMany({
where: {
OR: [
{
AND: [
{ status: 'processing' },
{ lastHeartbeat: { lt: timeoutThreshold } },
],
},
],
},
})
expect(prisma.videoTranscodeProgress.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: expect.arrayContaining([
expect.objectContaining({
AND: expect.arrayContaining([
{ status: 'processing' },
{ lastHeartbeat: { lt: timeoutThreshold } },
]),
}),
]),
}),
})
)
})
})
describe('cleanupCompletedStages 功能', () => {
it('应该删除 completed 状态的记录', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.deleteMany = jest.fn().mockResolvedValue({ count: 3 })
await prisma.videoTranscodeProgress.deleteMany({
where: {
materialId: 'material-123',
status: { in: ['completed', 'failed'] },
},
})
expect(prisma.videoTranscodeProgress.deleteMany).toHaveBeenCalledWith({
where: {
materialId: 'material-123',
status: { in: ['completed', 'failed'] },
},
})
})
it('应该删除 failed 状态的记录', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.deleteMany = jest.fn().mockResolvedValue({ count: 1 })
await prisma.videoTranscodeProgress.deleteMany({
where: {
materialId: 'material-456',
status: { in: ['completed', 'failed'] },
},
})
expect(prisma.videoTranscodeProgress.deleteMany).toHaveBeenCalled()
})
})
describe('心跳超时检测', () => {
it('应该正确计算超时阈值(10分钟)', () => {
const now = Date.now()
const timeoutThreshold = new Date(now - 10 * 60 * 1000)
const timeDiff = now - timeoutThreshold.getTime()
expect(timeDiff).toBe(10 * 60 * 1000)
})
it('应该只标记超过 10 分钟无心跳的任务为超时', () => {
const now = Date.now()
const tenMinutesAgo = new Date(now - 10 * 60 * 1000)
const fiveMinutesAgo = new Date(now - 5 * 60 * 1000)
const fifteenMinutesAgo = new Date(now - 15 * 60 * 1000)
expect(fifteenMinutesAgo < tenMinutesAgo).toBe(true) // 超时
expect(fiveMinutesAgo > tenMinutesAgo).toBe(true) // 未超时
})
})
describe('任务优先级排序', () => {
it('应该按优先级降序、创建时间升序排序', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.findMany = jest.fn().mockResolvedValue([])
await prisma.videoTranscodeProgress.findMany({
orderBy: [{ priority: 'desc' }, { createdAt: 'asc' }],
})
expect(prisma.videoTranscodeProgress.findMany).toHaveBeenCalledWith(
expect.objectContaining({
orderBy: [
{ priority: 'desc' },
{ createdAt: 'asc' },
],
})
)
})
})
describe('错误处理', () => {
it('数据库错误应该记录但不中断程序', async () => {
const { prisma } = await import('@/lib/prisma')
prisma.videoTranscodeProgress.upsert = jest.fn().mockRejectedValue(new Error('Database error'))
try {
await prisma.videoTranscodeProgress.upsert({
where: { materialId_stage: { materialId: 'material-123', stage: 'preview' } },
create: {
materialId: 'material-123',
stage: 'preview',
status: 'processing',
progress: 50,
errorMessage: null,
lastHeartbeat: new Date(),
},
update: {},
})
fail('Should have thrown')
} catch (error) {
expect(error).toBeInstanceOf(Error)
}
})
})
})