unsloth/tests/sh/test_launcher_no_browser.sh

222 lines
9 KiB
Bash
Executable file

#!/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
# The generated desktop launchers must support suppressing the automatic
# default-browser open after the server becomes healthy (PWA users run Studio
# in a browser-app window, not the OS default browser). Covered surface:
# - launch-studio.sh: --no-browser flag, UNSLOTH_STUDIO_NO_BROWSER env var,
# persisted STUDIO_OPEN_BROWSER from studio.conf; prints the URL when off.
# - install.sh: --no-browser flag, interactive prompt, studio.conf persistence
# that survives the --shortcuts-only refresh run by `unsloth studio update`.
# - install.ps1: same feature baked into launch-studio.ps1 (Open-StudioUrl).
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_SH="$SCRIPT_DIR/../../install.sh"
INSTALL_PS1="$SCRIPT_DIR/../../install.ps1"
PASS=0
FAIL=0
# Here-strings, not `echo | grep -q`: grep exiting on first match SIGPIPEs
# the echo on large haystacks, spamming "write error: Broken pipe" in CI logs.
assert_contains() {
_label="$1"; _haystack="$2"; _needle="$3"
if grep -qF -- "$_needle" <<< "$_haystack"; then
echo " PASS: $_label"
PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected to find '$_needle')"
FAIL=$((FAIL + 1))
fi
}
assert_not_contains() {
_label="$1"; _haystack="$2"; _needle="$3"
if grep -qF -- "$_needle" <<< "$_haystack"; then
echo " FAIL: $_label (found '$_needle' but should not)"
FAIL=$((FAIL + 1))
else
echo " PASS: $_label"
PASS=$((PASS + 1))
fi
}
echo ""
echo "=== install.sh launcher template ==="
# Extract the heredoc that generates ~/.local/share/unsloth/launch-studio.sh.
_launcher=$(awk '/cat > "\$_css_launcher"/{found=1} found{print} /^LAUNCHER_EOF$/{found=0}' "$INSTALL_SH")
assert_contains \
"launcher template: --no-browser argument handled" \
"$_launcher" "--no-browser) OPEN_BROWSER=0"
assert_contains \
"launcher template: UNSLOTH_STUDIO_NO_BROWSER env var handled" \
"$_launcher" "UNSLOTH_STUDIO_NO_BROWSER"
# Mixed-case falsy values (False, Off) must not disable, matching the
# PowerShell launcher's case-insensitive -notin.
assert_contains \
"launcher template: env var check is case-insensitive" \
"$_launcher" "tr '[:upper:]' '[:lower:]'"
assert_contains \
"launcher template: studio.conf preference is the default" \
"$_launcher" 'OPEN_BROWSER="${STUDIO_OPEN_BROWSER:-1}"'
assert_contains \
"launcher template: _open_browser is gated on OPEN_BROWSER" \
"$_launcher" '[ "$OPEN_BROWSER" = "0" ]'
assert_contains \
"launcher template: URL still printed when auto-open is off" \
"$_launcher" "Unsloth Studio is running at:"
echo ""
echo "=== install.sh installer plumbing ==="
_installer=$(cat "$INSTALL_SH")
assert_contains \
"install.sh: --no-browser flag parsed" \
"$_installer" "--no-browser) _STUDIO_OPEN_BROWSER=0"
assert_contains \
"install.sh: preference persisted into studio.conf" \
"$_installer" "STUDIO_OPEN_BROWSER='\$_css_open_browser'"
assert_contains \
"install.sh: existing studio.conf choice preserved on refresh" \
"$_installer" "s/^STUDIO_OPEN_BROWSER="
assert_contains \
"install.sh: interactive prompt asks about browser auto-open" \
"$_installer" "Open Unsloth Studio in your default browser after launch?"
# A reinstall that accepts the prompt default must keep the saved choice.
assert_contains \
"install.sh: prompt Enter keeps the persisted preference" \
"$_installer" '*) _STUDIO_OPEN_BROWSER="${_existing_open_browser:-1}"'
# The WSL Strix Halo reroute must forward an explicit browser choice.
assert_contains \
"install.sh: reroute forwards --no-browser" \
"$_installer" '[ "$_STUDIO_OPEN_BROWSER" = "0" ] && _rr_args="$_rr_args --no-browser"'
# The post-install foreground launch honors the preference too.
assert_contains \
"install.sh: post-install launch opens browser via gated watcher" \
"$_installer" '_post_install_browser_watch "$_post_install_port"'
assert_contains \
"install.sh: post-install launch selects a free port" \
"$_installer" "_find_post_install_port()"
assert_contains \
"install.sh: server uses the watcher-selected port" \
"$_installer" 'studio -p "$_post_install_port"'
# The --shortcuts-only early exit must not EPIPE a curl | sh pipeline.
assert_contains \
"install.sh: shortcuts-only exit drains piped stdin" \
"$_installer" '[ ! -t 0 ] && cat > /dev/null'
echo ""
echo "=== install.sh _open_browser gating (functional) ==="
# Extract the _open_browser function from the (column-0) launcher heredoc and
# drive it with stubbed browser openers on PATH.
_fn=$(printf '%s\n' "$_launcher" | awk '/^_open_browser\(\) \{/{found=1} found{print} found && /^\}/{exit}')
if [ -z "$_fn" ]; then
echo " FAIL: could not extract _open_browser from launcher template"
FAIL=$((FAIL + 1))
else
_tmp=$(mktemp -d)
trap 'rm -rf "$_tmp"' EXIT
for _stub in open xdg-open; do
printf '#!/bin/sh\necho "BROWSER_OPENED:$1" >> "$RECORD"\n' > "$_tmp/$_stub"
chmod +x "$_tmp/$_stub"
done
# Off: no browser process, URL echoed instead.
_out=$(RECORD="$_tmp/record_off" PATH="$_tmp:$PATH" bash -c \
"OPEN_BROWSER=0; $_fn; _open_browser http://localhost:9999")
assert_contains \
"OPEN_BROWSER=0 prints the URL" \
"$_out" "Unsloth Studio is running at: http://localhost:9999"
if [ -f "$_tmp/record_off" ]; then
echo " FAIL: OPEN_BROWSER=0 still invoked a browser opener"
FAIL=$((FAIL + 1))
else
echo " PASS: OPEN_BROWSER=0 does not invoke a browser opener"
PASS=$((PASS + 1))
fi
# On (default): browser opener invoked with the URL.
RECORD="$_tmp/record_on" PATH="$_tmp:$PATH" bash -c \
"OPEN_BROWSER=1; $_fn; _open_browser http://localhost:9999" > /dev/null
# xdg-open is backgrounded inside _open_browser; give the stub a moment.
_i=0
while [ ! -s "$_tmp/record_on" ] && [ "$_i" -lt 20 ]; do
sleep 0.1
_i=$((_i + 1))
done
assert_contains \
"OPEN_BROWSER=1 invokes a browser opener with the URL" \
"$(cat "$_tmp/record_on" 2>/dev/null)" "BROWSER_OPENED:http://localhost:9999"
fi
echo ""
echo "=== install.ps1 launcher template ==="
# grep the file directly: piping the whole installer into grep -q trips
# SIGPIPE noise from echo once grep exits on first match.
assert_file_contains() {
_label="$1"; _file="$2"; _needle="$3"
if grep -qF -- "$_needle" "$_file"; then
echo " PASS: $_label"
PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected to find '$_needle')"
FAIL=$((FAIL + 1))
fi
}
assert_file_contains \
"install.ps1: --no-browser flag parsed" \
"$INSTALL_PS1" "\"--no-browser\" { \$OpenBrowserPref = '0' }"
assert_file_contains \
"install.ps1: preference baked into launch-studio.ps1" \
"$INSTALL_PS1" "openBrowserDefault = '\$_openBrowser'"
assert_file_contains \
"install.ps1: launcher honors UNSLOTH_STUDIO_NO_BROWSER" \
"$INSTALL_PS1" "UNSLOTH_STUDIO_NO_BROWSER"
assert_file_contains \
"install.ps1: gated helper defined" \
"$INSTALL_PS1" "function Open-StudioUrl {"
assert_file_contains \
"install.ps1: interactive prompt asks about browser auto-open" \
"$INSTALL_PS1" "Open Unsloth Studio in your default browser after launch?"
assert_file_contains \
"install.ps1: prompt Enter keeps the baked preference" \
"$INSTALL_PS1" 'elseif ($_existingPref) { $_existingPref }'
assert_file_contains \
"install.ps1: post-install launch opens browser via gated watcher" \
"$INSTALL_PS1" 'Start-Job -ScriptBlock $_browserWatch'
assert_file_contains \
"install.ps1: post-install launch selects a free port" \
"$INSTALL_PS1" 'function Find-PostInstallStudioPort {'
assert_file_contains \
"install.ps1: watcher receives the selected port" \
"$INSTALL_PS1" 'ArgumentList @($_watchRootId, $_launchPort)'
assert_file_contains \
"install.ps1: server uses the watcher-selected port" \
"$INSTALL_PS1" '& $UnslothExe studio -p $_launchPort'
# All launcher URL opens must route through the gated helper. The one
# allowed direct call is inside the post-install $_browserWatch scriptblock,
# whose Start-Job call site is itself gated on the preference.
_ps1_direct_open=$(grep -cE 'Start-Process "http://localhost:' "$INSTALL_PS1" || true)
_ps1_watch_open=$(awk '/\$_browserWatch = \{/{f=1} f && /^ \}$/{exit} f' "$INSTALL_PS1" \
| grep -cE 'Start-Process "http://localhost:' || true)
if [ "$_ps1_direct_open" -eq 1 ] && [ "$_ps1_watch_open" -eq 1 ]; then
echo " PASS: only the gated browser watcher opens a URL directly"
PASS=$((PASS + 1))
else
echo " FAIL: found $_ps1_direct_open direct URL opens ($_ps1_watch_open in the watcher); all others must route through Open-StudioUrl"
FAIL=$((FAIL + 1))
fi
echo ""
echo "=== Results ==="
echo " PASS: $PASS"
echo " FAIL: $FAIL"
if [ "$FAIL" -gt 0 ]; then
echo "FAILED"
exit 1
fi
echo "ALL PASSED"