Merge remote-tracking branch 'origin/main' into codex/repair-pr-353

# Conflicts:
#	tests/contracts/test_phase3_contracts.py
This commit is contained in:
calesthio
2026-08-13 11:11:48 -07:00
20 changed files with 2349 additions and 34 deletions

View File

@@ -0,0 +1,103 @@
from __future__ import annotations
from unittest.mock import MagicMock, patch
from tools.audio.fal_elevenlabs_music import FalElevenLabsMusic
from tools.base_tool import ToolStatus
from tools.tool_registry import ToolRegistry
def _response(*, json_data=None, content=b""):
response = MagicMock()
response.json.return_value = json_data
response.content = content
response.raise_for_status.return_value = None
return response
def test_contract_and_rounded_cost(monkeypatch):
tool = FalElevenLabsMusic()
monkeypatch.setenv("FAL_KEY", "test-key")
assert tool.get_status() == ToolStatus.AVAILABLE
assert tool.estimate_cost({"duration_seconds": 20}) == 0.80
assert tool.estimate_cost({"duration_seconds": 61}) == 1.60
assert tool.get_info()["capability"] == "music_generation"
assert tool.get_info()["provider"] == "fal.ai"
def test_registry_discovers_provider(monkeypatch):
monkeypatch.setenv("FAL_KEY", "test-key")
registry = ToolRegistry()
registry.discover()
tool = registry.get("fal_elevenlabs_music")
assert tool is not None
assert tool.get_status() == ToolStatus.AVAILABLE
def test_execute_submits_once_and_downloads_audio(tmp_path, monkeypatch):
monkeypatch.setenv("FAL_KEY", "test-key")
output_path = tmp_path / "music.mp3"
tool = FalElevenLabsMusic()
tool._POLL_INTERVAL_SECONDS = 0
post_response = _response(
json_data={
"status_url": "https://queue.example/status",
"response_url": "https://queue.example/result",
}
)
status_response = _response(json_data={"status": "COMPLETED"})
result_response = _response(
json_data={"audio": {"url": "https://media.example/music.mp3"}}
)
audio_response = _response(content=b"fake-mp3")
with (
patch("requests.post", return_value=post_response) as mock_post,
patch(
"requests.get",
side_effect=[status_response, result_response, audio_response],
) as mock_get,
):
result = tool.execute(
{
"prompt": "gentle felt piano",
"duration_seconds": 20,
"force_instrumental": True,
"output_path": str(output_path),
}
)
assert result.success is True
assert result.cost_usd == 0.80
assert result.model == "fal-ai/elevenlabs/music"
assert output_path.read_bytes() == b"fake-mp3"
assert mock_post.call_count == 1
assert mock_post.call_args.kwargs["json"]["music_length_ms"] == 20000
assert mock_post.call_args.kwargs["json"]["force_instrumental"] is True
assert mock_get.call_count == 3
def test_execute_rejects_missing_duration_without_request(monkeypatch):
monkeypatch.setenv("FAL_KEY", "test-key")
with patch("requests.post") as mock_post:
result = FalElevenLabsMusic().execute({"prompt": "gentle piano"})
assert result.success is False
assert result.error == "duration_seconds is required"
mock_post.assert_not_called()
def test_error_redacts_fal_key(monkeypatch):
secret = "test-production-shaped-fal-key"
monkeypatch.setenv("FAL_KEY", secret)
with patch("requests.post", side_effect=RuntimeError(f"request used {secret}")):
result = FalElevenLabsMusic().execute(
{"prompt": "gentle piano", "duration_seconds": 20}
)
assert result.success is False
assert secret not in result.error
assert "[REDACTED]" in result.error

View File

@@ -0,0 +1,132 @@
from __future__ import annotations
from unittest.mock import MagicMock, patch
from tools.audio.fal_elevenlabs_tts import FalElevenLabsTTS
from tools.base_tool import ToolStatus
from tools.tool_registry import ToolRegistry
def _response(*, json_data=None, content=b""):
response = MagicMock()
response.json.return_value = json_data
response.content = content
response.raise_for_status.return_value = None
return response
def test_contract_models_and_cost(monkeypatch):
monkeypatch.setenv("FAL_KEY", "test-key")
tool = FalElevenLabsTTS()
assert tool.get_status() == ToolStatus.AVAILABLE
assert tool.get_info()["capability"] == "tts"
assert tool.get_info()["provider"] == "fal.ai"
assert tool.estimate_cost({"text": "a" * 1000, "model_id": "eleven-v3"}) == 0.1
assert tool.estimate_cost({"text": "a" * 1000, "model_id": "multilingual-v2"}) == 0.1
assert tool.estimate_cost({"text": "a" * 1000, "model_id": "turbo-v2.5"}) == 0.05
def test_registry_discovers_fal_tts(monkeypatch):
monkeypatch.setenv("FAL_KEY", "test-key")
registry = ToolRegistry()
registry.discover()
tool = registry.get("fal_elevenlabs_tts")
assert tool is not None
assert tool.get_status() == ToolStatus.AVAILABLE
def test_tts_selector_routes_to_fal_provider(monkeypatch):
from tools.audio.tts_selector import TTSSelector
from tools.base_tool import ToolResult
monkeypatch.setenv("FAL_KEY", "test-key")
tool = FalElevenLabsTTS()
selector = TTSSelector()
monkeypatch.setattr(selector, "_providers", lambda: [tool])
monkeypatch.setattr(
selector,
"_select_best_tool",
lambda _inputs, _candidates, _context: (tool, None),
)
monkeypatch.setattr(
tool,
"execute",
lambda inputs: ToolResult(success=True, data={"received": inputs}),
)
result = selector.execute(
{"text": "hello", "preferred_provider": "fal.ai", "voice_id": "Rachel"}
)
assert result.success
assert result.data["selected_tool"] == "fal_elevenlabs_tts"
assert result.data["selected_provider"] == "fal.ai"
def test_execute_submits_once_and_downloads_audio(tmp_path, monkeypatch):
monkeypatch.setenv("FAL_KEY", "test-key")
output_path = tmp_path / "speech.mp3"
tool = FalElevenLabsTTS()
tool._POLL_INTERVAL_SECONDS = 0
post_response = _response(
json_data={
"status_url": "https://queue.example/status",
"response_url": "https://queue.example/result",
}
)
status_response = _response(json_data={"status": "COMPLETED"})
result_response = _response(
json_data={"audio": {"url": "https://media.example/speech.mp3"}}
)
audio_response = _response(content=b"fake-mp3")
with (
patch("requests.post", return_value=post_response) as mock_post,
patch(
"requests.get",
side_effect=[status_response, result_response, audio_response],
) as mock_get,
):
result = tool.execute(
{
"text": "A calm, measured test.",
"voice_id": "Rachel",
"model_id": "eleven-v3",
"stability": 0.65,
"language_code": "en",
"output_path": str(output_path),
}
)
assert result.success is True
assert result.model == "fal-ai/elevenlabs/tts/eleven-v3"
assert output_path.read_bytes() == b"fake-mp3"
assert mock_post.call_count == 1
assert mock_post.call_args.args[0].endswith("/fal-ai/elevenlabs/tts/eleven-v3")
assert mock_post.call_args.kwargs["json"]["voice"] == "Rachel"
assert mock_post.call_args.kwargs["json"]["stability"] == 0.65
assert mock_get.call_count == 3
def test_invalid_model_does_not_submit(monkeypatch):
monkeypatch.setenv("FAL_KEY", "test-key")
with patch("requests.post") as mock_post:
result = FalElevenLabsTTS().execute(
{"text": "hello", "model_id": "not-a-model"}
)
assert result.success is False
assert "model_id must be one of" in result.error
mock_post.assert_not_called()
def test_error_redacts_fal_key(monkeypatch):
secret = "test-production-shaped-fal-key"
monkeypatch.setenv("FAL_KEY", secret)
with patch("requests.post", side_effect=RuntimeError(f"request used {secret}")):
result = FalElevenLabsTTS().execute({"text": "hello"})
assert result.success is False
assert secret not in result.error
assert "[REDACTED]" in result.error

View File

@@ -0,0 +1,156 @@
"""Tests for the Gemini image backend in google_imagen.
Models named `gemini-*` (e.g. gemini-2.5-flash-image) are not served by the
Imagen `:predict` endpoint — they generate images through generate_content
with an image_config. This backend matters on Vertex projects that have no
Imagen catalog access, where it is the only working Google image path.
"""
import sys
import types as pytypes
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
class _FakeInline:
def __init__(self, data: bytes):
self.data = data
class _FakePart:
def __init__(self, data: bytes):
self.inline_data = _FakeInline(data)
class _FakeContent:
def __init__(self, parts):
self.parts = parts
class _FakeCandidate:
def __init__(self, parts):
self.content = _FakeContent(parts)
class _FakeResponse:
def __init__(self, parts):
self.candidates = [_FakeCandidate(parts)]
@pytest.fixture
def imagen_tool(monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "test-key")
calls: list[dict] = []
class _FakeModels:
def generate_content(self, model=None, contents=None, config=None):
calls.append({"model": model, "contents": contents, "config": config})
return _FakeResponse([_FakePart(b"GEMINI_IMG")])
class _FakeClient:
models = _FakeModels()
import tools.google_credentials as gc
monkeypatch.setattr(
gc, "get_genai_client", lambda http_options=None, location=None: _FakeClient()
)
from tools.graphics.google_imagen import GoogleImagen
return GoogleImagen(), calls
def test_gemini_model_routes_to_generate_content(imagen_tool, tmp_path):
tool, calls = imagen_tool
out = tmp_path / "img.png"
result = tool.execute(
{
"prompt": "a flower",
"model": "gemini-2.5-flash-image",
"aspect_ratio": "16:9",
"output_path": str(out),
}
)
assert result.success
assert result.data["model"] == "gemini-2.5-flash-image"
assert out.read_bytes() == b"GEMINI_IMG"
assert len(calls) == 1
assert calls[0]["model"] == "gemini-2.5-flash-image"
# Aspect ratio must reach the API through image_config, not be dropped.
assert calls[0]["config"].image_config.aspect_ratio == "16:9"
def test_image_selector_maps_model_name_to_google_model(
imagen_tool, monkeypatch, tmp_path
):
"""The governed selector must be able to reach the Gemini backend."""
from tools.graphics.image_selector import ImageSelector
tool, calls = imagen_tool
selector = ImageSelector()
monkeypatch.setattr(selector, "_providers", lambda: [tool])
result = selector.execute(
{
"prompt": "a flower",
"preferred_provider": "google_imagen",
"model_name": "gemini-2.5-flash-image",
"output_path": str(tmp_path / "selected.png"),
}
)
assert result.success, result.error
assert calls[0]["model"] == "gemini-2.5-flash-image"
assert result.data["selected_tool"] == "google_imagen"
assert result.data["model"] == "gemini-2.5-flash-image"
def test_gemini_cost_estimate_is_per_image():
from tools.graphics.google_imagen import GoogleImagen
tool = GoogleImagen()
assert tool.estimate_cost(
{"model": "gemini-2.5-flash-image", "number_of_images": 2}
) == pytest.approx(0.039 * 2)
def test_text_only_response_is_a_clear_error(monkeypatch, tmp_path):
monkeypatch.setenv("GOOGLE_API_KEY", "test-key")
class _TextPart:
inline_data = None
class _FakeModels:
def generate_content(self, model=None, contents=None, config=None):
return _FakeResponse([_TextPart()])
class _FakeClient:
models = _FakeModels()
import tools.google_credentials as gc
monkeypatch.setattr(
gc, "get_genai_client", lambda http_options=None, location=None: _FakeClient()
)
from tools.graphics.google_imagen import GoogleImagen
result = GoogleImagen().execute(
{
"prompt": "a flower",
"model": "gemini-2.5-flash-image",
"output_path": str(tmp_path / "img.png"),
}
)
assert not result.success
assert "No image data" in result.error

View File

@@ -0,0 +1,36 @@
from tools.audio.google_tts import GoogleTTS
from tools.google_credentials import has_google_credentials
def test_tts_only_key_does_not_enable_shared_google_providers(monkeypatch):
monkeypatch.setenv("GOOGLE_TTS_API_KEY", "test-tts-only-key")
monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False)
assert GoogleTTS().get_status().value == "available"
assert has_google_credentials() is False
def test_tts_key_uses_header_and_is_redacted_from_errors(monkeypatch, tmp_path):
import requests
secret = "test-production-shaped-tts-key"
monkeypatch.setenv("GOOGLE_TTS_API_KEY", secret)
monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False)
def fail_request(url, **kwargs):
assert kwargs["headers"]["x-goog-api-key"] == secret
assert "params" not in kwargs
raise requests.HTTPError(f"403 for {url}?key={secret}")
monkeypatch.setattr(requests, "post", fail_request)
result = GoogleTTS().execute(
{"text": "safe test sentence", "output_path": str(tmp_path / "speech.mp3")}
)
assert result.success is False
assert secret not in result.error
assert "[REDACTED]" in result.error

View File

@@ -0,0 +1,534 @@
"""Unit tests for hunyuan_image — TokenHub 混元生图 3.0 tool."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from tools.base_tool import ToolStatus
# ---------------------------------------------------------------------------
# Tool discovery & metadata
# ---------------------------------------------------------------------------
def test_hunyuan_image_is_discovered_by_registry():
from tools.tool_registry import ToolRegistry
registry = ToolRegistry()
registry.discover()
tool = registry.get("hunyuan_image")
assert tool is not None
assert tool.provider == "hunyuan_cloud"
assert tool.capability == "image_generation"
assert tool.name == "hunyuan_image"
def test_hunyuan_image_metadata():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
info = tool.get_info()
assert info["tier"] == "generate"
assert info["stability"] == "experimental"
assert info["runtime"] == "api"
assert "text_to_image" in info["capabilities"]
assert info["supports"]["seed"] is True
assert info["supports"]["reference_image"] is True
assert info["supports"]["prompt_rewrite"] is True
assert info["supports"]["negative_prompt"] is False
assert "env:TENCENT_TOKENHUB_API_KEY" in info["dependencies"]
assert "visual-style" in info["agent_skills"]
def test_idempotency_includes_custom_watermark():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
base = {"prompt": "x"}
assert tool.idempotency_key(base) != tool.idempotency_key(
{**base, "logo_param": {"logo_url": "https://example.com/logo.png"}}
)
# ---------------------------------------------------------------------------
# Status reporting
# ---------------------------------------------------------------------------
def test_status_unavailable_when_no_api_key(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.delenv("TENCENT_TOKENHUB_API_KEY", raising=False)
assert HunyuanImage().get_status() == ToolStatus.UNAVAILABLE
def test_status_available_when_api_key_set(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "test-key-123")
assert HunyuanImage().get_status() == ToolStatus.AVAILABLE
def test_api_key_filters_comment_like_values(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "# this-is-a-comment")
assert HunyuanImage()._api_key() is None
assert HunyuanImage().get_status() == ToolStatus.UNAVAILABLE
def test_api_key_strips_whitespace(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", " my-key ")
assert HunyuanImage()._api_key() == "my-key"
# ---------------------------------------------------------------------------
# Cost & runtime estimation
# ---------------------------------------------------------------------------
def test_estimate_cost():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
cost = tool.estimate_cost({})
assert cost > 0
assert cost == pytest.approx(0.08, rel=0.1)
def test_estimate_runtime():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
runtime = tool.estimate_runtime({})
assert runtime == 120.0
# ---------------------------------------------------------------------------
# Payload construction (_build_payload)
# ---------------------------------------------------------------------------
def test_build_payload_minimal():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
payload = tool._build_payload({"prompt": "a cat"})
assert payload == {"prompt": "a cat"}
def test_build_payload_all_params():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
payload = tool._build_payload({
"prompt": "a cat",
"resolution": "768:1024",
"seed": 42,
"revise": 0,
"logo_add": 1,
"images": ["https://example.com/ref.jpg"],
})
assert payload["prompt"] == "a cat"
assert payload["resolution"] == "768:1024"
assert payload["seed"] == 42
assert payload["revise"] == 0
assert payload["logo_add"] == 1
assert payload["images"] == ["https://example.com/ref.jpg"]
def test_build_payload_with_logo_param():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
payload = tool._build_payload({
"prompt": "a cat",
"logo_param": {"logo_url": "https://example.com/wm.png"},
})
assert payload["logo_param"] == {"logo_url": "https://example.com/wm.png"}
def test_build_payload_logo_param_skips_empty():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
payload = tool._build_payload({
"prompt": "a cat",
"logo_param": {},
})
assert "logo_param" not in payload
def test_build_payload_omits_none_seed():
from tools.graphics.hunyuan_image import HunyuanImage
tool = HunyuanImage()
payload = tool._build_payload({"prompt": "a cat", "seed": None})
assert "seed" not in payload
# ---------------------------------------------------------------------------
# Image resolution (_resolve_images)
# ---------------------------------------------------------------------------
def test_resolve_images_passes_urls_through():
from tools.graphics.hunyuan_image import HunyuanImage
refs = [
"https://example.com/a.jpg",
"data:image/png;base64,abc123",
]
resolved = HunyuanImage._resolve_images(refs)
assert resolved == refs
def test_resolve_images_encodes_local_file(tmp_path):
from tools.graphics.hunyuan_image import HunyuanImage
img = tmp_path / "test.png"
img.write_bytes(b"fake-png-data")
resolved = HunyuanImage._resolve_images([str(img)])
assert len(resolved) == 1
assert resolved[0].startswith("data:image/png;base64,")
def test_resolve_images_raises_on_missing_file():
from tools.graphics.hunyuan_image import HunyuanImage
with pytest.raises(FileNotFoundError):
HunyuanImage._resolve_images(["/nonexistent/path.jpg"])
def test_resolve_images_raises_on_oversized_file(tmp_path):
from tools.graphics.hunyuan_image import HunyuanImage
big = tmp_path / "big.jpg"
big.write_bytes(b"x" * (7 * 1024 * 1024)) # 7MB > 6MB limit
with pytest.raises(ValueError, match="too large"):
HunyuanImage._resolve_images([str(big)])
def test_resolve_images_detects_mime_from_extension(tmp_path):
from tools.graphics.hunyuan_image import HunyuanImage
cases = [
("ref.jpg", "image/jpeg"),
("ref.jpeg", "image/jpeg"),
("ref.png", "image/png"),
("ref.bmp", "image/bmp"),
("ref.tiff", "image/tiff"),
("ref.tif", "image/tiff"),
("ref.webp", "image/webp"),
("ref.unknown", "image/png"), # fallback
]
for filename, expected_mime in cases:
f = tmp_path / filename
f.write_bytes(b"data")
resolved = HunyuanImage._resolve_images([str(f)])
assert resolved[0].startswith(f"data:{expected_mime};base64,")
# ---------------------------------------------------------------------------
# Output path resolution (_resolve_output_paths)
# ---------------------------------------------------------------------------
def test_resolve_output_paths_single():
from tools.graphics.hunyuan_image import HunyuanImage
paths = HunyuanImage._resolve_output_paths("/out/img.png", 1)
assert len(paths) == 1
assert paths[0] == Path("/out/img.png")
def test_resolve_output_paths_multi():
from tools.graphics.hunyuan_image import HunyuanImage
paths = HunyuanImage._resolve_output_paths("/out/img.png", 3)
assert len(paths) == 3
assert [p.name for p in paths] == ["img_1.png", "img_2.png", "img_3.png"]
assert len(set(paths)) == 3
# ---------------------------------------------------------------------------
# Auth headers
# ---------------------------------------------------------------------------
def test_auth_headers():
from tools.graphics.hunyuan_image import HunyuanImage
headers = HunyuanImage._auth_headers("my-api-key")
assert headers["Authorization"] == "Bearer my-api-key"
assert headers["Content-Type"] == "application/json"
# ---------------------------------------------------------------------------
# JSON error handling
# ---------------------------------------------------------------------------
def test_json_or_raise_parses_valid_json():
from tools.graphics.hunyuan_image import HunyuanImage
class FakeResp:
status_code = 200
def json(self):
return {"status": "ok"}
assert HunyuanImage._json_or_raise(FakeResp()) == {"status": "ok"}
def test_json_or_raise_raises_on_invalid_json():
from tools.graphics.hunyuan_image import HunyuanImage
class FakeResp:
status_code = 500
def json(self):
raise ValueError("not json")
with pytest.raises(RuntimeError, match="Non-JSON response"):
HunyuanImage._json_or_raise(FakeResp())
def test_check_response_passes_clean_payload():
from tools.graphics.hunyuan_image import HunyuanImage
HunyuanImage._check_response({"status": "completed"}) # no error -> no raise
def test_check_response_raises_on_error_field():
from tools.graphics.hunyuan_image import HunyuanImage
with pytest.raises(RuntimeError, match="TokenHub API error"):
HunyuanImage._check_response({
"error": {"code": "AUTH_FAILED", "message": "invalid key"},
})
def test_check_response_raises_on_error_without_code():
from tools.graphics.hunyuan_image import HunyuanImage
with pytest.raises(RuntimeError, match="TokenHub API error"):
HunyuanImage._check_response({"error": {"message": "something broke"}})
# ---------------------------------------------------------------------------
# Safe error redaction
# ---------------------------------------------------------------------------
def test_safe_error_redacts_api_key(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "secret-key-abc")
msg = HunyuanImage._safe_error(Exception("failed with secret-key-abc"))
assert "secret-key-abc" not in msg
assert "[redacted]" in msg
def test_safe_error_preserves_other_text(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "sk-123")
msg = HunyuanImage._safe_error(Exception("network timeout: connection refused"))
assert "network timeout" in msg
assert "sk-123" not in msg
# ---------------------------------------------------------------------------
# Execute guards
# ---------------------------------------------------------------------------
def test_execute_returns_error_without_api_key(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.delenv("TENCENT_TOKENHUB_API_KEY", raising=False)
result = HunyuanImage().execute({"prompt": "a cat"})
assert not result.success
assert "TENCENT_TOKENHUB_API_KEY" in result.error
def test_image_selector_maps_shared_reference_input(monkeypatch, tmp_path):
from tools.base_tool import ToolResult
from tools.graphics.hunyuan_image import HunyuanImage
from tools.graphics.image_selector import ImageSelector
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "test-key")
tool = HunyuanImage()
selector = ImageSelector()
monkeypatch.setattr(selector, "_providers", lambda: [tool])
monkeypatch.setattr(
selector,
"_select_best_tool",
lambda _inputs, _candidates, _context: (tool, None),
)
observed = {}
def fake_execute(inputs):
observed.update(inputs)
return ToolResult(success=True, data={}, artifacts=[inputs["output_path"]])
monkeypatch.setattr(tool, "execute", fake_execute)
result = selector.execute(
{
"prompt": "adapt this frame",
"preferred_provider": "hunyuan_cloud",
"image_path": str(tmp_path / "reference.png"),
"output_path": str(tmp_path / "out.png"),
}
)
assert result.success
assert observed["images"] == [str(tmp_path / "reference.png")]
assert result.data["selected_tool"] == "hunyuan_image"
# ---------------------------------------------------------------------------
# Dry run
# ---------------------------------------------------------------------------
def test_dry_run_no_side_effects(monkeypatch):
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "test-key")
tool = HunyuanImage()
info = tool.dry_run({"prompt": "a cat"})
assert info["tool"] == "hunyuan_image"
assert info["estimated_cost_usd"] > 0
assert info["would_execute"] is True
# ---------------------------------------------------------------------------
# End-to-end with mocked API
# ---------------------------------------------------------------------------
def test_execute_full_flow_with_mocked_api(monkeypatch, tmp_path):
"""Simulate the full submit → poll → download flow."""
from tools.graphics.hunyuan_image import HunyuanImage, _MODEL, _HOST
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "test-key")
# Track calls across all mocked endpoints
api_calls = []
poll_count = [0] # mutable counter for poll iteration
class _FakeResp:
status_code = 200
def __init__(self, data, content=None):
self._data = data
self.content = content
def json(self):
return self._data
def raise_for_status(self):
pass
submit_url = f"https://{_HOST}/v1/api/image/submit"
query_url = f"https://{_HOST}/v1/api/image/query"
# Safety sentinel: if a real network call slips through the mock,
# this flag is flipped and we fail-fast instead of leaking files.
mock_active = [False]
def fake_post(url, *, json, headers, timeout):
mock_active[0] = True
api_calls.append(("post", url, json))
if "submit" in url:
return _FakeResp({"id": "job-001", "status": "queued"})
elif "query" in url:
poll_count[0] += 1
if poll_count[0] == 1:
return _FakeResp({"status": "running"})
return _FakeResp({
"status": "completed",
"data": [{"url": "https://example.com/result.png"}],
})
raise RuntimeError(f"Unexpected URL: {url}")
def fake_get(url, timeout):
mock_active[0] = True
api_calls.append(("get", url))
return _FakeResp({}, content=b"fake-image-data")
with (
patch("requests.post", side_effect=fake_post),
patch("requests.get", side_effect=fake_get),
):
out = tmp_path / "gen.png"
result = HunyuanImage().execute({
"prompt": "a programmer coding",
"resolution": "1024:1024",
"seed": 12345,
"revise": 1,
"logo_add": 0,
"output_path": str(out),
})
# Guard: mock must have been exercised — if not, a real API call leaked
assert mock_active[0], (
"Mock was never triggered — a real API call may have leaked. "
"Check that requests.post / requests.get patching is effective."
)
assert result.success, result.error
assert result.data["provider"] == "hunyuan_cloud"
assert result.data["model"] == _MODEL
assert result.data["task_id"] == "job-001"
assert result.data["resolution"] == "1024:1024"
assert result.data["images_generated"] == 1
assert result.artifacts == [str(out)]
assert out.read_bytes() == b"fake-image-data"
# Verify submit payload was correct
submit_calls = [c for c in api_calls if "submit" in c[1]]
assert len(submit_calls) == 1
_, _, submit_body = submit_calls[0]
assert submit_body["prompt"] == "a programmer coding"
assert submit_body["resolution"] == "1024:1024"
assert submit_body["seed"] == 12345
assert submit_body["revise"] == 1
assert submit_body["logo_add"] == 0
assert submit_body["model"] == _MODEL
# Verify polling happened (submit + at least 1 query + download)
assert any("query" in c[1] for c in api_calls)
assert any(c[0] == "get" for c in api_calls)
def test_execute_with_local_reference_images(monkeypatch, tmp_path):
"""Reference images from local paths should be base64-encoded in payload."""
from tools.graphics.hunyuan_image import HunyuanImage
monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "test-key")
ref_img = tmp_path / "ref.png"
ref_img.write_bytes(b"reference-data")
tool = HunyuanImage()
payload = tool._build_payload({
"prompt": "enhance this",
"images": [str(ref_img)],
})
assert "images" in payload
assert len(payload["images"]) == 1
assert payload["images"][0].startswith("data:image/png;base64,")