import { afterEach, describe, expect, it, vi } from 'vitest' import { directionLabel, mergeDesignedShots, referenceImageUrl, storyboardPrerequisites } from '@/features/storyboard/model' import { directionsResult, episodeShots, storyboardCheckpoint, visualStatesResult } from '@/features/storyboard/testing/fixtures' afterEach(() => vi.unstubAllEnvs()) describe('分镜正式数据与生成依赖', () => { it('用 Shot ID 关联状态,用 Beat + Shot 编号补充描述,不串同编号镜头', () => { const directions = directionsResult('p') const states = visualStatesResult('p', 1, true) states.beats.reverse() states.beats[0]!.shots[0]!.visualState!.continuityNote = '第二个 Beat' const shots = mergeDesignedShots(directions, states, episodeShots()) expect(shots.map(shot => [shot.shotId, shot.title, shot.visualState?.continuityNote])).toEqual([ ['shot-db-1-1', '第1集镜头1', '信封始终在右手'], ['shot-db-1-2', '第1集镜头2', '第二个 Beat'] ]) }) it('没有 Direction 的正式 Shot 仍可显示,不用 checkpoint 伪造数据库 ID', () => { const rows = mergeDesignedShots(directionsResult('p', 1, false), visualStatesResult('p'), episodeShots()) expect(rows).toHaveLength(2) expect(rows[0]?.direction).toBeNull() expect(mergeDesignedShots(null, null, episodeShots())).toEqual([]) }) it('仅最新 Breakdown 决定生成前置条件,不能回退到历史完整快照', () => { const ready = storyboardCheckpoint() expect(storyboardPrerequisites([ready], 1)).toMatchObject({ directionEpisode: true, visualEpisode: true }) expect(storyboardPrerequisites([ready], 3)).toMatchObject({ directionEpisode: false, visualEpisode: false }) const latest = { ...ready, checkpointId: 'failed', createdAt: '2026-08-28T01:00:00Z', state: {} } expect(storyboardPrerequisites([latest, ready], 1)).toMatchObject({ directionProject: false, visualProject: false }) expect(storyboardPrerequisites([{ ...latest, workflowName: 'create-drama' }, ready], 1)).toMatchObject({ directionProject: true }) }) it('保留后端嵌套 Direction 与顶层 VisualState 的不同依赖', () => { const checkpoint = storyboardCheckpoint() delete checkpoint.state.breakdownResult expect(storyboardPrerequisites([checkpoint], 1)).toMatchObject({ directionEpisode: false, visualEpisode: true }) checkpoint.state.subjectForms = [] expect(storyboardPrerequisites([checkpoint], 1).visualEpisode).toBe(false) expect(directionLabel('future-camera-mode')).toBe('future-camera-mode') }) it('参考图只允许 http(s) 或后端 storage 地址,拦截不可信协议和路径逃逸', () => { vi.stubEnv('VITE_API_BASE_URL', 'https://api.example.test/api') expect(referenceImageUrl('/storage/image.png')).toBe('https://api.example.test/storage/image.png') expect(referenceImageUrl('https://images.example.test/a.png')).toBe('https://images.example.test/a.png') for (const value of [ 'javascript:alert(1)', 'data:image/svg+xml,anything', '//evil.test/img', '/storage/../api/projects', '/storage/\\evil.test/img' ]) { expect(referenceImageUrl(value)).toBeNull() } }) })