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