Skip to main content
Glama
test_generate_image_helpers.py15 kB
""" Property-based tests for generate_image helper functions. **Feature: service-layer-refactoring** **Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.6** This module tests: - Property 6: Input Path Collection - Property 7: Input Validation - Property 8: Mode Detection - Property 9: Model Selection (partial - requires service mocking) - Property 10: Response Building """ import os import tempfile from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st import pytest from banana_image_mcp.config.settings import ModelTier from banana_image_mcp.core.exceptions import ValidationError from banana_image_mcp.tools.generate_image import ( _build_structured_content, _build_summary, _collect_input_paths, _detect_mode, _validate_inputs, ) # ============================================================================= # Hypothesis Strategies # ============================================================================= # Strategy for generating file paths (non-empty strings) path_strategy = st.text( min_size=1, max_size=100, alphabet=st.characters(whitelist_categories=("L", "N", "Pc"), whitelist_characters="/-_."), ) # Strategy for optional paths optional_path_strategy = st.one_of(st.none(), path_strategy) # Strategy for mode values mode_strategy = st.sampled_from(["auto", "generate", "edit"]) # Strategy for invalid mode values invalid_mode_strategy = st.text(min_size=1, max_size=20).filter( lambda x: x not in ["auto", "generate", "edit"] ) # Strategy for file IDs file_id_strategy = st.one_of( st.none(), st.text(min_size=5, max_size=50, alphabet=st.characters(whitelist_categories=("L", "N"))), ) # ============================================================================= # Property 6: Input Path Collection # ============================================================================= class TestInputPathCollection: """ **Feature: service-layer-refactoring, Property 6: Input Path Collection** *For any* combination of three optional path strings, `_collect_input_paths()` SHALL return a list containing only the non-None values, or None if all inputs are None. """ @given( path1=optional_path_strategy, path2=optional_path_strategy, path3=optional_path_strategy, ) @settings(max_examples=100) def test_collects_non_none_paths( self, path1: str | None, path2: str | None, path3: str | None, ): """ **Feature: service-layer-refactoring, Property 6: Input Path Collection** Should return list of non-None paths. """ result = _collect_input_paths(path1, path2, path3) # Count non-None inputs non_none_inputs = [p for p in [path1, path2, path3] if p] if non_none_inputs: assert result is not None assert len(result) == len(non_none_inputs) for p in non_none_inputs: assert p in result else: assert result is None def test_all_none_returns_none(self): """ **Feature: service-layer-refactoring, Property 6: Input Path Collection** Should return None when all inputs are None. """ result = _collect_input_paths(None, None, None) assert result is None def test_single_path_returns_list(self): """ **Feature: service-layer-refactoring, Property 6: Input Path Collection** Should return list with single path. """ result = _collect_input_paths("/path/to/image.png", None, None) assert result == ["/path/to/image.png"] def test_preserves_order(self): """ **Feature: service-layer-refactoring, Property 6: Input Path Collection** Should preserve order of paths. """ result = _collect_input_paths("/first.png", "/second.png", "/third.png") assert result == ["/first.png", "/second.png", "/third.png"] # ============================================================================= # Property 7: Input Validation # ============================================================================= class TestInputValidation: """ **Feature: service-layer-refactoring, Property 7: Input Validation** *For any* mode, input_paths, and file_id combination: - Invalid mode values SHALL raise ValidationError - Non-existent paths SHALL raise ValidationError - Exceeding MAX_INPUT_IMAGES SHALL raise ValidationError - Valid inputs SHALL not raise any exception """ @given(mode=invalid_mode_strategy) @settings(max_examples=50) def test_invalid_mode_raises_error(self, mode: str): """ **Feature: service-layer-refactoring, Property 7: Input Validation** Invalid mode values should raise ValidationError. """ with pytest.raises(ValidationError) as exc_info: _validate_inputs(mode, None, None) assert "mode" in str(exc_info.value).lower() or exc_info.value.field == "mode" @given(mode=mode_strategy) @settings(max_examples=50) def test_valid_mode_no_error(self, mode: str): """ **Feature: service-layer-refactoring, Property 7: Input Validation** Valid mode values should not raise error. """ # Should not raise _validate_inputs(mode, None, None) def test_nonexistent_path_raises_error(self): """ **Feature: service-layer-refactoring, Property 7: Input Validation** Non-existent paths should raise ValidationError. """ with pytest.raises(ValidationError) as exc_info: _validate_inputs("auto", ["/nonexistent/path/image.png"], None) assert "not found" in str(exc_info.value).lower() def test_exceeding_max_images_raises_error(self): """ **Feature: service-layer-refactoring, Property 7: Input Validation** Exceeding MAX_INPUT_IMAGES should raise ValidationError. """ # Create temporary files with tempfile.TemporaryDirectory() as tmpdir: paths = [] for i in range(10): # More than MAX_INPUT_IMAGES (3) path = os.path.join(tmpdir, f"image_{i}.png") with open(path, "wb") as f: f.write(b"fake image data") paths.append(path) with pytest.raises(ValidationError) as exc_info: _validate_inputs("auto", paths, None) assert "maximum" in str(exc_info.value).lower() def test_valid_existing_paths_no_error(self): """ **Feature: service-layer-refactoring, Property 7: Input Validation** Valid existing paths should not raise error. """ with tempfile.TemporaryDirectory() as tmpdir: path = os.path.join(tmpdir, "test_image.png") with open(path, "wb") as f: f.write(b"fake image data") # Should not raise _validate_inputs("auto", [path], None) # ============================================================================= # Property 8: Mode Detection # ============================================================================= class TestModeDetection: """ **Feature: service-layer-refactoring, Property 8: Mode Detection** *For any* mode, file_id, and input_paths combination: - When mode is "auto" and file_id is provided, detected mode SHALL be "edit" - When mode is "auto" and single input_path is provided, detected mode SHALL be "edit" - When mode is "auto" and no file_id and multiple/no input_paths, detected mode SHALL be "generate" - When mode is explicit ("edit" or "generate"), detected mode SHALL match """ @given(file_id=st.text(min_size=5, max_size=20)) @settings(max_examples=50) def test_auto_with_file_id_returns_edit(self, file_id: str): """ **Feature: service-layer-refactoring, Property 8: Mode Detection** Auto mode with file_id should return "edit". """ result = _detect_mode("auto", file_id, None) assert result == "edit" def test_auto_with_single_path_returns_edit(self): """ **Feature: service-layer-refactoring, Property 8: Mode Detection** Auto mode with single input path should return "edit". """ result = _detect_mode("auto", None, ["/path/to/image.png"]) assert result == "edit" def test_auto_with_no_inputs_returns_generate(self): """ **Feature: service-layer-refactoring, Property 8: Mode Detection** Auto mode with no inputs should return "generate". """ result = _detect_mode("auto", None, None) assert result == "generate" def test_auto_with_multiple_paths_returns_generate(self): """ **Feature: service-layer-refactoring, Property 8: Mode Detection** Auto mode with multiple input paths should return "generate". """ result = _detect_mode("auto", None, ["/path1.png", "/path2.png"]) assert result == "generate" @given(mode=st.sampled_from(["edit", "generate"])) @settings(max_examples=50) def test_explicit_mode_returns_same(self, mode: str): """ **Feature: service-layer-refactoring, Property 8: Mode Detection** Explicit mode should return the same mode. """ result = _detect_mode(mode, None, None) assert result == mode @given( mode=st.sampled_from(["edit", "generate"]), file_id=file_id_strategy, ) @settings(max_examples=50) def test_explicit_mode_ignores_inputs(self, mode: str, file_id: str | None): """ **Feature: service-layer-refactoring, Property 8: Mode Detection** Explicit mode should ignore file_id and input_paths. """ result = _detect_mode(mode, file_id, ["/some/path.png"]) assert result == mode # ============================================================================= # Property 10: Response Building # ============================================================================= class TestResponseBuilding: """ **Feature: service-layer-refactoring, Property 10: Response Building** *For any* valid metadata list and model info, `_build_response()` SHALL return a ToolResult containing: - TextContent with summary - Thumbnail images - structured_content with mode, model_tier, and images fields """ @pytest.fixture def sample_metadata(self): """Sample metadata for testing.""" return [ { "full_path": "/tmp/image_001.png", "size_bytes": 1024 * 100, "width": 1024, "height": 768, "files_api": {"name": "files/abc123"}, } ] @pytest.fixture def sample_model_info(self): """Sample model info for testing.""" return { "name": "Gemini Flash", "emoji": "⚡", "model_id": "gemini-2.5-flash-image", } def test_build_summary_contains_required_info(self, sample_metadata, sample_model_info): """ **Feature: service-layer-refactoring, Property 10: Response Building** Summary should contain required information. """ summary = _build_summary( mode="generate", metadata=sample_metadata, model_info=sample_model_info, selected_tier=ModelTier.FLASH, thinking_level="high", resolution="high", enable_grounding=False, file_id=None, input_paths=None, aspect_ratio=None, ) # Should contain key information assert "Generated" in summary assert "1 image" in summary assert "Gemini Flash" in summary assert "FLASH" in summary def test_build_summary_edit_mode(self, sample_metadata, sample_model_info): """ **Feature: service-layer-refactoring, Property 10: Response Building** Summary should reflect edit mode. """ summary = _build_summary( mode="edit", metadata=sample_metadata, model_info=sample_model_info, selected_tier=ModelTier.FLASH, thinking_level="high", resolution="high", enable_grounding=False, file_id="files/abc123", input_paths=None, aspect_ratio=None, ) assert "Edited" in summary assert "files/abc123" in summary def test_build_structured_content_contains_required_fields( self, sample_metadata, sample_model_info ): """ **Feature: service-layer-refactoring, Property 10: Response Building** Structured content should contain required fields. """ content = _build_structured_content( mode="generate", metadata=sample_metadata, model_info=sample_model_info, selected_tier=ModelTier.FLASH, tier=ModelTier.AUTO, model_tier="auto", thinking_level="high", resolution="high", enable_grounding=False, n=1, thumbnail_count=1, negative_prompt=None, input_paths=None, file_id=None, aspect_ratio=None, prompt="test prompt", ) # Required fields assert content["mode"] == "generate" assert content["model_tier"] == "flash" assert "images" in content assert content["images"] == sample_metadata @given(mode=st.sampled_from(["generate", "edit"])) @settings( max_examples=20, suppress_health_check=[HealthCheck.function_scoped_fixture], ) def test_build_structured_content_mode_specific_fields( self, sample_metadata, sample_model_info, mode: str ): """ **Feature: service-layer-refactoring, Property 10: Response Building** Structured content should have mode-specific fields. """ content = _build_structured_content( mode=mode, metadata=sample_metadata, model_info=sample_model_info, selected_tier=ModelTier.FLASH, tier=ModelTier.AUTO, model_tier="auto", thinking_level="high", resolution="high", enable_grounding=False, n=1, thumbnail_count=1, negative_prompt=None, input_paths=None, file_id=None, aspect_ratio=None, prompt="test prompt", ) if mode == "edit": assert content["edit_instruction"] == "test prompt" assert content["generation_prompt"] is None else: assert content["generation_prompt"] == "test prompt" assert content["edit_instruction"] is None

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zengwenliang416/banana-image-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server