- 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
362 lines
10 KiB
TypeScript
362 lines
10 KiB
TypeScript
/**
|
||
* 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)
|
||
}
|
||
})
|
||
})
|
||
})
|