mirror of
https://github.com/hanzoai/video.git
synced 2026-10-30 18:18:27 +00:00
11 production pipelines, 47 tools, 124 agent skills. Supports cloud APIs (fal.ai, OpenAI, ElevenLabs, Suno, HeyGen, Runway) and free local providers (diffusers, Piper TTS, WAN 2.1, Hunyuan, CogVideo). Co-authored-by: Hanzo Dev <[email protected]>
267 lines
8.7 KiB
Python
267 lines
8.7 KiB
Python
"""Phase 2 contract tests — Enhancement Layer tools."""
|
|
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from tools.base_tool import BaseTool, ToolResult, ToolTier, ToolStatus
|
|
from tools.tool_registry import ToolRegistry
|
|
|
|
from tools.enhancement.face_enhance import FaceEnhance, PRESETS as FACE_PRESETS
|
|
from tools.analysis.scene_detect import SceneDetect
|
|
from tools.enhancement.color_grade import ColorGrade, PROFILES as COLOR_PROFILES
|
|
from tools.audio.audio_enhance import AudioEnhance, PRESETS as AUDIO_PRESETS
|
|
from tools.graphics.image_selector import ImageSelector
|
|
from tools.graphics.code_snippet import CodeSnippet, THEMES as CODE_THEMES
|
|
from tools.graphics.diagram_gen import DiagramGen
|
|
|
|
|
|
PHASE2_TOOLS = [
|
|
FaceEnhance,
|
|
SceneDetect,
|
|
ColorGrade,
|
|
AudioEnhance,
|
|
ImageSelector,
|
|
CodeSnippet,
|
|
DiagramGen,
|
|
]
|
|
|
|
|
|
# ---- Contract: every tool inherits BaseTool and has required fields ----
|
|
|
|
class TestPhase2ToolContracts:
|
|
@pytest.mark.parametrize("tool_cls", PHASE2_TOOLS)
|
|
def test_inherits_base_tool(self, tool_cls):
|
|
assert issubclass(tool_cls, BaseTool)
|
|
|
|
@pytest.mark.parametrize("tool_cls", PHASE2_TOOLS)
|
|
def test_has_required_identity(self, tool_cls):
|
|
tool = tool_cls()
|
|
assert tool.name
|
|
assert tool.version
|
|
assert tool.tier in ToolTier
|
|
assert len(tool.capabilities) > 0
|
|
|
|
@pytest.mark.parametrize("tool_cls", PHASE2_TOOLS)
|
|
def test_get_info_returns_valid_dict(self, tool_cls):
|
|
tool = tool_cls()
|
|
info = tool.get_info()
|
|
assert isinstance(info, dict)
|
|
assert info["name"] == tool.name
|
|
assert info["status"] in ["available", "unavailable", "degraded"]
|
|
|
|
@pytest.mark.parametrize("tool_cls", PHASE2_TOOLS)
|
|
def test_execute_is_implemented(self, tool_cls):
|
|
tool = tool_cls()
|
|
assert callable(tool.execute)
|
|
|
|
@pytest.mark.parametrize("tool_cls", PHASE2_TOOLS)
|
|
def test_dry_run_returns_dict(self, tool_cls):
|
|
tool = tool_cls()
|
|
result = tool.dry_run({})
|
|
assert isinstance(result, dict)
|
|
assert result["tool"] == tool.name
|
|
|
|
|
|
# ---- Contract: unique names, all CORE tier ----
|
|
|
|
class TestPhase2ToolNames:
|
|
def test_unique_names(self):
|
|
names = [cls().name for cls in PHASE2_TOOLS]
|
|
assert len(names) == len(set(names))
|
|
|
|
def test_expected_names(self):
|
|
names = {cls().name for cls in PHASE2_TOOLS}
|
|
expected = {
|
|
"face_enhance", "scene_detect", "color_grade", "audio_enhance",
|
|
"image_selector", "code_snippet", "diagram_gen",
|
|
}
|
|
assert names == expected
|
|
|
|
def test_expected_tiers(self):
|
|
for cls in PHASE2_TOOLS:
|
|
tool = cls()
|
|
if tool.name == "image_selector":
|
|
assert tool.tier == ToolTier.GENERATE
|
|
else:
|
|
assert tool.tier == ToolTier.CORE, f"{tool.name} should be CORE"
|
|
|
|
|
|
# ---- Contract: discoverable via registry ----
|
|
|
|
class TestPhase2ToolDiscovery:
|
|
def test_all_phase2_tools_discoverable(self):
|
|
reg = ToolRegistry()
|
|
reg.discover("tools")
|
|
for cls in PHASE2_TOOLS:
|
|
name = cls().name
|
|
assert reg.get(name) is not None, f"{name} not discovered"
|
|
|
|
|
|
# ---- Contract: presets/profiles are well-formed ----
|
|
|
|
class TestPresets:
|
|
def test_face_presets_have_vf(self):
|
|
for name, preset in FACE_PRESETS.items():
|
|
assert "vf" in preset, f"Face preset {name} missing 'vf'"
|
|
assert "description" in preset
|
|
|
|
def test_color_profiles_have_vf(self):
|
|
for name, profile in COLOR_PROFILES.items():
|
|
assert "vf" in profile, f"Color profile {name} missing 'vf'"
|
|
assert "description" in profile
|
|
|
|
def test_audio_presets_have_af(self):
|
|
for name, preset in AUDIO_PRESETS.items():
|
|
assert "af" in preset, f"Audio preset {name} missing 'af'"
|
|
assert "description" in preset
|
|
|
|
def test_code_themes_have_required_fields(self):
|
|
for name, theme in CODE_THEMES.items():
|
|
assert "pygments_style" in theme
|
|
assert "bg_color" in theme
|
|
assert "text_color" in theme
|
|
|
|
def test_face_list_presets(self):
|
|
presets = FaceEnhance.list_presets()
|
|
assert len(presets) >= 8
|
|
assert "talking_head_standard" in presets
|
|
|
|
def test_color_list_profiles(self):
|
|
profiles = ColorGrade.list_profiles()
|
|
assert len(profiles) >= 7
|
|
assert "cinematic_warm" in profiles
|
|
|
|
def test_audio_list_presets(self):
|
|
presets = AudioEnhance.list_presets()
|
|
assert len(presets) >= 5
|
|
assert "clean_speech" in presets
|
|
|
|
|
|
# ---- Contract: error handling for missing inputs ----
|
|
|
|
class TestPhase2ErrorHandling:
|
|
def test_face_enhance_missing_file(self):
|
|
tool = FaceEnhance()
|
|
r = tool.execute({"input_path": "/nonexistent.mp4"})
|
|
assert not r.success
|
|
|
|
def test_scene_detect_missing_file(self):
|
|
tool = SceneDetect()
|
|
r = tool.execute({"input_path": "/nonexistent.mp4"})
|
|
assert not r.success
|
|
|
|
def test_color_grade_missing_file(self):
|
|
tool = ColorGrade()
|
|
r = tool.execute({"input_path": "/nonexistent.mp4"})
|
|
assert not r.success
|
|
|
|
def test_audio_enhance_missing_file(self):
|
|
tool = AudioEnhance()
|
|
r = tool.execute({"input_path": "/nonexistent.mp4"})
|
|
assert not r.success
|
|
|
|
def test_image_selector_no_provider(self):
|
|
tool = ImageSelector()
|
|
# Will fail if no API key or local model
|
|
r = tool.execute({"prompt": "test"})
|
|
# Either succeeds (provider available) or fails gracefully
|
|
assert isinstance(r, ToolResult)
|
|
|
|
def test_diagram_gen_empty_boxes(self):
|
|
tool = DiagramGen()
|
|
if tool.get_status() == ToolStatus.AVAILABLE:
|
|
r = tool.execute({"diagram_type": "boxes", "boxes": []})
|
|
assert isinstance(r, ToolResult)
|
|
|
|
|
|
# ---- Contract: code_snippet renders valid images ----
|
|
|
|
class TestCodeSnippetUnit:
|
|
@pytest.fixture
|
|
def has_deps(self):
|
|
tool = CodeSnippet()
|
|
if tool.get_status() != ToolStatus.AVAILABLE:
|
|
pytest.skip("Pygments/Pillow not installed")
|
|
|
|
def test_render_python(self, has_deps, tmp_path):
|
|
tool = CodeSnippet()
|
|
r = tool.execute({
|
|
"code": "def hello():\n print('Hello, world!')\n",
|
|
"language": "python",
|
|
"theme": "monokai",
|
|
"output_path": str(tmp_path / "test.png"),
|
|
})
|
|
assert r.success
|
|
assert Path(r.data["output"]).exists()
|
|
assert r.data["line_count"] == 3
|
|
|
|
def test_render_with_title(self, has_deps, tmp_path):
|
|
tool = CodeSnippet()
|
|
r = tool.execute({
|
|
"code": "console.log('test');",
|
|
"language": "javascript",
|
|
"theme": "dracula",
|
|
"title": "example.js",
|
|
"output_path": str(tmp_path / "titled.png"),
|
|
})
|
|
assert r.success
|
|
|
|
def test_render_different_themes(self, has_deps, tmp_path):
|
|
tool = CodeSnippet()
|
|
for theme_name in ["monokai", "github_dark", "light"]:
|
|
r = tool.execute({
|
|
"code": "x = 42",
|
|
"language": "python",
|
|
"theme": theme_name,
|
|
"output_path": str(tmp_path / f"{theme_name}.png"),
|
|
})
|
|
assert r.success, f"Theme {theme_name} failed"
|
|
|
|
|
|
# ---- Contract: diagram_gen renders valid images ----
|
|
|
|
class TestDiagramGenUnit:
|
|
@pytest.fixture
|
|
def has_deps(self):
|
|
tool = DiagramGen()
|
|
if tool.get_status() != ToolStatus.AVAILABLE:
|
|
pytest.skip("No diagram renderer available")
|
|
|
|
def test_render_box_diagram(self, has_deps, tmp_path):
|
|
tool = DiagramGen()
|
|
r = tool.execute({
|
|
"diagram_type": "boxes",
|
|
"title": "Test Flow",
|
|
"boxes": [
|
|
{"label": "Input", "color": "#2563eb"},
|
|
{"label": "Process", "color": "#7c3aed"},
|
|
{"label": "Output", "color": "#059669"},
|
|
],
|
|
"connections": [
|
|
{"from": 0, "to": 1, "label": "data"},
|
|
{"from": 1, "to": 2, "label": "result"},
|
|
],
|
|
"theme": "dark",
|
|
"output_path": str(tmp_path / "boxes.png"),
|
|
})
|
|
assert r.success
|
|
assert Path(r.data["output"]).exists()
|
|
assert r.data["box_count"] == 3
|
|
|
|
def test_render_mermaid_fallback(self, has_deps, tmp_path):
|
|
"""If mmdc not installed, falls back to text card."""
|
|
tool = DiagramGen()
|
|
r = tool.execute({
|
|
"diagram_type": "mermaid",
|
|
"definition": "graph TD\n A[Start] --> B[End]",
|
|
"output_path": str(tmp_path / "mermaid.png"),
|
|
})
|
|
assert r.success
|