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
17 changes: 17 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ desktop-tauri-check: _ensure-sidecar-stubs
desktop-tauri-test: _ensure-sidecar-stubs
cd desktop/src-tauri && cargo test

# Run the containerised npm-preflight E2E scenarios (4 tests, ~30s for the timeout test).
# Builds a Docker image with Node.js + zsh and runs the four #[ignore]d E2E tests
# against a crafted environment. Does NOT touch the host's npm, PATH, or installs.
desktop-preflight-e2e:
#!/usr/bin/env bash
set -euo pipefail
if ! command -v docker &>/dev/null; then
echo "docker is required for desktop-preflight-e2e"
echo "Install it from https://docs.docker.com/get-docker/"
exit 1
fi
docker build \
-f desktop/src-tauri/e2e/Dockerfile.preflight-e2e \
-t buzz-preflight-e2e \
.
docker run --rm buzz-preflight-e2e

# Build the full desktop Tauri app locally (unsigned, for testing)
# Sidecar binary list must stay in sync with _ensure-sidecar-stubs above.
# pnpm install is unconditional here: release builds must start from a clean dep tree.
Expand Down
6 changes: 5 additions & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ const overrides = new Map([
// getProfile/updateProfile/getUserProfile/getUsersBatch/searchUsers) moved to
// tauriProfiles.ts; limit ratcheted down 1360 → 1241 to bank the headroom.
// baked-env fold-in: getBakedBuildEnv + BakedEnvEntry type adds ~28 lines.
["src/shared/api/tauri.ts", 1271],
// doctor-npm-eacces-preflight: hint field on RawInstallStepResult + mapper
// passthrough (+2 lines).
["src/shared/api/tauri.ts", 1273],
// doctor-npm-eacces-preflight: hint field added to InstallStepResult (+1 line).
["src/shared/api/types.ts", 1001],
// readiness-gate: PersonaDialog.tsx threads computeLocalModeGate +
// requiredCredentialEnvKeys + RequiredFieldLabel so the "New agent" dialog
// shows required markers and credential amber rows (parity with
Expand Down
75 changes: 75 additions & 0 deletions desktop/src-tauri/e2e/Dockerfile.preflight-e2e
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#
# Containerised E2E harness for the npm-preflight scenarios in
# desktop/src-tauri/src/commands/agent_discovery.rs.
#
# This image:
# 1. Compiles the buzz_lib test binary (via cargo test --no-run).
# 2. Runs four #[ignore]d E2E tests in sequence, each in a crafted
# environment that controls npm presence and prefix writability.
#
# Build context: repository root (so COPY . . captures everything needed).
# Invoked via: just desktop-preflight-e2e
#
# Host isolation: npm is installed INSIDE the image; no host volume is
# mounted that could write to the host's npm prefix or PATH.

ARG RUST_VERSION=1.95
FROM rust:${RUST_VERSION}-slim-bookworm

# Install Node.js LTS (provides npm) and zsh plus the system libs required by
# Tauri / buzz-desktop build dependencies (webkitgtk etc. are test-binary deps).
RUN apt-get update -y && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates \
pkg-config \
cmake \
zsh \
libasound2-dev \
libwebkit2gtk-4.1-dev \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

# Create a non-root user for running the tests. Writability / EACCES
# scenarios require a genuinely unprivileged user.
RUN useradd --create-home --shell /bin/bash testuser

# Copy the repo source. The per-Dockerfile .dockerignore excludes target/,
# node_modules/, and web/ to keep the context lean while including
# desktop/src-tauri/ (which the root .dockerignore would exclude).
WORKDIR /build
COPY . .

# Create sidecar stubs, compile the buzz_lib test binary, and install it.
# All done as root so the resulting binary is available system-wide.
RUN arch=$(rustc -vV | sed -n 's|host: ||p') && \
mkdir -p /build/desktop/src-tauri/binaries && \
for bin in buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz; do \
touch "/build/desktop/src-tauri/binaries/${bin}-${arch}"; \
done && \
cd /build/desktop/src-tauri && \
cargo test --lib --no-run --message-format=json 2>/dev/null \
| grep '"executable"' \
| grep 'buzz_lib' \
| sed 's/.*"executable":"\([^"]*\)".*/\1/' \
| head -1 > /tmp/test-bin-path.txt && \
TEST_BIN=$(cat /tmp/test-bin-path.txt) && \
echo "Test binary: $TEST_BIN" && \
test -f "$TEST_BIN" && \
cp "$TEST_BIN" /usr/local/bin/buzz-lib-tests && \
chmod 755 /usr/local/bin/buzz-lib-tests

# Install the scenario entrypoint.
COPY desktop/src-tauri/e2e/run-e2e-scenarios.sh /usr/local/bin/run-e2e-scenarios.sh
RUN chmod 755 /usr/local/bin/run-e2e-scenarios.sh

# Run as the non-root user so the EACCES writability test behaves correctly.
USER testuser
WORKDIR /home/testuser

ENTRYPOINT ["/usr/local/bin/run-e2e-scenarios.sh"]
33 changes: 33 additions & 0 deletions desktop/src-tauri/e2e/Dockerfile.preflight-e2e.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Per-Dockerfile .dockerignore for the npm-preflight E2E harness.
# Overrides the root .dockerignore for this build target.
# The root .dockerignore excludes desktop/ (correct for the relay image)
# but this harness needs desktop/src-tauri/ for the Rust source.

# Large build artifacts — exclude to keep context lean.
**/target/
desktop/src-tauri/target/
node_modules/
**/node_modules/
web/dist/

# VCS, IDE
.git/
.github/
.vscode/
.idea/
.scratch/

# Secrets
.env
.env.*
!.env.example
*.pem
*.key
secrets/

# Non-Rust things the E2E build doesn't need.
web/
mobile/
docs/
desktop/dist/
desktop/node_modules/
110 changes: 110 additions & 0 deletions desktop/src-tauri/e2e/run-e2e-scenarios.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# run-e2e-scenarios.sh — containerised E2E runner for the npm-preflight tests.
#
# Runs four scenarios in sequence. Each scenario:
# 1. Creates a dedicated $HOME directory under /tmp with crafted shell init
# files and optional ~/.npmrc that control npm visibility and prefix.
# 2. Exports HOME to that directory so login_shell_path() (OnceLock) initialises
# from the correct init files for that test.
# 3. Runs the specific #[ignore]d test via the pre-compiled test binary.
#
# Each test runs in a SEPARATE PROCESS so the OnceLock for login_shell_path()
# is fresh. All paths are inside the container; nothing touches the host.
#
# Exit code: 0 if all four pass, 1 if any fail.

set -euo pipefail

NPM_BIN=$(command -v npm || true)
if [ -z "$NPM_BIN" ]; then
echo "FATAL: npm not found in container PATH — check the Dockerfile installs nodejs" >&2
exit 1
fi

TEST_BIN=/usr/local/bin/buzz-lib-tests
PASS=0
FAIL=0

run_scenario() {
local name="$1"
local home_dir="$2"
local test_filter="$3"
echo ""
echo "════════════════════════════════════════════════════════════"
echo " Scenario: $name"
echo " HOME: $home_dir"
echo "════════════════════════════════════════════════════════════"
if HOME="$home_dir" "$TEST_BIN" "$test_filter" --ignored --nocapture 2>&1; then
echo " ✅ PASSED"
PASS=$((PASS + 1))
else
echo " ❌ FAILED"
FAIL=$((FAIL + 1))
fi
}

# ── Scenario (a): writable prefix → proceed ──────────────────────────────────
HOME_A=$(mktemp -d /tmp/e2e-home-writable-XXXXXX)
mkdir -p "$HOME_A/.npm-global/lib/node_modules"
# .npmrc: point npm prefix at a user-owned directory.
echo "prefix=$HOME_A/.npm-global" > "$HOME_A/.npmrc"
# Login shell init: put npm on PATH.
# install_shell_command selects /bin/zsh if present, else /bin/bash.
SHELL_INIT_A="$HOME_A/.bash_profile"
[ -x /bin/zsh ] && SHELL_INIT_A="$HOME_A/.zprofile"
echo "export PATH=\"$(dirname "$NPM_BIN"):\$PATH\"" > "$SHELL_INIT_A"

run_scenario "writable prefix → proceed" "$HOME_A" "test_e2e_writable_prefix_proceeds"

# ── Scenario (b): read-only prefix → EACCES abort ────────────────────────────
HOME_B=$(mktemp -d /tmp/e2e-home-readonly-XXXXXX)
# No ~/.npmrc → npm uses its compiled-in default (/usr/local), which is
# root-owned and not writable by testuser.
# Login shell init: put npm on PATH.
SHELL_INIT_B="$HOME_B/.bash_profile"
[ -x /bin/zsh ] && SHELL_INIT_B="$HOME_B/.zprofile"
echo "export PATH=\"$(dirname "$NPM_BIN"):\$PATH\"" > "$SHELL_INIT_B"

run_scenario "read-only prefix → EACCES abort" "$HOME_B" "test_e2e_readonly_prefix_aborts_with_eacces_guidance"

# ── Scenario (c): npm missing → NPM_MISSING_HINT abort ───────────────────────
HOME_C=$(mktemp -d /tmp/e2e-home-no-npm-XXXXXX)
# Create a temp dir that has no npm binary, then set PATH to only that dir.
NO_NPM_DIR=$(mktemp -d /tmp/e2e-no-npm-bin-XXXXXX)
# Login shell init: restrict PATH to a directory confirmed to have no npm.
SHELL_INIT_C="$HOME_C/.bash_profile"
[ -x /bin/zsh ] && SHELL_INIT_C="$HOME_C/.zprofile"
echo "export PATH=\"$NO_NPM_DIR\"" > "$SHELL_INIT_C"

run_scenario "npm missing → NPM_MISSING_HINT abort" "$HOME_C" "test_e2e_npm_missing_aborts_with_missing_hint"

# ── Scenario (d): wedged shell → 30s timeout → proceed ───────────────────────
HOME_D=$(mktemp -d /tmp/e2e-home-wedged-XXXXXX)
# Create an npm shim that blocks for longer than the 30s deadline.
# The login shell init puts the shim dir FIRST on PATH so it shadows real npm.
SHIM_DIR="$HOME_D/.npm-shim"
mkdir -p "$SHIM_DIR"
cat > "$SHIM_DIR/npm" << 'SHIM'
#!/bin/sh
# Simulate a wedged npm (e.g. a slow version-manager hook).
sleep 60
SHIM
chmod 755 "$SHIM_DIR/npm"

SHELL_INIT_D="$HOME_D/.bash_profile"
[ -x /bin/zsh ] && SHELL_INIT_D="$HOME_D/.zprofile"
# The init file adds the shim dir first, so 'npm' resolves to the shim.
# Real npm is also on PATH so login_shell_path() (echo $PATH) works fine —
# the block only triggers when 'npm prefix -g' is actually invoked.
echo "export PATH=\"$SHIM_DIR:$(dirname "$NPM_BIN"):\$PATH\"" > "$SHELL_INIT_D"

echo ""
echo " NOTE: scenario (d) intentionally waits ~30s for the timeout to fire."
run_scenario "wedged shell → 30s timeout → proceed" "$HOME_D" "test_e2e_wedged_shell_timeout_proceeds"

# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════════════════"
echo " Results: $PASS passed, $FAIL failed"
echo "════════════════════════════════════════════════════════════"
[ "$FAIL" -eq 0 ]
Loading
Loading