"""Offline regression tests for the current O1Key Grok Video API contract.""" import sys import unittest from inspect import signature from pathlib import Path from unittest.mock import AsyncMock, patch PLUGIN_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PLUGIN_ROOT.parent)) from comfyui_o1key.clients.grok_video_client import GrokVideoClient from comfyui_o1key.nodes import grok_video class GrokVideoPayloadTests(unittest.TestCase): def test_poll_deadline_is_shared_2000_seconds(self): self.assertEqual(GrokVideoClient.POLL_DEADLINE_SECONDS, 2000) self.assertEqual( signature(GrokVideoClient.run_video_sync) .parameters["timeout"].default, 2000, ) def build(self, **overrides): values = { "operation": "generate", "prompt": "cinematic shot", "model": "grok-imagine-video-1.5", "duration": 8, "aspect_ratio": "16:9", "resolution": "480p", } values.update(overrides) return GrokVideoClient.build_video_body(**values) def test_text_generation_supports_15_seconds_and_1080p_on_15(self): body = self.build(duration=15, resolution="1080p") self.assertEqual(body, { "model": "grok-imagine-video-1.5", "prompt": "cinematic shot", "duration": 15, "aspect_ratio": "16:9", "resolution": "1080p", }) def test_image_generation_prompt_is_optional_and_preserves_image_url(self): body = self.build( prompt="", resolution="1080p", image={"image_url": "data:image/png;base64,AAAA"}, ) self.assertNotIn("prompt", body) self.assertEqual(body["image"], {"image_url": "data:image/png;base64,AAAA"}) def test_multireference_generation_supports_15_seconds_on_both_models(self): for model in GrokVideoClient.MODEL_OPTIONS: with self.subTest(model=model): body = self.build( model=model, duration=15, resolution="720p", reference_images=[{"url": "https://example.invalid/character.png"}], reference_audios=[{"voice_id": "nova"}], ) self.assertEqual(body["duration"], 15) self.assertEqual(body["reference_audios"], [{"voice_id": "nova"}]) def test_multireference_generation_rejects_1080p(self): with self.assertRaisesRegex(ValueError, "不支持 1080p"): self.build( resolution="1080p", reference_images=[{"url": "https://example.invalid/reference.png"}], ) def test_base_model_rejects_1080p(self): with self.assertRaisesRegex(ValueError, "仅支持 grok-imagine-video-1.5"): self.build(model="grok-imagine-video", resolution="1080p") def test_image_and_reference_images_are_mutually_exclusive(self): with self.assertRaisesRegex(ValueError, "image 和 reference_images"): self.build( image={"url": "https://example.invalid/input.png"}, reference_images=[{"url": "https://example.invalid/reference.png"}], ) def test_reference_audio_accepts_only_url_or_voice_id_and_caps_at_three(self): body = self.build(reference_audios=[ {"url": "https://example.invalid/one.wav"}, {"voice_id": "nova"}, {"voice_id": "alloy"}, ]) self.assertEqual(len(body["reference_audios"]), 3) with self.assertRaisesRegex(ValueError, "最多支持 3"): self.build(reference_audios=[{"voice_id": str(index)} for index in range(4)]) with self.assertRaisesRegex(ValueError, "url、voice_id"): self.build(reference_audios=[{"file_id": "not-supported"}]) def test_edit_and_extension_payloads_use_their_exact_parameter_sets(self): for model in GrokVideoClient.MODEL_OPTIONS: with self.subTest(operation="edit", model=model): edit = self.build( operation="edit", model=model, video={"file_id": "file_grok_1"}, duration=9, resolution="1080p", ) self.assertEqual(edit, { "model": model, "prompt": "cinematic shot", "video": {"file_id": "file_grok_1"}, }) with self.subTest(operation="extend", model=model): extension = self.build( operation="extend", model=model, video={"url": "data:video/mp4;base64,AAAA"}, duration=5, ) self.assertEqual(extension, { "model": model, "prompt": "cinematic shot", "video": {"url": "data:video/mp4;base64,AAAA"}, "duration": 5, }) def test_extension_duration_is_limited_to_two_through_ten_seconds(self): for duration in (1, 11): with self.subTest(duration=duration): with self.assertRaisesRegex(ValueError, "2 到 10 秒"): self.build( operation="extend", video={"url": "https://example.invalid/input.mp4"}, duration=duration, ) def test_error_sanitizer_removes_temporary_urls_and_base64(self): message = GrokVideoClient._safe_error_message( "failed https://cdn.example.invalid/result.mp4?signature=secret " "data:video/mp4;base64,QUJDREVGRw==" ) self.assertNotIn("signature=secret", message) self.assertNotIn("QUJDREVGRw", message) self.assertIn("", message) self.assertIn("", message) class GrokVideoNodeTests(unittest.TestCase): def test_generation_schema_exposes_current_models_defaults_and_three_audio_inputs(self): required = grok_video.O1keyGrokVideo.INPUT_TYPES()["required"] optional = grok_video.O1keyGrokVideo.INPUT_TYPES()["optional"] self.assertEqual(required["模型"][1]["default"], "grok-imagine-video-1.5") self.assertEqual(required["分辨率"][1]["default"], "480p") self.assertIn("参考音色ID(逗号分隔)", required) self.assertEqual( [name for name in optional if name.startswith("音频素材")], ["音频素材", "音频素材2", "音频素材3"], ) def test_edit_schema_appends_model_selector(self): required = grok_video.O1keyGrokVideoEdit.INPUT_TYPES()["required"] self.assertEqual( list(required), ["操作", "提示词", "续写时长(秒)", "模型"], ) self.assertEqual(required["模型"][1]["default"], "grok-imagine-video-1.5") def test_edit_rejects_video_over_87_seconds_before_upload(self): video = type("Video", (), {"get_duration": lambda self: 8.71})() with ( patch.object(grok_video, "VideoFromFile", object), patch.object(grok_video, "upload_video", new=AsyncMock()) as upload, ): with self.assertRaisesRegex(ValueError, "不能超过 8.7 秒"): grok_video.O1keyGrokVideoEdit().generate(**{ "操作": "编辑视频", "提示词": "make the sky golden", "续写时长(秒)": 6, "模型": "grok-imagine-video-1.5", "视频素材": video, }) upload.assert_not_awaited() def test_voice_ids_split_on_chinese_comma_comma_and_newline(self): self.assertEqual( grok_video._parse_voice_ids("nova,alloy\necho"), ["nova", "alloy", "echo"], ) if __name__ == "__main__": unittest.main()