feat: 接入分镜导演设计与即时视觉状态工作台
增加单集和批量生成、修复诊断、正式镜头检查、参考图、生成规格与视频提示词。 补齐接口契约、竞态保护和确认交互测试,保持现有两条工作流。
This commit is contained in:
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiError, optionalResource, request } from './http'
|
||||
import { projectsApi } from '../features/projects/api'
|
||||
import { breakdownApi } from '../features/breakdown/api'
|
||||
import { storyboardApi } from '../features/storyboard/api'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
@@ -9,6 +10,66 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('后端 API 契约', () => {
|
||||
it('分镜单集显式持久化,批量保持 force 和零次修复参数', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => new Response('{"data":{}}'))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await storyboardApi.generateDirection('a/b', 2)
|
||||
await storyboardApi.generateDirections('p', { concurrency: 2, force: false })
|
||||
await storyboardApi.generateVisualState('p', 2, 0)
|
||||
await storyboardApi.generateVisualStates('p', { concurrency: 3, force: true, maxRepairAttempts: 0 })
|
||||
expect(fetcher.mock.calls.map(([url, init]) => [url, init?.method, JSON.parse(init!.body as string)])).toEqual([
|
||||
['/api/projects/a%2Fb/storyboard-directions/generate-test', 'POST', { episodeNo: 2, persist: true }],
|
||||
['/api/projects/p/storyboard-directions/generate', 'POST', { concurrency: 2, force: false }],
|
||||
[
|
||||
'/api/projects/p/storyboard-visual-states/generate-test',
|
||||
'POST',
|
||||
{ episodeNo: 2, persist: true, maxRepairAttempts: 0 }
|
||||
],
|
||||
[
|
||||
'/api/projects/p/storyboard-visual-states/generate',
|
||||
'POST',
|
||||
{ concurrency: 3, force: true, maxRepairAttempts: 0 }
|
||||
]
|
||||
])
|
||||
})
|
||||
|
||||
it('查询按剧集,镜头工具使用数据库 ID,读取提示词仍需显式 POST', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => new Response('{"data":{}}'))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await storyboardApi.directions('p', 4)
|
||||
await storyboardApi.visualStates('p', 4)
|
||||
await storyboardApi.references('shot/id')
|
||||
await storyboardApi.generationSpec('shot/id')
|
||||
await storyboardApi.generatePrompt('shot/id', false)
|
||||
await storyboardApi.generatePrompts('p', { concurrency: 2, force: false })
|
||||
expect(fetcher.mock.calls.map(([url]) => url)).toEqual([
|
||||
'/api/projects/p/storyboard-directions?episodeNo=4',
|
||||
'/api/projects/p/storyboard-visual-states?episodeNo=4',
|
||||
'/api/storyboard-shots/shot%2Fid/references',
|
||||
'/api/storyboard-shots/shot%2Fid/generation-spec',
|
||||
'/api/storyboard-shots/shot%2Fid/video-prompt',
|
||||
'/api/projects/p/video-prompts/generate'
|
||||
])
|
||||
expect(JSON.parse(fetcher.mock.calls[4]![1]!.body as string)).toEqual({ force: false })
|
||||
})
|
||||
|
||||
it('分镜生成保留 200 中的校验失败和持久化标志,长请求不自动超时或重试', async () => {
|
||||
vi.useFakeTimers()
|
||||
let finish!: (response: Response) => void
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const pending = storyboardApi.generateVisualState('p', 1, 2)
|
||||
await vi.advanceTimersByTimeAsync(120_000)
|
||||
expect(fetcher).toHaveBeenCalledOnce()
|
||||
expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(false)
|
||||
finish(new Response('{"data":{"validation":{"valid":false,"issues":[]},"persisted":false}}'))
|
||||
await expect(pending).resolves.toMatchObject({ validation: { valid: false }, persisted: false })
|
||||
})
|
||||
it('解包 data,同时保留创建项目的顶层 202 结构', async () => {
|
||||
const fetcher = vi
|
||||
.fn<(url: string, options: RequestInit) => Promise<Response>>()
|
||||
|
||||
Reference in New Issue
Block a user