Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
16 changes: 16 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Playwright base image version (v1.50.1-jammy) is far behind the repo’s @playwright/test version (currently ^1.58.2). Version mismatches between the runner and the preinstalled browsers/drivers can cause flaky or broken E2E runs. Update the image tag to match the Playwright version used by the repo (and consider pinning playwright/@playwright/test consistently).

Suggested change
FROM mcr.microsoft.com/playwright:v1.50.1-jammy
FROM mcr.microsoft.com/playwright:v1.58.2-jammy

Copilot uses AI. Check for mistakes.

# ── 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
3 changes: 2 additions & 1 deletion e2e/chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Comment on lines 15 to +16

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.env.E2E_MODEL ?? ... / process.env.E2E_DTYPE ?? ... will treat an empty string as a valid override. With the new Docker runner currently passing empty env vars, this can result in MODEL_ID/MODEL_DTYPE being '' and the test failing. Prefer || (or a small normalization helper) so empty strings fall back to defaults.

Suggested change
const MODEL_ID = process.env.E2E_MODEL ?? 'onnx-community/Qwen3.5-0.8B-ONNX';
const MODEL_DTYPE = process.env.E2E_DTYPE ?? 'q4';
const MODEL_ID = process.env.E2E_MODEL || 'onnx-community/Qwen3.5-0.8B-ONNX';
const MODEL_DTYPE = process.env.E2E_DTYPE || 'q4';

Copilot uses AI. Check for mistakes.

test.describe('Real chat interaction', () => {
test('sends a message and receives a streamed response', async ({ page }, testInfo) => {
Expand All @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions e2e/model-loading.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
* 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.

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment still says the env vars are for “faster CI runs”, but this PR removes E2E from CI. Consider updating the wording to “faster local runs” (or similar) to avoid misleading guidance.

Suggested change
* the E2E_MODEL and E2E_DTYPE environment variables for faster CI runs.
* the E2E_MODEL and E2E_DTYPE environment variables for faster local runs.

Copilot uses AI. Check for mistakes.
*/
import { test, expect } from '@playwright/test';

/* Allow up to 5 minutes per test — model download + WASM init is slow. */
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';
Comment on lines 17 to +18

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as in chat.spec.ts: using ?? here will not fall back when E2E_MODEL/E2E_DTYPE are present but empty (which the new Docker runner currently does). Use || (or normalize env vars) so empty values don’t break model selection.

Suggested change
const MODEL_ID = process.env.E2E_MODEL ?? 'onnx-community/Qwen3.5-0.8B-ONNX';
const MODEL_DTYPE = process.env.E2E_DTYPE ?? 'q4';
const MODEL_ID = process.env.E2E_MODEL || 'onnx-community/Qwen3.5-0.8B-ONNX';
const MODEL_DTYPE = process.env.E2E_DTYPE || 'q4';

Copilot uses AI. Check for mistakes.

test.describe('Real model loading via transformers.js', () => {
test('loads a model and transitions to ready state', async ({ page }, testInfo) => {
Expand All @@ -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 });

Expand Down
63 changes: 63 additions & 0 deletions e2e/run-local.sh
Original file line number Diff line number Diff line change
@@ -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:-}" \
Comment on lines +50 to +61

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run-local.sh always injects E2E_MODEL/E2E_DTYPE into the container, even when they’re unset on the host (they become empty strings). Because the specs use nullish coalescing (??), an empty string prevents the defaults from being applied and selectOption/model id filling can fail. Only pass these -e flags when the vars are non-empty, or pass explicit defaults (e.g., omit the env vars unless set).

Suggested change
# ── 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:-}" \
# ── Optional E2E env flags ────────────────────────────────────────────────
E2E_FLAGS=()
if [ -n "${E2E_MODEL:-}" ]; then
E2E_FLAGS+=(-e "E2E_MODEL=$E2E_MODEL")
fi
if [ -n "${E2E_DTYPE:-}" ]; then
E2E_FLAGS+=(-e "E2E_DTYPE=$E2E_DTYPE")
fi
# ── Run tests ─────────────────────────────────────────────────────────────
echo "▶ Running E2E tests..."
exec docker run --rm \
--ipc=host \
${GPU_FLAGS[@]+"${GPU_FLAGS[@]}"} \
${E2E_FLAGS[@]+"${E2E_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}" \

Copilot uses AI. Check for mistakes.
"$IMAGE" \
sh -c "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun install --frozen-lockfile && bun run build && npx playwright test"
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +19 to +21

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims --use-gl=swiftshader “forces the Mesa software Vulkan rasterizer (lavapipe)”, but SwiftShader is Chromium’s software GL backend and is distinct from Mesa’s Vulkan lavapipe. This is likely to confuse future debugging of WebGPU/Vulkan issues; please adjust the comment to describe the actual fallback(s) being used.

Copilot uses AI. Check for mistakes.
*/
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',
Expand All @@ -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: [
Expand Down
Loading