The IQ3_XXS quants the Linux smoke uses are pathological at
temperature=0 on Apple Silicon Metal:
- Qwen3.5-2B IQ3_XXS emits 'The The The...' for tool-call prompts
(no tool_calls in the response, hits max_tokens).
- gemma-4-E2B IQ3_XXS emits '<unused5><unused5>...' for any prompt
(model degenerates to padding tokens).
Both are inference-path-correct but quant-degenerate; the Linux CPU
backend hides the issue. Bump both to UD-Q4_K_XL, the smallest
published variant that generates real text + well-formed tool calls
on M1. Inference time goes up modestly (CI is cache-warm so download
cost is one-shot per HF release).
Also bump STUDIO_UI_TURN_TIMEOUT_MS to 540s for the Mac UI job:
the macos-14 free runner is 3-5x slower than ubuntu-latest at
gemma-3-270m CPU inference, and the existing 180s ceiling crowded
turn 4 ('say tree').
206 lines
7.2 KiB
YAML
206 lines
7.2 KiB
YAML
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
# Mac counterpart to studio-ui-smoke.yml. Same Playwright + Chromium
|
|
# end-to-end chat UI flow, but on macos-14 (M1) so we catch
|
|
# Mac-specific frontend / backend wiring regressions that the Linux
|
|
# job would miss (e.g. the Mac Tauri shell loading the same React
|
|
# bundle, or the Mac llama.cpp prebuilt's HTTP layer behaving
|
|
# differently from the Linux build).
|
|
|
|
name: Mac Studio UI CI
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'studio/**'
|
|
- 'unsloth/**'
|
|
- 'unsloth_cli/**'
|
|
- 'install.sh'
|
|
- 'pyproject.toml'
|
|
- 'tests/studio/**'
|
|
- '.github/workflows/studio-mac-ui-smoke.yml'
|
|
push:
|
|
branches: [main, pip]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
ui-smoke:
|
|
name: Chat UI Tests
|
|
runs-on: macos-14
|
|
timeout-minutes: 35
|
|
env:
|
|
GGUF_REPO: unsloth/gemma-3-270m-it-GGUF
|
|
GGUF_VARIANT: UD-Q4_K_XL
|
|
GGUF_FILE: gemma-3-270m-it-UD-Q4_K_XL.gguf
|
|
STUDIO_PORT: '18896'
|
|
HF_HOME: ${{ github.workspace }}/hf-cache
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: '22'
|
|
cache: 'npm'
|
|
cache-dependency-path: studio/frontend/package-lock.json
|
|
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: '3.12'
|
|
cache: 'pip'
|
|
|
|
- name: Cache HF_HOME for ${{ env.GGUF_REPO }}
|
|
id: cache-hf
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: hf-cache
|
|
key: ${{ runner.os }}-hf-${{ env.GGUF_REPO }}-${{ env.GGUF_VARIANT }}-v1
|
|
|
|
- name: Prime HF_HOME with the GGUF
|
|
if: steps.cache-hf.outputs.cache-hit != 'true'
|
|
run: |
|
|
python -m pip install --upgrade huggingface_hub hf_transfer
|
|
mkdir -p hf-cache
|
|
HF_HUB_ENABLE_HF_TRANSFER=1 \
|
|
hf download "$GGUF_REPO" "$GGUF_FILE"
|
|
|
|
- name: Install Studio (--local, --no-torch)
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
mkdir -p logs
|
|
set -o pipefail
|
|
bash install.sh --local --no-torch 2>&1 | tee logs/install.log
|
|
|
|
- name: Assert install.sh used the Mac llama.cpp prebuilt
|
|
run: |
|
|
if grep -q "falling back to source build" logs/install.log; then
|
|
echo "::error::install.sh fell back to source-build llama.cpp on Mac. Studio must install the prebuilt llama-bNNNN-bin-macos-arm64 on Apple Silicon."
|
|
grep -E "llama-prebuilt|llama.cpp" logs/install.log | tail -60
|
|
exit 1
|
|
fi
|
|
|
|
- name: Install Playwright + Chromium
|
|
# No --with-deps on Mac: that flag installs Linux apt packages.
|
|
# GitHub-hosted macos-14 ships the system frameworks Chromium
|
|
# needs already.
|
|
run: |
|
|
pip install 'playwright>=1.45'
|
|
python -m playwright install chromium
|
|
|
|
- name: Reset auth + boot Studio
|
|
run: |
|
|
unsloth studio reset-password
|
|
mkdir -p logs
|
|
UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \
|
|
> logs/studio.log 2>&1 &
|
|
echo "STUDIO_PID=$!" >> "$GITHUB_ENV"
|
|
|
|
- name: Wait for /api/health
|
|
run: |
|
|
for i in $(seq 1 180); do
|
|
if curl -fs "http://127.0.0.1:${STUDIO_PORT}/api/health" > /tmp/health.json; then
|
|
jq -e '.status == "healthy"' /tmp/health.json && break
|
|
fi
|
|
sleep 1
|
|
done
|
|
jq -e '.status == "healthy"' /tmp/health.json
|
|
|
|
- name: Pass bootstrap password to the Playwright step
|
|
run: |
|
|
OLD=$(cat ~/.unsloth/studio/auth/.bootstrap_password)
|
|
NEW="CIUi-$(python -c 'import secrets; print(secrets.token_urlsafe(16))')"
|
|
NEW2="CIUi-$(python -c 'import secrets; print(secrets.token_urlsafe(16))')"
|
|
echo "::add-mask::$OLD"
|
|
echo "::add-mask::$NEW"
|
|
echo "::add-mask::$NEW2"
|
|
echo "STUDIO_OLD_PW=$OLD" >> "$GITHUB_ENV"
|
|
echo "STUDIO_NEW_PW=$NEW" >> "$GITHUB_ENV"
|
|
echo "STUDIO_NEW2_PW=$NEW2" >> "$GITHUB_ENV"
|
|
|
|
- name: Drive the chat UI with Playwright
|
|
env:
|
|
BASE_URL: http://127.0.0.1:18896
|
|
PW_ART_DIR: logs/playwright
|
|
STUDIO_UI_STRICT: '1'
|
|
# macos-14 free runner is 3 vCPU / 7 GB / no Metal-accel
|
|
# available to llama.cpp from CI; gemma-3-270m turn latency
|
|
# has been observed to crowd the 180s default. Triple it.
|
|
STUDIO_UI_TURN_TIMEOUT_MS: '540000'
|
|
run: |
|
|
mkdir -p logs/playwright
|
|
python tests/studio/playwright_chat_ui.py
|
|
|
|
- name: Stop Studio (chat-ui ends with Shutdown click; this is belt-and-suspenders)
|
|
if: always()
|
|
run: |
|
|
kill "${STUDIO_PID}" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
- name: Reset auth + boot Studio for extra UI tests (port 18897)
|
|
run: |
|
|
unsloth studio reset-password
|
|
mkdir -p logs
|
|
UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18897 \
|
|
> logs/studio_extra.log 2>&1 &
|
|
echo "STUDIO_EXTRA_PID=$!" >> "$GITHUB_ENV"
|
|
|
|
- name: Wait for /api/health on 18897
|
|
run: |
|
|
for i in $(seq 1 180); do
|
|
if curl -fs "http://127.0.0.1:18897/api/health" > /tmp/health2.json; then
|
|
jq -e '.status == "healthy"' /tmp/health2.json && break
|
|
fi
|
|
sleep 1
|
|
done
|
|
jq -e '.status == "healthy"' /tmp/health2.json
|
|
|
|
- name: Pass bootstrap pw for extra UI test
|
|
run: |
|
|
OLD=$(cat ~/.unsloth/studio/auth/.bootstrap_password)
|
|
NEW="CIUiExtra-$(python -c 'import secrets; print(secrets.token_urlsafe(16))')"
|
|
echo "::add-mask::$OLD"
|
|
echo "::add-mask::$NEW"
|
|
echo "STUDIO_EXTRA_OLD_PW=$OLD" >> "$GITHUB_ENV"
|
|
echo "STUDIO_EXTRA_NEW_PW=$NEW" >> "$GITHUB_ENV"
|
|
|
|
- name: Drive Compare/Recipes/Export/Studio/Settings with Playwright
|
|
env:
|
|
BASE_URL: http://127.0.0.1:18897
|
|
STUDIO_OLD_PW: ${{ env.STUDIO_EXTRA_OLD_PW }}
|
|
STUDIO_NEW_PW: ${{ env.STUDIO_EXTRA_NEW_PW }}
|
|
PW_ART_DIR: logs/playwright_extra
|
|
STUDIO_UI_STRICT: '1'
|
|
# See "Drive the chat UI" step.
|
|
STUDIO_UI_TURN_TIMEOUT_MS: '540000'
|
|
GGUF_REPO: ${{ env.GGUF_REPO }}
|
|
GGUF_VARIANT: ${{ env.GGUF_VARIANT }}
|
|
run: |
|
|
mkdir -p logs/playwright_extra
|
|
python tests/studio/playwright_extra_ui.py
|
|
|
|
- name: Stop second Studio
|
|
if: always()
|
|
run: |
|
|
kill "${STUDIO_EXTRA_PID}" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
- name: Upload Playwright artifacts
|
|
if: always()
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
with:
|
|
name: mac-studio-ui-smoke-artifacts
|
|
path: |
|
|
logs/studio.log
|
|
logs/studio_extra.log
|
|
logs/install.log
|
|
logs/playwright
|
|
logs/playwright_extra
|
|
retention-days: 7
|