Files
waooplus/tests/unit/worker/script-to-storyboard-atomic-retry.test.ts
2026-03-08 17:10:06 +08:00

226 lines
7.6 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest'
import {
parseStoryboardRetryTarget,
runScriptToStoryboardAtomicRetry,
} from '@/lib/workers/handlers/script-to-storyboard-atomic-retry'
const listArtifactsMock = vi.hoisted(() => vi.fn())
vi.mock('@/lib/run-runtime/service', () => ({
listArtifacts: listArtifactsMock,
}))
describe('script-to-storyboard atomic retry', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('解析 clip+phase stepKey', () => {
expect(parseStoryboardRetryTarget('clip_clip-1_phase3_detail')).toEqual({
stepKey: 'clip_clip-1_phase3_detail',
clipId: 'clip-1',
phase: 'phase3_detail',
})
expect(parseStoryboardRetryTarget('voice_analyze')).toBeNull()
expect(parseStoryboardRetryTarget('clip__phase3')).toBeNull()
})
it('phase3 重试只执行 phase3 并读取 phase1/phase2 artifact 续跑', async () => {
listArtifactsMock.mockImplementation(async (params: {
runId: string
artifactType?: string
refId?: string
}) => {
if (params.refId !== 'clip-1') return []
if (params.artifactType === 'storyboard.clip.phase1') {
return [{
id: 'a1',
runId: params.runId,
stepKey: 'clip_clip-1_phase1',
artifactType: 'storyboard.clip.phase1',
refId: 'clip-1',
versionHash: null,
payload: {
panels: [{ panel_number: 1, description: 'p1', location: 'Office', source_text: 'src', characters: [] }],
},
createdAt: '2026-03-03T00:00:00.000Z',
}]
}
if (params.artifactType === 'storyboard.clip.phase2.cine') {
return [{
id: 'a2',
runId: params.runId,
stepKey: 'clip_clip-1_phase2_cinematography',
artifactType: 'storyboard.clip.phase2.cine',
refId: 'clip-1',
versionHash: null,
payload: {
rules: [{
panel_number: 1,
composition: '居中',
lighting: '顶光',
color_palette: '冷色',
atmosphere: '紧张',
technical_notes: 'note',
}],
},
createdAt: '2026-03-03T00:00:00.000Z',
}]
}
if (params.artifactType === 'storyboard.clip.phase2.acting') {
return [{
id: 'a3',
runId: params.runId,
stepKey: 'clip_clip-1_phase2_acting',
artifactType: 'storyboard.clip.phase2.acting',
refId: 'clip-1',
versionHash: null,
payload: {
directions: [{ panel_number: 1, characters: [{ name: 'Narrator', expression: 'serious' }] }],
},
createdAt: '2026-03-03T00:00:00.000Z',
}]
}
if (params.artifactType === 'storyboard.clip.phase3') {
return []
}
return []
})
const runStep = vi.fn(async (_meta, _prompt, action: string) => {
if (action !== 'storyboard_phase3_detail') {
throw new Error(`unexpected action ${action}`)
}
return {
text: JSON.stringify([{ panel_number: 1, description: 'phase3-new', location: 'Office', source_text: 'src', characters: [] }]),
reasoning: '',
}
})
const result = await runScriptToStoryboardAtomicRetry({
runId: 'run-1',
retryTarget: {
stepKey: 'clip_clip-1_phase3_detail',
clipId: 'clip-1',
phase: 'phase3_detail',
},
retryStepAttempt: 4,
clip: {
id: 'clip-1',
content: 'clip content',
characters: JSON.stringify([{ name: 'Narrator' }]),
location: 'Office',
screenplay: null,
},
clipIndex: 0,
totalClipCount: 1,
novelPromotionData: {
characters: [{ name: 'Narrator', appearances: [] }],
locations: [{ name: 'Office', images: [{ description: 'room desc' }] }],
},
promptTemplates: {
phase1PlanTemplate: '{clip_content}',
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(runStep).toHaveBeenCalledTimes(1)
expect(runStep.mock.calls[0]?.[2]).toBe('storyboard_phase3_detail')
expect(result.phase1PanelsByClipId).toEqual({})
expect(result.phase2CinematographyByClipId).toEqual({})
expect(result.phase2ActingByClipId).toEqual({})
expect(result.phase3PanelsByClipId['clip-1']).toEqual([
{ panel_number: 1, description: 'phase3-new', location: 'Office', source_text: 'src', characters: [] },
])
expect(result.clipPanels).toHaveLength(1)
expect(result.clipPanels[0]?.finalPanels[0]).toEqual(expect.objectContaining({
panel_number: 1,
description: 'phase3-new',
photographyPlan: expect.objectContaining({
composition: '居中',
lighting: '顶光',
}),
actingNotes: [{ name: 'Narrator', expression: 'serious' }],
}))
expect(result.totalPanelCount).toBe(1)
})
it('phase2 重试缺少 phase3 artifact 时显式失败', async () => {
listArtifactsMock.mockImplementation(async (params: {
runId: string
artifactType?: string
refId?: string
}) => {
if (params.refId !== 'clip-1') return []
if (params.artifactType === 'storyboard.clip.phase1') {
return [{
id: 'a1',
runId: params.runId,
stepKey: 'clip_clip-1_phase1',
artifactType: 'storyboard.clip.phase1',
refId: 'clip-1',
versionHash: null,
payload: { panels: [{ panel_number: 1, description: 'p1', location: 'Office' }] },
createdAt: '2026-03-03T00:00:00.000Z',
}]
}
if (params.artifactType === 'storyboard.clip.phase2.acting') {
return [{
id: 'a2',
runId: params.runId,
stepKey: 'clip_clip-1_phase2_acting',
artifactType: 'storyboard.clip.phase2.acting',
refId: 'clip-1',
versionHash: null,
payload: { directions: [{ panel_number: 1, characters: [] }] },
createdAt: '2026-03-03T00:00:00.000Z',
}]
}
return []
})
const runStep = vi.fn(async (_meta, _prompt, action: string) => {
if (action !== 'storyboard_phase2_cinematography') {
throw new Error(`unexpected action ${action}`)
}
return {
text: JSON.stringify([{ panel_number: 1, composition: '居中' }]),
reasoning: '',
}
})
await expect(runScriptToStoryboardAtomicRetry({
runId: 'run-2',
retryTarget: {
stepKey: 'clip_clip-1_phase2_cinematography',
clipId: 'clip-1',
phase: 'phase2_cinematography',
},
retryStepAttempt: 2,
clip: {
id: 'clip-1',
content: 'clip content',
characters: JSON.stringify([{ name: 'Narrator' }]),
location: 'Office',
screenplay: null,
},
clipIndex: 0,
totalClipCount: 1,
novelPromotionData: {
characters: [{ name: 'Narrator', appearances: [] }],
locations: [{ name: 'Office', images: [{ description: 'room desc' }] }],
},
promptTemplates: {
phase1PlanTemplate: '{clip_content}',
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,
})).rejects.toThrow('missing dependency artifact: storyboard.clip.phase3')
})
})