# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 import asyncio import sys import types from pathlib import Path from types import SimpleNamespace # Keep this test runnable in lightweight environments where optional logging # deps are not installed. if "structlog" not in sys.modules: class _DummyLogger: def __getattr__(self, _name): return lambda *args, **kwargs: None sys.modules["structlog"] = types.SimpleNamespace( BoundLogger = _DummyLogger, get_logger = lambda *args, **kwargs: _DummyLogger(), ) import routes.models as models_route def _repo( repo_id: str, files: list[SimpleNamespace], repo_path: Path, *, revisions: list[SimpleNamespace] | None = None, ) -> SimpleNamespace: return SimpleNamespace( repo_id = repo_id, repo_type = "model", repo_path = repo_path, revisions = revisions or [SimpleNamespace(files = files)], ) def _file( name: str, size_on_disk: int, *, blob_path: str | None = None, ) -> SimpleNamespace: return SimpleNamespace( file_name = name, size_on_disk = size_on_disk, blob_path = blob_path, ) def test_iter_gguf_paths_matches_extension_case_insensitively(tmp_path): nested = tmp_path / "snapshots" / "rev" nested.mkdir(parents = True) lower = nested / "Q4_K_M.gguf" upper = nested / "Q8_0.GGUF" other = nested / "README.md" lower.write_text("a") upper.write_text("b") other.write_text("c") result = sorted(path.name for path in models_route._iter_gguf_paths(tmp_path)) assert result == ["Q4_K_M.gguf", "Q8_0.GGUF"] def test_list_cached_gguf_includes_non_suffix_repo_when_cache_contains_gguf( monkeypatch, tmp_path ): repo = _repo( "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive", [_file("Q4_K_M.gguf", 5_000), _file("README.md", 10)], tmp_path / "models--HauhauCS--Gemma", ) scan = SimpleNamespace(repos = [repo]) monkeypatch.setattr(models_route, "_all_hf_cache_scans", lambda: [scan]) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive", "size_bytes": 5_000, "cache_path": str(repo.repo_path), } ] def test_list_cached_gguf_matches_extension_case_insensitively(monkeypatch, tmp_path): repo = _repo( "Org/Model-Without-Suffix", [_file("Q8_0.GGUF", 7_000)], tmp_path / "models--Org--Model-Without-Suffix", ) scan = SimpleNamespace(repos = [repo]) monkeypatch.setattr(models_route, "_all_hf_cache_scans", lambda: [scan]) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/Model-Without-Suffix", "size_bytes": 7_000, "cache_path": str(repo.repo_path), } ] def test_list_cached_gguf_skips_repos_without_positive_gguf_size(monkeypatch, tmp_path): missing = _repo( "Org/ReadmeOnly", [_file("README.md", 10)], tmp_path / "models--Org--ReadmeOnly", ) zero = _repo( "Org/ZeroSize", [_file("Q4_K_M.gguf", 0)], tmp_path / "models--Org--ZeroSize", ) scan = SimpleNamespace(repos = [missing, zero]) monkeypatch.setattr(models_route, "_all_hf_cache_scans", lambda: [scan]) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [] def test_list_cached_gguf_keeps_largest_duplicate_repo_across_scans( monkeypatch, tmp_path ): smaller = _repo( "Org/Dupe", [_file("Q4_K_M.gguf", 2_000)], tmp_path / "models--Org--Dupe-a", ) larger = _repo( "org/dupe", [_file("Q4_K_M.gguf", 5_000), _file("Q6_K.gguf", 1_000)], tmp_path / "models--Org--Dupe-b", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [ SimpleNamespace(repos = [smaller]), SimpleNamespace(repos = [larger]), ], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "org/dupe", "size_bytes": 6_000, "cache_path": str(larger.repo_path), } ] def test_list_cached_gguf_dedupes_shared_blobs_across_revisions(monkeypatch, tmp_path): shared = "blobs/shared-q4" repo = _repo( "Org/SharedBlobRepo", [], tmp_path / "models--Org--SharedBlobRepo", revisions = [ SimpleNamespace(files = [_file("Q4_K_M.gguf", 5_000, blob_path = shared)]), SimpleNamespace(files = [_file("Q4_K_M.gguf", 5_000, blob_path = shared)]), ], ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [repo])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/SharedBlobRepo", "size_bytes": 5_000, "cache_path": str(repo.repo_path), } ] def test_list_cached_models_skips_non_suffix_repo_when_gguf_files_exist( monkeypatch, tmp_path ): mixed = _repo( "Org/MixedRepo", [ _file("Q4_K_M.gguf", 5_000), _file("model.safetensors", 10_000), ], tmp_path / "models--Org--MixedRepo", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [mixed])], ) result = asyncio.run(models_route.list_cached_models(current_subject = "test-user")) assert result["cached"] == [] def test_list_cached_gguf_includes_mixed_repo_with_gguf_and_safetensors( monkeypatch, tmp_path ): """Mirror of the _skips_ test: the mixed repo should still surface in cached-gguf so the picker can show it as a GGUF download.""" mixed = _repo( "Org/MixedRepo", [ _file("Q4_K_M.gguf", 5_000), _file("model.safetensors", 10_000), ], tmp_path / "models--Org--MixedRepo", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [mixed])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/MixedRepo", "size_bytes": 5_000, "cache_path": str(mixed.repo_path), } ] def test_list_cached_gguf_handles_none_size_on_disk(monkeypatch, tmp_path): """A partial/interrupted GGUF download has ``size_on_disk = None``. The route must treat the unknown bytes as zero instead of raising TypeError out of ``sum()`` and wiping the entire response.""" partial = _repo( "Org/PartialDownload", [_file("Q4_K_M.gguf", None), _file("Q6_K.gguf", 5_000)], tmp_path / "models--Org--PartialDownload", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [partial])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/PartialDownload", "size_bytes": 5_000, "cache_path": str(partial.repo_path), } ] def test_list_cached_gguf_skips_malformed_repo_without_wiping_response( monkeypatch, tmp_path ): """One repo raising during classification must not poison the response for every other repo in the scan.""" class _ExplodingRepo: repo_id = "Org/Broken" repo_type = "model" repo_path = tmp_path / "models--Org--Broken" @property def revisions(self): raise RuntimeError("boom") healthy = _repo( "Org/Healthy", [_file("Q4_K_M.gguf", 5_000)], tmp_path / "models--Org--Healthy", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [_ExplodingRepo(), healthy])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/Healthy", "size_bytes": 5_000, "cache_path": str(healthy.repo_path), } ] def test_list_cached_gguf_skips_repo_with_only_mmproj_gguf(monkeypatch, tmp_path): """A repo whose only ``.gguf`` artifact is an mmproj vision adapter must not be classified as a GGUF repo: the variant selector filters mmproj out and the picker would otherwise show zero variants.""" mmproj_only = _repo( "Org/MmprojOnly", [ _file("mmproj-Q8_0.gguf", 5_000), _file("model.safetensors", 10_000), ], tmp_path / "models--Org--MmprojOnly", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [mmproj_only])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [] def test_list_cached_models_includes_repo_with_only_mmproj_gguf(monkeypatch, tmp_path): """Mirror of the cached-gguf skip: a safetensors repo with an auxiliary mmproj vision adapter must still surface in cached-models so the user can load it as a normal model.""" mmproj_aux = _repo( "Org/MmprojAux", [ _file("mmproj-Q8_0.gguf", 5_000), _file("model.safetensors", 10_000), ], tmp_path / "models--Org--MmprojAux", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [mmproj_aux])], ) result = asyncio.run(models_route.list_cached_models(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/MmprojAux", "size_bytes": 15_000, } ] def test_list_cached_gguf_includes_vision_repo_with_main_gguf_and_mmproj( monkeypatch, tmp_path ): """A vision-capable GGUF repo (main weight + mmproj adapter) is still a GGUF repo. The reported size is the main weight size; mmproj is excluded from the GGUF-size accounting because it is filtered out at classification time.""" vision_repo = _repo( "Org/VisionGguf", [ _file("Q4_K_M.gguf", 5_000), _file("mmproj-Q8_0.gguf", 1_000), ], tmp_path / "models--Org--VisionGguf", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [vision_repo])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/VisionGguf", "size_bytes": 5_000, "cache_path": str(vision_repo.repo_path), } ]