feat: 实现剧本创作与拆解前端工作台
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('后端 API 契约', () => {
|
||||
it('解包 data,同时保留创建项目的顶层 202 结构', async () => {
|
||||
const fetcher = vi
|
||||
.fn<(url: string, options: RequestInit) => Promise<Response>>()
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ data: [{ id: 'p1' }] })))
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ projectId: 'p2', status: 'generating' }), { status: 202 })
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
expect(await projectsApi.list()).toEqual([{ id: 'p1' }])
|
||||
expect(await projectsApi.create({ topic: '故事', style: '悬疑', episodeCount: 3 })).toEqual({
|
||||
projectId: 'p2',
|
||||
status: 'generating'
|
||||
})
|
||||
expect(JSON.parse(fetcher.mock.calls[1]![1].body as string)).toEqual({
|
||||
topic: '故事',
|
||||
style: '悬疑',
|
||||
episodeCount: 3
|
||||
})
|
||||
})
|
||||
|
||||
it('只将缺少 checkpoint 的 404 视为可选结果,保留服务器错误', async () => {
|
||||
expect(await optionalResource(Promise.reject(new ApiError('没有 checkpoint', 404)))).toBeNull()
|
||||
await expect(optionalResource(Promise.reject(new ApiError('数据库异常', 500)))).rejects.toMatchObject({
|
||||
status: 500
|
||||
})
|
||||
})
|
||||
|
||||
it('保留错误细节且拒绝 HTML 代理错误', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi
|
||||
.fn<(url: string, options: RequestInit) => Promise<Response>>()
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ message: '校验失败', issues: ['缺少形态'] }), { status: 400 })
|
||||
)
|
||||
.mockResolvedValueOnce(new Response('<html>Bad Gateway</html>', { status: 502 }))
|
||||
)
|
||||
await expect(request('/projects')).rejects.toMatchObject({ status: 400, details: ['缺少形态'] })
|
||||
await expect(request('/projects')).rejects.toThrow('接口未返回 JSON')
|
||||
})
|
||||
|
||||
it('预览模块使用逗号参数,三种恢复不误发到 start', async () => {
|
||||
const fetcher = vi
|
||||
.fn<(url: string, options: RequestInit) => Promise<Response>>()
|
||||
.mockImplementation(() => Promise.resolve(new Response('{"data":{}}')))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await breakdownApi.preview('a/b', { groupSize: 3, modules: ['character', 'scene'] })
|
||||
for (const action of ['retry', 'resume-shots', 'resume-storyboard'] as const)
|
||||
await breakdownApi.run('p', action)
|
||||
expect(fetcher.mock.calls.map(call => call[0])).toEqual([
|
||||
'/api/projects/a%2Fb/breakdown-preview?groupSize=3&modules=character%2Cscene',
|
||||
'/api/projects/p/breakdown/retry',
|
||||
'/api/projects/p/breakdown/resume-shots',
|
||||
'/api/projects/p/breakdown/resume-storyboard'
|
||||
])
|
||||
})
|
||||
|
||||
it('长工作流不使用普通查询的超时,不自动重试 POST', async () => {
|
||||
vi.useFakeTimers()
|
||||
let finish!: (value: Response) => void
|
||||
const fetcher = vi.fn<(url: string, options: RequestInit) => Promise<Response>>().mockImplementation(
|
||||
() =>
|
||||
new Promise<Response>(resolve => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const running = breakdownApi.run('p', 'start', { groupSize: 3, modules: ['prop'] })
|
||||
await vi.advanceTimersByTimeAsync(120_000)
|
||||
expect(fetcher).toHaveBeenCalledTimes(1)
|
||||
expect(fetcher.mock.calls[0]![1].signal?.aborted).toBe(false)
|
||||
finish(new Response('{"data":{"workflowExecution":{"status":"failed"}}}'))
|
||||
expect(await running).toMatchObject({ workflowExecution: { status: 'failed' } })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user