From 8733080b155fa88c7b0167d11a7321dbf603ee41 Mon Sep 17 00:00:00 2001 From: Tobias Brandt Date: Fri, 6 Mar 2026 13:45:46 +0200 Subject: [PATCH 1/6] Adds simplified install scripts --- docs/content/worktrunk.md | 12 ++++++++++++ docs/static/install.ps1 | 22 ++++++++++++++++++++++ docs/static/install.sh | 27 +++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 docs/static/install.ps1 create mode 100644 docs/static/install.sh diff --git a/docs/content/worktrunk.md b/docs/content/worktrunk.md index 68190d39ba..e8153f6a82 100644 --- a/docs/content/worktrunk.md +++ b/docs/content/worktrunk.md @@ -99,6 +99,18 @@ A demo with some advanced features: ## Install +**Script installation - Static Binary (macOS & Linux):** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | bash +``` + +**Script installation - Static Binary (Windows):** + +```bash +powershell -c "irm http://worktrunk.dev/install.ps1 | iex" +``` + **Homebrew (macOS & Linux):** ```bash diff --git a/docs/static/install.ps1 b/docs/static/install.ps1 new file mode 100644 index 0000000000..4bc77f5182 --- /dev/null +++ b/docs/static/install.ps1 @@ -0,0 +1,22 @@ +# Worktrunk Installer (Windows) +# https://worktrunk.dev/install.ps1 + +if ($IsWindows -eq $false -and $PSVersionTable.PSVersion.Major -ge 6) { + Write-Host "Non-Windows environment detected. Please use the shell installer instead:" -ForegroundColor Yellow + Write-Host " curl -fsSL https://worktrunk.dev/install.sh | sh" + exit 1 +} + +Write-Host "Installing worktrunk..." +irm https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.ps1 | iex + +# cargo-dist installs to ~/.cargo/bin by default. +# On Windows, winget or direct install might use git-wt to avoid conflict with Windows Terminal. +$env:Path += ";$HOME\.cargo\bin" +if (Get-Command git-wt -ErrorAction SilentlyContinue) { + git-wt config shell install +} else { + Write-Host "" + Write-Host "Warning: worktrunk installed but neither 'git-wt' nor 'wt' found in PATH." -ForegroundColor Yellow + Write-Host "Please restart your shell and run 'git-wt config shell install' manually." +} diff --git a/docs/static/install.sh b/docs/static/install.sh new file mode 100644 index 0000000000..a46bc7a4b0 --- /dev/null +++ b/docs/static/install.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu + +# Worktrunk Installer (Unix) +# https://worktrunk.dev/install.sh + +if [ "${OS:-}" = "Windows_NT" ]; then + echo "Windows detected. Please use the PowerShell installer instead:" + echo " irm https://worktrunk.dev/install.ps1 | iex" + exit 1 +fi + +echo "Installing worktrunk..." +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh | sh + +# cargo-dist installs to ~/.cargo/bin by default on Unix. +# We use < /dev/tty to ensure the interactive prompt can read from the terminal +# even when the script itself was piped into sh (e.g. curl ... | sh). +if [ -x "$HOME/.cargo/bin/wt" ]; then + "$HOME/.cargo/bin/wt" config shell install < /dev/tty +elif command -v wt >/dev/null 2>&1; then + wt config shell install < /dev/tty +else + echo "" + echo "Warning: worktrunk installed but 'wt' not found in PATH." + echo "Please restart your shell and run 'wt config shell install' manually." +fi From 6f2ae76d5e712b31d03e1d763e28e33e267d72d0 Mon Sep 17 00:00:00 2001 From: Tobias Brandt Date: Fri, 6 Mar 2026 16:42:56 +0200 Subject: [PATCH 2/6] Apply suggestions from code review This integrates the worktrunk-bot suggestions which I have reviewed. They are good additions. Co-authored-by: worktrunk-bot --- docs/content/worktrunk.md | 4 ++-- docs/static/install.ps1 | 6 +++++- docs/static/install.sh | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/content/worktrunk.md b/docs/content/worktrunk.md index e8153f6a82..ddce463895 100644 --- a/docs/content/worktrunk.md +++ b/docs/content/worktrunk.md @@ -102,13 +102,13 @@ A demo with some advanced features: **Script installation - Static Binary (macOS & Linux):** ```bash -curl -fsSL https://worktrunk.dev/install.sh | bash +curl -fsSL https://worktrunk.dev/install.sh | sh ``` **Script installation - Static Binary (Windows):** ```bash -powershell -c "irm http://worktrunk.dev/install.ps1 | iex" +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" ``` **Homebrew (macOS & Linux):** diff --git a/docs/static/install.ps1 b/docs/static/install.ps1 index 4bc77f5182..480bd9c51c 100644 --- a/docs/static/install.ps1 +++ b/docs/static/install.ps1 @@ -13,7 +13,11 @@ irm https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-in # cargo-dist installs to ~/.cargo/bin by default. # On Windows, winget or direct install might use git-wt to avoid conflict with Windows Terminal. $env:Path += ";$HOME\.cargo\bin" -if (Get-Command git-wt -ErrorAction SilentlyContinue) { +if ((Get-Command wt -ErrorAction SilentlyContinue) -and (wt --version 2>&1 | Select-String 'worktrunk')) { + wt config shell install +} elseif (Get-Command git-wt -ErrorAction SilentlyContinue) { + git-wt config shell install +} else { git-wt config shell install } else { Write-Host "" diff --git a/docs/static/install.sh b/docs/static/install.sh index a46bc7a4b0..bdcee44bf4 100644 --- a/docs/static/install.sh +++ b/docs/static/install.sh @@ -11,7 +11,7 @@ if [ "${OS:-}" = "Windows_NT" ]; then fi echo "Installing worktrunk..." -curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh | sh +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh | sh || { echo "Installation failed."; exit 1; } # cargo-dist installs to ~/.cargo/bin by default on Unix. # We use < /dev/tty to ensure the interactive prompt can read from the terminal From 45477d43078bc7843e7705ee93221f0691abe54e Mon Sep 17 00:00:00 2001 From: Tobias Brandt Date: Fri, 6 Mar 2026 17:16:08 +0200 Subject: [PATCH 3/6] Remove duplicate else clause --- docs/static/install.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/static/install.ps1 b/docs/static/install.ps1 index 480bd9c51c..44ed2c90b8 100644 --- a/docs/static/install.ps1 +++ b/docs/static/install.ps1 @@ -17,8 +17,6 @@ if ((Get-Command wt -ErrorAction SilentlyContinue) -and (wt --version 2>&1 | Sel wt config shell install } elseif (Get-Command git-wt -ErrorAction SilentlyContinue) { git-wt config shell install -} else { - git-wt config shell install } else { Write-Host "" Write-Host "Warning: worktrunk installed but neither 'git-wt' nor 'wt' found in PATH." -ForegroundColor Yellow From a822cc86e65ca92468d380296381dc91e1deddb3 Mon Sep 17 00:00:00 2001 From: Tobias Brandt Date: Fri, 6 Mar 2026 17:18:46 +0200 Subject: [PATCH 4/6] Sync docs changes to README and SKILLS --- README.md | 12 ++++++++++++ skills/worktrunk/reference/worktrunk.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index ce6c56bbd3..b0b016b3ad 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,18 @@ A demo with some advanced features: ## Install +**Script installation - Static Binary (macOS & Linux):** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Script installation - Static Binary (Windows):** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + **Homebrew (macOS & Linux):** ```bash diff --git a/skills/worktrunk/reference/worktrunk.md b/skills/worktrunk/reference/worktrunk.md index 153ed41126..075df91edc 100644 --- a/skills/worktrunk/reference/worktrunk.md +++ b/skills/worktrunk/reference/worktrunk.md @@ -79,6 +79,18 @@ A demo with some advanced features: ## Install +**Script installation - Static Binary (macOS & Linux):** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Script installation - Static Binary (Windows):** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + **Homebrew (macOS & Linux):** ```bash From 4d284314bd0d2bb3fe3bfc92a0ad4d1122375a84 Mon Sep 17 00:00:00 2001 From: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:18:03 +0000 Subject: [PATCH 5/6] Harden install scripts: move below brew/winget, fix PATH handling, add tests - Move script installer below brew & winget in docs, label as experimental - Replace `curl | sh` pipe with temp file download (pipe swallows curl failures) - Source cargo env instead of hardcoding ~/.cargo/bin (supports custom CARGO_HOME) - Add graceful /dev/tty fallback for non-interactive environments - Add install.ps1 CARGO_HOME support and duplicate PATH prevention - Add test suite (25 tests) covering structure, Windows detection, download failures, PATH resolution, and non-interactive environments Co-Authored-By: Claude Opus 4.6 --- README.md | 31 ++- docs/content/worktrunk.md | 31 ++- docs/static/install.ps1 | 14 +- docs/static/install.sh | 43 +++- docs/static/install_test.sh | 317 ++++++++++++++++++++++++ skills/worktrunk/reference/worktrunk.md | 31 ++- 6 files changed, 416 insertions(+), 51 deletions(-) create mode 100644 docs/static/install_test.sh diff --git a/README.md b/README.md index b0b016b3ad..d471164bf7 100644 --- a/README.md +++ b/README.md @@ -99,18 +99,6 @@ A demo with some advanced features: ## Install -**Script installation - Static Binary (macOS & Linux):** - -```bash -curl -fsSL https://worktrunk.dev/install.sh | sh -``` - -**Script installation - Static Binary (Windows):** - -```bash -powershell -c "irm https://worktrunk.dev/install.ps1 | iex" -``` - **Homebrew (macOS & Linux):** ```bash @@ -145,6 +133,25 @@ Alternatively, disable Windows Terminal's alias (Settings → Privacy & security paru worktrunk-bin && wt config shell install ``` +
+Script installer (experimental) + +Downloads a static binary — no package manager required. + +**macOS & Linux:** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Windows:** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + +
+ ## Quick start Create a worktree for a new feature: diff --git a/docs/content/worktrunk.md b/docs/content/worktrunk.md index ddce463895..d2ca4a0ff2 100644 --- a/docs/content/worktrunk.md +++ b/docs/content/worktrunk.md @@ -99,18 +99,6 @@ A demo with some advanced features: ## Install -**Script installation - Static Binary (macOS & Linux):** - -```bash -curl -fsSL https://worktrunk.dev/install.sh | sh -``` - -**Script installation - Static Binary (Windows):** - -```bash -powershell -c "irm https://worktrunk.dev/install.ps1 | iex" -``` - **Homebrew (macOS & Linux):** ```bash @@ -145,6 +133,25 @@ Alternatively, disable Windows Terminal's alias (Settings → Privacy & security paru worktrunk-bin && wt config shell install ``` +
+Script installer (experimental) + +Downloads a static binary — no package manager required. + +**macOS & Linux:** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Windows:** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + +
+ ## Quick start Create a worktree for a new feature: diff --git a/docs/static/install.ps1 b/docs/static/install.ps1 index 44ed2c90b8..be09f25751 100644 --- a/docs/static/install.ps1 +++ b/docs/static/install.ps1 @@ -10,15 +10,19 @@ if ($IsWindows -eq $false -and $PSVersionTable.PSVersion.Major -ge 6) { Write-Host "Installing worktrunk..." irm https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.ps1 | iex -# cargo-dist installs to ~/.cargo/bin by default. -# On Windows, winget or direct install might use git-wt to avoid conflict with Windows Terminal. -$env:Path += ";$HOME\.cargo\bin" +# Update PATH to pick up the newly installed binary. +# Respect CARGO_HOME if set, otherwise use the default location. +$cargoBin = if ($env:CARGO_HOME) { "$env:CARGO_HOME\bin" } else { "$HOME\.cargo\bin" } +if ($env:Path -notlike "*$cargoBin*") { + $env:Path += ";$cargoBin" +} + if ((Get-Command wt -ErrorAction SilentlyContinue) -and (wt --version 2>&1 | Select-String 'worktrunk')) { wt config shell install } elseif (Get-Command git-wt -ErrorAction SilentlyContinue) { git-wt config shell install } else { Write-Host "" - Write-Host "Warning: worktrunk installed but neither 'git-wt' nor 'wt' found in PATH." -ForegroundColor Yellow - Write-Host "Please restart your shell and run 'git-wt config shell install' manually." + Write-Host "Warning: worktrunk installed but neither 'wt' nor 'git-wt' found in PATH." -ForegroundColor Yellow + Write-Host "Restart your shell and run 'wt config shell install' (or 'git-wt config shell install') manually." } diff --git a/docs/static/install.sh b/docs/static/install.sh index bdcee44bf4..1778918735 100644 --- a/docs/static/install.sh +++ b/docs/static/install.sh @@ -6,22 +6,45 @@ set -eu if [ "${OS:-}" = "Windows_NT" ]; then echo "Windows detected. Please use the PowerShell installer instead:" - echo " irm https://worktrunk.dev/install.ps1 | iex" + echo " powershell -c \"irm https://worktrunk.dev/install.ps1 | iex\"" exit 1 fi echo "Installing worktrunk..." -curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh | sh || { echo "Installation failed."; exit 1; } -# cargo-dist installs to ~/.cargo/bin by default on Unix. -# We use < /dev/tty to ensure the interactive prompt can read from the terminal -# even when the script itself was piped into sh (e.g. curl ... | sh). -if [ -x "$HOME/.cargo/bin/wt" ]; then - "$HOME/.cargo/bin/wt" config shell install < /dev/tty -elif command -v wt >/dev/null 2>&1; then +# Download to a temp file instead of piping curl to sh. This avoids two issues: +# 1. Pipe swallows curl failures (pipefail is not POSIX) +# 2. Piping consumes stdin, blocking interactive prompts in the installer +installer="$(mktemp)" +trap 'rm -f "$installer"' EXIT +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh -o "$installer" || { + echo "Download failed." + exit 1 +} +sh "$installer" || { + echo "Installation failed." + exit 1 +} + +# Source the cargo env to pick up PATH changes from the installer. +# This handles custom CARGO_HOME and avoids hardcoding ~/.cargo/bin. +# shellcheck disable=SC1091 +. "${CARGO_HOME:-$HOME/.cargo}/env" 2>/dev/null || true + +if ! command -v wt >/dev/null 2>&1; then + echo "" + echo "Warning: worktrunk installed but 'wt' not found in PATH." + echo "Restart your shell and run 'wt config shell install' manually." + exit 0 +fi + +# Configure shell integration. We use < /dev/tty to ensure the interactive +# prompt can read from the terminal even when this script was piped into sh +# (e.g. curl ... | sh). +if [ -e /dev/tty ]; then wt config shell install < /dev/tty else echo "" - echo "Warning: worktrunk installed but 'wt' not found in PATH." - echo "Please restart your shell and run 'wt config shell install' manually." + echo "Non-interactive environment detected." + echo "Run 'wt config shell install' after restarting your shell." fi diff --git a/docs/static/install_test.sh b/docs/static/install_test.sh new file mode 100644 index 0000000000..13d7ce9cd6 --- /dev/null +++ b/docs/static/install_test.sh @@ -0,0 +1,317 @@ +#!/bin/sh +set -eu + +# Test suite for install.sh +# Run: sh docs/static/install_test.sh +# +# Tests the install script's logic by mocking external commands (curl, wt). +# Each test runs in a subshell so failures are isolated. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INSTALL_SH="$SCRIPT_DIR/install.sh" +PASS=0 +FAIL=0 + +pass() { + PASS=$((PASS + 1)) + echo " PASS: $1" +} + +fail() { + FAIL=$((FAIL + 1)) + echo " FAIL: $1" + if [ -n "${2:-}" ]; then + echo " $2" + fi +} + +# Create a temp directory for mock binaries +MOCK_DIR="$(mktemp -d)" +trap 'rm -rf "$MOCK_DIR"' EXIT + +# Helper: create a mock executable +mock_bin() { + cat > "$MOCK_DIR/$1" << MOCKEOF +#!/bin/sh +$2 +MOCKEOF + chmod +x "$MOCK_DIR/$1" +} + +echo "=== install.sh test suite ===" +echo "" + +# =========================================================================== +echo "--- Script structure tests ---" + +# Test: script starts with proper shebang +if head -1 "$INSTALL_SH" | grep -q '^#!/bin/sh'; then + pass "has POSIX sh shebang" +else + fail "has POSIX sh shebang" +fi + +# Test: script uses set -eu +if grep -q '^set -eu' "$INSTALL_SH"; then + pass "uses set -eu for strict error handling" +else + fail "uses set -eu for strict error handling" +fi + +# Test: script does NOT hardcode ~/.cargo/bin path for wt lookup +if grep -q '"\$HOME/\.cargo/bin/wt"' "$INSTALL_SH"; then + fail "should not hardcode \$HOME/.cargo/bin/wt" "Use 'command -v wt' after sourcing cargo env" +else + pass "does not hardcode \$HOME/.cargo/bin/wt path" +fi + +# Test: script sources cargo env +if grep -q 'CARGO_HOME:-\$HOME/\.cargo' "$INSTALL_SH" && grep -q '\..*env' "$INSTALL_SH"; then + pass "sources cargo env with CARGO_HOME support" +else + fail "sources cargo env with CARGO_HOME support" +fi + +# Test: uses /dev/tty for interactive input +if grep -q '/dev/tty' "$INSTALL_SH"; then + pass "uses /dev/tty for interactive prompts" +else + fail "uses /dev/tty for interactive prompts" +fi + +# Test: has graceful fallback when /dev/tty is unavailable +if grep -q '\-e /dev/tty' "$INSTALL_SH"; then + pass "checks /dev/tty existence before using it" +else + fail "checks /dev/tty existence before using it" +fi + +# Test: uses --proto and --tlsv1.2 for secure curl +if grep -q "\-\-proto '=https' --tlsv1.2" "$INSTALL_SH"; then + pass "enforces HTTPS-only with TLS 1.2+" +else + fail "enforces HTTPS-only with TLS 1.2+" +fi + +# Test: downloads to temp file instead of piping curl to sh +if grep -q 'mktemp' "$INSTALL_SH" && grep -q '\-o ' "$INSTALL_SH"; then + pass "downloads to temp file (avoids pipe swallowing failures)" +else + fail "downloads to temp file (avoids pipe swallowing failures)" +fi + +# Test: cleans up temp file on exit +if grep -q "trap.*rm.*EXIT" "$INSTALL_SH"; then + pass "cleans up temp file via trap" +else + fail "cleans up temp file via trap" +fi + +echo "" + +# =========================================================================== +echo "--- Windows detection tests ---" + +# Test: detects Windows_NT and exits +output=$(OS=Windows_NT sh "$INSTALL_SH" 2>&1) && rc=$? || rc=$? +if [ "$rc" -eq 1 ] && echo "$output" | grep -q "Windows detected"; then + pass "detects Windows and exits with error" +else + fail "detects Windows and exits with error" "rc=$rc output: $output" +fi + +# Test: suggests PowerShell installer +if echo "$output" | grep -q "install.ps1"; then + pass "suggests PowerShell installer for Windows" +else + fail "suggests PowerShell installer for Windows" +fi + +# Test: does not crash on unset OS variable +if grep -q '"\${OS:-}"' "$INSTALL_SH"; then + pass "uses \${OS:-} to handle unset OS variable" +else + fail "uses \${OS:-} to handle unset OS variable" +fi + +echo "" + +# =========================================================================== +echo "--- Download failure tests ---" + +# Test: curl download failure is caught (not hidden by pipe) +mock_bin "curl" 'exit 1' + +output=$(PATH="$MOCK_DIR:/usr/bin:/bin" OS="" sh "$INSTALL_SH" 2>&1) && rc=$? || rc=$? +if [ "$rc" -ne 0 ]; then + pass "exits with error when curl download fails" +else + fail "exits with error when curl download fails" "rc=$rc output: $output" +fi + +if echo "$output" | grep -qi "download failed\|failed"; then + pass "shows failure message when curl fails" +else + fail "shows failure message when curl fails" "output: $output" +fi + +# Test: installer script failure is caught +# Mock curl that succeeds but writes a failing installer +mock_bin "curl" 'echo "exit 1" > "$(echo "$@" | sed "s/.*-o //")" 2>/dev/null || true' + +output=$(PATH="$MOCK_DIR:/usr/bin:/bin" OS="" sh "$INSTALL_SH" 2>&1) && rc=$? || rc=$? +if [ "$rc" -ne 0 ]; then + pass "exits with error when installer script fails" +else + fail "exits with error when installer script fails" "rc=$rc output: $output" +fi + +echo "" + +# =========================================================================== +echo "--- PATH resolution tests ---" + +# Test: wt found on PATH after sourcing env +mock_bin "wt" 'if [ "${1:-}" = "config" ]; then echo "SHELL_INSTALL_CALLED"; else echo "worktrunk 0.1.0"; fi' + +MOCK_CARGO="$(mktemp -d)" +mkdir -p "$MOCK_CARGO/bin" +cat > "$MOCK_CARGO/env" << EOF +export PATH="$MOCK_DIR:\$PATH" +EOF + +# Test the post-install logic in isolation +cat > "$MOCK_DIR/test_post_install.sh" << 'HARNESS' +#!/bin/sh +set -eu +HARNESS + +cat >> "$MOCK_DIR/test_post_install.sh" << HARNESS +CARGO_HOME="$MOCK_CARGO" +. "\${CARGO_HOME:-\$HOME/.cargo}/env" 2>/dev/null || true +if command -v wt >/dev/null 2>&1; then + echo "FOUND_WT" +else + echo "NOT_FOUND" +fi +HARNESS +chmod +x "$MOCK_DIR/test_post_install.sh" + +output=$(sh "$MOCK_DIR/test_post_install.sh" 2>&1) +if echo "$output" | grep -q "FOUND_WT"; then + pass "finds wt after sourcing cargo env with custom CARGO_HOME" +else + fail "finds wt after sourcing cargo env with custom CARGO_HOME" "output: $output" +fi + +# Test: wt NOT found produces warning +cat > "$MOCK_DIR/test_not_found.sh" << 'HARNESS2' +#!/bin/sh +set -eu +EMPTY_CARGO="$(mktemp -d)" +mkdir -p "$EMPTY_CARGO/bin" +echo "" > "$EMPTY_CARGO/env" +CARGO_HOME="$EMPTY_CARGO" +. "${CARGO_HOME:-$HOME/.cargo}/env" 2>/dev/null || true +PATH="/usr/bin:/bin" +export PATH +if ! command -v wt >/dev/null 2>&1; then + echo "Warning: worktrunk installed but 'wt' not found in PATH." + echo "NOT_FOUND_WARNING" +fi +rm -rf "$EMPTY_CARGO" +HARNESS2 +chmod +x "$MOCK_DIR/test_not_found.sh" + +output=$(sh "$MOCK_DIR/test_not_found.sh" 2>&1) +if echo "$output" | grep -q "NOT_FOUND_WARNING"; then + pass "shows warning when wt is not on PATH" +else + fail "shows warning when wt is not on PATH" "output: $output" +fi + +# Cleanup mock cargo +rm -rf "$MOCK_CARGO" + +echo "" + +# =========================================================================== +echo "--- Non-interactive environment tests ---" + +# Test: script mentions non-interactive fallback +if grep -q 'Non-interactive environment' "$INSTALL_SH"; then + pass "has non-interactive environment fallback message" +else + fail "has non-interactive environment fallback message" +fi + +# Test: fallback tells user to run shell install manually +if grep -q 'wt config shell install' "$INSTALL_SH"; then + pass "fallback message includes 'wt config shell install' command" +else + fail "fallback message includes 'wt config shell install' command" +fi + +echo "" + +# =========================================================================== +echo "--- install.ps1 structure tests ---" + +INSTALL_PS1="$SCRIPT_DIR/install.ps1" + +if [ -f "$INSTALL_PS1" ]; then + # Test: PS1 script has non-Windows detection + if grep -q 'IsWindows' "$INSTALL_PS1"; then + pass "install.ps1 has non-Windows detection" + else + fail "install.ps1 has non-Windows detection" + fi + + # Test: PS1 script respects CARGO_HOME + if grep -q 'CARGO_HOME' "$INSTALL_PS1"; then + pass "install.ps1 respects CARGO_HOME" + else + fail "install.ps1 respects CARGO_HOME" + fi + + # Test: PS1 handles both wt and git-wt + if grep -q 'git-wt' "$INSTALL_PS1"; then + pass "install.ps1 handles git-wt fallback" + else + fail "install.ps1 handles git-wt fallback" + fi + + # Test: PS1 checks wt --version for worktrunk (not Windows Terminal) + if grep -q 'worktrunk' "$INSTALL_PS1" && grep -q '\-\-version' "$INSTALL_PS1"; then + pass "install.ps1 verifies wt is worktrunk, not Windows Terminal" + else + fail "install.ps1 verifies wt is worktrunk, not Windows Terminal" + fi + + # Test: PS1 avoids duplicate PATH entries + if grep -q 'notlike' "$INSTALL_PS1"; then + pass "install.ps1 avoids duplicate PATH entries" + else + fail "install.ps1 avoids duplicate PATH entries" + fi + + # Test: PS1 cross-references shell installer + if grep -q 'install.sh' "$INSTALL_PS1"; then + pass "install.ps1 cross-references shell installer for non-Windows" + else + fail "install.ps1 cross-references shell installer for non-Windows" + fi +else + fail "install.ps1 exists" "File not found at $INSTALL_PS1" +fi + +echo "" + +# =========================================================================== +echo "=== Results ===" +echo " $PASS passed, $FAIL failed" +echo "" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi diff --git a/skills/worktrunk/reference/worktrunk.md b/skills/worktrunk/reference/worktrunk.md index 075df91edc..1abd928675 100644 --- a/skills/worktrunk/reference/worktrunk.md +++ b/skills/worktrunk/reference/worktrunk.md @@ -79,18 +79,6 @@ A demo with some advanced features: ## Install -**Script installation - Static Binary (macOS & Linux):** - -```bash -curl -fsSL https://worktrunk.dev/install.sh | sh -``` - -**Script installation - Static Binary (Windows):** - -```bash -powershell -c "irm https://worktrunk.dev/install.ps1 | iex" -``` - **Homebrew (macOS & Linux):** ```bash @@ -125,6 +113,25 @@ Alternatively, disable Windows Terminal's alias (Settings → Privacy & security paru worktrunk-bin && wt config shell install ``` +
+Script installer (experimental) + +Downloads a static binary — no package manager required. + +**macOS & Linux:** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Windows:** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + +
+ ## Quick start Create a worktree for a new feature: From 5828f2ee79e167c51398e2bab1a957d560e8f719 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Thu, 16 Apr 2026 18:47:18 -0700 Subject: [PATCH 6/6] fix: harden install.sh + add containerised hand-test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard cargo env sourcing with `[ -r ]` — `. missing || true` exits the shell on strict POSIX (dash), so the original guard was a no-op. - Probe /dev/tty openability with `{ true < /dev/tty; }` instead of `[ -e /dev/tty ]`. `:` is a POSIX special builtin, so a redirect failure on it exits the shell; using `true` and checking openability is correct. - Remove redundant `|| { echo ...; exit 1; }` blocks — `set -e` propagates curl / installer failures with their native error messages. - install.ps1: set `$ErrorActionPreference = 'Stop'` and wrap the `wt --version` probe in try/catch so a non-worktrunk `wt` on PATH falls through to the `git-wt` branch instead of throwing. Tests: - Moved install_test.sh out of docs/static/ (it was being published at worktrunk.dev/install_test.sh) into dev/install/. - Rewrote as behavioral tests that invoke install.sh with a mocked curl on PATH and assert on exit codes + user-visible output. Dropped the grep-for-literals tests that couldn't detect broken implementations. - Wired the unit tests into the `lint` CI job. - Added dev/install/test-containers.sh — hand-test that runs install.sh inside Ubuntu / Debian / Fedora / Alpine / Arch containers against real GitHub releases. Not wired into CI (needs Docker + network). Co-authored-by: Claude --- .github/workflows/ci.yaml | 3 + dev/install/install_test.sh | 165 +++++++++++++++++ dev/install/test-containers.sh | 101 +++++++++++ docs/static/install.ps1 | 16 +- docs/static/install.sh | 29 +-- docs/static/install_test.sh | 317 --------------------------------- 6 files changed, 299 insertions(+), 332 deletions(-) create mode 100755 dev/install/install_test.sh create mode 100755 dev/install/test-containers.sh delete mode 100644 docs/static/install_test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b940ecbcb5..3b879bc548 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,6 +44,9 @@ jobs: - name: 🔍 Pre-commit hooks uses: pre-commit/action@v3.0.1 + - name: 🧪 install.sh unit tests + run: sh dev/install/install_test.sh + feature-check: # Guards the library/CLI cleave: the `cli` feature gates clap, skim, # crossterm, termimad, env_logger, humantime. If anything reachable from diff --git a/dev/install/install_test.sh b/dev/install/install_test.sh new file mode 100755 index 0000000000..7162792f82 --- /dev/null +++ b/dev/install/install_test.sh @@ -0,0 +1,165 @@ +#!/bin/sh +set -eu + +# Unit tests for docs/static/install.sh. +# +# These are fast behavioral tests that run install.sh with a mocked curl on +# PATH and verify exit codes + output. They cover the error/edge paths only. +# The happy path (curl | sh yielding a working `wt`) is covered by the +# container hand-test: dev/install/test-containers.sh + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh" + +PASS=0 +FAIL=0 + +pass() { + PASS=$((PASS + 1)) + echo " PASS: $1" +} + +fail() { + FAIL=$((FAIL + 1)) + echo " FAIL: $1" + if [ -n "${2:-}" ]; then + printf ' %s\n' "$2" + fi +} + +MOCK_DIR="$(mktemp -d)" +CARGO_DIR="$(mktemp -d)" +trap 'rm -rf "$MOCK_DIR" "$CARGO_DIR"' EXIT + +# Write an executable shell script to $MOCK_DIR/$1 with the given body. +mock_bin() { + name="$1" + body="$2" + path="$MOCK_DIR/$name" + printf '#!/bin/sh\n%s\n' "$body" > "$path" + chmod +x "$path" +} + +# Mock curl to find the `-o ` arg and write a minimal installer there +# that exits with the given code. Simulates a successful download of an +# installer whose *execution* then succeeds ($1=0) or fails ($1!=0). +mock_curl_writes_installer() { + mock_bin curl "while [ \$# -gt 0 ]; do + if [ \"\$1\" = \"-o\" ]; then + shift + printf '%s\n' '#!/bin/sh' 'exit $1' > \"\$1\" + exit 0 + fi + shift +done +exit 1" +} + +# Run install.sh with $MOCK_DIR first on PATH and a curated environment. +# Captures exit code in $rc and combined output in $output. Pass a value for +# $OS as the first argument (default empty). +run_install() { + os_val="${1:-}" + set +e + output="$(PATH="$MOCK_DIR:/usr/bin:/bin" \ + OS="$os_val" \ + HOME="$CARGO_DIR/home" \ + CARGO_HOME="$CARGO_DIR/cargo" \ + sh "$INSTALL_SH" 2>&1)" + rc=$? + set -e +} + +echo "=== install.sh test suite ===" +echo "" + +# --------------------------------------------------------------------------- +echo "--- Platform gate ---" + +# Windows detection: sets OS=Windows_NT, expects exit 1 with guidance. +run_install Windows_NT +if [ "$rc" -eq 1 ] && echo "$output" | grep -q "Windows detected" \ + && echo "$output" | grep -q "install.ps1"; then + pass "exits with PowerShell guidance when OS=Windows_NT" +else + fail "exits with PowerShell guidance when OS=Windows_NT" "rc=$rc output: $output" +fi + +echo "" + +# --------------------------------------------------------------------------- +echo "--- Download / installer failures ---" + +# curl fails to download: expect non-zero exit. +mock_bin curl 'exit 22' +run_install +if [ "$rc" -ne 0 ]; then + pass "exits non-zero when curl fails" +else + fail "exits non-zero when curl fails" "rc=$rc output: $output" +fi + +# curl succeeds but writes a failing installer: expect non-zero exit. +mock_curl_writes_installer 5 +run_install +if [ "$rc" -ne 0 ]; then + pass "exits non-zero when upstream installer fails" +else + fail "exits non-zero when upstream installer fails" "rc=$rc output: $output" +fi + +echo "" + +# --------------------------------------------------------------------------- +echo "--- Post-install path resolution ---" + +# curl writes a no-op installer; CARGO_HOME is empty and wt is absent from +# PATH → script should warn and exit 0 (installer succeeded but wt missing). +mock_curl_writes_installer 0 +mkdir -p "$CARGO_DIR/cargo" +run_install +if [ "$rc" -eq 0 ] && echo "$output" | grep -q "'wt' not found in PATH"; then + pass "warns and exits 0 when wt is missing after install" +else + fail "warns and exits 0 when wt is missing after install" "rc=$rc output: $output" +fi + +# As above, but wt exists in CARGO_HOME/bin with an env file. The script +# should source env, find wt, and reach the post-wt-found stage. Whether the +# final `wt config shell install` actually runs depends on /dev/tty being +# openable — that's a property of the test environment, so accept either the +# sentinel (TTY) or the non-interactive fallback message (no TTY). +sentinel="$CARGO_DIR/wt.args" +mkdir -p "$CARGO_DIR/cargo/bin" +cat > "$CARGO_DIR/cargo/bin/wt" < "$sentinel" +WT +chmod +x "$CARGO_DIR/cargo/bin/wt" +cat > "$CARGO_DIR/cargo/env" </dev/null) output: $output" +fi + +echo "" + +# --------------------------------------------------------------------------- +echo "=== Results ===" +echo " $PASS passed, $FAIL failed" +echo "" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi diff --git a/dev/install/test-containers.sh b/dev/install/test-containers.sh new file mode 100755 index 0000000000..bcb513f18e --- /dev/null +++ b/dev/install/test-containers.sh @@ -0,0 +1,101 @@ +#!/bin/sh +set -eu + +# Hand-test: run docs/static/install.sh inside clean Docker containers and +# verify it produces a working `wt`. Not wired into CI because it hits the +# real network (GitHub releases) and needs Docker. +# +# Usage: +# sh dev/install/test-containers.sh # test all images +# sh dev/install/test-containers.sh ubuntu # test one image +# +# The script under test is the one checked into this repo (not fetched from +# worktrunk.dev) — we're testing the current source, not what's published. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh" + +if ! command -v docker >/dev/null 2>&1; then + echo "docker is required for the container test. Install Docker Desktop or" + echo "equivalent, then re-run." + exit 1 +fi + +# Each entry: image | setup. The upstream cargo-dist installer downloads a +# tar.xz release and extracts it, so `xz` must be on PATH alongside curl. +IMAGES=" +ubuntu:24.04|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils +debian:12|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils +fedora:41|dnf install -y -q curl xz +alpine:3.20|apk add --no-cache curl ca-certificates xz +archlinux:latest|pacman -Sy --noconfirm curl ca-certificates xz +" + +FILTER="${1:-}" +PASS=0 +FAIL=0 +FAILED="" + +run_one() { + image="$1" + setup="$2" + + echo "" + echo "=== $image ===" + + # Copy install.sh into the container, run setup + install, then verify + # `wt --version` prints a worktrunk version. Use `sh -c` as entrypoint so + # the script runs regardless of the image's default command. We capture + # into a temp file rather than piping — a pipe would mask docker's exit + # code behind sed's. + log="$(mktemp)" + set +e + docker run --rm \ + -v "$INSTALL_SH:/tmp/install.sh:ro" \ + "$image" \ + sh -c "set -e; $setup >/dev/null; sh /tmp/install.sh; . \${CARGO_HOME:-\$HOME/.cargo}/env; wt --version" \ + >"$log" 2>&1 + rc=$? + set -e + sed 's/^/ /' "$log" + rm -f "$log" + + if [ "$rc" -eq 0 ]; then + echo " PASS: $image" + PASS=$((PASS + 1)) + else + echo " FAIL: $image (exit $rc)" + FAIL=$((FAIL + 1)) + FAILED="$FAILED $image" + fi +} + +echo "Testing install.sh in containers..." + +# Shell-splitting on newlines in POSIX sh: set IFS to newline, iterate. +old_ifs="$IFS" +IFS=' +' +for entry in $IMAGES; do + IFS='|' + # shellcheck disable=SC2086 + set -- $entry + IFS="$old_ifs" + image="$1" + setup="$2" + if [ -n "$FILTER" ] && ! echo "$image" | grep -q "$FILTER"; then + continue + fi + run_one "$image" "$setup" + IFS=' +' +done +IFS="$old_ifs" + +echo "" +echo "=== Results ===" +echo " $PASS passed, $FAIL failed" +if [ "$FAIL" -gt 0 ]; then + echo " Failed:$FAILED" + exit 1 +fi diff --git a/docs/static/install.ps1 b/docs/static/install.ps1 index be09f25751..19acae2237 100644 --- a/docs/static/install.ps1 +++ b/docs/static/install.ps1 @@ -1,6 +1,8 @@ # Worktrunk Installer (Windows) # https://worktrunk.dev/install.ps1 +$ErrorActionPreference = 'Stop' + if ($IsWindows -eq $false -and $PSVersionTable.PSVersion.Major -ge 6) { Write-Host "Non-Windows environment detected. Please use the shell installer instead:" -ForegroundColor Yellow Write-Host " curl -fsSL https://worktrunk.dev/install.sh | sh" @@ -17,7 +19,19 @@ if ($env:Path -notlike "*$cargoBin*") { $env:Path += ";$cargoBin" } -if ((Get-Command wt -ErrorAction SilentlyContinue) -and (wt --version 2>&1 | Select-String 'worktrunk')) { +# Check whether `wt` on PATH is actually worktrunk (Windows Terminal uses +# the same alias). Wrap in try/catch — with ErrorActionPreference='Stop', +# a failing `wt --version` would throw instead of falling through. +$wtIsWorktrunk = $false +if (Get-Command wt -ErrorAction SilentlyContinue) { + try { + $wtIsWorktrunk = [bool](wt --version 2>&1 | Select-String 'worktrunk') + } catch { + $wtIsWorktrunk = $false + } +} + +if ($wtIsWorktrunk) { wt config shell install } elseif (Get-Command git-wt -ErrorAction SilentlyContinue) { git-wt config shell install diff --git a/docs/static/install.sh b/docs/static/install.sh index 1778918735..01b8d210c2 100644 --- a/docs/static/install.sh +++ b/docs/static/install.sh @@ -17,19 +17,18 @@ echo "Installing worktrunk..." # 2. Piping consumes stdin, blocking interactive prompts in the installer installer="$(mktemp)" trap 'rm -f "$installer"' EXIT -curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh -o "$installer" || { - echo "Download failed." - exit 1 -} -sh "$installer" || { - echo "Installation failed." - exit 1 -} +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh -o "$installer" +sh "$installer" # Source the cargo env to pick up PATH changes from the installer. # This handles custom CARGO_HOME and avoids hardcoding ~/.cargo/bin. -# shellcheck disable=SC1091 -. "${CARGO_HOME:-$HOME/.cargo}/env" 2>/dev/null || true +# POSIX sh exits the whole script when `.` can't read the file, even with +# `|| true`, so guard with an explicit existence check. +cargo_env="${CARGO_HOME:-$HOME/.cargo}/env" +if [ -r "$cargo_env" ]; then + # shellcheck disable=SC1090 + . "$cargo_env" +fi if ! command -v wt >/dev/null 2>&1; then echo "" @@ -38,10 +37,12 @@ if ! command -v wt >/dev/null 2>&1; then exit 0 fi -# Configure shell integration. We use < /dev/tty to ensure the interactive -# prompt can read from the terminal even when this script was piped into sh -# (e.g. curl ... | sh). -if [ -e /dev/tty ]; then +# Configure shell integration. We use < /dev/tty so the interactive prompt +# works even when this script was piped into sh (e.g. curl ... | sh). On +# non-interactive contexts /dev/tty exists but isn't openable; probe before +# redirecting. Use `true` (a regular builtin) rather than `:` — POSIX exits +# the shell on a redirect failure against a special builtin. +if { true < /dev/tty; } 2>/dev/null; then wt config shell install < /dev/tty else echo "" diff --git a/docs/static/install_test.sh b/docs/static/install_test.sh deleted file mode 100644 index 13d7ce9cd6..0000000000 --- a/docs/static/install_test.sh +++ /dev/null @@ -1,317 +0,0 @@ -#!/bin/sh -set -eu - -# Test suite for install.sh -# Run: sh docs/static/install_test.sh -# -# Tests the install script's logic by mocking external commands (curl, wt). -# Each test runs in a subshell so failures are isolated. - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -INSTALL_SH="$SCRIPT_DIR/install.sh" -PASS=0 -FAIL=0 - -pass() { - PASS=$((PASS + 1)) - echo " PASS: $1" -} - -fail() { - FAIL=$((FAIL + 1)) - echo " FAIL: $1" - if [ -n "${2:-}" ]; then - echo " $2" - fi -} - -# Create a temp directory for mock binaries -MOCK_DIR="$(mktemp -d)" -trap 'rm -rf "$MOCK_DIR"' EXIT - -# Helper: create a mock executable -mock_bin() { - cat > "$MOCK_DIR/$1" << MOCKEOF -#!/bin/sh -$2 -MOCKEOF - chmod +x "$MOCK_DIR/$1" -} - -echo "=== install.sh test suite ===" -echo "" - -# =========================================================================== -echo "--- Script structure tests ---" - -# Test: script starts with proper shebang -if head -1 "$INSTALL_SH" | grep -q '^#!/bin/sh'; then - pass "has POSIX sh shebang" -else - fail "has POSIX sh shebang" -fi - -# Test: script uses set -eu -if grep -q '^set -eu' "$INSTALL_SH"; then - pass "uses set -eu for strict error handling" -else - fail "uses set -eu for strict error handling" -fi - -# Test: script does NOT hardcode ~/.cargo/bin path for wt lookup -if grep -q '"\$HOME/\.cargo/bin/wt"' "$INSTALL_SH"; then - fail "should not hardcode \$HOME/.cargo/bin/wt" "Use 'command -v wt' after sourcing cargo env" -else - pass "does not hardcode \$HOME/.cargo/bin/wt path" -fi - -# Test: script sources cargo env -if grep -q 'CARGO_HOME:-\$HOME/\.cargo' "$INSTALL_SH" && grep -q '\..*env' "$INSTALL_SH"; then - pass "sources cargo env with CARGO_HOME support" -else - fail "sources cargo env with CARGO_HOME support" -fi - -# Test: uses /dev/tty for interactive input -if grep -q '/dev/tty' "$INSTALL_SH"; then - pass "uses /dev/tty for interactive prompts" -else - fail "uses /dev/tty for interactive prompts" -fi - -# Test: has graceful fallback when /dev/tty is unavailable -if grep -q '\-e /dev/tty' "$INSTALL_SH"; then - pass "checks /dev/tty existence before using it" -else - fail "checks /dev/tty existence before using it" -fi - -# Test: uses --proto and --tlsv1.2 for secure curl -if grep -q "\-\-proto '=https' --tlsv1.2" "$INSTALL_SH"; then - pass "enforces HTTPS-only with TLS 1.2+" -else - fail "enforces HTTPS-only with TLS 1.2+" -fi - -# Test: downloads to temp file instead of piping curl to sh -if grep -q 'mktemp' "$INSTALL_SH" && grep -q '\-o ' "$INSTALL_SH"; then - pass "downloads to temp file (avoids pipe swallowing failures)" -else - fail "downloads to temp file (avoids pipe swallowing failures)" -fi - -# Test: cleans up temp file on exit -if grep -q "trap.*rm.*EXIT" "$INSTALL_SH"; then - pass "cleans up temp file via trap" -else - fail "cleans up temp file via trap" -fi - -echo "" - -# =========================================================================== -echo "--- Windows detection tests ---" - -# Test: detects Windows_NT and exits -output=$(OS=Windows_NT sh "$INSTALL_SH" 2>&1) && rc=$? || rc=$? -if [ "$rc" -eq 1 ] && echo "$output" | grep -q "Windows detected"; then - pass "detects Windows and exits with error" -else - fail "detects Windows and exits with error" "rc=$rc output: $output" -fi - -# Test: suggests PowerShell installer -if echo "$output" | grep -q "install.ps1"; then - pass "suggests PowerShell installer for Windows" -else - fail "suggests PowerShell installer for Windows" -fi - -# Test: does not crash on unset OS variable -if grep -q '"\${OS:-}"' "$INSTALL_SH"; then - pass "uses \${OS:-} to handle unset OS variable" -else - fail "uses \${OS:-} to handle unset OS variable" -fi - -echo "" - -# =========================================================================== -echo "--- Download failure tests ---" - -# Test: curl download failure is caught (not hidden by pipe) -mock_bin "curl" 'exit 1' - -output=$(PATH="$MOCK_DIR:/usr/bin:/bin" OS="" sh "$INSTALL_SH" 2>&1) && rc=$? || rc=$? -if [ "$rc" -ne 0 ]; then - pass "exits with error when curl download fails" -else - fail "exits with error when curl download fails" "rc=$rc output: $output" -fi - -if echo "$output" | grep -qi "download failed\|failed"; then - pass "shows failure message when curl fails" -else - fail "shows failure message when curl fails" "output: $output" -fi - -# Test: installer script failure is caught -# Mock curl that succeeds but writes a failing installer -mock_bin "curl" 'echo "exit 1" > "$(echo "$@" | sed "s/.*-o //")" 2>/dev/null || true' - -output=$(PATH="$MOCK_DIR:/usr/bin:/bin" OS="" sh "$INSTALL_SH" 2>&1) && rc=$? || rc=$? -if [ "$rc" -ne 0 ]; then - pass "exits with error when installer script fails" -else - fail "exits with error when installer script fails" "rc=$rc output: $output" -fi - -echo "" - -# =========================================================================== -echo "--- PATH resolution tests ---" - -# Test: wt found on PATH after sourcing env -mock_bin "wt" 'if [ "${1:-}" = "config" ]; then echo "SHELL_INSTALL_CALLED"; else echo "worktrunk 0.1.0"; fi' - -MOCK_CARGO="$(mktemp -d)" -mkdir -p "$MOCK_CARGO/bin" -cat > "$MOCK_CARGO/env" << EOF -export PATH="$MOCK_DIR:\$PATH" -EOF - -# Test the post-install logic in isolation -cat > "$MOCK_DIR/test_post_install.sh" << 'HARNESS' -#!/bin/sh -set -eu -HARNESS - -cat >> "$MOCK_DIR/test_post_install.sh" << HARNESS -CARGO_HOME="$MOCK_CARGO" -. "\${CARGO_HOME:-\$HOME/.cargo}/env" 2>/dev/null || true -if command -v wt >/dev/null 2>&1; then - echo "FOUND_WT" -else - echo "NOT_FOUND" -fi -HARNESS -chmod +x "$MOCK_DIR/test_post_install.sh" - -output=$(sh "$MOCK_DIR/test_post_install.sh" 2>&1) -if echo "$output" | grep -q "FOUND_WT"; then - pass "finds wt after sourcing cargo env with custom CARGO_HOME" -else - fail "finds wt after sourcing cargo env with custom CARGO_HOME" "output: $output" -fi - -# Test: wt NOT found produces warning -cat > "$MOCK_DIR/test_not_found.sh" << 'HARNESS2' -#!/bin/sh -set -eu -EMPTY_CARGO="$(mktemp -d)" -mkdir -p "$EMPTY_CARGO/bin" -echo "" > "$EMPTY_CARGO/env" -CARGO_HOME="$EMPTY_CARGO" -. "${CARGO_HOME:-$HOME/.cargo}/env" 2>/dev/null || true -PATH="/usr/bin:/bin" -export PATH -if ! command -v wt >/dev/null 2>&1; then - echo "Warning: worktrunk installed but 'wt' not found in PATH." - echo "NOT_FOUND_WARNING" -fi -rm -rf "$EMPTY_CARGO" -HARNESS2 -chmod +x "$MOCK_DIR/test_not_found.sh" - -output=$(sh "$MOCK_DIR/test_not_found.sh" 2>&1) -if echo "$output" | grep -q "NOT_FOUND_WARNING"; then - pass "shows warning when wt is not on PATH" -else - fail "shows warning when wt is not on PATH" "output: $output" -fi - -# Cleanup mock cargo -rm -rf "$MOCK_CARGO" - -echo "" - -# =========================================================================== -echo "--- Non-interactive environment tests ---" - -# Test: script mentions non-interactive fallback -if grep -q 'Non-interactive environment' "$INSTALL_SH"; then - pass "has non-interactive environment fallback message" -else - fail "has non-interactive environment fallback message" -fi - -# Test: fallback tells user to run shell install manually -if grep -q 'wt config shell install' "$INSTALL_SH"; then - pass "fallback message includes 'wt config shell install' command" -else - fail "fallback message includes 'wt config shell install' command" -fi - -echo "" - -# =========================================================================== -echo "--- install.ps1 structure tests ---" - -INSTALL_PS1="$SCRIPT_DIR/install.ps1" - -if [ -f "$INSTALL_PS1" ]; then - # Test: PS1 script has non-Windows detection - if grep -q 'IsWindows' "$INSTALL_PS1"; then - pass "install.ps1 has non-Windows detection" - else - fail "install.ps1 has non-Windows detection" - fi - - # Test: PS1 script respects CARGO_HOME - if grep -q 'CARGO_HOME' "$INSTALL_PS1"; then - pass "install.ps1 respects CARGO_HOME" - else - fail "install.ps1 respects CARGO_HOME" - fi - - # Test: PS1 handles both wt and git-wt - if grep -q 'git-wt' "$INSTALL_PS1"; then - pass "install.ps1 handles git-wt fallback" - else - fail "install.ps1 handles git-wt fallback" - fi - - # Test: PS1 checks wt --version for worktrunk (not Windows Terminal) - if grep -q 'worktrunk' "$INSTALL_PS1" && grep -q '\-\-version' "$INSTALL_PS1"; then - pass "install.ps1 verifies wt is worktrunk, not Windows Terminal" - else - fail "install.ps1 verifies wt is worktrunk, not Windows Terminal" - fi - - # Test: PS1 avoids duplicate PATH entries - if grep -q 'notlike' "$INSTALL_PS1"; then - pass "install.ps1 avoids duplicate PATH entries" - else - fail "install.ps1 avoids duplicate PATH entries" - fi - - # Test: PS1 cross-references shell installer - if grep -q 'install.sh' "$INSTALL_PS1"; then - pass "install.ps1 cross-references shell installer for non-Windows" - else - fail "install.ps1 cross-references shell installer for non-Windows" - fi -else - fail "install.ps1 exists" "File not found at $INSTALL_PS1" -fi - -echo "" - -# =========================================================================== -echo "=== Results ===" -echo " $PASS passed, $FAIL failed" -echo "" - -if [ "$FAIL" -gt 0 ]; then - exit 1 -fi