"""Tests for the AgentCLI module (py/agent_cli/). All tests mock the underlying services (scanner, MetadataManager, downloader) since the AgentCLI is a thin delegation layer. Mock targets must match where imports are resolved inside each function (lazy imports via ``from X import Y`` inside function body). """ from __future__ import annotations from unittest import mock import pytest from py.agent_cli import ( list_base_models, read_metadata, apply_metadata_updates, download_preview, refresh_cache, ) # ====================================================================== # Helpers # ====================================================================== class MockCache: def __init__(self, raw_data: list[dict] | None = None): self.raw_data = raw_data or [] class MockScanner: """Simulates a ModelScanner for testing.""" def __init__(self, raw_data: list[dict] | None = None): self._raw_data = raw_data or [] self.update_single_model_cache = mock.AsyncMock(return_value=True) async def get_cached_data(self): return MockCache(self._raw_data) # ====================================================================== # list_base_models -- imports ServiceRegistry internally # ====================================================================== class TestListBaseModels: _MOCK_MODELS = ["SDXL 1.0", "Flux.1 D", "SD 1.5"] @pytest.mark.asyncio async def test_returns_all_models(self): """Verifies the function delegates to CivitaiBaseModelService. Uses a monkey-patch on ``get_instance`` to return a controlled mock so we don't need to work around ``mock.patch``'s dotted-path limitations with lazy imports inside function bodies.""" import py.services.civitai_base_model_service as _svc orig = _svc.CivitaiBaseModelService.get_instance mock_svc = mock.AsyncMock() mock_svc.get_base_models.return_value = { "models": self._MOCK_MODELS, } _svc.CivitaiBaseModelService.get_instance = mock.AsyncMock( return_value=mock_svc, ) try: result = await list_base_models() assert result == self._MOCK_MODELS finally: _svc.CivitaiBaseModelService.get_instance = orig @pytest.mark.asyncio async def test_limit(self): import py.services.civitai_base_model_service as _svc orig = _svc.CivitaiBaseModelService.get_instance mock_svc = mock.AsyncMock() mock_svc.get_base_models.return_value = {"models": ["A", "B", "C"]} _svc.CivitaiBaseModelService.get_instance = mock.AsyncMock( return_value=mock_svc, ) try: result = await list_base_models(limit=2) assert result == ["A", "B"] finally: _svc.CivitaiBaseModelService.get_instance = orig @pytest.mark.asyncio async def test_empty_list_when_service_returns_empty(self): import py.services.civitai_base_model_service as _svc orig = _svc.CivitaiBaseModelService.get_instance mock_svc = mock.AsyncMock() mock_svc.get_base_models.return_value = {"models": []} _svc.CivitaiBaseModelService.get_instance = mock.AsyncMock( return_value=mock_svc, ) try: result = await list_base_models() assert result == [] finally: _svc.CivitaiBaseModelService.get_instance = orig @pytest.mark.asyncio async def test_handles_exception(self): import py.services.civitai_base_model_service as _svc orig = _svc.CivitaiBaseModelService.get_instance mock_svc = mock.AsyncMock() mock_svc.get_base_models.side_effect = RuntimeError("API error") _svc.CivitaiBaseModelService.get_instance = mock.AsyncMock( return_value=mock_svc, ) try: result = await list_base_models() assert result == [] finally: _svc.CivitaiBaseModelService.get_instance = orig # ====================================================================== # read_metadata -- imports MetadataManager from py.utils.metadata_manager # ====================================================================== class TestReadMetadata: @pytest.mark.asyncio async def test_delegates_to_metadata_manager(self): fake = {"file_name": "test", "base_model": "SDXL 1.0"} with mock.patch("py.utils.metadata_manager.MetadataManager") as mm: mm.load_metadata_payload = mock.AsyncMock(return_value=fake) result = await read_metadata("/p.safetensors") assert result == fake @pytest.mark.asyncio async def test_exception_returns_empty_dict(self): with mock.patch("py.utils.metadata_manager.MetadataManager") as mm: mm.load_metadata_payload = mock.AsyncMock(side_effect=ValueError("x")) result = await read_metadata("/p.safetensors") assert result == {} @pytest.mark.asyncio async def test_none_coerces_to_empty_dict(self): with mock.patch("py.utils.metadata_manager.MetadataManager") as mm: mm.load_metadata_payload = mock.AsyncMock(return_value=None) result = await read_metadata("/p.safetensors") assert result == {} # ====================================================================== # apply_metadata_updates -- uses read_metadata + MetadataManager.save_metadata # ====================================================================== class TestApplyMetadataUpdates: @pytest.mark.asyncio async def test_updates_field(self): with ( mock.patch("py.agent_cli.read_metadata") as mock_read, mock.patch("py.utils.metadata_manager.MetadataManager") as mm, ): mock_read.return_value = {"base_model": "", "tags": []} mm.save_metadata = mock.AsyncMock(return_value=True) updated = await apply_metadata_updates( "/p.safetensors", {"base_model": "Flux.1 D"} ) assert updated == ["base_model"] mm.save_metadata.assert_awaited_once_with( "/p.safetensors", {"base_model": "Flux.1 D", "tags": []}, ) @pytest.mark.asyncio async def test_noop_when_value_unchanged(self): with ( mock.patch("py.agent_cli.read_metadata") as mock_read, mock.patch("py.utils.metadata_manager.MetadataManager") as mm, ): mock_read.return_value = {"base_model": "Flux.1 D"} updated = await apply_metadata_updates( "/p.safetensors", {"base_model": "Flux.1 D"} ) assert updated == [] mm.save_metadata.assert_not_called() @pytest.mark.asyncio async def test_multiple_fields(self): with ( mock.patch("py.agent_cli.read_metadata") as mock_read, mock.patch("py.utils.metadata_manager.MetadataManager") as mm, ): mm.save_metadata = mock.AsyncMock(return_value=True) mock_read.return_value = { "base_model": "", "modelDescription": "", "tags": [], } updated = await apply_metadata_updates( "/p.safetensors", {"base_model": "SDXL 1.0", "modelDescription": "A", "tags": ["flux"]}, ) assert sorted(updated) == sorted(["base_model", "modelDescription", "tags"]) saved = mm.save_metadata.call_args[0][1] assert saved["base_model"] == "SDXL 1.0" @pytest.mark.asyncio async def test_empty_updates_noop(self): with ( mock.patch("py.agent_cli.read_metadata"), mock.patch("py.utils.metadata_manager.MetadataManager") as mm, ): updated = await apply_metadata_updates("/p.safetensors", {}) assert updated == [] mm.save_metadata.assert_not_called() # ====================================================================== # download_preview -- imports get_downloader + ExifUtils # ====================================================================== class TestDownloadPreview: @pytest.mark.asyncio async def test_empty_url_returns_none(self, tmp_path): mp = tmp_path / "m.safetensors" mp.write_bytes(b"fake") assert await download_preview(str(mp), "") is None assert await download_preview(str(mp), " ") is None @pytest.mark.asyncio async def test_successful_download_and_optimise(self, tmp_path): mp = tmp_path / "t.safetensors" mp.write_bytes(b"fake") with ( mock.patch("py.services.downloader.get_downloader") as get_dl, mock.patch("py.utils.exif_utils.ExifUtils") as exif, ): dl = mock.AsyncMock() dl.download_to_memory = mock.AsyncMock(return_value=(True, b"raw", {})) get_dl.return_value = dl exif.optimize_image.return_value = (b"optimized_webp", {}) result = await download_preview(str(mp), "https://ex.com/i.png") assert result == str(tmp_path / "t.webp") assert (tmp_path / "t.webp").exists() assert (tmp_path / "t.webp").read_bytes() == b"optimized_webp" @pytest.mark.asyncio async def test_download_failure_returns_none(self, tmp_path): mp = tmp_path / "t.safetensors" mp.write_bytes(b"fake") with mock.patch("py.services.downloader.get_downloader") as get_dl: dl = mock.AsyncMock() dl.download_to_memory = mock.AsyncMock(return_value=(False, None, {})) dl.download_file = mock.AsyncMock(return_value=(False, None)) get_dl.return_value = dl result = await download_preview(str(mp), "https://ex.com/i.png") assert result is None assert not (tmp_path / "t.webp").exists() # ====================================================================== # refresh_cache -- uses _find_scanner_for_model (ServiceRegistry) # ====================================================================== class TestRefreshCache: @pytest.mark.asyncio async def test_found_and_refreshed(self): scanner = MockScanner([{"file_path": "/some/path.safetensors"}]) with ( mock.patch( "py.services.service_registry.ServiceRegistry", get_lora_scanner=mock.AsyncMock(return_value=scanner), get_checkpoint_scanner=mock.AsyncMock(return_value=None), get_embedding_scanner=mock.AsyncMock(return_value=None), ), mock.patch("py.agent_cli.read_metadata") as mock_read, ): mock_read.return_value = {"base_model": "SDXL 1.0"} result = await refresh_cache("/some/path.safetensors") assert result is True scanner.update_single_model_cache.assert_awaited_once() @pytest.mark.asyncio async def test_not_found_in_any_scanner(self): scanner = MockScanner([]) with mock.patch( "py.services.service_registry.ServiceRegistry", get_lora_scanner=mock.AsyncMock(return_value=scanner), get_checkpoint_scanner=mock.AsyncMock(return_value=None), get_embedding_scanner=mock.AsyncMock(return_value=None), ): result = await refresh_cache("/nonexistent/path.safetensors") assert result is False @pytest.mark.asyncio async def test_no_metadata_returns_false(self): scanner = MockScanner([{"file_path": "/some/path.safetensors"}]) with ( mock.patch( "py.services.service_registry.ServiceRegistry", get_lora_scanner=mock.AsyncMock(return_value=scanner), get_checkpoint_scanner=mock.AsyncMock(return_value=None), get_embedding_scanner=mock.AsyncMock(return_value=None), ), mock.patch("py.agent_cli.read_metadata") as mock_read, ): mock_read.return_value = {} result = await refresh_cache("/some/path.safetensors") assert result is False # ====================================================================== # convert_readme_to_html — pure function, no mocks needed # ====================================================================== class TestConvertReadmeToHtml: """Tests for the inline markdown→HTML converter.""" def test_empty_input(self): from py.services.agent.skills.enrich_hf_metadata.readme_processor import \ convert_readme_to_html assert convert_readme_to_html("") == "" assert convert_readme_to_html(None) == "" # type: ignore[arg-type] def test_heading(self): from py.services.agent.skills.enrich_hf_metadata.readme_processor import \ convert_readme_to_html result = convert_readme_to_html("# Title") assert "
" in result and "model.train()" in result
def test_fenced_code_block(self):
from py.services.agent.skills.enrich_hf_metadata.readme_processor import \
convert_readme_to_html
md = "```python\nprint('hello')\n```"
result = convert_readme_to_html(md)
assert "" in result
assert "print" in result and "hello" in result
def test_unordered_list(self):
from py.services.agent.skills.enrich_hf_metadata.readme_processor import \
convert_readme_to_html
md = "- item one\n- item two"
result = convert_readme_to_html(md)
assert "" in result
assert "- item one
" in result
assert "- item two
" in result
def test_ordered_list(self):
from py.services.agent.skills.enrich_hf_metadata.readme_processor import \
convert_readme_to_html
md = "1. first\n2. second"
result = convert_readme_to_html(md)
assert "" in result
assert "- first
" in result
assert "- second
" in result
def test_link(self):
from py.services.agent.skills.enrich_hf_metadata.readme_processor import \
convert_readme_to_html
md = "[click here](https://example.com)"
result = convert_readme_to_html(md)
assert 'click here' in result
def test_badge_image_stripped(self):
from py.services.agent.skills.enrich_hf_metadata.readme_processor import \
convert_readme_to_html
md = ""
result = convert_readme_to_html(md)
assert "img.shields.io" not in result
def test_gallery_stripped(self):
from py.services.agent.skills.enrich_hf_metadata.readme_processor import \
convert_readme_to_html
md = "Some text\n \nmore text"
result = convert_readme_to_html(md)
assert "Real content
| A | " in result assert "1 | " in result def test_horizontal_rule(self): from py.services.agent.skills.enrich_hf_metadata.readme_processor import \ convert_readme_to_html md = "before\n\n---\n\nafter" result = convert_readme_to_html(md) assert "
|---|