fix: 同步制作接口并修复浅色主题层次
This commit is contained in:
@@ -54,17 +54,67 @@ function server() {
|
||||
}
|
||||
const receipt = {
|
||||
projectId: 'capability-test',
|
||||
completed: true,
|
||||
completed: false,
|
||||
dispatchCompleted: true,
|
||||
needsManualReview: false,
|
||||
stopReason: '',
|
||||
errors: ['一个提示词未生成'],
|
||||
videoGenerationResult: { total: 1, created: 1, skipped: 0, failed: 0 }
|
||||
keyframeQualityResult: {
|
||||
total: 1,
|
||||
passed: 1,
|
||||
skipped: 0,
|
||||
blocked: 0,
|
||||
repairFailed: 0,
|
||||
failed: 0,
|
||||
stalePrimaryKeyframe: 0
|
||||
},
|
||||
videoGenerationResult: { total: 1, created: 1, skipped: 0, inProgress: 0, blocked: 0, failed: 0 }
|
||||
}
|
||||
const stage = () => ({ total: 1, planned: 1, skipped: 0, upstreamResolvable: 0, manualBlocked: 0 })
|
||||
const plan = {
|
||||
projectId: 'capability-test',
|
||||
completed: false,
|
||||
stage: 'subject_images',
|
||||
hasManualBlockers: false,
|
||||
manualBlocked: 0,
|
||||
subjectImages: stage(),
|
||||
keyframes: stage(),
|
||||
videoPrompts: stage(),
|
||||
videos: stage(),
|
||||
details: {
|
||||
subjectImages: [
|
||||
{
|
||||
subjectFormId: form.id,
|
||||
subjectRef: 'C1',
|
||||
subjectName: '角色',
|
||||
formName: '基础形态',
|
||||
module: 'character',
|
||||
status: 'ready',
|
||||
primaryImageId: null,
|
||||
primaryImageStale: false,
|
||||
issues: [] as { code: string; reason: string }[]
|
||||
}
|
||||
],
|
||||
keyframes: readiness.items,
|
||||
videoPrompts: readiness.items,
|
||||
videos: readiness.items
|
||||
}
|
||||
}
|
||||
const status = {
|
||||
projectId: 'capability-test',
|
||||
completed: false,
|
||||
stage: 'subject_images',
|
||||
subjectImages: { total: 1, current: 0, missing: 1, stale: 0, complete: false, items: [] },
|
||||
keyframes: readiness,
|
||||
videos: readiness
|
||||
}
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async (url, init) => {
|
||||
const path = String(url)
|
||||
let data: unknown = context.project.value
|
||||
if (path.endsWith('/checkpoints')) data = context.checkpoints.value
|
||||
else if (path.endsWith('/subject-forms')) data = [form]
|
||||
else if (path.endsWith('/production/plan')) data = plan
|
||||
else if (path.endsWith('/production/status')) data = status
|
||||
else if (path.includes('/readiness')) data = readiness
|
||||
else if (path.includes('/storyboard-directions')) data = directionsResult('capability-test')
|
||||
else if (path.endsWith('/videos/status'))
|
||||
@@ -91,6 +141,8 @@ function server() {
|
||||
form,
|
||||
readiness,
|
||||
receipt,
|
||||
plan,
|
||||
status,
|
||||
fetcher,
|
||||
posts: () => fetcher.mock.calls.filter(([, init]) => init?.method === 'POST')
|
||||
}
|
||||
@@ -113,7 +165,7 @@ async function advanced() {
|
||||
}
|
||||
|
||||
describe('高级串联生产的安全边界', () => {
|
||||
it('挂载不查询或生成,通过预检仍需显式启动,再次预检后提交固定 Provider', async () => {
|
||||
it('挂载不查询或生成,通过预检仍需显式启动,再次预检后提交空参数', async () => {
|
||||
const { service, fetcher, posts } = await advanced()
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
await service.start()
|
||||
@@ -125,37 +177,56 @@ describe('高级串联生产的安全边界', () => {
|
||||
expect(posts()).toHaveLength(1)
|
||||
expect(posts()[0]?.[0]).toBe('/api/projects/capability-test/production/start')
|
||||
expect(JSON.parse(String(posts()[0]?.[1]?.body))).toEqual({})
|
||||
expect(
|
||||
fetcher.mock.calls.filter(([url]) => String(url).includes('/keyframes/readiness?force=true'))
|
||||
).toHaveLength(2)
|
||||
expect(fetcher.mock.calls.filter(([url]) => String(url).endsWith('/production/plan'))).toHaveLength(2)
|
||||
expect(service.session.value.pipelineReceipt?.errors).toEqual(['一个提示词未生成'])
|
||||
expect(service.check.value).toBeNull()
|
||||
})
|
||||
it('预检通过后主首帧变旧,确认时再次预检会拦截,不提交', async () => {
|
||||
it('预检后新增活动任务,确认时重新检查并阻止重复提交', async () => {
|
||||
const { service, readiness, posts } = await advanced()
|
||||
await service.preflight()
|
||||
readiness.items[0]!.primaryKeyframeStale = true
|
||||
readiness.inProgress = 1
|
||||
await service.start()
|
||||
expect(posts()).toHaveLength(0)
|
||||
expect(service.check.value?.issues.join()).toContain('有效主首帧')
|
||||
expect(service.check.value?.issues.join()).toContain('活动视频任务')
|
||||
expect(getOperation('capability-test').error).toContain('条件变化')
|
||||
})
|
||||
it('后台活动视频、缺失主图与剧本未完成均阻止预检通过', async () => {
|
||||
it('缺失主图和过期首帧允许后端计划补齐,但未完成剧本和活动任务仍阻止启动', async () => {
|
||||
const { context, form, readiness } = server()
|
||||
context.data.value!.project.status = 'need_review'
|
||||
form.images = []
|
||||
readiness.items[0]!.primaryKeyframeStale = true
|
||||
readiness.items[0]!.issues = [{ code: 'missing_reference', reason: '先补参考图' }]
|
||||
expect((await checkPipeline('capability-test')).issues).toEqual([])
|
||||
context.data.value!.project.status = 'need_review'
|
||||
readiness.inProgress = 1
|
||||
const result = await checkPipeline('capability-test')
|
||||
expect(result.issues.join()).toContain('剧本尚未完成')
|
||||
expect(result.issues.join()).toContain('所有形态主图')
|
||||
expect(result.issues.join()).toContain('活动视频任务')
|
||||
})
|
||||
it('已有视频的 skipped 也不能掩盖缺失首帧,只有缺少提示词允许本流程补齐', async () => {
|
||||
const { readiness } = server()
|
||||
readiness.items[0]!.status = 'skipped'
|
||||
readiness.items[0]!.issues = [{ code: 'missing_keyframe', reason: '无首帧' }]
|
||||
const result = await checkPipeline('capability-test')
|
||||
expect(result.issues.join()).toContain('视频前置检查未通过')
|
||||
it('人工阻塞保留后端原因,同时允许其他就绪形态先生成', async () => {
|
||||
const { service, plan, posts } = await advanced()
|
||||
plan.hasManualBlockers = true
|
||||
plan.manualBlocked = 1
|
||||
plan.keyframes.manualBlocked = 1
|
||||
plan.details.keyframes[0]!.issues = [{ code: 'missing_identity_anchor', reason: '缺少身份母版' }]
|
||||
await service.preflight()
|
||||
expect(service.check.value?.issues).toEqual([])
|
||||
expect(service.check.value?.plan.details.keyframes[0]?.issues[0]?.reason).toBe('缺少身份母版')
|
||||
await service.start()
|
||||
expect(posts()).toHaveLength(1)
|
||||
})
|
||||
it('没有可执行任务或真实资产已全部完成时不再启动', async () => {
|
||||
const { plan, status } = server()
|
||||
for (const stage of [plan.subjectImages, plan.keyframes, plan.videoPrompts, plan.videos]) stage.planned = 0
|
||||
expect((await checkPipeline('capability-test')).issues.join()).toContain('没有可执行')
|
||||
status.completed = true
|
||||
expect((await checkPipeline('capability-test')).issues.join()).toContain('制作已完成')
|
||||
})
|
||||
it('计划项目不匹配或镜头集合改变时拒绝采用旧计划', async () => {
|
||||
const { plan } = server()
|
||||
plan.details.videoPrompts = []
|
||||
expect((await checkPipeline('capability-test')).issues.join()).toContain('资产列表不一致')
|
||||
plan.projectId = 'other'
|
||||
await expect(checkPipeline('capability-test')).rejects.toThrow('其他项目')
|
||||
})
|
||||
it('项目锁和未完成剧本下不启动,切项目清空旧预检', async () => {
|
||||
const { service, context, posts } = await advanced()
|
||||
@@ -223,6 +294,9 @@ describe('高级串联生产的安全边界', () => {
|
||||
expect(document.body.textContent).toContain('流程返回不等于成片完成')
|
||||
expect(document.body.textContent).toContain(receipt.errors[0])
|
||||
expect(document.body.textContent).toContain('已提交 1')
|
||||
expect(document.body.textContent).toContain('任务已分发,等待视频完成')
|
||||
expect(document.body.textContent).toContain('校验通过 1')
|
||||
expect(document.body.textContent).not.toContain('不会生成首帧')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import ProviderInputInspector from '@/features/production/components/ProviderInputInspector.vue'
|
||||
|
||||
let wrapper: VueWrapper | undefined
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** 仅提供公开输入规格,不连接生成服务。 */
|
||||
function inputSpec(shotId = 'shot-1') {
|
||||
return {
|
||||
shotId,
|
||||
targetProvider: 'seedance',
|
||||
businessKeyframe: { id: 'keyframe-1', imageUrl: '/key.png' },
|
||||
providerInputKeyframe: {
|
||||
id: 'variant-1',
|
||||
imageUrl: '/variant.png',
|
||||
source: 'provider_variant',
|
||||
variantReason: '模型输入适配'
|
||||
},
|
||||
references: [
|
||||
{
|
||||
subjectId: 'subject-1',
|
||||
subjectFormId: 'form-1',
|
||||
subjectRef: 'C1',
|
||||
subjectName: '角色',
|
||||
subjectFormName: '基础形态',
|
||||
module: 'character',
|
||||
providerAsset: { provider: 'seedance', assetId: 'asset-1' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
it('按显式模型发出只读查询并展示授权素材,切换镜头清除旧结果', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({ data: inputSpec() })))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(ProviderInputInspector, { props: { shotId: 'shot-1' } })
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
await wrapper.get('input').setValue(' Seedance ')
|
||||
await wrapper.get('button').trigger('click')
|
||||
await flushPromises()
|
||||
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/storyboard-shots/shot-1/provider-input-spec?provider=seedance')
|
||||
expect(fetcher.mock.calls[0]?.[1]?.method).toBe('GET')
|
||||
expect(wrapper.text()).toContain('asset-1')
|
||||
expect(wrapper.text()).toContain('variant-1')
|
||||
await wrapper.setProps({ shotId: 'shot-2' })
|
||||
expect(wrapper.text()).not.toContain('asset-1')
|
||||
})
|
||||
|
||||
it('模型切换使晚到的旧响应失效,错误归属不发布结果', async () => {
|
||||
let finish!: (response: Response) => void
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(ProviderInputInspector, { props: { shotId: 'shot-1' } })
|
||||
await wrapper.get('input').setValue('seedance')
|
||||
await wrapper.get('button').trigger('click')
|
||||
await wrapper.get('input').setValue('other')
|
||||
finish(new Response(JSON.stringify({ data: inputSpec() })))
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('asset-1')
|
||||
await wrapper.get('input').setValue('seedance')
|
||||
fetcher.mockResolvedValue(new Response(JSON.stringify({ data: inputSpec('wrong-shot') })))
|
||||
await wrapper.get('button').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('不匹配')
|
||||
expect(wrapper.text()).not.toContain('asset-1')
|
||||
})
|
||||
Reference in New Issue
Block a user