feat: refine UI, improve UX, optimize the analysis pipeline, and add character standing positions
This commit is contained in:
@@ -185,6 +185,7 @@ describe('worker analyze-novel behavior', () => {
|
||||
locationId: 'loc-new-1',
|
||||
imageIndex: 0,
|
||||
description: '雨夜街道',
|
||||
availableSlots: '[]',
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -194,6 +195,7 @@ describe('worker analyze-novel behavior', () => {
|
||||
locationId: 'prop-new-1',
|
||||
imageIndex: 0,
|
||||
description: '一根两头包裹金片的黑铁长棍',
|
||||
availableSlots: '[]',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -78,7 +78,10 @@ describe('worker asset-hub-ai-design behavior', () => {
|
||||
projectId: 'global-asset-hub',
|
||||
skipBilling: true,
|
||||
}))
|
||||
expect(result).toEqual({ prompt: 'generated prompt' })
|
||||
expect(result).toEqual({
|
||||
prompt: 'generated prompt',
|
||||
availableSlots: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('location type success -> passes location assetType', async () => {
|
||||
|
||||
@@ -124,6 +124,7 @@ describe('worker asset-hub-ai-modify behavior', () => {
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
modifiedDescription: 'modified description',
|
||||
availableSlots: [],
|
||||
})
|
||||
expect(llmStreamMock.flush).toHaveBeenCalled()
|
||||
})
|
||||
@@ -140,6 +141,7 @@ describe('worker asset-hub-ai-modify behavior', () => {
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
modifiedDescription: 'modified description',
|
||||
availableSlots: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -63,6 +63,9 @@ describe('worker location-image-task-handler behavior', () => {
|
||||
locationId: 'location-1',
|
||||
imageIndex: 0,
|
||||
description: '雨夜街道',
|
||||
availableSlots: JSON.stringify([
|
||||
'街道左侧靠墙的留白位置',
|
||||
]),
|
||||
location: { name: 'Old Town' },
|
||||
})
|
||||
|
||||
@@ -75,6 +78,9 @@ describe('worker location-image-task-handler behavior', () => {
|
||||
locationId: 'location-1',
|
||||
imageIndex: 0,
|
||||
description: '雨夜街道',
|
||||
availableSlots: JSON.stringify([
|
||||
'街道左侧靠墙的留白位置',
|
||||
]),
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -96,12 +102,27 @@ describe('worker location-image-task-handler behavior', () => {
|
||||
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: `雨夜街道,${animeStylePrompt}`,
|
||||
prompt: expect.stringContaining('雨夜街道'),
|
||||
label: 'Old Town',
|
||||
targetId: 'location-image-1',
|
||||
options: expect.objectContaining({ aspectRatio: '1:1' }),
|
||||
}),
|
||||
)
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('可站位置:'),
|
||||
}),
|
||||
)
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('街道左侧靠墙的留白位置'),
|
||||
}),
|
||||
)
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('必须使用宽广完整的场景全景构图'),
|
||||
}),
|
||||
)
|
||||
const generationCall = sharedMock.generateLabeledImageToCos.mock.calls[0] as unknown as [{ prompt: string }] | undefined
|
||||
expect(generationCall).toBeTruthy()
|
||||
if (!generationCall) throw new Error('expected generateLabeledImageToCos call')
|
||||
@@ -119,7 +140,7 @@ describe('worker location-image-task-handler behavior', () => {
|
||||
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: `雨夜街道,${getArtStylePrompt('realistic', 'zh')}`,
|
||||
prompt: expect.stringContaining(getArtStylePrompt('realistic', 'zh')),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -21,7 +21,20 @@ const sharedMock = vi.hoisted(() => ({
|
||||
resolveNovelData: vi.fn(async () => ({
|
||||
videoRatio: '16:9',
|
||||
characters: [],
|
||||
locations: [],
|
||||
locations: [
|
||||
{
|
||||
name: 'Old Town',
|
||||
images: [
|
||||
{
|
||||
isSelected: true,
|
||||
description: '雨夜街道',
|
||||
availableSlots: JSON.stringify([
|
||||
'街道左侧靠墙的留白位置',
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})),
|
||||
}))
|
||||
|
||||
@@ -29,6 +42,10 @@ const outboundMock = vi.hoisted(() => ({
|
||||
normalizeReferenceImagesForGeneration: vi.fn(async () => ['normalized-ref-1']),
|
||||
}))
|
||||
|
||||
const promptMock = vi.hoisted(() => ({
|
||||
buildPrompt: vi.fn(() => 'panel-image-prompt'),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/prisma', () => ({ prisma: prismaMock }))
|
||||
vi.mock('@/lib/workers/utils', () => utilsMock)
|
||||
vi.mock('@/lib/media/outbound-image', () => outboundMock)
|
||||
@@ -56,7 +73,7 @@ vi.mock('@/lib/workers/handlers/image-task-handler-shared', async () => {
|
||||
})
|
||||
vi.mock('@/lib/prompt-i18n', () => ({
|
||||
PROMPT_IDS: { NP_SINGLE_PANEL_IMAGE: 'np_single_panel_image' },
|
||||
buildPrompt: vi.fn(() => 'panel-image-prompt'),
|
||||
buildPrompt: promptMock.buildPrompt,
|
||||
}))
|
||||
|
||||
import { handlePanelImageTask } from '@/lib/workers/handlers/panel-image-task-handler'
|
||||
@@ -88,9 +105,10 @@ describe('worker panel-image-task-handler behavior', () => {
|
||||
shotType: 'close-up',
|
||||
cameraMove: 'static',
|
||||
description: 'hero close-up',
|
||||
imagePrompt: 'panel anchor prompt',
|
||||
videoPrompt: 'dramatic',
|
||||
location: 'Old Town',
|
||||
characters: JSON.stringify([{ name: 'Hero', appearance: 'default' }]),
|
||||
characters: JSON.stringify([{ name: 'Hero', appearance: 'default', slot: '街道左侧靠墙的留白位置' }]),
|
||||
srtSegment: '台词片段',
|
||||
photographyRules: null,
|
||||
actingNotes: null,
|
||||
@@ -134,6 +152,16 @@ describe('worker panel-image-task-handler behavior', () => {
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(promptMock.buildPrompt).toHaveBeenCalledWith(expect.objectContaining({
|
||||
variables: expect.objectContaining({
|
||||
storyboard_text_json_input: expect.stringContaining('"slot": "街道左侧靠墙的留白位置"'),
|
||||
}),
|
||||
}))
|
||||
expect(promptMock.buildPrompt).toHaveBeenCalledWith(expect.objectContaining({
|
||||
variables: expect.objectContaining({
|
||||
storyboard_text_json_input: expect.stringContaining('"available_slots"'),
|
||||
}),
|
||||
}))
|
||||
|
||||
expect(prismaMock.novelPromotionPanel.update).toHaveBeenCalledWith({
|
||||
where: { id: 'panel-1' },
|
||||
@@ -155,6 +183,7 @@ describe('worker panel-image-task-handler behavior', () => {
|
||||
shotType: 'close-up',
|
||||
cameraMove: 'static',
|
||||
description: 'hero close-up',
|
||||
imagePrompt: null,
|
||||
videoPrompt: 'dramatic',
|
||||
location: 'Old Town',
|
||||
characters: '[]',
|
||||
|
||||
@@ -30,7 +30,16 @@ const sharedMock = vi.hoisted(() => ({
|
||||
imageUrl: 'cos/hero-default.png',
|
||||
}],
|
||||
}],
|
||||
locations: [{ name: 'Old Town', images: [] }],
|
||||
locations: [{
|
||||
name: 'Old Town',
|
||||
images: [{
|
||||
isSelected: true,
|
||||
description: '老街中央留出明确人物站位',
|
||||
availableSlots: JSON.stringify([
|
||||
'街道左侧靠墙的留白位置',
|
||||
]),
|
||||
}],
|
||||
}],
|
||||
})),
|
||||
}))
|
||||
|
||||
@@ -63,12 +72,15 @@ vi.mock('@/lib/prompt-i18n', () => ({
|
||||
|
||||
import { handlePanelVariantTask } from '@/lib/workers/handlers/panel-variant-task-handler'
|
||||
|
||||
function buildJob(payload: Record<string, unknown>): Job<TaskJobData> {
|
||||
function buildJob(
|
||||
payload: Record<string, unknown>,
|
||||
locale: TaskJobData['locale'] = 'zh',
|
||||
): Job<TaskJobData> {
|
||||
return {
|
||||
data: {
|
||||
taskId: 'task-panel-variant-1',
|
||||
type: TASK_TYPE.PANEL_VARIANT,
|
||||
locale: 'zh',
|
||||
locale,
|
||||
projectId: 'project-1',
|
||||
episodeId: 'episode-1',
|
||||
targetType: 'NovelPromotionPanel',
|
||||
@@ -90,7 +102,7 @@ describe('worker panel-variant-task-handler behavior', () => {
|
||||
storyboardId: 'storyboard-1',
|
||||
imageUrl: null,
|
||||
location: 'Old Town',
|
||||
characters: JSON.stringify([{ name: 'Hero', appearance: 'default' }]),
|
||||
characters: JSON.stringify([{ name: 'Hero', appearance: 'default', slot: '街道左侧靠墙的留白位置' }]),
|
||||
}
|
||||
}
|
||||
if (args.where.id === 'panel-source') {
|
||||
@@ -145,6 +157,12 @@ describe('worker panel-variant-task-handler behavior', () => {
|
||||
where: { id: 'panel-new' },
|
||||
data: { imageUrl: 'cos/panel-variant-new.png' },
|
||||
})
|
||||
expect(promptMock.buildPrompt).toHaveBeenCalledWith(expect.objectContaining({
|
||||
variables: expect.objectContaining({
|
||||
characters_info: expect.stringContaining('固定位置:街道左侧靠墙的留白位置'),
|
||||
location_asset: expect.stringContaining('街道左侧靠墙的留白位置'),
|
||||
}),
|
||||
}))
|
||||
|
||||
expect(result).toEqual({
|
||||
panelId: 'panel-new',
|
||||
@@ -178,4 +196,30 @@ describe('worker panel-variant-task-handler behavior', () => {
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('uses localized slot labels in english variant prompts', async () => {
|
||||
const payload = {
|
||||
newPanelId: 'panel-new',
|
||||
sourcePanelId: 'panel-source',
|
||||
variant: {
|
||||
title: 'Rainy night version',
|
||||
description: 'Keep the same staging but change the mood',
|
||||
video_prompt: 'Keep the same staging but change the mood',
|
||||
},
|
||||
}
|
||||
|
||||
await handlePanelVariantTask(buildJob(payload, 'en'))
|
||||
|
||||
expect(promptMock.buildPrompt).toHaveBeenCalledWith(expect.objectContaining({
|
||||
locale: 'en',
|
||||
variables: expect.objectContaining({
|
||||
location_asset: expect.stringContaining('Available character slots:'),
|
||||
}),
|
||||
}))
|
||||
expect(promptMock.buildPrompt).toHaveBeenCalledWith(expect.objectContaining({
|
||||
variables: expect.objectContaining({
|
||||
location_asset: expect.not.stringContaining('可站位置:'),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -274,4 +274,97 @@ describe('script-to-storyboard orchestrator retry', () => {
|
||||
expect(result.summary.clipCount).toBe(3)
|
||||
expect(maxActivePhase1).toBe(1)
|
||||
})
|
||||
|
||||
it('pipelines clips so one clip can enter phase2 before another clip finishes phase1', async () => {
|
||||
let releaseClip1Phase1: (() => void) | null = null
|
||||
const clip1Phase1Gate = new Promise<void>((resolve) => {
|
||||
releaseClip1Phase1 = resolve
|
||||
})
|
||||
let clip2Phase2Started = false
|
||||
let clip1Phase1ResolvedAfterClip2Phase2 = false
|
||||
|
||||
const runStep = vi.fn(async (meta, _prompt, action: string) => {
|
||||
const stepId = String(meta.stepId)
|
||||
|
||||
if (action === 'storyboard_phase1_plan' && stepId === 'clip_clip-1_phase1') {
|
||||
await clip1Phase1Gate
|
||||
clip1Phase1ResolvedAfterClip2Phase2 = clip2Phase2Started
|
||||
return {
|
||||
text: JSON.stringify([{ panel_number: 1, description: '镜头1', location: '场景A', source_text: '原文1', characters: [] }]),
|
||||
reasoning: '',
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'storyboard_phase1_plan' && stepId === 'clip_clip-2_phase1') {
|
||||
return {
|
||||
text: JSON.stringify([{ panel_number: 1, description: '镜头2', location: '场景A', source_text: '原文2', characters: [] }]),
|
||||
reasoning: '',
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'storyboard_phase2_cinematography' && stepId === 'clip_clip-2_phase2_cinematography') {
|
||||
clip2Phase2Started = true
|
||||
releaseClip1Phase1?.()
|
||||
return {
|
||||
text: JSON.stringify([{ panel_number: 1, composition: '居中', lighting: '顶光', color_palette: '冷色', atmosphere: '紧张', technical_notes: 'note' }]),
|
||||
reasoning: '',
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'storyboard_phase2_acting') {
|
||||
return { text: JSON.stringify([{ panel_number: 1, characters: [] }]), reasoning: '' }
|
||||
}
|
||||
|
||||
if (action === 'storyboard_phase2_cinematography') {
|
||||
return {
|
||||
text: JSON.stringify([{ panel_number: 1, composition: '居中', lighting: '顶光', color_palette: '冷色', atmosphere: '紧张', technical_notes: 'note' }]),
|
||||
reasoning: '',
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'storyboard_phase3_detail') {
|
||||
return {
|
||||
text: JSON.stringify([{ panel_number: 1, description: '细化镜头', location: '场景A', source_text: '原文', characters: [] }]),
|
||||
reasoning: '',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`unexpected action: ${action}:${stepId}`)
|
||||
})
|
||||
|
||||
const result = await runScriptToStoryboardOrchestrator({
|
||||
concurrency: 2,
|
||||
clips: [
|
||||
{
|
||||
id: 'clip-1',
|
||||
content: '文本1',
|
||||
characters: JSON.stringify([{ name: '角色A' }]),
|
||||
location: '场景A',
|
||||
screenplay: null,
|
||||
},
|
||||
{
|
||||
id: 'clip-2',
|
||||
content: '文本2',
|
||||
characters: JSON.stringify([{ name: '角色A' }]),
|
||||
location: '场景A',
|
||||
screenplay: null,
|
||||
},
|
||||
],
|
||||
novelPromotionData: {
|
||||
characters: [{ name: '角色A', appearances: [] }],
|
||||
locations: [{ name: '场景A', images: [] }],
|
||||
},
|
||||
promptTemplates: {
|
||||
phase1PlanTemplate: '{clip_content} {clip_json} {characters_lib_name} {locations_lib_name} {characters_introduction} {characters_appearance_list} {characters_full_description}',
|
||||
phase2CinematographyTemplate: '{panels_json} {panel_count} {locations_description} {characters_info}',
|
||||
phase2ActingTemplate: '{panels_json} {panel_count} {characters_info}',
|
||||
phase3DetailTemplate: '{panels_json} {characters_age_gender} {locations_description}',
|
||||
},
|
||||
runStep,
|
||||
})
|
||||
|
||||
expect(result.summary.clipCount).toBe(2)
|
||||
expect(clip2Phase2Started).toBe(true)
|
||||
expect(clip1Phase1ResolvedAfterClip2Phase2).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,13 +28,14 @@ const runScriptToStoryboardOrchestratorMock = vi.hoisted(() =>
|
||||
clipPanels: [
|
||||
{
|
||||
clipId: 'clip-1',
|
||||
panels: [
|
||||
clipIndex: 0,
|
||||
finalPanels: [
|
||||
{
|
||||
panelIndex: 1,
|
||||
shotType: 'close-up',
|
||||
cameraMove: 'static',
|
||||
panel_number: 1,
|
||||
shot_type: 'close-up',
|
||||
camera_move: 'static',
|
||||
description: 'panel desc',
|
||||
videoPrompt: 'panel prompt',
|
||||
video_prompt: 'panel prompt',
|
||||
location: 'room',
|
||||
characters: ['Narrator'],
|
||||
},
|
||||
@@ -48,7 +49,7 @@ const runScriptToStoryboardOrchestratorMock = vi.hoisted(() =>
|
||||
})),
|
||||
)
|
||||
const parseVoiceLinesJsonMock = vi.hoisted(() => vi.fn())
|
||||
const persistStoryboardsAndPanelsMock = vi.hoisted(() => vi.fn())
|
||||
const persistStoryboardOutputsMock = vi.hoisted(() => vi.fn())
|
||||
const parseStoryboardRetryTargetMock = vi.hoisted(() => vi.fn())
|
||||
const runScriptToStoryboardAtomicRetryMock = vi.hoisted(() => vi.fn())
|
||||
const workflowLeaseMock = vi.hoisted(() => ({
|
||||
@@ -158,11 +159,11 @@ vi.mock('@/lib/workers/handlers/script-to-storyboard-helpers', () => ({
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
||||
return value as Record<string, unknown>
|
||||
},
|
||||
buildStoryboardJson: vi.fn(() => '[]'),
|
||||
buildStoryboardJsonFromClipPanels: vi.fn(() => '[]'),
|
||||
parseEffort: vi.fn(() => null),
|
||||
parseTemperature: vi.fn(() => 0.7),
|
||||
parseVoiceLinesJson: parseVoiceLinesJsonMock,
|
||||
persistStoryboardsAndPanels: persistStoryboardsAndPanelsMock,
|
||||
persistStoryboardOutputs: persistStoryboardOutputsMock,
|
||||
toPositiveInt: (value: unknown) => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return null
|
||||
const n = Math.floor(value)
|
||||
@@ -255,33 +256,41 @@ describe('worker script-to-storyboard behavior', () => {
|
||||
],
|
||||
})
|
||||
|
||||
prismaMock.$transaction.mockImplementation(async (fn: (tx: {
|
||||
novelPromotionVoiceLine: {
|
||||
deleteMany: (args: { where: Record<string, unknown> }) => Promise<unknown>
|
||||
create: (args: { data: Record<string, unknown>; select: { id: boolean } }) => Promise<{ id: string }>
|
||||
}
|
||||
}) => Promise<unknown>) => {
|
||||
const tx = {
|
||||
novelPromotionVoiceLine: {
|
||||
deleteMany: async (args: { where: Record<string, unknown> }) => {
|
||||
txState.deletedWhereClauses.push(args.where)
|
||||
return undefined
|
||||
},
|
||||
create: async (args: { data: Record<string, unknown>; select: { id: boolean } }) => {
|
||||
txState.createdRows.push(args.data)
|
||||
return { id: `voice-${txState.createdRows.length}` }
|
||||
},
|
||||
},
|
||||
}
|
||||
return await fn(tx)
|
||||
})
|
||||
prismaMock.$transaction.mockReset()
|
||||
|
||||
persistStoryboardsAndPanelsMock.mockResolvedValue([
|
||||
{
|
||||
storyboardId: 'storyboard-1',
|
||||
panels: [{ id: 'panel-1', panelIndex: 1 }],
|
||||
},
|
||||
])
|
||||
persistStoryboardOutputsMock.mockImplementation(async ({ voiceLineRows }: { voiceLineRows: VoiceLineInput[] | null }) => {
|
||||
const rows = voiceLineRows || []
|
||||
txState.createdRows = rows.map((row) => ({
|
||||
episodeId: 'episode-1',
|
||||
lineIndex: row.lineIndex,
|
||||
speaker: row.speaker,
|
||||
content: row.content,
|
||||
emotionStrength: row.emotionStrength,
|
||||
matchedPanelId: 'panel-1',
|
||||
matchedStoryboardId: 'storyboard-1',
|
||||
matchedPanelIndex: row.matchedPanel.panelIndex,
|
||||
}))
|
||||
txState.deletedWhereClauses = [
|
||||
rows.length === 0
|
||||
? { episodeId: 'episode-1' }
|
||||
: {
|
||||
episodeId: 'episode-1',
|
||||
lineIndex: {
|
||||
notIn: rows.map((row) => row.lineIndex),
|
||||
},
|
||||
},
|
||||
]
|
||||
return {
|
||||
persistedStoryboards: [
|
||||
{
|
||||
storyboardId: 'storyboard-1',
|
||||
clipId: 'clip-1',
|
||||
panels: [{ id: 'panel-1', panelIndex: 1 }],
|
||||
},
|
||||
],
|
||||
voiceLineCount: rows.length,
|
||||
}
|
||||
})
|
||||
|
||||
parseVoiceLinesJsonMock.mockReturnValue(baseVoiceRows())
|
||||
})
|
||||
@@ -433,7 +442,7 @@ describe('worker script-to-storyboard behavior', () => {
|
||||
})
|
||||
expect(runScriptToStoryboardAtomicRetryMock).toHaveBeenCalledTimes(1)
|
||||
expect(runScriptToStoryboardOrchestratorMock).not.toHaveBeenCalled()
|
||||
expect(persistStoryboardsAndPanelsMock).toHaveBeenCalledWith({
|
||||
expect(persistStoryboardOutputsMock).toHaveBeenCalledWith({
|
||||
episodeId: 'episode-1',
|
||||
clipPanels: [
|
||||
{
|
||||
@@ -448,6 +457,7 @@ describe('worker script-to-storyboard behavior', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
voiceLineRows: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('worker shot-ai-prompt-location behavior', () => {
|
||||
vi.clearAllMocks()
|
||||
persistMock.resolveAnalysisModel.mockResolvedValue({ id: 'np-1', analysisModel: 'llm::analysis' })
|
||||
persistMock.requireProjectLocation.mockResolvedValue({ id: 'location-1', name: 'Old Town' })
|
||||
runtimeMock.runShotPromptCompletion.mockResolvedValue('{"prompt":"updated location description"}')
|
||||
runtimeMock.runShotPromptCompletion.mockResolvedValue('{"prompt":"updated location description","available_slots":["街道左侧靠墙的留白位置"]}')
|
||||
persistMock.persistLocationDescription.mockResolvedValue({ id: 'location-1', images: [] })
|
||||
})
|
||||
|
||||
@@ -85,10 +85,12 @@ describe('worker shot-ai-prompt-location behavior', () => {
|
||||
locationId: 'location-1',
|
||||
imageIndex: 2,
|
||||
modifiedDescription: 'updated location description',
|
||||
availableSlots: ['街道左侧靠墙的留白位置'],
|
||||
})
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
success: true,
|
||||
modifiedDescription: 'updated location description',
|
||||
availableSlots: ['街道左侧靠墙的留白位置'],
|
||||
location: { id: 'location-1', images: [] },
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -6,7 +6,9 @@ const prismaMock = vi.hoisted(() => ({
|
||||
project: { findUnique: vi.fn() },
|
||||
novelPromotionProject: { findUnique: vi.fn() },
|
||||
novelPromotionEpisode: { findUnique: vi.fn() },
|
||||
$transaction: vi.fn(),
|
||||
novelPromotionClip: { update: vi.fn(async () => ({})) },
|
||||
locationImage: { createMany: vi.fn(async () => ({ count: 0 })) },
|
||||
}))
|
||||
|
||||
const workerMock = vi.hoisted(() => ({
|
||||
@@ -120,6 +122,7 @@ function buildJob(payload: Record<string, unknown>, episodeId: string | null = '
|
||||
describe('worker story-to-script behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
prismaMock.$transaction.mockImplementation(async (fn: (tx: typeof prismaMock) => Promise<unknown>) => await fn(prismaMock))
|
||||
|
||||
prismaMock.project.findUnique.mockResolvedValue({
|
||||
id: 'project-1',
|
||||
@@ -181,10 +184,10 @@ describe('worker story-to-script behavior', () => {
|
||||
persistedClips: 1,
|
||||
})
|
||||
|
||||
expect(helperMock.persistClips).toHaveBeenCalledWith({
|
||||
expect(helperMock.persistClips).toHaveBeenCalledWith(expect.objectContaining({
|
||||
episodeId: 'episode-1',
|
||||
clipList: [{ clipId: 'clip-1', content: 'clip content', props: ['Knife'] }],
|
||||
})
|
||||
}))
|
||||
|
||||
expect(prismaMock.novelPromotionClip.update).toHaveBeenCalledWith({
|
||||
where: { id: 'clip-row-1' },
|
||||
|
||||
Reference in New Issue
Block a user