Files
df18925e8d Repair OpenRouter image, video and audio integration
Preserve existing workflows while adding capability-based catalogs, dedicated
image requests, resumable native video, validated audio and explicit chat tiers.
Cover compatibility, billing-sensitive retries, credentials and cancellation.

Co-authored-by: Arthur Reboul Salze <22814805+ArthurReboulSalze@users.noreply.github.com>
Co-authored-by: Dookie <me@djdookie.de>
2026-10-01 22:22:39 -05:00

365 lines
17 KiB
Python

"""No-spend regression tests for the video job lifecycle and request contract."""
import importlib.util
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
SPEC = importlib.util.spec_from_file_location(
"openrouter_video_test", Path(__file__).resolve().parents[1] / "openrouter_video.py"
)
video = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(video)
class FakeClock:
def __init__(self):
self.now = 0.0
def __call__(self):
return self.now
def sleep(self, seconds):
self.now += seconds
class Response:
def __init__(self, data=None, status=200, headers=None, chunks=None):
self.data = data
self.status_code = status
self.headers = headers or {}
self.chunks = [b"test-video"] if chunks is None else chunks
self.closed = False
def json(self):
if isinstance(self.data, Exception):
raise self.data
return self.data
def iter_content(self, chunk_size):
yield from self.chunks
def close(self):
self.closed = True
class HTTP:
def __init__(self, *responses):
self.responses = list(responses)
self.calls = []
def request(self, method, url, **kwargs):
self.calls.append((method, url, kwargs))
response = self.responses.pop(0)
if isinstance(response, BaseException):
raise response
if callable(response):
return response()
return response
class ComfyInterrupt(BaseException):
pass
class VideoTests(unittest.TestCase):
def setUp(self):
self.clock = FakeClock()
self.metadata = {
"id": "vendor/video",
"supported_frame_images": ["first_frame", "last_frame"],
"supported_durations": [5, 10],
"supported_resolutions": ["720p", "1080p"],
"supported_aspect_ratios": ["16:9", "9:16"],
"seed": True,
"generate_audio": True,
}
self.model_patch = patch.object(video, "_require_model", return_value=self.metadata)
self.require_model = self.model_patch.start()
self.factory = Mock(side_effect=lambda buffer: ("native-video", buffer.getvalue()))
self.validate = Mock()
self.native_patch = patch.object(video, "_native_video_support", return_value=(self.factory, self.validate))
self.native_patch.start()
self.addCleanup(self.model_patch.stop)
self.addCleanup(self.native_patch.stop)
def run_video(self, http, **kwargs):
args = dict(api_key="private-key", model="vendor/video", prompt="a cloud",
_http=http, _clock=self.clock, _sleep=self.clock.sleep,
_interrupt_check=lambda: None)
args.update(kwargs)
return video.generate_video(**args)
@staticmethod
def submitted(status="pending"):
return Response({"id": "job-123", "polling_url": "/api/v1/videos/job-123", "status": status}, 202)
@staticmethod
def completed():
return Response({"id": "job-123", "status": "completed", "usage": {"cost": 0.1, "is_byok": False}})
def test_submit_relative_poll_and_download_native_video(self):
responses = [self.submitted(), self.completed(), Response(headers={"Content-Type": "video/mp4"})]
http = HTTP(*responses)
callback = Mock()
result = self.run_video(http, duration="5", resolution="720p", aspect_ratio="16:9", on_job=callback)
self.assertEqual(result["job_id"], "job-123")
self.assertEqual(result["cost"], 0.1)
self.assertEqual(result["video"], ("native-video", b"test-video"))
self.assertEqual([call[0] for call in http.calls], ["POST", "GET", "GET"])
self.assertEqual(http.calls[1][1], video.API_BASE + "/job-123")
self.assertEqual(http.calls[2][1], video.API_BASE + "/job-123/content?index=0")
self.assertEqual(http.calls[0][2]["json"]["duration"], 5)
self.assertFalse(http.calls[0][2]["allow_redirects"])
callback.assert_called_once_with("job-123")
self.validate.assert_called_once()
self.assertTrue(all(response.closed for response in responses))
def test_resume_bypasses_new_job_validation_and_never_posts(self):
http = HTTP(self.completed(), Response())
result = self.run_video(http, model="", prompt="", job_id="job-123",
mode="invalid", duration=-1, reference_urls=["bad"])
self.assertEqual(result["job_id"], "job-123")
self.require_model.assert_not_called()
self.assertEqual([c[0] for c in http.calls], ["GET", "GET"])
def test_native_video_dependency_checked_before_submission(self):
http = HTTP()
with patch.object(video, "_native_video_support", side_effect=ImportError("update ComfyUI")):
with self.assertRaisesRegex(video.VideoJobError, "update ComfyUI"):
self.run_video(http)
self.assertEqual(http.calls, [])
def test_auto_values_are_omitted_instead_of_forced_to_minimum(self):
http = HTTP(self.submitted("completed"), Response())
self.run_video(http)
payload = http.calls[0][2]["json"]
for name in ("duration", "resolution", "aspect_ratio"):
self.assertNotIn(name, payload)
def test_image_modes_map_existing_inputs_in_order(self):
images = ["data:image/png;base64,Zmlyc3Q=", "data:image/png;base64,bGFzdA=="]
for mode, count in (("image_to_video", 1), ("start_end_frame_to_video", 2), ("reference_to_video", 2)):
with self.subTest(mode=mode):
http = HTTP(self.submitted("completed"), Response())
self.run_video(http, mode=mode, reference_urls=images[:count])
payload = http.calls[0][2]["json"]
entries = payload["input_references" if mode == "reference_to_video" else "frame_images"]
self.assertEqual([x["image_url"]["url"] for x in entries], images[:count])
if mode != "reference_to_video":
self.assertEqual([x["frame_type"] for x in entries], ["first_frame", "last_frame"][:count])
def test_public_node_mode_aliases(self):
for mode, count in (("first_frame", 1), ("first_last_frame", 2), ("reference_images", 3)):
with self.subTest(mode=mode):
http = HTTP(self.submitted("completed"), Response())
self.run_video(http, mode=mode, reference_urls=["https://example.com/a"] * count)
self.assertEqual(len(http.calls), 2)
def test_preflight_rejects_invalid_inputs_without_spending(self):
cases = [
{"prompt": ""}, {"mode": "unknown"}, {"duration": "7"},
{"duration": 5.1}, {"resolution": "4K"}, {"aspect_ratio": "1:1"},
{"mode": "image_to_video", "reference_urls": []},
{"mode": "start_end_frame_to_video", "reference_urls": ["https://example.com/image.png"]},
{"reference_urls": ["https://example.com/image.png"]},
{"mode": "reference_to_video", "reference_urls": []},
{"mode": "image_to_video", "reference_urls": ["file:///private"]},
{"mode": "image_to_video", "reference_urls": ["data:image/png;base64,bad!"]},
{"mode": "image_to_video", "reference_urls": ["https://user:password@example.com/image"]},
{"mode": "image_to_video", "reference_urls": ["garbage"]},
{"mode": "image_to_video", "reference_urls": ["//example.com/image"]},
{"seed": -1}, {"seed": 1.2}, {"seed": True},
{"generate_audio": "false"},
{"model": "black-forest-labs/flux-video-edit"},
]
for case in cases:
with self.subTest(case=case):
http = HTTP()
with self.assertRaises(video.VideoJobError):
self.run_video(http, **case)
self.assertEqual(http.calls, [])
def test_explicit_unsupported_seed_rejected_but_default_omitted(self):
self.metadata["seed"] = False
http = HTTP()
with self.assertRaisesRegex(video.VideoJobError, "seed support"):
self.run_video(http, seed=1)
self.assertEqual(http.calls, [])
http = HTTP(self.submitted("completed"), Response())
self.run_video(http, seed=0)
self.assertNotIn("seed", http.calls[0][2]["json"])
def test_large_comfy_seed_is_not_rounded_through_float(self):
http = HTTP(self.submitted("completed"), Response())
self.run_video(http, seed=2**64 - 1)
self.assertEqual(http.calls[0][2]["json"]["seed"], 2**64 - 1)
def test_download_checks_deadline_between_partial_socket_reads(self):
download = Response()
def read_once(size, decode_content):
self.clock.now += 0.4
return b"x"
download.raw = Mock(read1=Mock(side_effect=read_once))
http = HTTP(self.completed(), download)
with self.assertRaisesRegex(video.VideoJobError, "Timed out") as caught:
self.run_video(http, job_id="job-123", wait_timeout=1)
self.assertEqual(download.raw.read1.call_count, 3)
self.assertTrue(download.closed)
self.assertEqual(caught.exception.job_id, "job-123")
def test_advertised_frame_audio_reference_limits_enforced(self):
self.metadata["supported_frame_images"] = ["first_frame"]
self.metadata["generate_audio"] = False
self.metadata["supported_parameters"] = {"input_references": {"type": "range", "min": 1, "max": 1}}
for args in [
{"generate_audio": True},
{"mode": "start_end_frame_to_video", "reference_urls": ["https://example.com/a", "https://example.com/b"]},
{"mode": "reference_to_video", "reference_urls": ["https://example.com/a", "https://example.com/b"]},
]:
with self.subTest(args=args):
http = HTTP()
with self.assertRaises(video.VideoJobError):
self.run_video(http, **args)
self.assertEqual(http.calls, [])
def test_unknown_reference_limit_is_not_guessed(self):
http = HTTP(self.submitted("completed"), Response())
self.run_video(http, mode="reference_to_video", reference_urls=["https://example.com/a"] * 5)
self.assertEqual(len(http.calls[0][2]["json"]["input_references"]), 5)
def test_remote_polling_url_rejected_with_recoverable_job(self):
http = HTTP(Response({"id": "job-123", "status": "pending", "polling_url": "https://attacker.example/poll"}, 202))
with self.assertRaisesRegex(video.VideoJobError, "another origin") as caught:
self.run_video(http)
self.assertEqual(caught.exception.job_id, "job-123")
self.assertEqual(len(http.calls), 1)
def test_unsigned_content_redirect_never_forwards_key_to_cdn(self):
http = HTTP(self.completed(), Response(status=302, headers={"Location": "https://cdn.example/video.mp4"}), Response())
self.run_video(http, job_id="job-123")
self.assertIn("Authorization", http.calls[1][2]["headers"])
self.assertNotIn("Authorization", http.calls[2][2]["headers"])
def test_poll_redirect_cannot_leak_key(self):
redirect = Response(status=302, headers={"Location": "https://attacker.example/poll"})
http = HTTP(redirect)
with self.assertRaises(video.VideoJobError) as caught:
self.run_video(http, job_id="job-123")
self.assertEqual(len(http.calls), 1)
self.assertTrue(redirect.closed)
self.assertEqual(caught.exception.job_id, "job-123")
def test_http_download_redirect_rejected(self):
http = HTTP(self.completed(), Response(status=302, headers={"Location": "http://cdn.example/video.mp4"}))
with self.assertRaisesRegex(video.VideoJobError, "HTTPS"):
self.run_video(http, job_id="job-123")
self.assertEqual(len(http.calls), 2)
def test_terminal_states_stop_without_download_or_resubmit(self):
for status in ("failed", "cancelled", "expired", "nonsense"):
with self.subTest(status=status):
http = HTTP(Response({"status": status, "error": "provider message"}))
with self.assertRaises(video.VideoJobError) as caught:
self.run_video(http, job_id="job-123")
self.assertEqual(caught.exception.job_id, "job-123")
self.assertEqual(len(http.calls), 1)
def test_submission_timeout_is_not_retried(self):
http = HTTP(TimeoutError("read timed out"))
with self.assertRaisesRegex(video.VideoJobError, "outcome is unknown"):
self.run_video(http)
self.assertEqual(len(http.calls), 1)
def test_ambiguous_submit_server_or_decode_errors_never_retry(self):
for response in [Response({"error": "upstream timeout"}, 502),
Response(ValueError("bad JSON"), 202), Response([], 202),
Response(status=307, headers={"Location": video.API_BASE})]:
with self.subTest(status=response.status_code):
http = HTTP(response)
with self.assertRaisesRegex(video.VideoJobError, "outcome is unknown"):
self.run_video(http)
self.assertEqual(len(http.calls), 1)
def test_poll_response_must_belong_to_same_job(self):
http = HTTP(Response({"id": "job-wrong", "status": "completed"}))
with self.assertRaisesRegex(video.VideoJobError, "different video job ID") as caught:
self.run_video(http, job_id="job-123")
self.assertEqual(caught.exception.job_id, "job-123")
self.assertEqual(len(http.calls), 1)
def test_poll_transient_retry_retains_same_job(self):
http = HTTP(Response(status=429, headers={"Retry-After": "1"}), TimeoutError("temporary"), self.completed(), Response())
self.run_video(http, job_id="job-123")
self.assertEqual([c[0] for c in http.calls], ["GET"] * 4)
self.assertEqual(len({c[1] for c in http.calls[:3]}), 1)
def test_deadline_in_poll_wait_retains_job(self):
http = HTTP(self.submitted())
with self.assertRaisesRegex(video.VideoJobError, "Timed out") as caught:
self.run_video(http, wait_timeout=1)
self.assertEqual(caught.exception.job_id, "job-123")
self.assertLessEqual(self.clock.now, 1)
self.assertEqual(len(http.calls), 1)
def test_interruption_preserves_comfy_baseexception_and_submitted_id(self):
submitted = False
def submit():
nonlocal submitted
submitted = True
return self.submitted()
def interrupt():
if submitted:
raise ComfyInterrupt()
http = HTTP(submit)
callback = Mock()
with self.assertRaises(ComfyInterrupt) as caught:
self.run_video(http, on_job=callback, _interrupt_check=interrupt)
self.assertEqual(caught.exception.openrouter_job_id, "job-123")
callback.assert_called_once_with("job-123")
self.assertEqual(len(http.calls), 1)
def test_media_validation_failure_is_recoverable_without_false_success(self):
http = HTTP(self.completed(), Response())
self.validate.side_effect = ValueError("no video stream")
with self.assertRaisesRegex(video.VideoJobError, "no video stream") as caught:
self.run_video(http, job_id="job-123")
self.assertEqual(caught.exception.job_id, "job-123")
self.factory.assert_not_called()
def test_reject_empty_document_and_oversized_downloads(self):
downloads = [Response(chunks=[]), Response(headers={"Content-Type": "application/json"}),
Response(headers={"Content-Length": str(video.MAX_VIDEO_BYTES + 1)})]
for download in downloads:
with self.subTest(headers=download.headers):
http = HTTP(self.completed(), download)
with self.assertRaises(video.VideoJobError):
self.run_video(http, job_id="job-123")
self.assertTrue(download.closed)
def test_diagnostics_redact_secrets_and_image_data(self):
http = HTTP(Response({"error": "private-key data:image/png;base64,secretbytes"}, 400))
with self.assertRaises(video.VideoJobError) as caught:
self.run_video(http)
text = str(caught.exception)
self.assertNotIn("private-key", text)
self.assertNotIn("secretbytes", text)
def test_missing_submission_id_warns_of_ambiguous_outcome(self):
http = HTTP(Response({"status": "pending"}, 202))
with self.assertRaisesRegex(video.VideoJobError, "outcome is unknown"):
self.run_video(http)
self.assertEqual(len(http.calls), 1)
def test_request_timeout_never_exceeds_remaining_budget(self):
http = HTTP(self.completed(), Response())
self.run_video(http, job_id="job-123", request_timeout=120, wait_timeout=2)
for _, _, kwargs in http.calls:
self.assertTrue(all(t <= 2 for t in kwargs["timeout"]))
self.assertLessEqual(sum(kwargs["timeout"]), 2)
if __name__ == "__main__":
unittest.main()