diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd23eba..304e81b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,21 +21,12 @@ jobs: - run: bun run check - run: bun run test - e2e: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 'latest' - - run: bun install - - run: bun run build - - run: npx playwright install --with-deps chromium - - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: always() - with: - name: e2e-screenshots - path: e2e-results/ - retention-days: 30 + # E2E tests are NOT run in CI — they require GPU/Vulkan to run transformers.js + # models in WebGPU mode within a reasonable time budget, and GitHub Actions + # Pro does not offer GPU-enabled runners. + # + # Run E2E tests locally before pushing: + # bun run test:e2e:local (builds a Docker image with Mesa Vulkan + Playwright) + # + # See e2e/Dockerfile and e2e/run-local.sh for details. + # The pre-push git hook runs this automatically. diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..053b1a4 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# Run Playwright E2E tests before pushing. +# Requires Docker (see e2e/run-local.sh). +# +# Skip when Docker is unavailable or for a quick push: +# git push --no-verify + +if ! command -v docker > /dev/null 2>&1; then + echo "⚠ Docker not found — skipping E2E tests. Install Docker to enable local E2E." + exit 0 +fi + +echo "▶ Running E2E tests before push (skip with: git push --no-verify)..." +bun run test:e2e:local diff --git a/e2e/Dockerfile b/e2e/Dockerfile new file mode 100644 index 0000000..0d237aa --- /dev/null +++ b/e2e/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +# +# E2E local test image for Q². +# +# Built on the official Playwright Ubuntu Jammy image — Chromium and all of +# its system-level dependencies are pre-installed and version-matched. +# Mesa Vulkan drivers are layered on top so that Chromium can use WebGPU via: +# • Intel ANV (Intel integrated / discrete GPU) +# • AMD RADV (AMD GPU via /dev/dri passthrough) +# • lavapipe (Mesa software Vulkan — unconditional fallback, no GPU needed) +# +# Build: docker build -t q2-e2e -f e2e/Dockerfile . +# Run: bun run test:e2e:local (or directly: ./e2e/run-local.sh) +# +# Update this tag when you bump @playwright/test in package.json. +# Available tags: https://mcr.microsoft.com/v2/playwright/tags/list +FROM mcr.microsoft.com/playwright:v1.50.1-jammy + +# ── Vulkan support ──────────────────────────────────────────────────────── +# mesa-vulkan-drivers installs Intel ANV, AMD RADV, and lavapipe in one shot. +# libvulkan1 is the Vulkan loader (required by both drivers and Chromium). +RUN apt-get update && apt-get install -y --no-install-recommends \ + libvulkan1 \ + mesa-vulkan-drivers \ + && rm -rf /var/lib/apt/lists/* + +# ── Bun ─────────────────────────────────────────────────────────────────── +# Bun is the project's build tool and package manager. +RUN curl -fsSL https://bun.sh/install | bash +ENV PATH="/root/.bun/bin:$PATH" + +WORKDIR /app diff --git a/e2e/chat.spec.ts b/e2e/chat.spec.ts index b141c95..7e8d918 100644 --- a/e2e/chat.spec.ts +++ b/e2e/chat.spec.ts @@ -13,6 +13,7 @@ import { test, expect } from '@playwright/test'; test.setTimeout(480_000); const MODEL_ID = process.env.E2E_MODEL ?? 'onnx-community/Qwen3.5-0.8B-ONNX'; +const MODEL_DTYPE = process.env.E2E_DTYPE ?? 'q4'; test.describe('Real chat interaction', () => { test('sends a message and receives a streamed response', async ({ page }, testInfo) => { @@ -24,7 +25,7 @@ test.describe('Real chat interaction', () => { // ── Step 1: Load the model ───────────────────────────────────────────── await page.click('#tab-settings'); await page.fill('#model-custom-id', MODEL_ID); - await page.selectOption('#model-dtype', 'q4'); + await page.selectOption('#model-dtype', MODEL_DTYPE); await page.click('#load-btn'); // Wait for model to finish loading. diff --git a/e2e/model-loading.spec.ts b/e2e/model-loading.spec.ts index 86a73da..4b3b499 100644 --- a/e2e/model-loading.spec.ts +++ b/e2e/model-loading.spec.ts @@ -7,7 +7,7 @@ * ONNX Runtime WASM session, and the UI transitions to "ready". * * The default model is onnx-community/Qwen3.5-0.8B-ONNX (q4). Override with - * the E2E_MODEL environment variable for faster CI runs. + * the E2E_MODEL and E2E_DTYPE environment variables for faster CI runs. */ import { test, expect } from '@playwright/test'; @@ -15,6 +15,7 @@ import { test, expect } from '@playwright/test'; test.setTimeout(300_000); const MODEL_ID = process.env.E2E_MODEL ?? 'onnx-community/Qwen3.5-0.8B-ONNX'; +const MODEL_DTYPE = process.env.E2E_DTYPE ?? 'q4'; test.describe('Real model loading via transformers.js', () => { test('loads a model and transitions to ready state', async ({ page }, testInfo) => { @@ -28,8 +29,8 @@ test.describe('Real model loading via transformers.js', () => { await page.click('#tab-settings'); await page.fill('#model-custom-id', MODEL_ID); - // Ensure q4 dtype is selected (smallest download). - await page.selectOption('#model-dtype', 'q4'); + // Ensure the configured dtype is selected (q4 is the smallest download by default). + await page.selectOption('#model-dtype', MODEL_DTYPE); await page.screenshot({ path: testInfo.outputPath('model-before-load.png'), fullPage: true }); diff --git a/e2e/run-local.sh b/e2e/run-local.sh new file mode 100755 index 0000000..b07242c --- /dev/null +++ b/e2e/run-local.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Run Playwright E2E tests locally inside a Docker container. +# +# • Builds the q2-e2e Docker image on first run (one-time ~3 min). +# • Caches node_modules in a named Docker volume so subsequent bun installs +# are near-instant. +# • Passes /dev/dri to the container for Intel/AMD GPU acceleration when +# available; falls back to Mesa lavapipe (software Vulkan) otherwise. +# • The pre-installed Playwright browsers in the image are reused — no +# 200 MB chromium download on each run. +# +# Usage: +# ./e2e/run-local.sh +# E2E_MODEL=onnx-community/SmolLM2-135M-Instruct E2E_DTYPE=q4 ./e2e/run-local.sh +# +# Rebuild the image after updating the Playwright version in package.json: +# docker build -t q2-e2e -f e2e/Dockerfile . + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +IMAGE="q2-e2e:local" +NM_VOLUME="q2-e2e-node-modules" + +# ── Require Docker ──────────────────────────────────────────────────────── +if ! command -v docker > /dev/null 2>&1; then + echo "✖ Docker is not installed or not in PATH." >&2 + echo " Install Docker Desktop: https://docs.docker.com/get-docker/" >&2 + exit 1 +fi + +# ── Build image if it does not exist yet ───────────────────────────────── +if ! docker image inspect "$IMAGE" > /dev/null 2>&1; then + echo "▶ Building E2E Docker image (first-run setup — ~3 min)..." + docker build -t "$IMAGE" -f "$SCRIPT_DIR/Dockerfile" "$REPO_ROOT" +fi + +# ── GPU passthrough ─────────────────────────────────────────────────────── +GPU_FLAGS=() +GPU_AVAILABLE="" +if [ -d /dev/dri ]; then + GPU_FLAGS+=(--device /dev/dri) + GPU_AVAILABLE="1" + echo "▶ /dev/dri found — Intel/AMD GPU passthrough enabled (WebGPU)." +else + echo "▶ /dev/dri not found — using Mesa lavapipe (software Vulkan fallback)." +fi + +# ── Run tests ───────────────────────────────────────────────────────────── +echo "▶ Running E2E tests..." +exec docker run --rm \ + --ipc=host \ + ${GPU_FLAGS[@]+"${GPU_FLAGS[@]}"} \ + -v "$REPO_ROOT:/app" \ + -v "$NM_VOLUME:/app/node_modules" \ + -w /app \ + -e CI=true \ + -e E2E_GPU_AVAILABLE="${E2E_GPU_AVAILABLE:-$GPU_AVAILABLE}" \ + -e E2E_MODEL="${E2E_MODEL:-}" \ + -e E2E_DTYPE="${E2E_DTYPE:-}" \ + "$IMAGE" \ + sh -c "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun install --frozen-lockfile && bun run build && npx playwright test" diff --git a/package.json b/package.json index 9744c5c..d6bf492 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test": "vitest run --coverage", "pretest:e2e": "bun run build", "test:e2e": "playwright test", + "test:e2e:local": "bash e2e/run-local.sh", "test:browser": "vitest run --browser", "coverage": "vitest run --coverage", "deploy": "bun run build" diff --git a/playwright.config.ts b/playwright.config.ts index f28465e..d740f9c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,6 +6,30 @@ import { defineConfig } from '@playwright/test'; * Serves the built application via a lightweight static server and runs real * browser tests against it — no mocks, no fakes, no fake DOM. */ + +/** + * Chromium launch args for WebGPU support. + * + * Local Docker runs (e2e/run-local.sh): + * /dev/dri is passed to the container for Intel/AMD GPU access. The run + * script sets E2E_GPU_AVAILABLE=1 automatically when /dev/dri is present, + * which drops --use-gl=swiftshader and lets Chromium use hardware Vulkan + * (Intel ANV / AMD RADV). + * + * Without GPU (lavapipe fallback): + * --use-gl=swiftshader forces the Mesa software Vulkan rasterizer. + * WebGPU still works; inference falls back to WASM. + */ +const gpuArgs = [ + '--enable-gpu', + '--ignore-gpu-blocklist', + '--enable-unsafe-webgpu', + '--disable-gpu-sandbox', + // Use SwiftShader (software GL) when no GPU is available. + // Dropped when E2E_GPU_AVAILABLE=1 (set by run-local.sh on /dev/dri systems). + ...( process.env.E2E_GPU_AVAILABLE ? [] : ['--use-gl=swiftshader'] ), +]; + export default defineConfig({ testDir: './e2e', outputDir: './e2e-results', @@ -27,6 +51,12 @@ export default defineConfig({ screenshot: 'on', trace: 'retain-on-failure', headless: true, + /* Enable WebGPU and hardware-acceleration hints. + * On systems with Intel/AMD GPU: hardware Vulkan via /dev/dri passthrough. + * Without GPU: Mesa lavapipe (software Vulkan) via SwiftShader flag. */ + launchOptions: { + args: gpuArgs, + }, }, projects: [