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

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2026-05-07 04:09:26 +00:00
commit 3dfba6b1df
3 changed files with 97 additions and 43 deletions

View file

@ -336,6 +336,7 @@ with sync_playwright() as p:
);
return el ? (el.innerText || '').trim() : '';
}""")
search.fill("qwen")
page.wait_for_timeout(800)
qwen_text = picker_visible_text()
@ -557,8 +558,7 @@ with sync_playwright() as p:
acct.click(force = True)
except Exception as exc:
soft_fail(
f"theme cycle {cycle + 1}: account-menu click failed "
f"({exc!r})"
f"theme cycle {cycle + 1}: account-menu click failed " f"({exc!r})"
)
break
# Wait for the dropdown menu to actually render before

View file

@ -94,7 +94,8 @@ with sync_playwright() as p:
def shoot(name: str) -> None:
_n[0] += 1
page.screenshot(
path = str(ART / f"{_n[0]:02d}-{name}.png"), full_page = True,
path = str(ART / f"{_n[0]:02d}-{name}.png"),
full_page = True,
)
# ─────────────────────────────────────────────────────
@ -155,7 +156,8 @@ with sync_playwright() as p:
compare_nav = page.locator('[data-tour="chat-compare"]').first
if compare_nav.count() == 0:
compare_nav = page.get_by_role(
"button", name = re.compile(r"^\s*Compare\s*$", re.I),
"button",
name = re.compile(r"^\s*Compare\s*$", re.I),
).first
if compare_nav.count() == 0:
soft_fail("Compare nav not found")
@ -214,7 +216,9 @@ with sync_playwright() as p:
arg = ok_count_before + 4,
timeout = 180_000,
)
info("OK Compare: 4 total new assistant bubbles after second prompt")
info(
"OK Compare: 4 total new assistant bubbles after second prompt"
)
except Exception as exc:
soft_fail(f"Compare: 4 bubbles didn't appear: {exc!r}")
shoot("04-compare-after-B")
@ -232,7 +236,9 @@ with sync_playwright() as p:
page.wait_for_timeout(1500)
shoot("05-recipes-list")
# Template cards render as <button> elements.
templates = page.locator("main button").filter(has_not_text = re.compile(r"^(\+|Create)"))
templates = page.locator("main button").filter(
has_not_text = re.compile(r"^(\+|Create)")
)
n_templates = templates.count()
info(f"recipe templates visible: {n_templates}")
if n_templates == 0:
@ -266,7 +272,9 @@ with sync_playwright() as p:
shoot("07-export")
if chat_only:
if "/export" in page.url:
soft_fail(f"chat-only mode should redirect /export -> /chat; url={page.url}")
soft_fail(
f"chat-only mode should redirect /export -> /chat; url={page.url}"
)
else:
info(f"OK chat-only redirected /export -> {page.url}")
else:
@ -291,12 +299,16 @@ with sync_playwright() as p:
shoot("08-studio")
if chat_only:
if "/studio" in page.url:
soft_fail(f"chat-only mode should redirect /studio -> /chat; url={page.url}")
soft_fail(
f"chat-only mode should redirect /studio -> /chat; url={page.url}"
)
else:
info(f"OK chat-only redirected /studio -> {page.url}")
else:
for tab_name in ("Configure", "Current run", "History"):
tab = page.get_by_role("tab", name = re.compile(rf"^\s*{tab_name}\s*$", re.I)).first
tab = page.get_by_role(
"tab", name = re.compile(rf"^\s*{tab_name}\s*$", re.I)
).first
if tab.count() == 0:
soft_fail(f"tab '{tab_name}' not found in /studio")
else:
@ -328,11 +340,19 @@ with sync_playwright() as p:
shoot("09-settings-open")
# Each tab is a button with the visible text as accessible name.
# Tabs available depend on chat_only mode.
candidate_tabs = ("General", "Profile", "Appearance", "Chat", "Developer", "About")
candidate_tabs = (
"General",
"Profile",
"Appearance",
"Chat",
"Developer",
"About",
)
seen_tabs = []
for tab_name in candidate_tabs:
btn = page.get_by_role(
"button", name = re.compile(rf"^\s*{tab_name}\s*$", re.I),
"button",
name = re.compile(rf"^\s*{tab_name}\s*$", re.I),
).first
if btn.count() == 0:
continue
@ -350,7 +370,9 @@ with sync_playwright() as p:
info(f"OK Settings tab '{tab_name}' body length={body_text}")
seen_tabs.append(tab_name)
else:
soft_fail(f"Settings tab '{tab_name}' body suspiciously short: {body_text}")
soft_fail(
f"Settings tab '{tab_name}' body suspiciously short: {body_text}"
)
except Exception as exc:
soft_fail(f"Settings tab '{tab_name}' click failed: {exc!r}")
shoot("10-settings-tabs-visited")

View file

@ -39,7 +39,7 @@ from pathlib import Path
BASE = os.environ["BASE_URL"]
OLD = os.environ["STUDIO_OLD_PW"]
NEW = os.environ.get("STUDIO_NEW_PW", "ApiSmoke-NEW-2026!")
NEW = os.environ.get("STUDIO_NEW_PW", "ApiSmoke-NEW-2026!")
NEW2 = os.environ.get("STUDIO_NEW2_PW", "ApiSmoke-NEW2-2026!")
AUTH_DIR = Path(
os.environ.get("STUDIO_AUTH_DIR", str(Path.home() / ".unsloth" / "studio" / "auth"))
@ -98,7 +98,9 @@ def http(
def login(password: str) -> tuple[int, str | None]:
"""POST /api/auth/login. Returns (status, access_token-or-None)."""
code, body = http(
"POST", "/api/auth/login", body = {"username": "unsloth", "password": password},
"POST",
"/api/auth/login",
body = {"username": "unsloth", "password": password},
)
if code == 200 and isinstance(body, dict):
return code, body.get("access_token")
@ -129,7 +131,9 @@ try:
acao = r.headers.get("Access-Control-Allow-Origin", "")
acac = r.headers.get("Access-Control-Allow-Credentials", "")
if acao == "*" and acac.lower() == "true":
fail(f"CORS: wildcard origin + credentials=true (acao={acao!r}, acac={acac!r})")
fail(
f"CORS: wildcard origin + credentials=true (acao={acao!r}, acac={acac!r})"
)
else:
ok(f"CORS preflight acao={acao!r} acac={acac!r}")
except Exception as exc:
@ -144,7 +148,8 @@ if boot_path.exists():
bootstrap_pw = boot_path.read_text().strip()
if bootstrap_pw:
req = urllib.request.Request(
f"{BASE}/", headers = {"Origin": "https://evil.example"},
f"{BASE}/",
headers = {"Origin": "https://evil.example"},
)
try:
with urllib.request.urlopen(req, timeout = 10) as r:
@ -182,7 +187,8 @@ if code != 200 or not old_token:
sys.exit(1)
ok("bootstrap login -> 200")
code, body = http(
"POST", "/api/auth/change-password",
"POST",
"/api/auth/change-password",
body = {"current_password": OLD, "new_password": NEW},
headers = {"Authorization": f"Bearer {old_token}"},
)
@ -208,7 +214,8 @@ for endpoint in ("/api/system", "/api/system/hardware", "/api/system/gpu-visibil
# Load the model. Sections 5 + 7 below need a loaded model.
section("Load the GGUF for /v1 tests")
code, body = http(
"POST", "/api/inference/load",
"POST",
"/api/inference/load",
body = {
"model_path": GGUF_REPO,
"gguf_variant": os.environ.get("GGUF_VARIANT", "UD-Q4_K_XL"),
@ -264,12 +271,24 @@ section("JWT expiry")
# Forge a JWT with exp=now-1 using the install's signing secret.
# auth/storage.py:get_user_and_secret('unsloth') returns (salt, hash, jwt_secret, must_change_pw).
try:
sys.path.insert(0, str(Path.home() / ".unsloth" / "studio" / "unsloth_studio" / "lib" /
f"python{sys.version_info.major}.{sys.version_info.minor}" /
"site-packages" / "studio" / "backend"))
sys.path.insert(
0,
str(
Path.home()
/ ".unsloth"
/ "studio"
/ "unsloth_studio"
/ "lib"
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
/ "site-packages"
/ "studio"
/ "backend"
),
)
# Best-effort import; not all installs ship the backend at this path.
import jwt # type: ignore[import-not-found]
from auth import storage # type: ignore[import-not-found]
rec = storage.get_user_and_secret("unsloth")
if rec is None:
fail("get_user_and_secret returned None; can't forge JWT")
@ -281,7 +300,8 @@ try:
algorithm = "HS256",
)
code, _ = http(
"GET", "/api/inference/status",
"GET",
"/api/inference/status",
headers = {"Authorization": f"Bearer {expired}"},
)
if code == 401:
@ -298,7 +318,8 @@ except Exception as exc:
section("API key lifecycle")
code, body = http(
"POST", "/api/auth/api-keys",
"POST",
"/api/auth/api-keys",
body = {"name": "smoke-key"},
headers = AUTH_HEADER,
)
@ -327,7 +348,8 @@ else:
# Use the key against /v1/chat/completions (the workflow has
# already loaded gemma-3-270m).
code, body = http(
"POST", "/v1/chat/completions",
"POST",
"/v1/chat/completions",
body = {
"model": GGUF_REPO,
"messages": [{"role": "user", "content": "Reply with: ok"}],
@ -344,14 +366,17 @@ else:
# Delete + verify rejection.
code, _ = http(
"DELETE", f"/api/auth/api-keys/{api_id}", headers = AUTH_HEADER,
"DELETE",
f"/api/auth/api-keys/{api_id}",
headers = AUTH_HEADER,
)
if code in (200, 204):
ok(f"DELETE /api/auth/api-keys/{api_id} -> {code}")
else:
fail(f"DELETE /api/auth/api-keys/{api_id} -> {code}")
code, _ = http(
"POST", "/v1/chat/completions",
"POST",
"/v1/chat/completions",
body = {
"model": GGUF_REPO,
"messages": [{"role": "user", "content": "test"}],
@ -371,6 +396,7 @@ else:
# ─────────────────────────────────────────────────────────────────────────
section("Auth file-mode hardening")
import platform as _platform
if _platform.system() != "Linux":
ok("(non-Linux, skipping file-mode checks)")
else:
@ -389,9 +415,7 @@ else:
if actual_mode == expected_mode:
ok(f"{path} mode={oct(actual_mode)}")
else:
fail(
f"{path} mode={oct(actual_mode)} (expected {oct(expected_mode)})"
)
fail(f"{path} mode={oct(actual_mode)} (expected {oct(expected_mode)})")
# ─────────────────────────────────────────────────────────────────────────
@ -412,7 +436,8 @@ else:
# /v1/embeddings either returns embedding OR structured 4xx.
code, body = http(
"POST", "/v1/embeddings",
"POST",
"/v1/embeddings",
body = {"model": GGUF_REPO, "input": "hello"},
headers = AUTH_HEADER,
timeout = 30,
@ -426,7 +451,8 @@ else:
# /v1/responses minimal request.
code, body = http(
"POST", "/v1/responses",
"POST",
"/v1/responses",
body = {
"model": GGUF_REPO,
"input": "Reply with: ok",
@ -442,7 +468,8 @@ else:
# Bogus variant must be rejected.
code, _ = http(
"POST", "/api/inference/load",
"POST",
"/api/inference/load",
body = {
"model_path": GGUF_REPO,
"gguf_variant": "UD-Q9_BOGUS_DOES_NOT_EXIST",
@ -457,6 +484,7 @@ if 400 <= code < 500:
else:
fail(f"bogus gguf_variant returned {code} (expected 4xx)")
# Force-reload of the same repo: child PID must change.
# Read the inference status before.
def _llama_pid() -> int | None:
@ -465,9 +493,11 @@ def _llama_pid() -> int | None:
return None
return body.get("llama_server_pid") or body.get("pid")
before_pid = _llama_pid()
code, _ = http(
"POST", "/api/inference/load",
"POST",
"/api/inference/load",
body = {
"model_path": GGUF_REPO,
"gguf_variant": os.environ.get("GGUF_VARIANT", "UD-Q4_K_XL"),
@ -496,21 +526,21 @@ section("Endpoint auth audit")
# without an entry here fails the audit, forcing the author to make
# the auth decision explicit.
PUBLIC = {
("GET", "/api/health"),
("GET", "/api/auth/status"),
("GET", "/api/health"),
("GET", "/api/auth/status"),
("POST", "/api/auth/login"),
("POST", "/api/auth/desktop-login"),
("POST", "/api/auth/refresh"),
}
EXPECTED_AUTH_ENDPOINTS = [
# Auth-required (sample -- not exhaustive; covers the key surfaces)
("GET", "/api/inference/status"),
("GET", "/api/inference/models"),
("GET", "/v1/models"),
("GET", "/api/system"),
("GET", "/api/system/hardware"),
("GET", "/api/system/gpu-visibility"),
("GET", "/api/auth/api-keys"),
("GET", "/api/inference/status"),
("GET", "/api/inference/models"),
("GET", "/v1/models"),
("GET", "/api/system"),
("GET", "/api/system/hardware"),
("GET", "/api/system/gpu-visibility"),
("GET", "/api/auth/api-keys"),
("POST", "/api/inference/load"),
("POST", "/api/shutdown"), # don't actually fire it!
]
@ -535,7 +565,9 @@ for method, path in EXPECTED_AUTH_ENDPOINTS:
fail(f"{method} {path} unauthenticated returned {code} (expected 401/403)")
for method, path in PUBLIC:
code, _ = http(method, path)
if 200 <= code < 500: # public endpoints either 200 or 4xx (bad input), never connection-refused
if (
200 <= code < 500
): # public endpoints either 200 or 4xx (bad input), never connection-refused
ok(f"{method} {path} public -> {code}")
else:
fail(f"{method} {path} public returned unexpected {code}")