From bf702033a64c6a3226b29ac049771e113021e2ba Mon Sep 17 00:00:00 2001 From: shimmyshimmer <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:54:45 -0700 Subject: [PATCH 1/3] Setup: clear stale WebView caches on install/update, keep user data The desktop app's WebView caches (keyed by the Tauri bundle id ai.unsloth.studio) hold copies of the previous frontend and keep serving them after an update, so old styles linger even though the code on disk is new. Clear cache-only paths on every install/update via setup.sh and setup.ps1 (both fresh installs and 'unsloth studio update' route through them). Cleared: the HTTP/network caches, CacheStorage, service workers, and GPU caches for the bundle id. Kept: LocalStorage, IndexedDB, cookies, settings, models, and the studio database. macOS: ~/Library/Caches/ and WebsiteData/{CacheStorage, ServiceWorkers,DiskCache}. Linux: XDG cache dir. Windows: EBWebView\Default\{Cache,Code Cache,GPUCache,Service Worker}. Adds tests/sh/test_setup_webview_cache_clear.sh covering both OS branches, XDG overrides, and that user-facing storage survives. --- studio/setup.ps1 | 19 +++++ studio/setup.sh | 33 ++++++++ tests/run_all.sh | 1 + tests/sh/test_setup_webview_cache_clear.sh | 92 ++++++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100755 tests/sh/test_setup_webview_cache_clear.sh diff --git a/studio/setup.ps1 b/studio/setup.ps1 index c6932121c9..8e15976e8c 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -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) diff --git a/studio/setup.sh b/studio/setup.sh index 3c97f77065..749c585a26 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -475,6 +475,39 @@ 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) + _wvc_paths=("${XDG_CACHE_HOME:-$HOME/.cache}/$_wvc_bid") + ;; + *) 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) diff --git a/tests/run_all.sh b/tests/run_all.sh index eaa726f73c..1b5609c475 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -14,6 +14,7 @@ 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" +sh "$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" diff --git a/tests/sh/test_setup_webview_cache_clear.sh b/tests/sh/test_setup_webview_cache_clear.sh new file mode 100755 index 0000000000..de2ab157d2 --- /dev/null +++ b/tests/sh/test_setup_webview_cache_clear.sh @@ -0,0 +1,92 @@ +#!/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 dir removed, data/config kept ── +H=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$H/.cache/$BID" "$H/.local/share/$BID" "$H/.config/$BID" "$H/.cache/other.app" +uname() { echo Linux; } +HOME="$H" XDG_CACHE_HOME="" _clear_webview_caches +assert_gone "linux: ~/.cache/$BID removed" "$H/.cache/$BID" +assert_present "linux: ~/.local/share/$BID kept" "$H/.local/share/$BID" +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 override honored ── +H=$(mktemp -d -p "$_TMP_ROOT") +XDG=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$XDG/$BID" "$H/.cache/$BID" +HOME="$H" XDG_CACHE_HOME="$XDG" _clear_webview_caches +assert_gone "linux: XDG_CACHE_HOME/$BID removed" "$XDG/$BID" +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 ] From 1eb3ac26e72a62a5f3cb375e1b9f0fc7ebea7f35 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:56:09 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/studio/playwright_ui_font_scale.py | 18 ++++++++---------- tests/studio/test_ui_font_scale_contract.py | 12 +++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/studio/playwright_ui_font_scale.py b/tests/studio/playwright_ui_font_scale.py index 7dbe36127c..89c7894929 100644 --- a/tests/studio/playwright_ui_font_scale.py +++ b/tests/studio/playwright_ui_font_scale.py @@ -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) diff --git a/tests/studio/test_ui_font_scale_contract.py b/tests/studio/test_ui_font_scale_contract.py index 62bf80e82b..393c60dd6b 100644 --- a/tests/studio/test_ui_font_scale_contract.py +++ b/tests/studio/test_ui_font_scale_contract.py @@ -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("")[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) From cbb15085134fb81335bcd047f7569d3ab6e9391b Mon Sep 17 00:00:00 2001 From: shimmyshimmer <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 23 Jul 2026 02:35:45 -0700 Subject: [PATCH 3/3] Address review: Linux data-dir caches, bash invocation, pre-webview clear Three review findings, all verified: 1. wry keys the WebKitGTK base-cache dir to the app DATA dir (same as base-data), so on Linux the stale frontend cache also lives under ~/.local/share/ai.unsloth.studio. Clear the cache-typed subdirs there (WebKitCache, CacheStorage, serviceworkers) while keeping localstorage, indexeddb, and cookies. Tests extended to 23 assertions. 2. run_all.sh invoked the new test with sh, but the extracted setup.sh function uses bash arrays; dash aborts with a syntax error before any assertion. Invoke with bash. 3. The in-app desktop update runs start_backend_update before downloadAndInstall/relaunch, so setup.ps1/setup.sh clear caches while the live WebView still holds them and silently fail. Clear the caches from the Rust side in main() before the Builder runs: the config window (and the WebView lock) exists by the time setup hooks fire, so this is the one point where the profile is guaranteed unlocked. Same cache-only path lists per OS; cargo check passes. --- studio/setup.sh | 13 ++++- studio/src-tauri/src/main.rs | 65 +++++++++++++++++++++- tests/run_all.sh | 3 +- tests/sh/test_setup_webview_cache_clear.sh | 35 ++++++++---- 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/studio/setup.sh b/studio/setup.sh index 749c585a26..13d654cdd6 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -492,7 +492,18 @@ _clear_webview_caches() { ) ;; Linux) - _wvc_paths=("${XDG_CACHE_HOME:-$HOME/.cache}/$_wvc_bid") + # wry keys the WebKitGTK base-cache dir to the app DATA dir (same + # as base-data), so cache subdirs also live under + # ~/.local/share/. 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 diff --git a/studio/src-tauri/src/main.rs b/studio/src-tauri/src/main.rs index 405b390177..e422124f0c 100644 --- a/studio/src-tauri/src/main.rs +++ b/studio/src-tauri/src/main.rs @@ -161,6 +161,64 @@ fn setup_tray(app: &tauri::App) -> Result<(), Box> { 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 = 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 { diff --git a/tests/run_all.sh b/tests/run_all.sh index 1b5609c475..147aaf1645 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -14,7 +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" -sh "$TESTS_DIR/sh/test_setup_webview_cache_clear.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" diff --git a/tests/sh/test_setup_webview_cache_clear.sh b/tests/sh/test_setup_webview_cache_clear.sh index de2ab157d2..19a3ee0064 100755 --- a/tests/sh/test_setup_webview_cache_clear.sh +++ b/tests/sh/test_setup_webview_cache_clear.sh @@ -53,23 +53,34 @@ assert_present "macOS: IndexedDB kept" "$H/Library/WebKit/$BID 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 dir removed, data/config kept ── +# ── 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") -mkdir -p "$H/.cache/$BID" "$H/.local/share/$BID" "$H/.config/$BID" "$H/.cache/other.app" +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="" _clear_webview_caches -assert_gone "linux: ~/.cache/$BID removed" "$H/.cache/$BID" -assert_present "linux: ~/.local/share/$BID kept" "$H/.local/share/$BID" -assert_present "linux: ~/.config/$BID kept" "$H/.config/$BID" -assert_present "linux: unrelated app cache kept" "$H/.cache/other.app" +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 override honored ── +# ── 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/$BID" "$H/.cache/$BID" -HOME="$H" XDG_CACHE_HOME="$XDG" _clear_webview_caches -assert_gone "linux: XDG_CACHE_HOME/$BID removed" "$XDG/$BID" -assert_present "linux: ~/.cache/$BID kept under override" "$H/.cache/$BID" +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")