244 lines
8.8 KiB
Python
244 lines
8.8 KiB
Python
"""Unit tests for Gemini Prompt Engineer node."""
|
|
|
|
import pytest
|
|
import sys
|
|
import numpy as np
|
|
from unittest.mock import patch, MagicMock
|
|
from PIL import Image
|
|
|
|
from kikotools.tools.gemini_prompt import GeminiPromptNode
|
|
from kikotools.tools.gemini_prompt.logic import (
|
|
tensor_to_pil,
|
|
image_to_base64,
|
|
get_api_key,
|
|
validate_prompt_type,
|
|
analyze_image_with_gemini,
|
|
)
|
|
from kikotools.tools.gemini_prompt.prompts import (
|
|
PROMPT_OPTIONS,
|
|
PROMPT_TEMPLATES,
|
|
DEFAULT_GEMINI_MODELS,
|
|
)
|
|
|
|
|
|
class TestGeminiPromptNode:
|
|
"""Test cases for GeminiPromptNode."""
|
|
|
|
def test_node_properties(self):
|
|
"""Test node has correct properties."""
|
|
assert GeminiPromptNode.CATEGORY == "🫶 ComfyAssets/🧠 Prompts"
|
|
assert GeminiPromptNode.FUNCTION == "generate_prompt"
|
|
assert GeminiPromptNode.RETURN_TYPES == ("STRING", "STRING")
|
|
assert GeminiPromptNode.RETURN_NAMES == ("prompt", "negative_prompt")
|
|
|
|
def test_input_types(self):
|
|
"""Test INPUT_TYPES configuration."""
|
|
input_types = GeminiPromptNode.INPUT_TYPES()
|
|
|
|
# Check required inputs
|
|
assert "required" in input_types
|
|
assert "image" in input_types["required"]
|
|
assert input_types["required"]["image"] == ("IMAGE",)
|
|
assert "prompt_type" in input_types["required"]
|
|
assert input_types["required"]["prompt_type"][0] == PROMPT_OPTIONS
|
|
assert "model" in input_types["required"]
|
|
# Check that model is a list (can be dynamic from API or DEFAULT_GEMINI_MODELS)
|
|
model_list = input_types["required"]["model"][0]
|
|
assert isinstance(model_list, list)
|
|
assert len(model_list) > 0 # Should have at least one model
|
|
|
|
# Check optional inputs
|
|
assert "optional" in input_types
|
|
assert "api_key" in input_types["optional"]
|
|
assert "custom_prompt" in input_types["optional"]
|
|
|
|
def test_default_gemini_models_structure(self):
|
|
"""Test that DEFAULT_GEMINI_MODELS has proper structure."""
|
|
assert isinstance(DEFAULT_GEMINI_MODELS, list)
|
|
assert len(DEFAULT_GEMINI_MODELS) > 0
|
|
# Check at least some expected models are in the defaults
|
|
assert any("gemini" in model.lower() for model in DEFAULT_GEMINI_MODELS)
|
|
|
|
@patch("kikotools.tools.gemini_prompt.node.analyze_image_with_gemini")
|
|
def test_generate_prompt_success(self, mock_analyze):
|
|
"""Test successful prompt generation."""
|
|
# Setup
|
|
node = GeminiPromptNode()
|
|
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
|
mock_analyze.return_value = ("A beautiful landscape with mountains", None)
|
|
|
|
# Execute
|
|
result = node.generate_prompt(test_image, "flux", "gemini-2.5-flash")
|
|
|
|
# Assert
|
|
assert result == ("A beautiful landscape with mountains", "")
|
|
mock_analyze.assert_called_once()
|
|
|
|
@patch("kikotools.tools.gemini_prompt.node.analyze_image_with_gemini")
|
|
def test_generate_prompt_sdxl_format(self, mock_analyze):
|
|
"""Test SDXL format with positive and negative prompts."""
|
|
# Setup
|
|
node = GeminiPromptNode()
|
|
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
|
mock_analyze.return_value = (
|
|
"Positive: beautiful landscape, mountains, sunset\nNegative: blurry, low quality",
|
|
None,
|
|
)
|
|
|
|
# Execute
|
|
result = node.generate_prompt(test_image, "sdxl", "gemini-2.5-flash")
|
|
|
|
# Assert
|
|
assert result == (
|
|
"beautiful landscape, mountains, sunset",
|
|
"blurry, low quality",
|
|
)
|
|
|
|
@patch("kikotools.tools.gemini_prompt.node.analyze_image_with_gemini")
|
|
def test_generate_prompt_error(self, mock_analyze):
|
|
"""Test error handling in prompt generation."""
|
|
# Setup
|
|
node = GeminiPromptNode()
|
|
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
|
mock_analyze.return_value = ("", "API key not found")
|
|
|
|
# Execute
|
|
result = node.generate_prompt(test_image, "flux", "gemini-2.5-flash")
|
|
|
|
# Assert
|
|
assert result[0].startswith("Error:")
|
|
assert result[1] == ""
|
|
|
|
def test_invalid_prompt_type(self):
|
|
"""Test handling of invalid prompt type."""
|
|
node = GeminiPromptNode()
|
|
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
|
|
|
with pytest.raises(ValueError, match="Invalid prompt type"):
|
|
node.generate_prompt(test_image, "invalid_type", "gemini-2.5-flash")
|
|
|
|
|
|
class TestGeminiLogic:
|
|
"""Test cases for Gemini logic functions."""
|
|
|
|
def test_tensor_to_pil(self):
|
|
"""Test tensor to PIL conversion."""
|
|
# Test 4D tensor
|
|
tensor_4d = np.random.rand(1, 64, 64, 3)
|
|
result = tensor_to_pil(tensor_4d)
|
|
assert isinstance(result, Image.Image)
|
|
assert result.size == (64, 64)
|
|
assert result.mode == "RGB"
|
|
|
|
# Test 3D tensor
|
|
tensor_3d = np.random.rand(64, 64, 3)
|
|
result = tensor_to_pil(tensor_3d)
|
|
assert isinstance(result, Image.Image)
|
|
assert result.size == (64, 64)
|
|
|
|
def test_image_to_base64(self):
|
|
"""Test image to base64 conversion."""
|
|
# Create test image
|
|
image = Image.new("RGB", (64, 64), color="red")
|
|
|
|
# Convert to base64
|
|
result = image_to_base64(image)
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
# Test JPEG format
|
|
result_jpeg = image_to_base64(image, format="JPEG")
|
|
assert isinstance(result_jpeg, str)
|
|
assert (
|
|
result != result_jpeg
|
|
) # Different formats should produce different results
|
|
|
|
@patch.dict("os.environ", {"GEMINI_API_KEY": "test_key_123"})
|
|
def test_get_api_key_from_env(self):
|
|
"""Test getting API key from environment."""
|
|
result = get_api_key()
|
|
assert result == "test_key_123"
|
|
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
@patch("os.path.exists")
|
|
@patch("builtins.open")
|
|
def test_get_api_key_from_config(self, mock_open, mock_exists):
|
|
"""Test getting API key from config file."""
|
|
# Setup
|
|
mock_exists.return_value = True
|
|
mock_open.return_value.__enter__.return_value.read.return_value = (
|
|
'{"api_key": "config_key_456"}'
|
|
)
|
|
|
|
# Execute
|
|
result = get_api_key()
|
|
|
|
# Assert
|
|
assert result == "config_key_456"
|
|
|
|
def test_validate_prompt_type(self):
|
|
"""Test prompt type validation."""
|
|
# Valid types
|
|
for prompt_type in PROMPT_OPTIONS:
|
|
assert validate_prompt_type(prompt_type) is True
|
|
|
|
# Invalid types
|
|
assert validate_prompt_type("invalid") is False
|
|
assert validate_prompt_type("") is False
|
|
assert validate_prompt_type(None) is False
|
|
|
|
@pytest.mark.skip(reason="Requires google-generativeai library")
|
|
def test_analyze_image_with_gemini_success(self):
|
|
"""Test successful image analysis with Gemini."""
|
|
pass # Skipped as it requires google-generativeai
|
|
|
|
def test_analyze_image_no_api_key(self):
|
|
"""Test analysis without API key."""
|
|
test_image = np.random.rand(64, 64, 3)
|
|
|
|
with patch(
|
|
"kikotools.tools.gemini_prompt.logic.get_api_key", return_value=None
|
|
):
|
|
result, error = analyze_image_with_gemini(test_image, "flux")
|
|
|
|
assert result == ""
|
|
assert "API key not found" in error
|
|
|
|
@pytest.mark.skip(reason="Requires google-generativeai library")
|
|
def test_analyze_image_with_custom_prompt(self):
|
|
"""Test analysis with custom prompt."""
|
|
pass # Skipped as it requires google-generativeai
|
|
|
|
|
|
class TestPromptTemplates:
|
|
"""Test prompt template configurations."""
|
|
|
|
def test_all_prompt_types_have_templates(self):
|
|
"""Test that all prompt options have corresponding templates."""
|
|
for prompt_type in PROMPT_OPTIONS:
|
|
assert prompt_type in PROMPT_TEMPLATES
|
|
assert isinstance(PROMPT_TEMPLATES[prompt_type], str)
|
|
assert len(PROMPT_TEMPLATES[prompt_type]) > 0
|
|
|
|
def test_prompt_template_content(self):
|
|
"""Test that prompt templates contain expected content."""
|
|
# FLUX prompt should mention FLUX
|
|
assert "FLUX" in PROMPT_TEMPLATES["flux"]
|
|
|
|
# SDXL prompt should mention positive and negative
|
|
assert "Positive" in PROMPT_TEMPLATES["sdxl"]
|
|
assert "Negative" in PROMPT_TEMPLATES["sdxl"]
|
|
|
|
# Danbooru should mention tags and underscores
|
|
assert "tag" in PROMPT_TEMPLATES["danbooru"].lower()
|
|
assert "underscore" in PROMPT_TEMPLATES["danbooru"].lower()
|
|
|
|
# Video should mention movement or motion and dynamics
|
|
assert (
|
|
"movement" in PROMPT_TEMPLATES["video"].lower()
|
|
or "motion" in PROMPT_TEMPLATES["video"].lower()
|
|
)
|
|
assert (
|
|
"dynamic" in PROMPT_TEMPLATES["video"].lower()
|
|
) # Check for dynamics instead of temporal
|