Trim and tighten code comments and docstrings across the repository. Comment-only: every changed file verified code-identical to main via AST/token comparison.
373 lines
11 KiB
Python
373 lines
11 KiB
Python
# 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),
|
|
}
|
|
]
|