feat:Strengthen the testing framework
This commit is contained in:
184
tests/system/helpers/seed.ts
Normal file
184
tests/system/helpers/seed.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { prisma } from '../../helpers/prisma'
|
||||
import {
|
||||
createFixtureEpisode,
|
||||
createFixtureNovelProject,
|
||||
createFixtureProject,
|
||||
createFixtureUser,
|
||||
} from '../../helpers/fixtures'
|
||||
|
||||
function nextSuffix() {
|
||||
return randomUUID().slice(0, 8)
|
||||
}
|
||||
|
||||
export async function seedMinimalDomainState() {
|
||||
const user = await createFixtureUser()
|
||||
const project = await createFixtureProject(user.id)
|
||||
const novelProject = await createFixtureNovelProject(project.id)
|
||||
const episode = await createFixtureEpisode(novelProject.id)
|
||||
|
||||
const clip = await prisma.novelPromotionClip.create({
|
||||
data: {
|
||||
episodeId: episode.id,
|
||||
summary: 'seed clip',
|
||||
content: 'seed clip content',
|
||||
screenplay: 'seed screenplay',
|
||||
location: 'Office',
|
||||
characters: JSON.stringify(['Narrator']),
|
||||
},
|
||||
})
|
||||
|
||||
const storyboard = await prisma.novelPromotionStoryboard.create({
|
||||
data: {
|
||||
episodeId: episode.id,
|
||||
clipId: clip.id,
|
||||
panelCount: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const panel = await prisma.novelPromotionPanel.create({
|
||||
data: {
|
||||
storyboardId: storyboard.id,
|
||||
panelIndex: 0,
|
||||
panelNumber: 1,
|
||||
shotType: '中景',
|
||||
cameraMove: '固定',
|
||||
description: 'seed panel',
|
||||
videoPrompt: 'seed video prompt',
|
||||
location: 'Office',
|
||||
characters: JSON.stringify(['Narrator']),
|
||||
imageUrl: 'https://provider.example/panel.jpg',
|
||||
},
|
||||
})
|
||||
|
||||
const character = await prisma.novelPromotionCharacter.create({
|
||||
data: {
|
||||
novelPromotionProjectId: novelProject.id,
|
||||
name: 'Narrator',
|
||||
},
|
||||
})
|
||||
|
||||
const appearance = await prisma.characterAppearance.create({
|
||||
data: {
|
||||
characterId: character.id,
|
||||
appearanceIndex: 0,
|
||||
changeReason: 'default',
|
||||
description: 'Narrator appearance',
|
||||
imageUrls: JSON.stringify(['images/character-seed.jpg']),
|
||||
imageUrl: 'images/character-seed.jpg',
|
||||
selectedIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const location = await prisma.novelPromotionLocation.create({
|
||||
data: {
|
||||
novelPromotionProjectId: novelProject.id,
|
||||
name: 'Office',
|
||||
summary: 'Office summary',
|
||||
},
|
||||
})
|
||||
|
||||
const locationImage = await prisma.locationImage.create({
|
||||
data: {
|
||||
locationId: location.id,
|
||||
imageIndex: 0,
|
||||
description: 'Office image',
|
||||
imageUrl: 'images/location-seed.jpg',
|
||||
isSelected: true,
|
||||
},
|
||||
})
|
||||
|
||||
const voiceLine = await prisma.novelPromotionVoiceLine.create({
|
||||
data: {
|
||||
episodeId: episode.id,
|
||||
lineIndex: 1,
|
||||
speaker: 'Narrator',
|
||||
content: 'Hello world',
|
||||
matchedPanelId: panel.id,
|
||||
matchedStoryboardId: storyboard.id,
|
||||
matchedPanelIndex: panel.panelIndex,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.novelPromotionEpisode.update({
|
||||
where: { id: episode.id },
|
||||
data: {
|
||||
speakerVoices: JSON.stringify({
|
||||
Narrator: {
|
||||
provider: 'fal',
|
||||
voiceType: 'uploaded',
|
||||
audioUrl: 'https://provider.example/reference.wav',
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const secondaryPanel = await prisma.novelPromotionPanel.create({
|
||||
data: {
|
||||
storyboardId: storyboard.id,
|
||||
panelIndex: 1,
|
||||
panelNumber: 2,
|
||||
shotType: '近景',
|
||||
cameraMove: '推镜',
|
||||
description: 'secondary panel',
|
||||
videoPrompt: 'secondary prompt',
|
||||
location: 'Office',
|
||||
characters: JSON.stringify(['Narrator']),
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.novelPromotionStoryboard.update({
|
||||
where: { id: storyboard.id },
|
||||
data: { panelCount: 2 },
|
||||
})
|
||||
|
||||
const foreignStoryboard = await prisma.novelPromotionStoryboard.create({
|
||||
data: {
|
||||
episodeId: episode.id,
|
||||
clipId: (await prisma.novelPromotionClip.create({
|
||||
data: {
|
||||
episodeId: episode.id,
|
||||
summary: 'foreign clip',
|
||||
content: 'foreign clip content',
|
||||
screenplay: 'foreign screenplay',
|
||||
location: 'Office',
|
||||
characters: JSON.stringify(['Narrator']),
|
||||
},
|
||||
})).id,
|
||||
panelCount: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const foreignPanel = await prisma.novelPromotionPanel.create({
|
||||
data: {
|
||||
id: `panel-foreign-${nextSuffix()}`,
|
||||
storyboardId: foreignStoryboard.id,
|
||||
panelIndex: 0,
|
||||
panelNumber: 1,
|
||||
shotType: '远景',
|
||||
cameraMove: '固定',
|
||||
description: 'foreign panel',
|
||||
videoPrompt: 'foreign prompt',
|
||||
location: 'Office',
|
||||
characters: JSON.stringify(['Narrator']),
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
user,
|
||||
project,
|
||||
novelProject,
|
||||
episode,
|
||||
clip,
|
||||
storyboard,
|
||||
panel,
|
||||
secondaryPanel,
|
||||
foreignStoryboard,
|
||||
foreignPanel,
|
||||
character,
|
||||
appearance,
|
||||
location,
|
||||
locationImage,
|
||||
voiceLine,
|
||||
}
|
||||
}
|
||||
52
tests/system/helpers/tasks.ts
Normal file
52
tests/system/helpers/tasks.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { TASK_EVENT_TYPE, TASK_STATUS, type TaskEventType, type TaskStatus } from '@/lib/task/types'
|
||||
import { expect } from 'vitest'
|
||||
import { prisma } from '../../helpers/prisma'
|
||||
|
||||
type WaitTaskOptions = {
|
||||
timeoutMs?: number
|
||||
intervalMs?: number
|
||||
}
|
||||
|
||||
const TERMINAL_STATUSES = new Set<TaskStatus>([
|
||||
TASK_STATUS.COMPLETED,
|
||||
TASK_STATUS.FAILED,
|
||||
TASK_STATUS.DISMISSED,
|
||||
])
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export async function waitForTaskTerminalState(taskId: string, options: WaitTaskOptions = {}) {
|
||||
const timeoutMs = options.timeoutMs ?? 15_000
|
||||
const intervalMs = options.intervalMs ?? 100
|
||||
const startedAt = Date.now()
|
||||
|
||||
while (Date.now() - startedAt <= timeoutMs) {
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: taskId },
|
||||
})
|
||||
if (task && TERMINAL_STATUSES.has(task.status as TaskStatus)) {
|
||||
return task
|
||||
}
|
||||
await sleep(intervalMs)
|
||||
}
|
||||
|
||||
throw new Error(`TASK_WAIT_TIMEOUT: ${taskId}`)
|
||||
}
|
||||
|
||||
export async function listTaskEventTypes(taskId: string): Promise<TaskEventType[]> {
|
||||
const events = await prisma.taskEvent.findMany({
|
||||
where: { taskId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: { eventType: true },
|
||||
})
|
||||
return events.map((event) => event.eventType as TaskEventType)
|
||||
}
|
||||
|
||||
export function expectLifecycleEvents(types: ReadonlyArray<TaskEventType>, terminal: 'completed' | 'failed') {
|
||||
const expectedTerminal = terminal === 'completed' ? TASK_EVENT_TYPE.COMPLETED : TASK_EVENT_TYPE.FAILED
|
||||
expect(types).toContain(TASK_EVENT_TYPE.CREATED)
|
||||
expect(types).toContain(TASK_EVENT_TYPE.PROCESSING)
|
||||
expect(types).toContain(expectedTerminal)
|
||||
}
|
||||
40
tests/system/helpers/workers.ts
Normal file
40
tests/system/helpers/workers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Worker } from 'bullmq'
|
||||
import type { TaskJobData } from '@/lib/task/types'
|
||||
|
||||
export type SystemWorkerScope = 'image' | 'video' | 'voice' | 'text'
|
||||
|
||||
export type SystemWorkers = Partial<Record<SystemWorkerScope, Worker<TaskJobData>>>
|
||||
|
||||
async function createWorker(scope: SystemWorkerScope): Promise<Worker<TaskJobData>> {
|
||||
if (scope === 'image') {
|
||||
const mod = await import('@/lib/workers/image.worker')
|
||||
return mod.createImageWorker()
|
||||
}
|
||||
if (scope === 'video') {
|
||||
const mod = await import('@/lib/workers/video.worker')
|
||||
return mod.createVideoWorker()
|
||||
}
|
||||
if (scope === 'voice') {
|
||||
const mod = await import('@/lib/workers/voice.worker')
|
||||
return mod.createVoiceWorker()
|
||||
}
|
||||
const mod = await import('@/lib/workers/text.worker')
|
||||
return mod.createTextWorker()
|
||||
}
|
||||
|
||||
export async function startSystemWorkers(scopes: ReadonlyArray<SystemWorkerScope>): Promise<SystemWorkers> {
|
||||
const started: SystemWorkers = {}
|
||||
for (const scope of scopes) {
|
||||
const worker = await createWorker(scope)
|
||||
await worker.waitUntilReady()
|
||||
started[scope] = worker
|
||||
}
|
||||
return started
|
||||
}
|
||||
|
||||
export async function stopSystemWorkers(workers: SystemWorkers): Promise<void> {
|
||||
for (const worker of Object.values(workers)) {
|
||||
if (!worker) continue
|
||||
await worker.close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user