返回 ViMax
test_vimax_adapters.py
根目录 / tests / test_vimax_adapters.py
1 import asyncio
2 import contextlib
3 import io
4 import json
5 import tempfile
6 import unittest
7 from pathlib import Path
8 from types import SimpleNamespace
9 from unittest.mock import patch
10
11 from interfaces import Camera, CharacterInScene, ShotBriefDescription, ShotDescription
12 from agent_runtime.session_index import SessionIndex
13 from agent_runtime.vimax_adapters import ViMaxAdapters
14 from agent_runtime.tools import ToolRuntimeContext
15 from pipelines.idea2video_pipeline import Idea2VideoPipeline
16 from pipelines.script2video_pipeline import Script2VideoPipeline
17
18
19 class FakeIdeaPipeline:
20 def __init__(self, chat_model, image_generator, video_generator, working_dir, num_image_candidates=2):
21 self.num_image_candidates = num_image_candidates
22 self.working_dir = Path(working_dir)
23 self.working_dir.mkdir(parents=True, exist_ok=True)
24
25 async def develop_story(self, idea, user_requirement, quiet=False):
26 path = self.working_dir / "story.txt"
27 path.write_text("story", encoding="utf-8")
28 return "story"
29
30 async def extract_characters(self, story, quiet=False):
31 chars = [CharacterInScene(idx=0, identifier_in_scene="Cat", is_visible=True, static_features="black cat", dynamic_features="helmet")]
32 (self.working_dir / "characters.json").write_text(json.dumps([c.model_dump() for c in chars]), encoding="utf-8")
33 return chars
34
35 async def write_script_based_on_story(self, story, user_requirement, quiet=False):
36 script = [{"scene": "cat jumps"}]
37 (self.working_dir / "script.json").write_text(json.dumps(script), encoding="utf-8")
38 return script
39
40
41
42
43 class HangingIdeaPipeline(FakeIdeaPipeline):
44 async def develop_story(self, idea, user_requirement, quiet=False):
45 await asyncio.sleep(10)
46 return "story"
47
48
49
50 class FakeRevisionModel:
51 async def ainvoke(self, prompt):
52 return SimpleNamespace(content='[{"idx": 0, "description": "more oppressive"}]')
53
54
55 class FailRenderIdeaPipeline(FakeIdeaPipeline):
56 async def __call__(self, idea, user_requirement, style, quiet=False, progress=None):
57 raise RuntimeError("render failed")
58
59
60 class FailRender403IdeaPipeline(FakeIdeaPipeline):
61 async def __call__(self, idea, user_requirement, style, quiet=False, progress=None):
62 raise RuntimeError("OpenRouter video create failed with HTTP 403: {'error': {'message': 'Key limit exceeded (total limit). Manage it using token sk-short', 'code': 403}}")
63
64
65 class NoisyRenderIdeaPipeline(FakeIdeaPipeline):
66 async def __call__(self, idea, user_requirement, style, quiet=False, progress=None):
67 print("NOISE_FROM_RENDER_PIPELINE")
68 final = self.working_dir / "final_video.mp4"
69 final.write_text("video", encoding="utf-8")
70 return str(final)
71
72
73 class FakeScriptPipeline:
74 def __init__(self, chat_model, image_generator, video_generator, working_dir, num_image_candidates=2):
75 self.num_image_candidates = num_image_candidates
76 self.working_dir = Path(working_dir)
77 self.working_dir.mkdir(parents=True, exist_ok=True)
78
79 async def plan_text_artifacts(self, script, user_requirement, style, characters=None, progress=None, quiet=False):
80 if progress:
81 progress("design_storyboard", "Designing storyboard", {})
82 progress("decompose_shots", "Decomposing shot visual descriptions", {"shot_count": 1})
83 progress("construct_camera_tree", "Constructing camera tree", {"shot_count": 1})
84 (self.working_dir / "storyboard.json").write_text("[]", encoding="utf-8")
85 (self.working_dir / "camera_tree.json").write_text("[]", encoding="utf-8")
86 shot_dir = self.working_dir / "shots" / "0"
87 shot_dir.mkdir(parents=True, exist_ok=True)
88 (shot_dir / "shot_description.json").write_text("{}", encoding="utf-8")
89 if characters:
90 (self.working_dir / "characters.json").write_text(json.dumps([c.model_dump() for c in characters]), encoding="utf-8")
91 return {}
92
93
94
95
96 class FailingScriptPipeline(FakeScriptPipeline):
97 async def plan_text_artifacts(self, script, user_requirement, style, characters=None, progress=None, quiet=False):
98 if progress:
99 progress("design_storyboard", "Designing storyboard", {})
100 raise RuntimeError("storyboard failed")
101
102
103 class FakeInitChatModel:
104 def __init__(self):
105 self.calls = []
106
107 def __call__(self, **kwargs):
108 self.calls.append(kwargs)
109 return object()
110
111
112 class Script2VideoPlanningProgressTests(unittest.IsolatedAsyncioTestCase):
113 async def test_plan_text_artifacts_emits_progress_in_order(self):
114 with tempfile.TemporaryDirectory() as tmp:
115 pipeline = Script2VideoPipeline(chat_model=object(), image_generator=object(), video_generator=object(), working_dir=tmp)
116 chars = [CharacterInScene(idx=0, identifier_in_scene="Cat", is_visible=True, static_features="black cat", dynamic_features="helmet")]
117 storyboard = [ShotBriefDescription(idx=0, is_last=True, cam_idx=0, visual_desc="cat jumps", audio_desc="wind")]
118 shot = ShotDescription(idx=0, is_last=True, cam_idx=0, visual_desc="cat jumps", variation_type="small", variation_reason="simple motion", ff_desc="cat starts", ff_vis_char_idxs=[0], lf_desc="cat lands", lf_vis_char_idxs=[0], motion_desc="cat jumps", audio_desc="wind")
119 camera = [Camera(idx=0, active_shot_idxs=[0])]
120
121 async def design_storyboard(script, characters, user_requirement, quiet=False):
122 return storyboard
123
124 async def decompose_visual_descriptions(shot_brief_descriptions, characters, quiet=False):
125 return [shot]
126
127 async def construct_camera_tree(shot_descriptions, quiet=False):
128 return camera
129
130 pipeline.design_storyboard = design_storyboard
131 pipeline.decompose_visual_descriptions = decompose_visual_descriptions
132 pipeline.construct_camera_tree = construct_camera_tree
133 events = []
134 await pipeline.plan_text_artifacts("script", "req", "style", characters=chars, progress=lambda stage, message, metadata=None: events.append(stage))
135 self.assertEqual(events, ["extract_characters", "design_storyboard", "decompose_shots", "construct_camera_tree"])
136
137
138 async def test_idea_pipeline_quiet_suppresses_text_planning_prints(self):
139 with tempfile.TemporaryDirectory() as tmp:
140 pipeline = Idea2VideoPipeline(chat_model=object(), image_generator=object(), video_generator=object(), working_dir=tmp)
141
142 async def develop_story(idea, user_requirement):
143 return "story"
144
145 pipeline.screenwriter = SimpleNamespace(develop_story=develop_story)
146 stdout = io.StringIO()
147 with contextlib.redirect_stdout(stdout):
148 result = await pipeline.develop_story("idea", "req", quiet=True)
149 self.assertEqual(result, "story")
150 self.assertEqual(stdout.getvalue(), "")
151
152
153 class ViMaxAdapterTests(unittest.IsolatedAsyncioTestCase):
154 def test_build_chat_model_uses_bounded_init_chat_model_kwargs(self):
155 fake = FakeInitChatModel()
156 with patch.dict("os.environ", {
157 "VIMAX_LLM_API_KEY": "test-key",
158 "VIMAX_LLM_MODEL": "test-model",
159 "VIMAX_LLM_BASE_URL": "https://example.invalid/v1",
160 "VIMAX_LLM_REQUEST_TIMEOUT_SECONDS": "12",
161 "VIMAX_NARRATIVE_MAX_TOKENS": "1234",
162 }), patch("agent_runtime.vimax_adapters.init_chat_model", fake):
163 from agent_runtime.vimax_adapters import _build_chat_model
164
165 _build_chat_model()
166
167 self.assertEqual(fake.calls[0]["model"], "test-model")
168 self.assertEqual(fake.calls[0]["base_url"], "https://example.invalid/v1")
169 self.assertEqual(fake.calls[0]["timeout"], 12.0)
170 self.assertEqual(fake.calls[0]["max_retries"], 0)
171 self.assertEqual(fake.calls[0]["max_completion_tokens"], 1234)
172
173
174 async def test_narrative_planning_uses_text_only_pipeline(self):
175 with tempfile.TemporaryDirectory() as tmp:
176 index = SessionIndex(tmp)
177 adapter = ViMaxAdapters(Path(tmp), index)
178 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
179 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), \
180 patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
181 result = await adapter.vimax_narrative_planning({"idea": "moon cat", "user_requirement": "short", "style": "anime"})
182 self.assertTrue(result.ok)
183 payload = json.loads(result.content)
184 self.assertTrue(payload["ready_for_render"])
185 root = Path(tmp) / payload["working_dir"]
186 self.assertTrue((root / "idea2video" / "scene_0" / "storyboard.json").exists())
187 self.assertTrue((root / "idea2video" / "scene_0" / "camera_tree.json").exists())
188 self.assertTrue((root / "idea2video" / "scene_0" / "shots" / "0" / "shot_description.json").exists())
189 self.assertFalse((root / "script2video" / "storyboard.json").exists())
190 self.assertFalse((root / "script2video" / "final_video.mp4").exists())
191
192
193 async def test_script_mode_persists_source_script_for_render(self):
194 with tempfile.TemporaryDirectory() as tmp:
195 index = SessionIndex(tmp)
196 adapter = ViMaxAdapters(Path(tmp), index)
197 script = "A red ball rolls across a white table."
198 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
199 patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
200 result = await adapter.vimax_narrative_planning({"script": script, "user_requirement": "one shot"})
201 self.assertTrue(result.ok)
202 payload = json.loads(result.content)
203 root = Path(tmp) / payload["working_dir"]
204 self.assertEqual((root / "script2video" / "script.txt").read_text(encoding="utf-8"), script)
205 self.assertEqual(index.artifact_checklist(payload["session_id"])["script2video/script.txt"], True)
206 from agent_runtime.vimax_adapters import _load_script_text
207 self.assertEqual(_load_script_text(root), script)
208
209
210 async def test_narrative_planning_forwards_pipeline_progress(self):
211 with tempfile.TemporaryDirectory() as tmp:
212 index = SessionIndex(tmp)
213 adapter = ViMaxAdapters(Path(tmp), index)
214 events = []
215 runtime = ToolRuntimeContext("vimax_narrative_planning", "vimax_narrative_planning", turn_id="turn-test", progress_callback=events.append)
216 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
217 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), \
218 patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
219 result = await adapter.vimax_narrative_planning({"idea": "moon cat"}, runtime)
220 self.assertTrue(result.ok)
221 stages = [event["progress"]["stage"] for event in events if event.get("type") == "tool_progress"]
222 self.assertIn("initializing_llm", stages)
223 self.assertIn("develop_story", stages)
224 self.assertIn("design_storyboard", stages)
225 self.assertIn("decompose_shots", stages)
226 self.assertIn("construct_camera_tree", stages)
227
228
229 async def test_plan_scene_failure_marks_session_error(self):
230 with tempfile.TemporaryDirectory() as tmp:
231 index = SessionIndex(tmp)
232 adapter = ViMaxAdapters(Path(tmp), index)
233 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
234 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), \
235 patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FailingScriptPipeline):
236 result = await adapter.vimax_narrative_planning({"idea": "moon cat"})
237 self.assertFalse(result.ok)
238 self.assertEqual(result.metadata["error_type"], "recoverable_planning_step_failed")
239 self.assertTrue(result.metadata["retryable"])
240 session = index.active()
241 self.assertEqual(session["stage"], "error")
242 self.assertIn("storyboard failed", session["summary"])
243
244
245 async def test_narrative_planning_timeout_marks_session_error(self):
246 with tempfile.TemporaryDirectory() as tmp:
247 index = SessionIndex(tmp)
248 adapter = ViMaxAdapters(Path(tmp), index)
249 with patch.dict("os.environ", {"VIMAX_NARRATIVE_STEP_TIMEOUT_SECONDS": "0.01"}), \
250 patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
251 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", HangingIdeaPipeline):
252 result = await adapter.vimax_narrative_planning({"idea": "moon cat"})
253 self.assertFalse(result.ok)
254 self.assertEqual(result.metadata["error_type"], "recoverable_planning_step_failed")
255 session = index.active()
256 self.assertIsNotNone(session)
257 self.assertEqual(session["stage"], "error")
258 self.assertIn("timed out", session["summary"])
259
260
261
262 async def test_active_session_without_new_input_continues_existing_idea(self):
263 with tempfile.TemporaryDirectory() as tmp:
264 index = SessionIndex(tmp)
265 record = index.create(idea="moon cat", user_requirement="short", style="anime")
266 adapter = ViMaxAdapters(Path(tmp), index)
267 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
268 result = await adapter.vimax_narrative_planning({})
269 self.assertTrue(result.ok)
270 payload = json.loads(result.content)
271 self.assertEqual(payload["session_id"], record["session_id"])
272 self.assertEqual(index.active()["session_id"], record["session_id"])
273
274
275 async def test_active_session_continuation_preserves_existing_style(self):
276 with tempfile.TemporaryDirectory() as tmp:
277 index = SessionIndex(tmp)
278 record = index.create(idea="moon cat", user_requirement="short", style="anime")
279 adapter = ViMaxAdapters(Path(tmp), index)
280 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
281 result = await adapter.vimax_narrative_planning({"session_id": record["session_id"]})
282 self.assertTrue(result.ok)
283 self.assertEqual(index.get(record["session_id"])["style"], "anime")
284
285 async def test_new_idea_creates_new_session_instead_of_reusing_active(self):
286 with tempfile.TemporaryDirectory() as tmp:
287 index = SessionIndex(tmp)
288 adapter = ViMaxAdapters(Path(tmp), index)
289 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
290 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), \
291 patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
292 first = await adapter.vimax_narrative_planning({"idea": "moon cat"})
293 second = await adapter.vimax_narrative_planning({"idea": "ocean robot"})
294 self.assertNotEqual(json.loads(first.content)["session_id"], json.loads(second.content)["session_id"])
295
296 async def test_new_idea_initializes_named_empty_active_session(self):
297 with tempfile.TemporaryDirectory() as tmp:
298 index = SessionIndex(tmp)
299 empty = index.create(project_name="00")
300 adapter = ViMaxAdapters(Path(tmp), index)
301 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
302 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), \
303 patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
304 result = await adapter.vimax_narrative_planning({"idea": "moon cat"})
305 self.assertTrue(result.ok)
306 payload = json.loads(result.content)
307 self.assertEqual(payload["session_id"], empty["session_id"])
308 self.assertEqual(index.active()["project_name"], "00")
309 self.assertEqual(index.active()["idea"], "moon cat")
310 self.assertEqual(len(index.load()["sessions"]), 1)
311
312
313 async def test_explicit_session_with_different_idea_creates_new_session(self):
314 with tempfile.TemporaryDirectory() as tmp:
315 index = SessionIndex(tmp)
316 old = index.create(idea="old cat")
317 adapter = ViMaxAdapters(Path(tmp), index)
318 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
319 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FakeIdeaPipeline), \
320 patch("agent_runtime.vimax_adapters.Script2VideoPipeline", FakeScriptPipeline):
321 result = await adapter.vimax_narrative_planning({"session_id": old["session_id"], "idea": "new robot"})
322 self.assertTrue(result.ok)
323 payload = json.loads(result.content)
324 self.assertNotEqual(payload["session_id"], old["session_id"])
325 self.assertEqual(index.get(payload["session_id"])["idea"], "new robot")
326
327 async def test_revision_mode_rewrites_existing_artifact_and_logs(self):
328 with tempfile.TemporaryDirectory() as tmp:
329 index = SessionIndex(tmp)
330 record = index.create(idea="x")
331 target = Path(tmp) / record["working_dir"] / "idea2video" / "scene_0" / "storyboard.json"
332 target.parent.mkdir(parents=True, exist_ok=True)
333 target.write_text('[{"idx": 0, "description": "calm"}]', encoding="utf-8")
334 adapter = ViMaxAdapters(Path(tmp), index)
335 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=FakeRevisionModel()):
336 result = await adapter.vimax_narrative_planning({"revision_target": "idea2video/scene_0/storyboard.json", "revision_instruction": "make it oppressive"})
337 self.assertTrue(result.ok)
338 self.assertIn("more oppressive", target.read_text(encoding="utf-8"))
339 self.assertTrue((Path(tmp) / ".vimax" / "logs" / "revisions.jsonl").exists())
340 self.assertTrue(index.get(record["session_id"])["stale"]["final_video"])
341
342
343 async def test_revision_missing_instruction_marks_error(self):
344 with tempfile.TemporaryDirectory() as tmp:
345 index = SessionIndex(tmp)
346 record = index.create(idea="x")
347 target = Path(tmp) / record["working_dir"] / "idea2video" / "scene_0" / "storyboard.json"
348 target.parent.mkdir(parents=True, exist_ok=True)
349 target.write_text('[]', encoding="utf-8")
350 adapter = ViMaxAdapters(Path(tmp), index)
351 result = await adapter.vimax_narrative_planning({"revision_target": "idea2video/scene_0/storyboard.json"})
352 self.assertFalse(result.ok)
353 self.assertEqual(result.metadata["error_type"], "missing_revision_instruction")
354 self.assertEqual(index.get(record["session_id"])["stage"], "error")
355
356
357 async def test_revision_missing_target_marks_error(self):
358 with tempfile.TemporaryDirectory() as tmp:
359 index = SessionIndex(tmp)
360 record = index.create(idea="x")
361 adapter = ViMaxAdapters(Path(tmp), index)
362 result = await adapter.vimax_narrative_planning({"revision_target": "idea2video/scene_0/missing.json", "revision_instruction": "change it"})
363 self.assertFalse(result.ok)
364 self.assertEqual(result.metadata["error_type"], "dependency_missing")
365 self.assertEqual(index.get(record["session_id"])["stage"], "error")
366
367 async def test_render_setup_failure_marks_session_error(self):
368 with tempfile.TemporaryDirectory() as tmp:
369 index = SessionIndex(tmp)
370 record = index.create(idea="x")
371 root = Path(tmp) / record["working_dir"] / "idea2video"
372 (root / "scene_0" / "shots" / "0").mkdir(parents=True, exist_ok=True)
373 (root / "story.txt").write_text("story", encoding="utf-8")
374 (root / "characters.json").write_text("[]", encoding="utf-8")
375 (root / "script.json").write_text("[]", encoding="utf-8")
376 (root / "scene_0" / "storyboard.json").write_text("[]", encoding="utf-8")
377 (root / "scene_0" / "camera_tree.json").write_text("[]", encoding="utf-8")
378 (root / "scene_0" / "shots" / "0" / "shot_description.json").write_text("{}", encoding="utf-8")
379 adapter = ViMaxAdapters(Path(tmp), index)
380 with patch("agent_runtime.vimax_adapters._build_chat_model", side_effect=RuntimeError("missing key")):
381 result = await adapter.vimax_render_video({})
382 self.assertFalse(result.ok)
383 self.assertEqual(result.metadata["error_type"], "render_failed")
384 self.assertIn("missing key", result.content)
385 self.assertEqual(index.get(record["session_id"])["stage"], "error")
386
387 async def test_render_failure_marks_session_error(self):
388 with tempfile.TemporaryDirectory() as tmp:
389 index = SessionIndex(tmp)
390 record = index.create(idea="x")
391 root = Path(tmp) / record["working_dir"] / "idea2video"
392 (root / "scene_0" / "shots" / "0").mkdir(parents=True, exist_ok=True)
393 (root / "story.txt").write_text("story", encoding="utf-8")
394 (root / "characters.json").write_text("[]", encoding="utf-8")
395 (root / "script.json").write_text("[]", encoding="utf-8")
396 (root / "scene_0" / "storyboard.json").write_text("[]", encoding="utf-8")
397 (root / "scene_0" / "camera_tree.json").write_text("[]", encoding="utf-8")
398 (root / "scene_0" / "shots" / "0" / "shot_description.json").write_text("{}", encoding="utf-8")
399 adapter = ViMaxAdapters(Path(tmp), index)
400 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
401 patch("agent_runtime.vimax_adapters._build_image_generator", return_value=object()), \
402 patch("agent_runtime.vimax_adapters._build_video_generator", return_value=object()), \
403 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FailRenderIdeaPipeline):
404 result = await adapter.vimax_render_video({})
405 self.assertFalse(result.ok)
406 self.assertEqual(result.metadata["error_type"], "render_failed")
407 self.assertIn("render failed", result.content)
408 self.assertEqual(index.get(record["session_id"])["stage"], "error")
409 status_path = Path(tmp) / record["working_dir"] / "render_status.json"
410 events_path = Path(tmp) / record["working_dir"] / "render_events.jsonl"
411 self.assertTrue(status_path.exists())
412 self.assertTrue(events_path.exists())
413 status = json.loads(status_path.read_text(encoding="utf-8"))
414 self.assertEqual(status["status"], "error")
415 self.assertEqual(status["error_type"], "render_failed")
416
417 async def test_render_403_key_limit_is_non_retryable_and_sanitized(self):
418 with tempfile.TemporaryDirectory() as tmp:
419 index = SessionIndex(tmp)
420 record = index.create(idea="x")
421 root = Path(tmp) / record["working_dir"] / "idea2video"
422 (root / "scene_0" / "shots" / "0").mkdir(parents=True, exist_ok=True)
423 (root / "story.txt").write_text("story", encoding="utf-8")
424 (root / "characters.json").write_text("[]", encoding="utf-8")
425 (root / "script.json").write_text("[]", encoding="utf-8")
426 (root / "scene_0" / "storyboard.json").write_text("[]", encoding="utf-8")
427 (root / "scene_0" / "camera_tree.json").write_text("[]", encoding="utf-8")
428 (root / "scene_0" / "shots" / "0" / "shot_description.json").write_text("{}", encoding="utf-8")
429 adapter = ViMaxAdapters(Path(tmp), index)
430 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), \
431 patch("agent_runtime.vimax_adapters._build_image_generator", return_value=object()), \
432 patch("agent_runtime.vimax_adapters._build_video_generator", return_value=object()), \
433 patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", FailRender403IdeaPipeline):
434 result = await adapter.vimax_render_video({})
435 self.assertFalse(result.ok)
436 self.assertFalse(result.metadata["retryable"])
437 self.assertIn("<redacted>", result.metadata["error"])
438 self.assertNotIn("sk-short", result.metadata["error"])
439 status = json.loads((Path(tmp) / record["working_dir"] / "render_status.json").read_text(encoding="utf-8"))
440 self.assertFalse(status["retryable"])
441 self.assertNotIn("sk-short", status["error"])
442
443
444 async def test_render_pipeline_stdout_is_suppressed(self):
445 with tempfile.TemporaryDirectory() as tmp:
446 index = SessionIndex(tmp)
447 record = index.create(idea="x", style="anime")
448 root = Path(tmp) / record["working_dir"] / "idea2video"
449 (root / "scene_0" / "shots" / "0").mkdir(parents=True, exist_ok=True)
450 (root / "story.txt").write_text("story", encoding="utf-8")
451 (root / "characters.json").write_text("[]", encoding="utf-8")
452 (root / "script.json").write_text("[]", encoding="utf-8")
453 (root / "scene_0" / "storyboard.json").write_text("[]", encoding="utf-8")
454 (root / "scene_0" / "camera_tree.json").write_text("[]", encoding="utf-8")
455 (root / "scene_0" / "shots" / "0" / "shot_description.json").write_text("{}", encoding="utf-8")
456 adapter = ViMaxAdapters(Path(tmp), index)
457 stdout = io.StringIO()
458 with patch("agent_runtime.vimax_adapters._build_chat_model", return_value=object()), patch("agent_runtime.vimax_adapters._build_image_generator", return_value=object()), patch("agent_runtime.vimax_adapters._build_video_generator", return_value=object()), patch("agent_runtime.vimax_adapters.Idea2VideoPipeline", NoisyRenderIdeaPipeline), contextlib.redirect_stdout(stdout):
459 result = await adapter.vimax_render_video({})
460 self.assertTrue(result.ok)
461 self.assertNotIn("NOISE_FROM_RENDER_PIPELINE", stdout.getvalue())
462
463 async def test_render_dependency_missing(self):
464 with tempfile.TemporaryDirectory() as tmp:
465 index = SessionIndex(tmp)
466 index.create(idea="x")
467 adapter = ViMaxAdapters(Path(tmp), index)
468 result = await adapter.vimax_render_video({})
469 self.assertFalse(result.ok)
470 self.assertEqual(result.metadata["error_type"], "dependency_missing")
471
471 lines PYTHON