返回 ViMax
artifactPresentation.test.ts
根目录 / web / src / artifactPresentation.test.ts
1 import {describe, expect, it} from 'vitest';
2 import {activeRenderCheckpoint, deriveStoryboardReadiness, extractStoryboardPreviews, formatStructuredValue, friendlyArtifactTitle, friendlyFieldLabel, isArtifactPathField, isJsonArtifact, relatedVisualArtifacts} from './artifactPresentation';
3 import type {Artifact} from './types';
4
5 const artifact: Artifact = {
6 path: 'idea2video/scene_0/storyboard.json',
7 name: 'storyboard.json',
8 kind: 'document',
9 size: 1200,
10 updatedAt: '2026-07-19T00:00:00Z',
11 url: '/api/artifact',
12 };
13
14 describe('artifact presentation', () => {
15 it('extracts only storyboard descriptions', () => {
16 const previews = extractStoryboardPreviews([
17 {idx: 0, visual_desc: 'Wide beach shot', audio_desc: 'Waves'},
18 {idx: 1, visual_desc: 'Close portrait', audio_desc: 'Dialogue'},
19 ], artifact.path);
20 expect(previews.map((preview) => preview.description)).toEqual(['Wide beach shot', 'Close portrait']);
21 });
22
23 it('creates readable artifact and field labels', () => {
24 expect(friendlyArtifactTitle(artifact)).toBe('Scene 1 · Storyboard');
25 expect(friendlyFieldLabel('motion_desc')).toBe('Motion');
26 expect(friendlyFieldLabel('custom_camera_note')).toBe('Custom Camera Note');
27 });
28
29 it('presents indexes and booleans for non-technical readers', () => {
30 expect(formatStructuredValue(0, 'cam_idx')).toBe('1');
31 expect(formatStructuredValue([0, 2], 'ff_vis_char_idxs')).toBe('1, 3');
32 expect(formatStructuredValue(false, 'is_last')).toBe('No');
33 });
34
35 it('identifies filesystem metadata that should stay hidden in artifacts', () => {
36 expect(isArtifactPathField('path')).toBe(true);
37 expect(isArtifactPathField('image_path')).toBe(true);
38 expect(isArtifactPathField('working_dir')).toBe(true);
39 expect(isArtifactPathField('reference_image_path_and_text_pairs')).toBe(true);
40 expect(isArtifactPathField('visual_desc')).toBe(false);
41 });
42
43 it('keeps internal render status out of the artifact document list', () => {
44 expect(isJsonArtifact(makeArtifact('render_status.json'))).toBe(false);
45 expect(isJsonArtifact(makeArtifact('idea2video/script.json'))).toBe(true);
46 });
47
48 it('marks a complete storyboard package ready to render', () => {
49 const readiness = deriveStoryboardReadiness([
50 artifact,
51 makeArtifact('idea2video/scene_0/camera_tree.json'),
52 makeArtifact('idea2video/scene_0/shots/0/shot_description.json'),
53 makeArtifact('idea2video/scene_0/shots/1/shot_description.json'),
54 makeArtifact('idea2video/scene_0/shots/2/shot_description.json'),
55 ], 3);
56 expect(readiness.readyToRender).toBe(true);
57 expect(readiness.overall).toBe('ready');
58 });
59
60 it('keeps incomplete shot files out of render-ready state', () => {
61 const readiness = deriveStoryboardReadiness([
62 artifact,
63 makeArtifact('idea2video/scene_0/shots/0/shot_description.json'),
64 ], 3);
65 expect(readiness.readyToRender).toBe(false);
66 expect(readiness.overall).toBe('partial');
67 expect(readiness.shotDescriptions.status).toBe('partial');
68 expect(readiness.cameraPlans.status).toBe('missing');
69 });
70
71 it('marks an empty project as missing', () => {
72 expect(deriveStoryboardReadiness([], 0).overall).toBe('missing');
73 });
74
75 it('keeps render lights inactive before rendering starts', () => {
76 const readiness = deriveStoryboardReadiness([
77 artifact,
78 makeArtifact('idea2video/scene_0/camera_tree.json'),
79 makeArtifact('idea2video/scene_0/shots/0/shot_description.json'),
80 ], 1);
81 expect(readiness.readyToRender).toBe(true);
82 expect(readiness.render.started).toBe(false);
83 expect(readiness.render.frames.status).toBe('inactive');
84 expect(readiness.render.clips.status).toBe('inactive');
85 expect(readiness.render.finalVideo.status).toBe('inactive');
86 });
87
88 it('tracks partial and completed render output separately', () => {
89 const readiness = deriveStoryboardReadiness([
90 artifact,
91 makeArtifact('idea2video/scene_0/shots/0/first_frame.png'),
92 makeArtifact('idea2video/scene_0/shots/0/video.mp4'),
93 makeArtifact('idea2video/scene_0/shots/1/video.mp4'),
94 makeArtifact('idea2video/final_video.mp4'),
95 ], 2);
96 expect(readiness.render.started).toBe(true);
97 expect(readiness.render.frames.status).toBe('partial');
98 expect(readiness.render.clips.status).toBe('ready');
99 expect(readiness.render.finalVideo.status).toBe('ready');
100 });
101
102 it('maps live render stages to the light that should animate', () => {
103 expect(activeRenderCheckpoint('frame_start')).toBe('frames');
104 expect(activeRenderCheckpoint('video_clip_waiting_for_frames')).toBe('clips');
105 expect(activeRenderCheckpoint('concat_start')).toBe('finalVideo');
106 });
107
108 it('pairs shot documents with visual output from the same shot', () => {
109 const shotDocument = makeArtifact('idea2video/scene_0/shots/1/shot_description.json');
110 const visuals = relatedVisualArtifacts(shotDocument, [
111 makeMediaArtifact('idea2video/scene_0/shots/0/first_frame.png', 'image'),
112 makeMediaArtifact('idea2video/scene_0/shots/1/first_frame.png', 'image'),
113 makeMediaArtifact('idea2video/scene_0/shots/1/video.mp4', 'video'),
114 makeMediaArtifact('idea2video/scene_1/shots/1/first_frame.png', 'image'),
115 ]);
116 expect(visuals.map((item) => item.path)).toEqual([
117 'idea2video/scene_0/shots/1/first_frame.png',
118 'idea2video/scene_0/shots/1/video.mp4',
119 ]);
120 });
121
122 it('pairs scene planning documents with every visual in that scene', () => {
123 const visuals = relatedVisualArtifacts(artifact, [
124 makeMediaArtifact('idea2video/scene_0/shots/0/first_frame.png', 'image'),
125 makeMediaArtifact('idea2video/scene_0/shots/1/video.mp4', 'video'),
126 makeMediaArtifact('idea2video/scene_1/shots/0/first_frame.png', 'image'),
127 ]);
128 expect(visuals).toHaveLength(2);
129 });
130
131 it('pairs character documents with generated portraits', () => {
132 const visuals = relatedVisualArtifacts(makeArtifact('idea2video/characters.json'), [
133 makeMediaArtifact('idea2video/character_portraits/alex/front.png', 'image'),
134 makeMediaArtifact('idea2video/scene_0/shots/0/first_frame.png', 'image'),
135 ]);
136 expect(visuals.map((item) => item.path)).toEqual(['idea2video/character_portraits/alex/front.png']);
137 });
138 });
139
140 function makeArtifact(path: string): Artifact {
141 const name = path.split('/').at(-1) || path;
142 return {...artifact, path, name};
143 }
144
145 function makeMediaArtifact(path: string, kind: 'image' | 'video'): Artifact {
146 return {...makeArtifact(path), kind};
147 }
148
148 lines TYPESCRIPT