[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
This commit is contained in:
parent
240efa86f8
commit
3dfba6b1df
3 changed files with 97 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue