unsloth/studio/backend/tests/test_desktop_auth.py
Daniel Han 378e33c8a5
Studio macOS: faster startup, MLX self-heal, drop obsolete prebuilt pins (#6494)
* Studio: defer llama.cpp update probes and self-heal MLX on macOS

Two macOS startup problems shared one root area in the FastAPI lifespan:

- The llama.cpp capability + freshness probes ran inline before the server
  yielded, so a cold/slow/flaky network on the GitHub freshness check blocked
  'Application startup complete' (~34s on CI, longer in the field). Move both
  probes to a daemon thread; app.state stays None until ready (status routes
  already re-probe at request time). Opt out with UNSLOTH_DISABLE_UPDATE_CHECK=1.

- Train and Export were greyed out because mlx/mlx-lm/mlx-vlm arrive only
  transitively and a resolver backtrack silently drops them, so CHAT_ONLY stayed
  true. Add utils/mlx_repair.py: when Apple Silicon is detected without MLX,
  reinstall mlx/mlx-lm/mlx-vlm by name on a daemon thread and re-run hardware
  detection (opt out UNSLOTH_DISABLE_MLX_AUTOREPAIR=1). Surface a chat_only_reason
  in /api/health plus a sidebar tooltip so a greyed Train/Export explains itself
  instead of failing silently.

* Studio: guard model defaults against a None model name

load_model_defaults(None) called model_name.lower() with no guard, raising
'Error loading model defaults for None' before any model is selected. Return
an empty dict for a falsy/non-str name.

* Studio: drop obsolete upstream macOS + Windows Blackwell prebuilt pins

Both pins worked around gaps in ggml-org upstream prebuilts, but Studio now
routes every GPU host and all of macOS to the unslothai/llama.cpp fork
(published_repo_for_host), which ships the needed bundles, so both pins are
dead code on the default install path:

- macOS b9415: macOS always routes to the fork (its own macOS bundles), and
  host_supports_macos_minos() is the backstop. The pin only fired under an
  explicit --published-repo ggml-org override.
- Windows Blackwell b9360: Windows-NVIDIA routes to the fork, whose
  windows-x64-cuda13 bundle covers Blackwell (manifest max_sm 120, toolkit
  13.3), so the pin's self-disable check makes it dormant on every default
  install; it could only activate under the same upstream override on a
  13.0-13.2 driver.

Remove the pin constants, functions, and call sites. Keep the Blackwell
capability detection (_drop_blackwell_incapable_windows_cuda, _host_is_blackwell,
_windows_cuda_attempt_covers_blackwell) that still drops a non-sm_120 cuda-12.4
build on a Blackwell host. After this, an explicit --published-repo ggml-org
override on a Blackwell 13.0-13.2 host loses its GPU fallback and lands on CPU;
the default fork path is unaffected. Update the install selection-logic and
macOS-compat unit tests for the new no-pin behavior.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: walk back deeper on the macOS upstream prebuilt path

After removing the b9415 macOS pin, the explicit --published-repo ggml-org
upstream path still used the default 2-release fallback, so a pre-macOS-26 host
behind a run of macOS-26-only builds would exhaust two too-new plans (minos is
only checked post-download) and drop to a source build before reaching a
loadable older release. Walk back as deep as the fork macOS path
(DEFAULT_MAX_MACOS_RELEASE_FALLBACKS), turning the removed static pin into
dynamic discovery. Addresses review feedback on the macOS upstream fallback.

* Studio: pin transformers during MLX self-heal so it cannot break Studio

mlx-lm/mlx-vlm declare transformers>=5, but the single-env install pins
transformers==4.57.6. The self-heal used --upgrade with no constraint, so it
could upgrade transformers in the live venv and break the rest of Studio just to
make import mlx.core pass. Pin transformers to the installed version via a
constraint file: the resolver either finds an mlx build compatible with it or
fails (we stay chat-only), never upgrading transformers underneath Studio.
Addresses review feedback on the MLX repair install.

* Studio: harden MLX self-heal against an unsupported mlx-vlm

Pinning transformers alone made uv backtrack mlx-vlm to 0.3.9 (below unsloth-zoo's
mlx-vlm>=0.4.4), which imports but breaks VLM Train/Export -- so the self-heal
could clear chat-only onto a broken stack. Mirror the main installer: set
UV_OVERRIDE=overrides-darwin-arm64.txt so a current mlx-vlm coexists with the
transformers pin, require the same minimum versions unsloth-zoo declares, and
gate/validate on a full mlx_stack_available() check (not a bare import) so an
old or partial stack stays chat-only. Addresses PR review.

* Studio: filter Blackwell-incapable CUDA in resolve_upstream_asset_choice

resolve_upstream_asset_choice returned the first windows-cuda choice unfiltered,
so a Blackwell host could be handed an sm_120-incapable cuda-12.4 build while the
sibling planners drop it. Apply _drop_blackwell_incapable_windows_cuda here too
and fall through to the CPU bundle on a Blackwell host with no capable GPU asset.
Addresses PR review.

* Studio: re-poll health so MLX self-heal reaches an open UI

The sidebar cached the initial /api/health, so a successful background MLX
self-heal (chat_only flips false) did not re-enable Train/Export until a manual
reload. While chat-only for the recoverable mlx_unavailable reason, re-poll
/api/health and stop once Train/Export become available. Addresses PR review.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: make the disabled Train/Export tooltip reachable

The greyed Train/Export items pass a tooltip explaining why (e.g. MLX missing),
but a disabled <button> fires no pointer events and SidebarMenuButton only showed
tooltips while collapsed, so the explanation never appeared. Wrap a disabled
button in a focusable span and show its tooltip while expanded too; enabled items
keep the collapsed-only behavior. Addresses PR review.

* Studio: gate Train/Export on the full MLX stack, not bare mlx.core

detect_hardware enabled MLX training whenever `import mlx.core` worked, but the
MLX self-heal (utils/mlx_repair) treats a stack without mlx-lm/mlx-vlm at the
versions unsloth-zoo requires as inadequate. That asymmetry let the UI enable
Train/Export on exactly the partial/backtracked stack the self-heal is trying to
repair (greyed-in-but-broken VLM export). Gate on the same mlx_stack_available()
criterion so a partial stack stays chat-only (reason mlx_unavailable) and the
background repair restores it. Addresses PR review.

* Fix MLX repair and health auth for PR #6494

* Fix macOS upstream prebuilt fallback for PR #6494

* Fix MLX stack validation for PR #6494

* Fix MLX self-heal validation for PR #6494

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Review fixes: isolate hardware-state test, robust transformers pin

- test_chat_only_reason.py: detect_hardware() assigns module globals directly,
  which monkeypatch does not revert; the autouse fixture now saves and restores
  DEVICE/CHAT_ONLY/CHAT_ONLY_REASON/IS_ROCM so a chat-only verdict here cannot
  leak into other backend tests (e.g. test_utils.py) on a GPU host.
- mlx_repair.py: read the transformers version from importlib.metadata instead of
  importing transformers, so the install pin is not silently dropped when
  transformers has valid metadata but fails to import.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix CI: model full MLX stack in dispatch tests, keep selection test offline

dispatch (macOS) job:
- detect_hardware now gates MLX on the full stack (mlx_stack_available imports
  mlx_lm/mlx_vlm and checks dist versions), so faking only mlx.core makes the
  apple_silicon_mlx profile resolve to CPU. The dispatch tests assert the routing
  decision when the stack IS usable, so model a complete stack:
  test_hardware_dispatch_matrix patches utils.mlx_repair.mlx_stack_available and
  test_is_mlx_dispatch_gate patches hardware._has_usable_mlx_stack. The stack
  predicate's own internals stay covered by test_mlx_repair.py.

Repo tests (CPU) job:
- test_no_cuda_attempt_on_published_path_for_13_1 fell through to a live
  github_release_assets() upstream fetch after the Blackwell filter dropped every
  published attempt, which the offline security scanner blocks. Stub that fetch so
  the walk-back deterministically finds no usable CUDA build and raises
  PrebuiltFallback without network.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Harden MLX self-heal: prepare transformers constraint inside the try

attempt_mlx_repair runs on a daemon thread, but _transformers_constraint_args was
called before the try. A failure there (e.g. tempfile.mkstemp on a full disk or a
bad TMPDIR) would propagate unhandled and silently kill the self-heal thread.
Move the call inside the try and initialize constraint_path so any such failure
is caught and leaves Studio chat-only instead of crashing the thread.

---------

Co-authored-by: Daniel Han <michaelhan2050@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: wasimysaid <wasimysdev@gmail.com>
2026-06-22 02:20:08 -07:00

674 lines
22 KiB
Python

import importlib.util
import asyncio
import hashlib
import json
import os
import platform
import secrets
import sqlite3
import subprocess
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
import jwt
import pytest
from fastapi import APIRouter, FastAPI
from fastapi.security import HTTPAuthorizationCredentials
from fastapi.testclient import TestClient
from auth import storage
@pytest.fixture(autouse = True)
def isolated_auth_db(tmp_path, monkeypatch):
monkeypatch.setattr(storage, "DB_PATH", tmp_path / "auth.db")
monkeypatch.setattr(storage, "_BOOTSTRAP_PW_PATH", tmp_path / ".bootstrap_password")
monkeypatch.setattr(storage, "_bootstrap_password", None)
monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None)
yield
def seed_user(*, must_change_password = False):
storage.create_initial_user(
username = storage.DEFAULT_ADMIN_USERNAME,
password = "human-password-123",
jwt_secret = secrets.token_urlsafe(64),
must_change_password = must_change_password,
)
def auth_client():
route_path = Path(__file__).resolve().parents[1] / "routes" / "auth.py"
spec = importlib.util.spec_from_file_location("_desktop_auth_route", route_path)
auth_route = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(auth_route)
app = FastAPI()
app.include_router(auth_route.router, prefix = "/api/auth")
return TestClient(app)
def data_recipe_jobs_module():
route_path = Path(__file__).resolve().parents[1] / "routes" / "data_recipe" / "jobs.py"
spec = importlib.util.spec_from_file_location("_desktop_data_recipe_jobs", route_path)
jobs_route = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(jobs_route)
return jobs_route
def local_recipe():
return {
"model_providers": [{"name": "local", "is_local": True}],
"model_configs": [{"alias": "local-model", "provider": "local"}],
"columns": [{"column_type": "llm-text", "model_alias": "local-model"}],
}
def local_recipe_request(token):
return SimpleNamespace(
headers = {"authorization": f"Bearer {token}"},
app = SimpleNamespace(state = SimpleNamespace(server_port = 8888)),
scope = {},
base_url = "http://testserver/",
)
@pytest.fixture
def loaded_local_model(monkeypatch):
inference_module = SimpleNamespace(
get_llama_cpp_backend = lambda: SimpleNamespace(is_loaded = True),
)
monkeypatch.setitem(sys.modules, "routes.inference", inference_module)
def test_desktop_secret_round_trip_uses_real_admin_subject():
seed_user()
raw = storage.create_desktop_secret()
assert raw.startswith("desktop-")
assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME
assert storage.validate_desktop_secret(raw + "x") is None
def test_create_desktop_secret_rotates_old_secret():
seed_user()
old = storage.create_desktop_secret()
new = storage.create_desktop_secret()
assert old != new
assert storage.validate_desktop_secret(old) is None
assert storage.validate_desktop_secret(new) == storage.DEFAULT_ADMIN_USERNAME
def test_clear_desktop_secret_invalidates_secret():
seed_user()
raw = storage.create_desktop_secret()
storage.clear_desktop_secret()
assert storage.validate_desktop_secret(raw) is None
def test_ensure_default_admin_does_not_recreate_bootstrap_for_existing_admin():
seed_user()
created = storage.ensure_default_admin()
assert created is False
assert not storage._BOOTSTRAP_PW_PATH.exists()
def test_ensure_default_admin_loads_existing_bootstrap_after_restart(monkeypatch):
created = storage.ensure_default_admin()
bootstrap_pw = storage._BOOTSTRAP_PW_PATH.read_text().strip()
monkeypatch.setattr(storage, "_bootstrap_password", None)
created_again = storage.ensure_default_admin()
assert created is True
assert storage._BOOTSTRAP_PW_PATH.exists()
assert created_again is False
assert storage.get_bootstrap_password() == bootstrap_pw
def test_ensure_default_admin_does_not_generate_for_empty_existing_bootstrap():
seed_user()
storage._BOOTSTRAP_PW_PATH.write_text(" \n")
created = storage.ensure_default_admin()
assert created is False
assert storage._BOOTSTRAP_PW_PATH.read_text() == " \n"
assert storage.get_bootstrap_password() is None
def test_web_login_token_has_no_desktop_marker_and_keeps_password_gate():
seed_user(must_change_password = True)
client = auth_client()
response = client.post(
"/api/auth/login",
json = {
"username": storage.DEFAULT_ADMIN_USERNAME,
"password": "human-password-123",
},
)
assert response.status_code == 200
body = response.json()
assert body["must_change_password"] is True
payload = jwt.decode(
body["access_token"],
storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME),
algorithms = ["HS256"],
)
assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME
assert "desktop" not in payload
gated = client.post(
"/api/auth/api-keys",
headers = {"Authorization": f"Bearer {body['access_token']}"},
json = {"name": "web"},
)
assert gated.status_code == 403
def test_desktop_login_mints_admin_token_without_clearing_web_password_change():
seed_user(must_change_password = True)
raw = storage.create_desktop_secret()
client = auth_client()
response = client.post("/api/auth/desktop-login", json = {"secret": raw})
assert response.status_code == 200
body = response.json()
assert body["access_token"]
assert body["refresh_token"]
assert body["token_type"] == "bearer"
assert body["must_change_password"] is False
assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True
payload = jwt.decode(
body["access_token"],
storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME),
algorithms = ["HS256"],
)
assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME
assert payload["desktop"] is True
def test_desktop_refresh_preserves_desktop_marker():
seed_user(must_change_password = True)
raw = storage.create_desktop_secret()
client = auth_client()
login_body = client.post("/api/auth/desktop-login", json = {"secret": raw}).json()
response = client.post(
"/api/auth/refresh",
json = {"refresh_token": login_body["refresh_token"]},
)
assert response.status_code == 200
body = response.json()
assert body["must_change_password"] is False
payload = jwt.decode(
body["access_token"],
storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME),
algorithms = ["HS256"],
)
assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME
assert payload["desktop"] is True
def test_consume_refresh_token_second_call_returns_none():
"""Single-use rotation rejects the same token on a second consume."""
seed_user()
from datetime import datetime, timedelta, timezone
raw = secrets.token_urlsafe(48)
expires = (datetime.now(timezone.utc) + timedelta(days = 30)).isoformat()
storage.save_refresh_token(raw, storage.DEFAULT_ADMIN_USERNAME, expires)
first = storage.consume_refresh_token(raw)
assert first == (storage.DEFAULT_ADMIN_USERNAME, False)
second = storage.consume_refresh_token(raw)
assert second is None
def test_consume_refresh_token_concurrent_only_one_succeeds(tmp_path, monkeypatch):
"""64-thread pile-up on one token; DELETE RETURNING permits one winner."""
seed_user()
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
raw = secrets.token_urlsafe(48)
expires = (datetime.now(timezone.utc) + timedelta(days = 30)).isoformat()
storage.save_refresh_token(raw, storage.DEFAULT_ADMIN_USERNAME, expires)
workers = 64
def attempt(_idx: int):
try:
return storage.consume_refresh_token(raw)
except sqlite3.OperationalError:
# "database is locked" under contention; treat as losing the race.
return None
with ThreadPoolExecutor(max_workers = workers) as pool:
results = list(pool.map(attempt, range(workers)))
successes = [r for r in results if r is not None]
assert len(successes) == 1, f"expected exactly one consumer to win, got {len(successes)}"
assert successes[0] == (storage.DEFAULT_ADMIN_USERNAME, False)
def test_consume_refresh_token_expired_returns_none():
seed_user()
from datetime import datetime, timedelta, timezone
raw = secrets.token_urlsafe(48)
expires = (datetime.now(timezone.utc) - timedelta(hours = 1)).isoformat()
storage.save_refresh_token(raw, storage.DEFAULT_ADMIN_USERNAME, expires)
assert storage.consume_refresh_token(raw) is None
def test_desktop_session_uses_real_admin_identity_for_api_keys():
seed_user(must_change_password = True)
raw = storage.create_desktop_secret()
client = auth_client()
token = client.post("/api/auth/desktop-login", json = {"secret": raw}).json()["access_token"]
response = client.post(
"/api/auth/api-keys",
headers = {"Authorization": f"Bearer {token}"},
json = {"name": "desktop"},
)
assert response.status_code == 200
rows = storage.list_api_keys(storage.DEFAULT_ADMIN_USERNAME)
assert [row["name"] for row in rows] == ["desktop"]
def test_local_recipe_token_authenticates_as_admin_for_desktop_user(loaded_local_model):
# _inject_local_providers mints an internal sk-unsloth-* API key (not a
# forwarded JWT) that validates as admin whether the session was desktop or web.
from auth.authentication import create_access_token, get_current_subject
seed_user(must_change_password = True)
jobs_route = data_recipe_jobs_module()
incoming_token = create_access_token(
subject = storage.DEFAULT_ADMIN_USERNAME,
desktop = True,
)
recipe = local_recipe()
jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token))
local_token = recipe["model_providers"][0]["api_key"]
assert local_token.startswith(storage.API_KEY_PREFIX)
credentials = HTTPAuthorizationCredentials(
scheme = "Bearer",
credentials = local_token,
)
assert asyncio.run(get_current_subject(credentials)) == storage.DEFAULT_ADMIN_USERNAME
def test_local_recipe_token_authenticates_as_admin_for_web_user(loaded_local_model):
# Mirror of the desktop variant: API-key issuance is identical for web/desktop tokens.
from auth.authentication import create_access_token, get_current_subject
seed_user(must_change_password = False)
jobs_route = data_recipe_jobs_module()
incoming_token = create_access_token(subject = storage.DEFAULT_ADMIN_USERNAME)
recipe = local_recipe()
jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token))
local_token = recipe["model_providers"][0]["api_key"]
assert local_token.startswith(storage.API_KEY_PREFIX)
credentials = HTTPAuthorizationCredentials(
scheme = "Bearer",
credentials = local_token,
)
assert asyncio.run(get_current_subject(credentials)) == storage.DEFAULT_ADMIN_USERNAME
def test_desktop_login_rejects_invalid_secret():
seed_user(must_change_password = False)
client = auth_client()
response = client.post(
"/api/auth/desktop-login",
json = {"secret": "desktop-invalid"},
)
assert response.status_code == 401
def test_write_desktop_secret_file_is_0600_on_unix(tmp_path):
from unsloth_cli.commands import studio as studio_cli
path = tmp_path / ".desktop_secret"
if platform.system() != "Windows":
path.write_text("old-secret")
os.chmod(path, 0o644)
studio_cli._write_auth_secret(path, "desktop-secret")
assert path.read_text() == "desktop-secret"
if platform.system() != "Windows":
assert oct(path.stat().st_mode & 0o777) == "0o600"
def test_reset_password_removes_desktop_secret_files(tmp_path, monkeypatch):
from typer.testing import CliRunner
from unsloth_cli.commands import studio as studio_cli
auth_dir = tmp_path / "auth"
auth_dir.mkdir()
(auth_dir / "auth.db").write_text("db")
(auth_dir / ".bootstrap_password").write_text("boot")
(auth_dir / ".desktop_secret").write_text("new")
monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path)
result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"])
assert result.exit_code == 0
assert not (auth_dir / "auth.db").exists()
assert not (auth_dir / ".bootstrap_password").exists()
assert not (auth_dir / ".desktop_secret").exists()
def test_reset_password_removes_desktop_secret_files_without_db(tmp_path, monkeypatch):
from typer.testing import CliRunner
from unsloth_cli.commands import studio as studio_cli
auth_dir = tmp_path / "auth"
auth_dir.mkdir()
(auth_dir / ".desktop_secret").write_text("new")
monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path)
result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"])
assert result.exit_code == 0
assert not (auth_dir / ".desktop_secret").exists()
def test_desktop_capabilities_json_reports_rollout_safe_flags():
from typer.testing import CliRunner
import unsloth_cli.commands.studio as studio_cli
result = CliRunner().invoke(
studio_cli.studio_app,
["desktop-capabilities", "--json"],
)
assert result.exit_code == 0
body = json.loads(result.output)
assert body["desktop_protocol_version"] == 1
assert body["supports_provision_desktop_auth"] is True
assert body["supports_api_only"] is True
assert isinstance(body["version"], str)
def test_health_response_reports_desktop_capability_fields(monkeypatch):
routes_module = ModuleType("routes")
routes_module.__path__ = []
settings_module = ModuleType("routes.settings")
settings_module.router = APIRouter()
llama_module = ModuleType("routes.llama")
llama_module.router = APIRouter()
prompts_module = ModuleType("routes.prompts")
prompts_module.router = APIRouter()
for name, router in {
"auth_router": APIRouter(),
"chat_history_router": APIRouter(),
"data_recipe_router": APIRouter(),
"datasets_router": APIRouter(),
"export_router": APIRouter(),
"inference_router": APIRouter(),
"inference_studio_router": APIRouter(),
"mcp_servers_router": APIRouter(),
"models_router": APIRouter(),
"providers_router": APIRouter(),
"rag_router": APIRouter(),
"settings_router": settings_module.router,
"training_history_router": APIRouter(),
"training_router": APIRouter(),
}.items():
setattr(routes_module, name, router)
routes_module.settings = settings_module
routes_module.llama = llama_module
monkeypatch.setitem(sys.modules, "routes", routes_module)
monkeypatch.setitem(sys.modules, "routes.settings", settings_module)
monkeypatch.setitem(sys.modules, "routes.llama", llama_module)
monkeypatch.setitem(sys.modules, "routes.prompts", prompts_module)
import studio.backend.main as backend_main
monkeypatch.setattr(backend_main._hw_module, "CHAT_ONLY", True)
monkeypatch.setattr(backend_main._hw_module, "CHAT_ONLY_REASON", "mlx_unavailable")
seed_user()
from auth.authentication import create_access_token
token = create_access_token(storage.DEFAULT_ADMIN_USERNAME)
app = FastAPI()
app.add_api_route("/api/health", backend_main.health_check, methods = ["GET"])
client = TestClient(app)
unauthenticated = client.get("/api/health")
assert unauthenticated.status_code == 200
unauthenticated_body = unauthenticated.json()
assert unauthenticated_body["chat_only"] is True
assert "chat_only_reason" not in unauthenticated_body
response = client.get(
"/api/health",
headers = {"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
body = response.json()
assert body["desktop_protocol_version"] == 1
assert body["supports_desktop_auth"] is True
assert body["chat_only_reason"] == "mlx_unavailable"
def test_provision_desktop_auth_writes_secret_and_creates_db_without_backend_deps(
tmp_path, monkeypatch
):
auth_dir = tmp_path / "auth"
auth_dir.mkdir()
code = """
import builtins
import sys
from pathlib import Path
from typer.testing import CliRunner
studio_home = Path(sys.argv[1])
real_import = builtins.__import__
def guarded_import(name, globals = None, locals = None, fromlist = (), level = 0):
# Only gate absolute imports; relative `from .utils import x` inside
# third-party packages (e.g. typer._click.decorators) hits level > 0
# with name="utils" and must pass through.
blocked = ("auth", "fastapi", "structlog", "utils")
if level == 0 and (name in blocked or name.startswith(("auth.", "utils."))):
raise ModuleNotFoundError(name)
return real_import(name, globals, locals, fromlist, level)
builtins.__import__ = guarded_import
from unsloth_cli.commands import studio as studio_cli
studio_cli.STUDIO_HOME = studio_home
result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"])
if result.exit_code != 0:
print(result.output)
if result.exception is not None:
raise result.exception
raise SystemExit(result.exit_code)
"""
result = subprocess.run(
[sys.executable, "-c", code, str(tmp_path)],
cwd = Path(__file__).resolve().parents[3],
env = {**os.environ, "PYTHONPATH": "."},
text = True,
capture_output = True,
)
assert result.returncode == 0, result.stderr + result.stdout
secret = (auth_dir / ".desktop_secret").read_text()
assert secret.startswith("desktop-")
conn = sqlite3.connect(auth_dir / "auth.db")
conn.row_factory = sqlite3.Row
try:
user = conn.execute(
"""
SELECT username, password_salt, password_hash, must_change_password
FROM auth_user
"""
).fetchone()
app_secrets = {
row["key"]: row["value"] for row in conn.execute("SELECT key, value FROM app_secrets")
}
refresh_columns = {row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)")}
finally:
conn.close()
bootstrap_password = (auth_dir / ".bootstrap_password").read_text().strip()
bootstrap_hash = hashlib.pbkdf2_hmac(
"sha256",
bootstrap_password.encode("utf-8"),
user["password_salt"].encode("utf-8"),
100_000,
).hex()
assert bootstrap_password
assert user["username"] == "unsloth"
assert user["must_change_password"] == 1
assert bootstrap_hash == user["password_hash"]
assert len(app_secrets["api_key_pbkdf2_salt"]) == 64
assert len(app_secrets["desktop_secret_hash"]) == 64
assert app_secrets["desktop_secret_created_at"]
assert "is_desktop" in refresh_columns
monkeypatch.setattr(storage, "DB_PATH", auth_dir / "auth.db")
monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None)
assert storage.validate_desktop_secret(secret) == storage.DEFAULT_ADMIN_USERNAME
assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True
def test_provision_desktop_auth_keeps_existing_admin_password(tmp_path, monkeypatch):
from typer.testing import CliRunner
from unsloth_cli.commands import studio as studio_cli
auth_dir = tmp_path / "auth"
auth_dir.mkdir()
monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path)
conn = sqlite3.connect(auth_dir / "auth.db")
try:
conn.execute(
"""
CREATE TABLE auth_user (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_salt TEXT NOT NULL,
password_hash TEXT NOT NULL,
jwt_secret TEXT NOT NULL,
must_change_password INTEGER NOT NULL DEFAULT 0
)
"""
)
conn.execute(
"""
INSERT INTO auth_user (
username, password_salt, password_hash, jwt_secret, must_change_password
)
VALUES (?, ?, ?, ?, ?)
""",
("unsloth", "existing-salt", "existing-hash", "existing-jwt", 0),
)
conn.commit()
finally:
conn.close()
result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"])
assert result.exit_code == 0
assert not (auth_dir / ".bootstrap_password").exists()
conn = sqlite3.connect(auth_dir / "auth.db")
conn.row_factory = sqlite3.Row
try:
user = conn.execute(
"""
SELECT password_salt, password_hash, jwt_secret, must_change_password
FROM auth_user WHERE username = ?
""",
("unsloth",),
).fetchone()
finally:
conn.close()
assert dict(user) == {
"password_salt": "existing-salt",
"password_hash": "existing-hash",
"jwt_secret": "existing-jwt",
"must_change_password": 0,
}
def test_update_password_clears_desktop_secret():
seed_user()
raw = storage.create_desktop_secret()
assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME
changed = storage.update_password(storage.DEFAULT_ADMIN_USERNAME, "new-admin-password")
assert changed is True
assert storage.validate_desktop_secret(raw) is None
def test_update_password_on_unknown_user_leaves_desktop_secret_intact():
seed_user()
raw = storage.create_desktop_secret()
changed = storage.update_password("not-a-user", "irrelevant")
assert changed is False
assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME
def test_desktop_auth_provision_has_bounded_timeout():
rs_path = (
Path(__file__).resolve().parents[3] / "studio" / "src-tauri" / "src" / "desktop_auth.rs"
)
src = rs_path.read_text()
start = src.index("async fn provision_desktop_auth(")
depth = 0
body_start = src.index("{", start)
body_end = None
for i in range(body_start, len(src)):
c = src[i]
if c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
body_end = i + 1
break
assert body_end is not None
body = src[start:body_end]
assert "tokio::time::timeout" in body
import re
m = re.search(r"Duration::from_secs\(\s*(\d+)\s*\)", body)
assert m is not None
seconds = int(m.group(1))
assert 5 <= seconds <= 120