# 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 without optional logging deps. 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): """Mixed repo still surfaces in cached-gguf 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): """``size_on_disk = None`` (partial download) is treated as zero, not a TypeError from ``sum()`` that wipes the 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.""" 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`` is an mmproj vision adapter is not a GGUF repo: mmproj is filtered out, leaving 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): """A safetensors repo with an auxiliary mmproj adapter still surfaces in cached-models 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 GGUF repo (main weight + mmproj) is a GGUF repo; reported size is the main weight only, since mmproj is filtered at classification.""" 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), } ] def _gfile(name: str, size: int, mtime: float) -> SimpleNamespace: """A cached file carrying a Hugging Face ``blob_last_modified`` timestamp.""" return SimpleNamespace( file_name = name, size_on_disk = size, blob_path = None, blob_last_modified = mtime, ) def test_all_hf_cache_scans_survives_inaccessible_aux_cache(monkeypatch, tmp_path): """An unreadable auxiliary cache (e.g. an inaccessible ``~/.cache/huggingface/hub``) must be skipped, not abort the scan. Regression guard for ``extra.is_dir()`` raising and wiping the response. """ import huggingface_hub import utils.paths as paths_mod active = SimpleNamespace( repos = [_repo("Org/Active", [_file("Q4_K_M.gguf", 5_000)], tmp_path / "active")] ) def _fake_scan(cache_dir = None): if cache_dir is None: return active raise AssertionError("auxiliary scan should have been skipped") class _Boom: def is_dir(self): raise PermissionError(13, "Permission denied") def resolve(self): raise PermissionError(13, "Permission denied") monkeypatch.setattr(huggingface_hub, "scan_cache_dir", _fake_scan) monkeypatch.setattr(paths_mod, "legacy_hf_cache_dir", lambda: _Boom()) monkeypatch.setattr(paths_mod, "hf_default_cache_dir", lambda: _Boom()) scans = models_route._all_hf_cache_scans() assert scans == [active] # End-to-end: the endpoint still returns the active cache's repo. monkeypatch.setattr(models_route, "_all_hf_cache_scans", lambda: [active]) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert result["cached"] == [ { "repo_id": "Org/Active", "size_bytes": 5_000, "cache_path": str(tmp_path / "active"), } ] def test_list_cached_gguf_sorts_newest_first_grouping_by_latest_quant(monkeypatch, tmp_path): """Downloaded is ordered newest-first, and a multi-quant repo is placed by its most recently downloaded quant (``last_modified`` = newest quant).""" older = _repo( "Org/Older", [_gfile("Older-Q4_K_M.gguf", 5_000, 1_000.0)], tmp_path / "models--Org--Older", ) newer = _repo( "Org/Newer", [ _gfile("Newer-Q4_K_M.gguf", 5_000, 2_000.0), _gfile("Newer-Q8_0.gguf", 9_000, 3_000.0), # newest quant in the repo ], tmp_path / "models--Org--Newer", ) monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [older, newer])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) assert [c["repo_id"] for c in result["cached"]] == ["Org/Newer", "Org/Older"] assert result["cached"][0]["last_modified"] == 3_000.0 assert result["cached"][1]["last_modified"] == 1_000.0 def test_list_cached_gguf_dedupe_keeps_newest_timestamp(monkeypatch, tmp_path): """Same repo in two caches with equal size keeps the newest last_modified, regardless of scan order.""" older = _repo("org/dupe", [_gfile("dupe-Q4_K_M.gguf", 5_000, 1_000.0)], tmp_path / "a") newer = _repo("org/dupe", [_gfile("dupe-Q4_K_M.gguf", 5_000, 9_000.0)], tmp_path / "b") for scans in ([older, newer], [newer, older]): # both orders monkeypatch.setattr( models_route, "_all_hf_cache_scans", lambda s = scans: [SimpleNamespace(repos = [s[0]]), SimpleNamespace(repos = [s[1]])], ) result = asyncio.run(models_route.list_cached_gguf(current_subject = "t")) assert len(result["cached"]) == 1 assert result["cached"][0]["last_modified"] == 9_000.0 def test_gguf_variants_mmproj_does_not_mark_quant_downloaded(monkeypatch, tmp_path): """The per-quant 'downloaded' flag is driven by the real weight file in a single snapshot; an mmproj vision adapter (matching a quant label) must not make that quant appear downloaded.""" import huggingface_hub.constants as hf_constants variants = [ SimpleNamespace(filename = "model-Q4_K_M.gguf", quant = "Q4_K_M", size_bytes = 10_000), SimpleNamespace(filename = "model-F16.gguf", quant = "F16", size_bytes = 20_000), ] monkeypatch.setattr( models_route, "list_gguf_variants", lambda repo_id, hf_token = None: (variants, True) ) monkeypatch.setattr(hf_constants, "HF_HUB_CACHE", str(tmp_path)) snap = tmp_path / "models--org--repo" / "snapshots" / "rev" snap.mkdir(parents = True) (snap / "model-Q4_K_M.gguf").write_bytes(b"x" * 10_000) # real weight, fully present (snap / "mmproj-F16.gguf").write_bytes(b"y" * 20_000) # mmproj adapter, label "F16" result = asyncio.run( models_route.get_gguf_variants( repo_id = "org/repo", hf_token = None, current_subject = "test-user" ) ) flags = {v.quant: v.downloaded for v in result.variants} assert flags["Q4_K_M"] is True assert flags["F16"] is False def test_gguf_download_progress_excludes_mmproj(monkeypatch, tmp_path): """A cached mmproj adapter must not count toward a same-label main variant's download progress (mmproj-F16 vs an F16 weight).""" import huggingface_hub.constants as hf_constants monkeypatch.setattr(hf_constants, "HF_HUB_CACHE", str(tmp_path)) snap = tmp_path / "models--org--repo" / "snapshots" / "rev" snap.mkdir(parents = True) (snap / "mmproj-F16.gguf").write_bytes(b"y" * 20_000) # only the adapter on disk result = asyncio.run( models_route.get_gguf_download_progress( repo_id = "org/repo", variant = "F16", expected_bytes = 20_000, current_subject = "test-user", ) ) assert result["downloaded_bytes"] == 0 assert result["progress"] == 0