chore(desktop): remove container-only npm-preflight E2E harness (#1749)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-07-10 21:05:17 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent 58aee8f913
commit 9895b4d390
5 changed files with 0 additions and 427 deletions
-17
View File
@@ -186,23 +186,6 @@ 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.
@@ -1,75 +0,0 @@
#
# 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"]
@@ -1,33 +0,0 @@
# 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
View File
@@ -1,110 +0,0 @@
#!/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 ]
@@ -726,196 +726,4 @@ mod tests {
std::fs::create_dir(dir.path().join("lib")).unwrap();
assert!(npm_install_target_is_writable(dir.path()));
}
// ── npm-preflight E2E (container-only) ───────────────────────────────────
//
// These tests drive the real `resolve_npm_prefix` / `npm_preflight_check`
// path against a crafted environment. They are `#[ignore]`d so normal
// `cargo test` / CI skips them; the dedicated Docker harness at
// `desktop/src-tauri/e2e/Dockerfile.preflight-e2e` runs them with
// `cargo test --lib -- --ignored <test_name>` in a pre-configured container
// where npm presence and global-prefix writability are controlled.
//
// Run locally via: just desktop-preflight-e2e
/// (a) npm present, global prefix writable → preflight returns None (proceed).
///
/// Preconditions (set by the container entrypoint for this scenario):
/// - npm is on PATH in the login shell.
/// - ~/.npmrc sets `prefix` to a writable user dir, e.g. ~/.npm-global.
/// - HOME is a temp dir with the npmrc and a minimal .zprofile / .bash_profile
/// that preserves a PATH containing npm.
#[cfg(unix)]
#[ignore]
#[test]
fn test_e2e_writable_prefix_proceeds() {
// resolve_npm_prefix() must find npm and return a writable prefix.
let prefix = match resolve_npm_prefix() {
NpmPrefix::Found(p) => p,
NpmPrefix::Unavailable => {
panic!("resolve_npm_prefix() returned Unavailable — npm must be on PATH in the login shell for this scenario. Check the container HOME setup.");
}
NpmPrefix::TimedOut => {
panic!("resolve_npm_prefix() timed out — check that the login shell init files do not block.");
}
};
assert!(
npm_install_target_is_writable(&prefix),
"npm global prefix '{}' must be writable for this scenario; \
check that ~/.npmrc sets prefix to a user-writable directory",
prefix.display()
);
// The full preflight must pass (return None = proceed).
let result = npm_preflight_check("adapter", "npm install -g some-pkg");
assert!(
result.is_none(),
"npm_preflight_check should return None (proceed) when prefix is writable, \
got: {result:?}"
);
}
/// (b) npm present, global prefix NOT writable → preflight aborts with EACCES guidance.
///
/// Preconditions (set by the container entrypoint for this scenario):
/// - npm is on PATH in the login shell.
/// - npm's global prefix resolves to a root-owned path (e.g. /usr/local).
/// No ~/.npmrc prefix override — npm uses its compiled-in default.
/// - Container user is non-root.
#[cfg(unix)]
#[ignore]
#[test]
fn test_e2e_readonly_prefix_aborts_with_eacces_guidance() {
// resolve_npm_prefix() must find npm.
let prefix = match resolve_npm_prefix() {
NpmPrefix::Found(p) => p,
NpmPrefix::Unavailable => {
panic!("resolve_npm_prefix() returned Unavailable — npm must be on PATH in the login shell for this scenario. Check the container HOME setup.");
}
NpmPrefix::TimedOut => {
panic!("resolve_npm_prefix() timed out — check that the login shell init files do not block.");
}
};
assert!(
!npm_install_target_is_writable(&prefix),
"npm global prefix '{}' must be NOT writable for this scenario; \
the container user must not own the prefix (e.g. /usr/local is root-owned)",
prefix.display()
);
// The preflight must abort with success:false and the EACCES guidance hint.
let step_result = npm_preflight_check("adapter", "npm install -g some-pkg")
.expect("npm_preflight_check should return Some(abort) when prefix is not writable");
assert!(
!step_result.success,
"InstallStepResult.success must be false for an EACCES abort"
);
let hint = step_result
.hint
.expect("InstallStepResult.hint must be Some for an EACCES abort");
assert!(
hint.contains("npm-global"),
"EACCES hint should contain npm-global remediation; got: {hint}"
);
assert!(
hint.contains(".zprofile"),
"EACCES hint should mention .zprofile; got: {hint}"
);
}
/// (c) npm not on PATH → preflight aborts with NPM_MISSING_HINT (regression guard).
///
/// This verifies the genuine npm-missing branch is byte-for-byte unchanged
/// after the NpmPrefix enum refactor.
///
/// Preconditions (set by the container entrypoint for this scenario):
/// - HOME is a temp dir whose .zprofile / .bash_profile explicitly sets
/// PATH to a directory that does NOT contain npm (e.g. PATH=/usr/bin:/bin
/// on a system where npm lives only in /usr/local/bin).
/// - No npm shim on any PATH component.
#[cfg(unix)]
#[ignore]
#[test]
fn test_e2e_npm_missing_aborts_with_missing_hint() {
// resolve_npm_prefix() must report Unavailable (not TimedOut).
match resolve_npm_prefix() {
NpmPrefix::Unavailable => {} // expected
NpmPrefix::Found(p) => {
panic!(
"resolve_npm_prefix() returned Found({}) — npm must NOT be on PATH \
for this scenario; check that HOME's shell init strips npm from PATH",
p.display()
);
}
NpmPrefix::TimedOut => {
panic!(
"resolve_npm_prefix() timed out — the shell init must fail quickly, not block"
);
}
}
// The preflight must hard-abort with NPM_MISSING_HINT.
let step_result = npm_preflight_check("adapter", "npm install -g some-pkg")
.expect("npm_preflight_check should return Some(abort) when npm is missing");
assert!(
!step_result.success,
"InstallStepResult.success must be false for a missing-npm abort"
);
let hint = step_result
.hint
.expect("InstallStepResult.hint must be Some for a missing-npm abort");
assert!(
hint.contains("nodejs.org"),
"missing-npm hint should contain nodejs.org; got: {hint}"
);
assert!(
hint.contains(".zprofile"),
"missing-npm hint should mention .zprofile; got: {hint}"
);
// Regression guard: the hint text must exactly match NPM_MISSING_HINT.
assert_eq!(
hint, NPM_MISSING_HINT,
"missing-npm hint text must be byte-for-byte NPM_MISSING_HINT"
);
}
/// (d) Login shell blocks (wedged version-manager init) → 30s timeout fires,
/// preflight returns None so Phase 2 install runs (stderr classifier is backstop).
///
/// This is the new behavior added by the NpmPrefix enum: TimedOut → proceed,
/// not abort with a misleading "npm not found" message.
///
/// Preconditions (set by the container entrypoint for this scenario):
/// - HOME is a temp dir with a .zprofile / .bash_profile that is NORMAL for
/// the `echo $PATH` call made by login_shell_path(), but where PATH contains
/// an npm shim script that runs `sleep 60` — so the npm invocation itself
/// blocks, not the PATH discovery.
///
/// Wall-clock: ~30s (the timeout deadline).
#[cfg(unix)]
#[ignore]
#[test]
fn test_e2e_wedged_shell_timeout_proceeds() {
// resolve_npm_prefix() must return TimedOut, not Unavailable or Found.
match resolve_npm_prefix() {
NpmPrefix::TimedOut => {} // expected
NpmPrefix::Unavailable => {
panic!(
"resolve_npm_prefix() returned Unavailable — expected TimedOut. \
Check that the npm shim on PATH blocks (sleeps) rather than failing immediately."
);
}
NpmPrefix::Found(p) => {
panic!(
"resolve_npm_prefix() returned Found({}) — npm shim must block, \
not return a valid prefix",
p.display()
);
}
}
// The preflight must return None (proceed), not abort.
let result = npm_preflight_check("adapter", "npm install -g some-pkg");
assert!(
result.is_none(),
"npm_preflight_check should return None (proceed to install) on timeout; \
a wedged shell must not abort with NPM_MISSING_HINT. Got: {result:?}"
);
}
}