Files
unslothai--unsloth/studio/backend/tests/test_frontend_resolution.py
T
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

240 lines
9.9 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
"""Tests for the frontend-dist resolver in studio/backend/run.py.
Loads only the relevant helpers via importlib to avoid pulling in
uvicorn / FastAPI / unsloth's deps. Pairs with AST-style test_host_defaults.py.
"""
import ast
import importlib.util
import os
import sys
from pathlib import Path
_RUN_PY = Path(__file__).resolve().parent.parent / "run.py"
_REPO_STUDIO_DIR = _RUN_PY.parent.parent # studio/
def _load_helpers_only():
"""Import just the resolver helpers from run.py, skipping server-side
imports (uvicorn, structlog, etc.)."""
source = _RUN_PY.read_text(encoding = "utf-8")
tree = ast.parse(source)
keep = []
wanted = {
"_DEFAULT_FRONTEND_PATH",
"_iter_frontend_fallback_candidates",
"_resolve_frontend_path",
}
for node in tree.body:
if isinstance(node, (ast.Import, ast.ImportFrom)):
keep.append(node)
elif isinstance(node, ast.Assign):
names = {t.id for t in node.targets if isinstance(t, ast.Name)}
if names & wanted:
keep.append(node)
elif isinstance(node, ast.FunctionDef) and node.name in wanted:
keep.append(node)
module = ast.Module(body = keep, type_ignores = [])
code = compile(module, str(_RUN_PY), "exec")
ns: dict = {"__file__": str(_RUN_PY), "__name__": "_run_helpers_test"}
exec(code, ns)
return ns
def test_resolver_returns_none_when_nothing_exists(tmp_path, monkeypatch):
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(tmp_path / "no_studio"))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
chosen, attempted = helpers["_resolve_frontend_path"](tmp_path / "missing")
assert chosen is None
assert attempted == [tmp_path / "missing"]
def test_resolver_picks_first_existing_candidate(tmp_path, monkeypatch):
dist = tmp_path / "good" / "frontend" / "dist"
dist.mkdir(parents = True)
(dist / "index.html").write_text("<!doctype html>", encoding = "utf-8")
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(tmp_path / "no_studio"))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
chosen, attempted = helpers["_resolve_frontend_path"](dist)
assert chosen == dist
assert attempted[-1] == dist
def test_resolver_falls_back_to_studio_home_site_packages(tmp_path, monkeypatch):
studio_home = tmp_path / "studio_home"
sp_dist = (
studio_home
/ "unsloth_studio"
/ "lib"
/ "python3.13"
/ "site-packages"
/ "studio"
/ "frontend"
/ "dist"
)
sp_dist.mkdir(parents = True)
(sp_dist / "index.html").write_text("<!doctype html>", encoding = "utf-8")
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(studio_home))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
chosen, attempted = helpers["_resolve_frontend_path"](tmp_path / "bogus")
assert chosen is not None
assert chosen.resolve() == sp_dist.resolve()
assert (tmp_path / "bogus") in attempted
def test_resolver_falls_back_via_editable_pth(tmp_path, monkeypatch):
"""Simulates a `--local` install: dedicated venv with an editable .pth
pointing at a cloned repo owning the built dist."""
studio_home = tmp_path / "studio_home"
sp = studio_home / "unsloth_studio" / "lib" / "python3.13" / "site-packages"
sp.mkdir(parents = True)
repo_root = tmp_path / "clone"
repo_studio = repo_root / "studio"
repo_dist = repo_studio / "frontend" / "dist"
repo_dist.mkdir(parents = True)
(repo_dist / "index.html").write_text("<!doctype html>", encoding = "utf-8")
# Minimal `__editable___pkg_finder.py` with the MAPPING dict that
# setuptools' editable install generator writes.
finder = sp / "__editable___unsloth_0_0_0_finder.py"
finder.write_text(
"MAPPING: dict[str, str] = "
f"{{'studio': {str(repo_studio)!r}, 'unsloth': '/x', 'unsloth_cli': '/y'}}\n",
encoding = "utf-8",
)
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(studio_home))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
chosen, attempted = helpers["_resolve_frontend_path"](tmp_path / "bogus")
assert chosen is not None
assert chosen.resolve() == repo_dist.resolve()
def test_iter_candidates_handles_missing_studio_home(tmp_path, monkeypatch):
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(tmp_path / "nonexistent"))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
# Glob over a non-existent dir is empty; must not raise.
candidates = helpers["_iter_frontend_fallback_candidates"]()
assert candidates == []
def test_resolver_falls_back_to_windows_layout_site_packages(tmp_path, monkeypatch):
"""Pins the `Lib/site-packages` (capital L) Windows venv layout
alongside the POSIX `lib/python*/site-packages`."""
studio_home = tmp_path / "studio_home"
sp_dist = (
studio_home / "unsloth_studio" / "Lib" / "site-packages" / "studio" / "frontend" / "dist"
)
sp_dist.mkdir(parents = True)
(sp_dist / "index.html").write_text("<!doctype html>", encoding = "utf-8")
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(studio_home))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
chosen, _ = helpers["_resolve_frontend_path"](tmp_path / "bogus")
assert chosen is not None
assert chosen.resolve() == sp_dist.resolve()
def test_resolver_does_not_crash_on_non_dict_mapping_literal(tmp_path, monkeypatch):
"""A finder whose MAPPING value is a set/list/non-dict literal (possible
if the regex matched a brace-delimited literal ast.literal_eval can parse)
must not AttributeError. The resolver should skip it and keep probing."""
studio_home = tmp_path / "studio_home"
sp = studio_home / "unsloth_studio" / "lib" / "python3.13" / "site-packages"
sp.mkdir(parents = True)
# Bad finder: set literal, not a dict. literal_eval parses it as a set,
# so any .get() call on it would raise AttributeError.
(sp / "__editable___bad_0_0_0_finder.py").write_text(
"MAPPING: dict[str, str] = {'studio', 'unsloth', 'unsloth_cli'}\n",
encoding = "utf-8",
)
# Good finder, still discovered after the bad one is skipped.
repo_root = tmp_path / "clone"
repo_dist = repo_root / "studio" / "frontend" / "dist"
repo_dist.mkdir(parents = True)
(repo_dist / "index.html").write_text("<!doctype html>", encoding = "utf-8")
(sp / "__editable___good_0_0_0_finder.py").write_text(
f"MAPPING: dict[str, str] = {{'studio': {str(repo_root / 'studio')!r}}}\n",
encoding = "utf-8",
)
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(studio_home))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
chosen, _ = helpers["_resolve_frontend_path"](tmp_path / "bogus")
assert chosen is not None
assert chosen.resolve() == repo_dist.resolve()
def test_resolver_handles_multiline_mapping_dict(tmp_path, monkeypatch):
"""A future setuptools/black reformat wrapping the MAPPING dict across
multiple lines must still parse and resolve. Locks in `[^}]*` + re.DOTALL."""
studio_home = tmp_path / "studio_home"
sp = studio_home / "unsloth_studio" / "lib" / "python3.13" / "site-packages"
sp.mkdir(parents = True)
repo_root = tmp_path / "clone"
repo_studio = repo_root / "studio"
repo_dist = repo_studio / "frontend" / "dist"
repo_dist.mkdir(parents = True)
(repo_dist / "index.html").write_text("<!doctype html>", encoding = "utf-8")
finder = sp / "__editable___unsloth_0_0_0_finder.py"
finder.write_text(
"MAPPING: dict[str, str] = {\n"
f" 'studio': {str(repo_studio)!r},\n"
" 'unsloth': '/x',\n"
" 'unsloth_cli': '/y',\n"
"}\n",
encoding = "utf-8",
)
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(studio_home))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
chosen, _ = helpers["_resolve_frontend_path"](tmp_path / "bogus")
assert chosen is not None
assert chosen.resolve() == repo_dist.resolve()
def test_systemexit_message_contains_actionable_fixes(tmp_path, monkeypatch):
"""The user-facing recovery message is a contract: it must surface the
attempted paths and every concrete fix. Pin its structure so a refactor
doesn't drop one."""
import os
import sys
monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(tmp_path / "no_studio"))
monkeypatch.delenv("STUDIO_HOME", raising = False)
helpers = _load_helpers_only()
bogus = tmp_path / "no_such_dist"
_, attempted = helpers["_resolve_frontend_path"](bogus)
home = Path(os.environ["UNSLOTH_STUDIO_HOME"]).expanduser()
if sys.platform == "win32":
installer_bin = home / "bin" / "unsloth.exe"
else:
installer_bin = home / "unsloth_studio" / "bin" / "unsloth"
tried_lines = "\n".join(f" - {p}" for p in attempted)
message = (
"[ERROR] Studio frontend build not found.\n"
f"Tried:\n{tried_lines}\n"
"\n"
"Likely cause: another 'unsloth' on PATH is shadowing the "
"installer's binary and points at a site-packages tree with "
"no built dist.\n"
"\n"
"Fix one of:\n"
f" - run the installer's binary directly: {installer_bin} studio\n"
" - pass --frontend <path/to/studio/frontend/dist>\n"
" - pass --api-only to skip serving the web UI\n"
" - reinstall: curl -fsSL https://unsloth.ai/install.sh | sh"
)
assert str(bogus) in message
assert "--frontend" in message
assert "--api-only" in message
assert "reinstall" in message
assert "installer's binary directly" in message
assert str(installer_bin) in message