Compare commits
3 commits
main
...
fix/setup-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbb1508513 | ||
|
|
1eb3ac26e7 | ||
|
|
bf702033a6 |
7 changed files with 245 additions and 18 deletions
|
|
@ -1129,6 +1129,25 @@ if ($script:StudioVtOk -and -not $env:NO_COLOR) {
|
|||
Write-Host " $Rule" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# WebView2 caches keyed by the app bundle id hold copies of the previous
|
||||
# frontend and can keep serving it after an update (old styles linger).
|
||||
# Cache-only paths: Local Storage, IndexedDB, cookies, settings, models,
|
||||
# and the studio database are untouched.
|
||||
if ($env:LOCALAPPDATA) {
|
||||
$wvDefault = Join-Path $env:LOCALAPPDATA "ai.unsloth.studio\EBWebView\Default"
|
||||
$wvCleared = $false
|
||||
foreach ($wvSub in @("Cache", "Code Cache", "GPUCache", "Service Worker")) {
|
||||
$wvPath = Join-Path $wvDefault $wvSub
|
||||
if (Test-Path -LiteralPath $wvPath) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $wvPath -Recurse -Force -ErrorAction Stop
|
||||
$wvCleared = $true
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
if ($wvCleared) { substep "cleared stale WebView caches (ai.unsloth.studio); settings and data kept" }
|
||||
}
|
||||
|
||||
# Back up User PATH under HKCU\Software\Unsloth before any modifications.
|
||||
try {
|
||||
$envKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $false)
|
||||
|
|
|
|||
|
|
@ -475,6 +475,50 @@ rm -rf "$REPO_ROOT/unsloth_compiled_cache"
|
|||
rm -rf "$SCRIPT_DIR/backend/unsloth_compiled_cache"
|
||||
rm -rf "$SCRIPT_DIR/tmp/unsloth_compiled_cache"
|
||||
|
||||
# WebView caches keyed by the app bundle id hold copies of the previous
|
||||
# frontend and can keep serving it after an update (old styles linger).
|
||||
# Cache-only paths: LocalStorage, IndexedDB, cookies, settings, models,
|
||||
# and the studio database are untouched.
|
||||
_clear_webview_caches() {
|
||||
_wvc_bid="ai.unsloth.studio"
|
||||
_wvc_paths=()
|
||||
case "$(uname -s 2>/dev/null)" in
|
||||
Darwin)
|
||||
_wvc_paths=(
|
||||
"$HOME/Library/Caches/$_wvc_bid"
|
||||
"$HOME/Library/WebKit/$_wvc_bid/WebsiteData/CacheStorage"
|
||||
"$HOME/Library/WebKit/$_wvc_bid/WebsiteData/ServiceWorkers"
|
||||
"$HOME/Library/WebKit/$_wvc_bid/WebsiteData/DiskCache"
|
||||
)
|
||||
;;
|
||||
Linux)
|
||||
# wry keys the WebKitGTK base-cache dir to the app DATA dir (same
|
||||
# as base-data), so cache subdirs also live under
|
||||
# ~/.local/share/<bid>. Clear only the cache-typed subdirs there;
|
||||
# sibling localstorage/, indexeddb/, and cookies stay.
|
||||
_wvc_data="${XDG_DATA_HOME:-$HOME/.local/share}/$_wvc_bid"
|
||||
_wvc_paths=(
|
||||
"${XDG_CACHE_HOME:-$HOME/.cache}/$_wvc_bid"
|
||||
"$_wvc_data/WebKitCache"
|
||||
"$_wvc_data/CacheStorage"
|
||||
"$_wvc_data/serviceworkers"
|
||||
"$_wvc_data/ServiceWorkers"
|
||||
)
|
||||
;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
_wvc_cleared=false
|
||||
for _wvc_p in "${_wvc_paths[@]}"; do
|
||||
[ -e "$_wvc_p" ] || continue
|
||||
rm -rf "$_wvc_p" 2>/dev/null && _wvc_cleared=true || true
|
||||
done
|
||||
if [ "$_wvc_cleared" = true ]; then
|
||||
substep "cleared stale WebView caches ($_wvc_bid); settings and data kept"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
_clear_webview_caches
|
||||
|
||||
# ── Detect Colab ──
|
||||
IS_COLAB=false
|
||||
keynames=$'\n'$(printenv | cut -d= -f1)
|
||||
|
|
|
|||
|
|
@ -161,6 +161,64 @@ fn setup_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// Clear WebView caches before the webview initializes. An in-app update runs
|
||||
// setup.sh/setup.ps1 while the old WebView still holds these files (its clear
|
||||
// silently fails), so without this a relaunch can serve the previous frontend
|
||||
// from cache. Cache-only paths; LocalStorage, IndexedDB, cookies, and app
|
||||
// data are kept. Mirrors setup.sh _clear_webview_caches / setup.ps1.
|
||||
fn clear_webview_caches(bundle_id: &str) {
|
||||
use std::path::PathBuf;
|
||||
let mut paths: Vec<PathBuf> = Vec::new();
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Ok(local) = std::env::var("LOCALAPPDATA") {
|
||||
if !local.is_empty() {
|
||||
let profile = PathBuf::from(local)
|
||||
.join(bundle_id)
|
||||
.join("EBWebView")
|
||||
.join("Default");
|
||||
for sub in ["Cache", "Code Cache", "GPUCache", "Service Worker"] {
|
||||
paths.push(profile.join(sub));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
if !home.is_empty() {
|
||||
let home = PathBuf::from(home);
|
||||
paths.push(home.join("Library/Caches").join(bundle_id));
|
||||
let data = home.join("Library/WebKit").join(bundle_id).join("WebsiteData");
|
||||
for sub in ["CacheStorage", "ServiceWorkers", "DiskCache"] {
|
||||
paths.push(data.join(sub));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
if !home.is_empty() {
|
||||
let home = PathBuf::from(home);
|
||||
let cache = std::env::var("XDG_CACHE_HOME")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| home.join(".cache"));
|
||||
paths.push(cache.join(bundle_id));
|
||||
// wry keys the WebKitGTK base-cache dir to the app data dir.
|
||||
let data = std::env::var("XDG_DATA_HOME")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| home.join(".local/share"))
|
||||
.join(bundle_id);
|
||||
for sub in ["WebKitCache", "CacheStorage", "serviceworkers", "ServiceWorkers"] {
|
||||
paths.push(data.join(sub));
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in paths {
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Fix PATH for GUI apps (macOS .app bundles, Linux AppImage, Windows)
|
||||
// GUI apps don't inherit shell dotfile PATH — this spawns the user's
|
||||
|
|
@ -171,6 +229,11 @@ fn main() {
|
|||
info!("Unsloth Studio desktop app starting");
|
||||
windows_job::initialize();
|
||||
|
||||
// Must run before the Builder: the config-defined window (and its
|
||||
// WebView, which locks these files) exists by the time setup hooks run.
|
||||
let context = tauri::generate_context!();
|
||||
clear_webview_caches(&context.config().identifier);
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
|
|
@ -247,7 +310,7 @@ fn main() {
|
|||
api.prevent_close();
|
||||
}
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.build(context)
|
||||
.expect("error while building tauri application")
|
||||
.run(|app, event| {
|
||||
if let tauri::RunEvent::Exit = event {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ sh "$TESTS_DIR/sh/test_nvcc_meets_llama_minimum.sh"
|
|||
sh "$TESTS_DIR/sh/test_resolve_cuda_archs.sh"
|
||||
sh "$TESTS_DIR/sh/test_strixhalo_wsl_reroute.sh"
|
||||
sh "$TESTS_DIR/sh/test_uninstall_shared_icon.sh"
|
||||
# bash, not sh: the extracted setup.sh function uses bash arrays (dash chokes).
|
||||
bash "$TESTS_DIR/sh/test_setup_webview_cache_clear.sh"
|
||||
sh "$TESTS_DIR/sh/test_torch_flavor.sh"
|
||||
sh "$TESTS_DIR/sh/test_redact_install_output.sh"
|
||||
sh "$TESTS_DIR/sh/test_install_uv_override_space.sh"
|
||||
|
|
|
|||
103
tests/sh/test_setup_webview_cache_clear.sh
Executable file
103
tests/sh/test_setup_webview_cache_clear.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
# Unit tests for _clear_webview_caches() from studio/setup.sh.
|
||||
#
|
||||
# The WebView caches keyed by the app bundle id (ai.unsloth.studio) hold copies
|
||||
# of the previous frontend, so an install/update must clear them or the app can
|
||||
# keep rendering old styles. Clearing must be cache-only: LocalStorage,
|
||||
# IndexedDB, app data, and unrelated apps' caches stay intact.
|
||||
#
|
||||
# Follows the extract-via-sed pattern of test_uninstall_shared_icon.sh; uname
|
||||
# is overridden per test with a shell function to select the OS branch.
|
||||
# shellcheck disable=SC2329 # uname stubs are invoked inside the extracted function
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SETUP_SH="$SCRIPT_DIR/../../studio/setup.sh"
|
||||
BID="ai.unsloth.studio"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
_TMP_ROOT=$(mktemp -d)
|
||||
trap 'rm -rf "$_TMP_ROOT"' EXIT
|
||||
|
||||
assert_gone() { _l="$1"; if [ -e "$2" ]; then echo " FAIL: $_l (still present: $2)"; FAIL=$((FAIL+1)); else echo " PASS: $_l"; PASS=$((PASS+1)); fi; }
|
||||
assert_present() { _l="$1"; if [ -e "$2" ]; then echo " PASS: $_l"; PASS=$((PASS+1)); else echo " FAIL: $_l (missing: $2)"; FAIL=$((FAIL+1)); fi; }
|
||||
|
||||
# Extract just the function definition (top-level, closes at column 0).
|
||||
FUNC_FILE=$(mktemp -p "$_TMP_ROOT")
|
||||
sed -n '/^_clear_webview_caches() {/,/^}/p' "$SETUP_SH" > "$FUNC_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
. "$FUNC_FILE"
|
||||
substep() { :; } # stub the setup.sh logger
|
||||
|
||||
# ── 1. macOS: cache paths removed, user-facing storage kept ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
mkdir -p "$H/Library/Caches/$BID/WebKit/NetworkCache" \
|
||||
"$H/Library/WebKit/$BID/WebsiteData/CacheStorage" \
|
||||
"$H/Library/WebKit/$BID/WebsiteData/ServiceWorkers" \
|
||||
"$H/Library/WebKit/$BID/WebsiteData/DiskCache" \
|
||||
"$H/Library/WebKit/$BID/WebsiteData/LocalStorage" \
|
||||
"$H/Library/WebKit/$BID/WebsiteData/IndexedDB" \
|
||||
"$H/Library/Application Support/$BID" \
|
||||
"$H/Library/Caches/com.other.app"
|
||||
uname() { echo Darwin; }
|
||||
HOME="$H" _clear_webview_caches
|
||||
assert_gone "macOS: Caches/$BID removed" "$H/Library/Caches/$BID"
|
||||
assert_gone "macOS: WebsiteData/CacheStorage removed" "$H/Library/WebKit/$BID/WebsiteData/CacheStorage"
|
||||
assert_gone "macOS: WebsiteData/ServiceWorkers removed" "$H/Library/WebKit/$BID/WebsiteData/ServiceWorkers"
|
||||
assert_gone "macOS: WebsiteData/DiskCache removed" "$H/Library/WebKit/$BID/WebsiteData/DiskCache"
|
||||
assert_present "macOS: LocalStorage kept" "$H/Library/WebKit/$BID/WebsiteData/LocalStorage"
|
||||
assert_present "macOS: IndexedDB kept" "$H/Library/WebKit/$BID/WebsiteData/IndexedDB"
|
||||
assert_present "macOS: Application Support kept" "$H/Library/Application Support/$BID"
|
||||
assert_present "macOS: unrelated app cache kept" "$H/Library/Caches/com.other.app"
|
||||
|
||||
# ── 2. Linux: cache paths removed (XDG cache dir AND the cache subdirs wry
|
||||
# keys to the app data dir), user-facing storage kept ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
D="$H/.local/share/$BID"
|
||||
mkdir -p "$H/.cache/$BID" "$D/WebKitCache" "$D/CacheStorage" "$D/serviceworkers" \
|
||||
"$D/localstorage" "$D/indexeddb" "$H/.config/$BID" "$H/.cache/other.app"
|
||||
: > "$D/cookies.sqlite"
|
||||
uname() { echo Linux; }
|
||||
HOME="$H" XDG_CACHE_HOME="" XDG_DATA_HOME="" _clear_webview_caches
|
||||
assert_gone "linux: ~/.cache/$BID removed" "$H/.cache/$BID"
|
||||
assert_gone "linux: data-dir WebKitCache removed" "$D/WebKitCache"
|
||||
assert_gone "linux: data-dir CacheStorage removed" "$D/CacheStorage"
|
||||
assert_gone "linux: data-dir serviceworkers removed" "$D/serviceworkers"
|
||||
assert_present "linux: localstorage kept" "$D/localstorage"
|
||||
assert_present "linux: indexeddb kept" "$D/indexeddb"
|
||||
assert_present "linux: cookies kept" "$D/cookies.sqlite"
|
||||
assert_present "linux: ~/.config/$BID kept" "$H/.config/$BID"
|
||||
assert_present "linux: unrelated app cache kept" "$H/.cache/other.app"
|
||||
|
||||
# ── 3. Linux: XDG_CACHE_HOME / XDG_DATA_HOME overrides honored ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
XDG=$(mktemp -d -p "$_TMP_ROOT")
|
||||
mkdir -p "$XDG/cache/$BID" "$XDG/data/$BID/WebKitCache" "$XDG/data/$BID/localstorage" "$H/.cache/$BID"
|
||||
HOME="$H" XDG_CACHE_HOME="$XDG/cache" XDG_DATA_HOME="$XDG/data" _clear_webview_caches
|
||||
assert_gone "linux: XDG_CACHE_HOME/$BID removed" "$XDG/cache/$BID"
|
||||
assert_gone "linux: XDG_DATA_HOME WebKitCache removed" "$XDG/data/$BID/WebKitCache"
|
||||
assert_present "linux: XDG_DATA_HOME localstorage kept" "$XDG/data/$BID/localstorage"
|
||||
assert_present "linux: ~/.cache/$BID kept under override" "$H/.cache/$BID"
|
||||
|
||||
# ── 4. Nothing to clear is a clean no-op ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
uname() { echo Darwin; }
|
||||
if HOME="$H" _clear_webview_caches; then
|
||||
echo " PASS: empty HOME -> no-op exit 0"; PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: empty HOME -> nonzero exit"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ── 5. Unknown OS is a no-op ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
mkdir -p "$H/Library/Caches/$BID"
|
||||
uname() { echo SunOS; }
|
||||
HOME="$H" _clear_webview_caches
|
||||
assert_present "unknown OS: nothing removed" "$H/Library/Caches/$BID"
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" = 0 ]
|
||||
|
|
@ -38,7 +38,11 @@ def fail(m):
|
|||
raise AssertionError(f"[font-scale] FAIL: {m}")
|
||||
|
||||
|
||||
def near(a, b, tol = 0.35):
|
||||
def near(
|
||||
a,
|
||||
b,
|
||||
tol = 0.35,
|
||||
):
|
||||
return a is not None and b is not None and abs(a - b) <= tol
|
||||
|
||||
|
||||
|
|
@ -86,9 +90,7 @@ def open_appearance(page):
|
|||
page.wait_for_timeout(700)
|
||||
if page.get_by_role("dialog").count() == 0:
|
||||
fail("settings dialog did not open")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(
|
||||
has_text = "Appearance"
|
||||
).first.click()
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click()
|
||||
page.wait_for_timeout(600)
|
||||
|
||||
|
||||
|
|
@ -153,9 +155,7 @@ def main():
|
|||
page.wait_for_timeout(400)
|
||||
|
||||
step("overflowing select scrolls its Radix viewport")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(
|
||||
has_text = "Voice"
|
||||
).first.click()
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first.click()
|
||||
page.wait_for_timeout(600)
|
||||
page.set_viewport_size({"width": 1440, "height": 480})
|
||||
page.locator("[aria-label='Dictation language']").click()
|
||||
|
|
@ -194,9 +194,7 @@ def main():
|
|||
page.wait_for_timeout(400)
|
||||
|
||||
step("default restores exactly")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(
|
||||
has_text = "Appearance"
|
||||
).first.click()
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click()
|
||||
page.wait_for_timeout(500)
|
||||
set_input(page, "UI font size", DEFAULT)
|
||||
final = measure(page)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ from pathlib import Path
|
|||
REPO = Path(__file__).resolve().parents[2]
|
||||
SRC = REPO / "studio/frontend/src"
|
||||
INDEX_CSS = (SRC / "index.css").read_text(encoding = "utf-8")
|
||||
STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(
|
||||
encoding = "utf-8"
|
||||
)
|
||||
STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(encoding = "utf-8")
|
||||
SELECT = (SRC / "components/ui/select.tsx").read_text(encoding = "utf-8")
|
||||
|
||||
# Raw numeric fontSize props are only allowed where a scaled stylesheet rule
|
||||
|
|
@ -70,9 +68,7 @@ def test_ui_token_families_exist():
|
|||
|
||||
|
||||
def test_explicit_code_font_size_is_never_multiplied():
|
||||
match = re.search(
|
||||
r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS
|
||||
)
|
||||
match = re.search(r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS)
|
||||
assert match is not None
|
||||
body = match.group(1)
|
||||
assert "var(--custom-code-font-size)" in body
|
||||
|
|
@ -83,7 +79,9 @@ def test_radix_select_viewport_owns_the_scroll_state():
|
|||
viewport = SELECT[SELECT.index("SelectPrimitive.Viewport") :]
|
||||
assert "overflow-y-auto" in viewport.split("</SelectPrimitive.Viewport>")[0]
|
||||
# The rounded surface itself must not scroll (WebKit squares its corners).
|
||||
content_cls = re.search(r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT)
|
||||
content_cls = re.search(
|
||||
r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT
|
||||
)
|
||||
assert content_cls is not None
|
||||
assert "overflow-hidden" in content_cls.group(1)
|
||||
assert "overflow-y-auto" not in content_cls.group(1)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue