diff --git a/py/services/tag_update_service.py b/py/services/tag_update_service.py index d560e7d6..35a0c5a4 100644 --- a/py/services/tag_update_service.py +++ b/py/services/tag_update_service.py @@ -33,7 +33,8 @@ class TagUpdateService: tags_added: List[str] = [] for tag in new_tags: if isinstance(tag, str) and tag.strip(): - normalized = tag.strip() + # Convert all tags to lowercase to avoid case sensitivity issues on Windows + normalized = tag.strip().lower() if normalized.lower() not in existing_lower: existing_tags.append(normalized) existing_lower.append(normalized.lower()) diff --git a/py/utils/utils.py b/py/utils/utils.py index 77b2a6c2..2d7ea125 100644 --- a/py/utils/utils.py +++ b/py/utils/utils.py @@ -205,7 +205,9 @@ def calculate_relative_path_for_model(model_data: Dict, model_type: str = 'lora' base_model_mappings = settings_manager.get('base_model_path_mappings', {}) mapped_base_model = base_model_mappings.get(base_model, base_model) - first_tag = settings_manager.resolve_priority_tag_for_model(model_tags, model_type) + # Convert all tags to lowercase to avoid case sensitivity issues on Windows + lowercase_tags = [tag.lower() for tag in model_tags if isinstance(tag, str)] + first_tag = settings_manager.resolve_priority_tag_for_model(lowercase_tags, model_type) if not first_tag: first_tag = 'no tags' # Default if no tags available diff --git a/tests/services/test_route_support_services.py b/tests/services/test_route_support_services.py index aa10e483..9f95b3f3 100644 --- a/tests/services/test_route_support_services.py +++ b/tests/services/test_route_support_services.py @@ -240,7 +240,7 @@ def test_download_coordinator_emits_progress() -> None: def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None: metadata_path = tmp_path / "model.metadata.json" - metadata_path.write_text(json.dumps({"tags": ["Existing"]})) + metadata_path.write_text(json.dumps({"tags": ["existing"]})) async def loader(path: str) -> Dict[str, Any]: return json.loads(Path(path).read_text()) @@ -258,12 +258,12 @@ def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None: tags = asyncio.run( service.add_tags( file_path=str(tmp_path / "model.safetensors"), - new_tags=["New", "existing"], + new_tags=["new", "existing"], metadata_loader=loader, update_cache=update_cache, ) ) - assert tags == ["Existing", "New"] + assert tags == ["existing", "new"] assert manager.saved assert cache_updates diff --git a/tests/services/test_tag_case_sensitivity.py b/tests/services/test_tag_case_sensitivity.py new file mode 100644 index 00000000..3396d6b3 --- /dev/null +++ b/tests/services/test_tag_case_sensitivity.py @@ -0,0 +1,96 @@ +"""Tests for tag case sensitivity handling to prevent issues on Windows.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict + +import pytest + +from py.services.tag_update_service import TagUpdateService + + +class RecordingMetadataManager: + def __init__(self) -> None: + self.saved: list[tuple[str, Dict[str, Any]]] = [] + + async def save_metadata(self, path: str, metadata: Dict[str, Any]) -> bool: + self.saved.append((path, json.loads(json.dumps(metadata)))) + return True + + +class DummyProvider: + async def __call__(self, path: str) -> Dict[str, Any]: + return {"tags": []} + + +@pytest.mark.asyncio +async def test_tag_update_service_handles_case_insensitive_tags(tmp_path: Path) -> None: + """Test that tag update service treats tags case-insensitively.""" + metadata_path = tmp_path / "model.metadata.json" + metadata_path.write_text(json.dumps({"tags": ["test"]})) + + async def loader(path: str) -> Dict[str, Any]: + return json.loads(Path(path).read_text()) + + manager = RecordingMetadataManager() + service = TagUpdateService(metadata_manager=manager) + + cache_updates: list[Dict[str, Any]] = [] + + async def update_cache(original: str, new: str, metadata: Dict[str, Any]) -> bool: + cache_updates.append(metadata) + return True + + # Try to add "Test" (different case) - should not be added since "test" already exists + tags = await service.add_tags( + file_path=str(tmp_path / "model.safetensors"), + new_tags=["Test"], + metadata_loader=loader, + update_cache=update_cache, + ) + + # Should still only have "test" (lowercase) in the tags + assert tags == ["test"] + assert len(manager.saved) == 1 + saved_metadata = manager.saved[0][1] + assert saved_metadata["tags"] == ["test"] + + +@pytest.mark.asyncio +async def test_tag_update_service_adds_new_tags_in_lowercase(tmp_path: Path) -> None: + """Test that new tags are stored in lowercase.""" + metadata_path = tmp_path / "model.metadata.json" + metadata_path.write_text(json.dumps({"tags": ["existing"]})) + + async def loader(path: str) -> Dict[str, Any]: + return json.loads(Path(path).read_text()) + + manager = RecordingMetadataManager() + service = TagUpdateService(metadata_manager=manager) + + cache_updates: list[Dict[str, Any]] = [] + + async def update_cache(original: str, new: str, metadata: Dict[str, Any]) -> bool: + cache_updates.append(metadata) + return True + + # Add new tags with mixed case + tags = await service.add_tags( + file_path=str(tmp_path / "model.safetensors"), + new_tags=["NewTag", "ANOTHER_TAG"], + metadata_loader=loader, + update_cache=update_cache, + ) + + # New tags should be stored in lowercase + assert "existing" in tags + assert "newtag" in tags + assert "another_tag" in tags + assert len(manager.saved) == 1 + saved_metadata = manager.saved[0][1] + assert "newtag" in saved_metadata["tags"] + assert "another_tag" in saved_metadata["tags"] + # Ensure all tags are lowercase + for tag in saved_metadata["tags"]: + assert tag == tag.lower() \ No newline at end of file diff --git a/tests/utils/test_utils_case_sensitivity.py b/tests/utils/test_utils_case_sensitivity.py new file mode 100644 index 00000000..168ddeba --- /dev/null +++ b/tests/utils/test_utils_case_sensitivity.py @@ -0,0 +1,50 @@ +"""Tests for utils module case sensitivity handling.""" + +import pytest +from unittest.mock import Mock + +from py.utils.utils import calculate_relative_path_for_model +from py.services.settings_manager import SettingsManager + + +def test_calculate_relative_path_handles_case_insensitive_tags(): + """Test that calculate_relative_path_for_model handles case insensitive tags correctly.""" + # Create a mock settings manager + mock_settings = Mock(spec=SettingsManager) + mock_settings.get.return_value = {} # base_model_path_mappings + mock_settings.resolve_priority_tag_for_model.return_value = "test" + mock_settings.get_download_path_template.return_value = "{base_model}/{first_tag}" + + # Mock the settings manager function + import py.utils.utils as utils_module + original_get_settings_manager = utils_module.get_settings_manager + utils_module.get_settings_manager = Mock(return_value=mock_settings) + + try: + # Test model data with mixed case tags + model_data = { + "base_model": "SDXL", + "tags": ["Test", "ANOTHER_TAG"], # Mixed case tags + "model_name": "Test Model" + } + + model_type = "lora" + + # Call the function + result = calculate_relative_path_for_model(model_data, model_type) + + # Verify that resolve_priority_tag_for_model was called with lowercase tags + called_args = mock_settings.resolve_priority_tag_for_model.call_args[0] + lowercase_tags = called_args[0] + + # Check that tags are converted to lowercase + assert all(tag == tag.lower() for tag in lowercase_tags) + assert "test" in lowercase_tags + assert "another_tag" in lowercase_tags + + # Verify the result format + assert result == "SDXL/test" + + finally: + # Restore original function + utils_module.get_settings_manager = original_get_settings_manager \ No newline at end of file