Files
OpenJarvis/tests/install/bash/test_install.bats
T
de3e86c544 fix(install): use requires-python range; bootstrap shims are Python-free (#476, #484) (#492)
Consolidates two reports that overlap in scope:

- #476 (@senki): install.sh hardcoded `--python 3.11` but
  pyproject.toml declares `requires-python = ">=3.10,<3.14"`. The
  installer should track the project's allowed range, not pin a
  conservative-three-years-ago version.
- #484 (@sanjayravit): install.sh crashed on hosts with no
  python3/python on PATH because `mark_done`, `beacon`, and
  `get_anon_id` all called $PY_CMD via inline Python heredocs.

This PR closes both gaps with five surgical edits to install.sh
(all behavior-preserving for the existing happy path; the bats
tests prove it):

1. get_anon_id — replace `python3 -c uuid` with POSIX
   `/dev/urandom + od` + bash substring expansion. Same UUID v4
   shape; works on Python-less hosts.

2. beacon — replace the 40-line Python heredoc with curl + a
   shell-built JSON payload. All inputs are from controlled
   sources (event ∈ fixed vocabulary, stage from stage_label(),
   numeric ids/codes from validated arithmetic, anon_id from a
   fresh UUID) so no general-purpose JSON escaping is needed.
   `|| true` is load-bearing — PostHog 5xx must never abort an
   install via the ERR trap.

3. mark_done — replace `python3 -c json.load+update+dump` with
   awk that regenerates the file from scratch. Idempotent: if
   the key is already marked, return early. Robust against
   prior format drift. wsl key is always rewritten last so a
   later FORCE_WSL=1 re-run correctly updates it.

4. parse_requires_python — new helper. Greps the project's
   requires-python field, handles inclusive (`<=3.13`) and
   exclusive (`<3.14`) upper bounds correctly, falls back to
   3.11 if pyproject can't be parsed (the previous hardcoded
   value — safe under the existing 3.10-3.13 range).

5. create_venv — call parse_requires_python instead of
   hardcoding 3.11. The existing uv-managed-Python fallback
   (from #444) still kicks in if the host doesn't have the
   target version installed.

Adversarial review caught two real bugs before commit:

- HIGH: parse_requires_python's exclusive-bound regex would
  also match the digits after `<=` (inclusive bound) and then
  incorrectly subtract 1 — producing 3.12 from `<=3.13`. Fixed
  by checking the inclusive form first.
- MEDIUM: the new "no Python on PATH" bats test silently skips
  symlinking `pgrep` on hosts where it isn't at /usr/bin or
  /bin (some minimal BusyBox configurations). Added an explicit
  "no matching process" fallback so start_ollama's check works.

New bats coverage:

- `install succeeds with no system Python on PATH (#484)` —
  builds a PATH that excludes python3/python and exercises the
  full install. Asserts state file is written and contains
  greppable step keys.
- `mark_done is idempotent — second mark of same key doesn't
  duplicate` — re-runs the install and verifies install_uv
  appears exactly once in install-state.json.
- `create_venv picks newest in requires-python range, not
  hardcoded 3.11 (#476)` — asserts uv was called with
  `--python 3.13` (the upper minor of `>=3.10,<3.14`).

The git stub now includes `requires-python = ">=3.10,<3.14"`
in its fake pyproject.toml so create_venv has something
realistic to parse.

@sanjayravit — your PR #484 motivated this consolidation;
closing that one as superseded with credit.

Co-authored-by: krypticmouse <herumbshandilya123@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:59:29 -07:00

231 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bats
setup() {
TEST_TMPDIR=$(mktemp -d)
export FAKE_HOME="$TEST_TMPDIR/home"
mkdir -p "$FAKE_HOME"
export HOME="$FAKE_HOME"
export OPENJARVIS_HOME="$FAKE_HOME/.openjarvis"
export STUBS_DIR="$BATS_TEST_DIRNAME/stubs"
# Use a copy of stubs without uv-style real binaries that might be on the host.
# Just put our stubs first in PATH.
export PATH="$STUBS_DIR:/usr/bin:/bin"
export GIT_STUB_LOG="$TEST_TMPDIR/git.log"
export OLLAMA_STUB_LOG="$TEST_TMPDIR/ollama.log"
export UV_STUB_LOG="$TEST_TMPDIR/uv.log"
export CURL_STUB_LOG="$TEST_TMPDIR/curl.log"
: > "$GIT_STUB_LOG"
: > "$OLLAMA_STUB_LOG"
: > "$UV_STUB_LOG"
: > "$CURL_STUB_LOG"
# uv stub needs to fake creating a venv with a jarvis binary.
# Replace the stubs/uv with one that creates the venv tree on `venv` command.
# Since we don't want to mutate the real stub, override via a per-test stub dir.
export PER_TEST_STUBS="$TEST_TMPDIR/stubs"
mkdir -p "$PER_TEST_STUBS"
cat "$STUBS_DIR/uv" > "$PER_TEST_STUBS/uv"
chmod +x "$PER_TEST_STUBS/uv"
# Wrap uv to handle 'venv' specially.
cat > "$PER_TEST_STUBS/uv" <<EOF
#!/usr/bin/env bash
echo "\$@" >> "\$UV_STUB_LOG"
case "\$1" in
venv)
# uv venv [--python X.Y] <path>
# Find the path arg (last non-flag).
for a in "\$@"; do
if [[ "\$a" != --* ]] && [[ "\$a" != -* ]] && [[ "\$a" != "venv" ]] && [[ ! "\$a" =~ ^[0-9]+\\.[0-9]+\$ ]]; then
venv_path="\$a"
fi
done
if [[ -n "\$venv_path" ]]; then
mkdir -p "\$venv_path/bin"
cat > "\$venv_path/bin/jarvis" <<'EOJ'
#!/usr/bin/env bash
# fake jarvis for tests
echo "fake jarvis: \$@"
exit 0
EOJ
chmod +x "\$venv_path/bin/jarvis"
cat > "\$venv_path/bin/python" <<'EOJ'
#!/usr/bin/env bash
# fake python that prints empty for the inline embedded scripts (recommend_model, etc.)
exit 0
EOJ
chmod +x "\$venv_path/bin/python"
fi
;;
esac
exit 0
EOF
chmod +x "$PER_TEST_STUBS/uv"
# Copy other stubs, with the per-test uv first.
cp "$STUBS_DIR"/{git,curl,ollama,cargo,rustup} "$PER_TEST_STUBS/"
chmod +x "$PER_TEST_STUBS"/*
export PATH="$PER_TEST_STUBS:/usr/bin:/bin"
export SCRIPT="$BATS_TEST_DIRNAME/../../../scripts/install/install.sh"
}
teardown() {
[[ -n "${TEST_TMPDIR:-}" ]] && rm -rf "$TEST_TMPDIR"
}
@test "refuses to run as root" {
# EUID is readonly in bash, so we stub `id` to return uid 0 instead.
local root_stubs="$TEST_TMPDIR/root_stubs"
mkdir -p "$root_stubs"
cp "$PER_TEST_STUBS"/* "$root_stubs/"
cat > "$root_stubs/id" <<'IDEOF'
#!/usr/bin/env bash
if [[ "$1" == "-u" ]]; then echo 0; else echo "uid=0(root) gid=0(root) groups=0(root)"; fi
IDEOF
chmod +x "$root_stubs/id"
PATH="$root_stubs:/usr/bin:/bin" run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -ne 0 ]
echo "$output" | grep -qi "root"
}
@test "creates ~/.openjarvis directory tree" {
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
[ -d "$OPENJARVIS_HOME/src" ]
[ -d "$OPENJARVIS_HOME/.state" ]
[ -d "$OPENJARVIS_HOME/.scripts" ]
}
@test "writes install-state.json on success" {
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
[ -f "$OPENJARVIS_HOME/.state/install-state.json" ]
}
@test "calls git clone for the OpenJarvis repo" {
run bash "$SCRIPT" --no-bg-orchestrator
grep -q "clone" "$GIT_STUB_LOG"
}
@test "creates jarvis symlink in ~/.local/bin" {
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
[ -L "$FAKE_HOME/.local/bin/jarvis" ] || [ -f "$FAKE_HOME/.local/bin/jarvis" ]
}
@test "is idempotent — second run skips completed steps" {
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
: > "$GIT_STUB_LOG"
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
! grep -q "clone" "$GIT_STUB_LOG"
}
@test "detects WSL2 and writes platform note to install-state" {
OPENJARVIS_FORCE_WSL=1 run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
grep -q "wsl" "$OPENJARVIS_HOME/.state/install-state.json"
}
@test "fails loudly when git is missing" {
# Build a minimal stubs dir that has curl/uv/ollama but NO git.
# We must NOT include /usr/bin or /bin in PATH because those dirs have real
# git binaries on this system. Instead, provide only the commands the script
# needs before it reaches the `need git` check (id, cat, bash).
local no_git_stubs="$TEST_TMPDIR/no_git_stubs"
mkdir -p "$no_git_stubs"
for f in curl uv ollama cargo rustup; do
cp "$PER_TEST_STUBS/$f" "$no_git_stubs/"
chmod +x "$no_git_stubs/$f"
done
# Provide minimal system utilities (no git) using explicit symlinks.
cat > "$no_git_stubs/id" <<'IDEOF'
#!/bin/bash
echo "1000"
IDEOF
chmod +x "$no_git_stubs/id"
ln -sf /bin/cat "$no_git_stubs/cat"
ln -sf /bin/bash "$no_git_stubs/bash"
PATH="$no_git_stubs" run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -ne 0 ]
echo "$output" | grep -qi "git"
}
@test "--force re-runs completed steps" {
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
: > "$GIT_STUB_LOG"
run bash "$SCRIPT" --no-bg-orchestrator --force
[ "$status" -eq 0 ]
# With --force, second run re-clones (clone is in git.log again).
grep -q "clone" "$GIT_STUB_LOG"
}
@test "--minimal skips foreground model pull" {
run bash "$SCRIPT" --no-bg-orchestrator --minimal
[ "$status" -eq 0 ]
# Ollama 'pull' should NOT have been called.
if [ -s "$OLLAMA_STUB_LOG" ]; then
! grep -q "^pull " "$OLLAMA_STUB_LOG"
fi
}
@test "install succeeds with no system Python on PATH (#484)" {
# Build a stubs dir with NO python3 / python — verifies the
# bootstrap shims (get_anon_id, beacon, mark_done) are Python-free.
local no_py_stubs="$TEST_TMPDIR/no_py_stubs"
mkdir -p "$no_py_stubs"
for f in git curl uv ollama cargo rustup; do
cp "$PER_TEST_STUBS/$f" "$no_py_stubs/"
chmod +x "$no_py_stubs/$f"
done
# Provide minimal core utilities the shims need (od, sed, awk, tr, grep).
for util in id cat bash sh head tail awk sed grep od tr cut mv rm mkdir cp chmod ln test pgrep date sleep ps; do
if [[ -x "/usr/bin/$util" ]]; then
ln -sf "/usr/bin/$util" "$no_py_stubs/$util"
elif [[ -x "/bin/$util" ]]; then
ln -sf "/bin/$util" "$no_py_stubs/$util"
fi
done
# pgrep may be absent on minimal BusyBox/Alpine hosts; stub it as
# "no matching process" so start_ollama's existing-daemon check
# falls through to the spawn path (which the ollama stub handles).
if [[ ! -x "$no_py_stubs/pgrep" ]]; then
printf '#!/bin/sh\nexit 1\n' > "$no_py_stubs/pgrep"
chmod +x "$no_py_stubs/pgrep"
fi
PATH="$no_py_stubs" run bash "$SCRIPT" --no-bg-orchestrator --minimal
[ "$status" -eq 0 ]
# State file was still written (mark_done is Python-free).
[ -f "$OPENJARVIS_HOME/.state/install-state.json" ]
# And it still has valid-looking JSON for state_done's grep.
grep -q '"install_uv":' "$OPENJARVIS_HOME/.state/install-state.json"
}
@test "mark_done is idempotent — second mark of same key doesn't duplicate" {
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
# Capture first state file
local first_count
first_count="$(grep -c '"install_uv":' "$OPENJARVIS_HOME/.state/install-state.json")"
[ "$first_count" -eq 1 ]
# Second run reuses state — keys should not duplicate even if some path re-marks.
run bash "$SCRIPT" --no-bg-orchestrator
[ "$status" -eq 0 ]
local second_count
second_count="$(grep -c '"install_uv":' "$OPENJARVIS_HOME/.state/install-state.json")"
[ "$second_count" -eq 1 ]
}
@test "create_venv picks newest in requires-python range, not hardcoded 3.11 (#476)" {
run bash "$SCRIPT" --no-bg-orchestrator --minimal
[ "$status" -eq 0 ]
# uv stub logs every invocation. We should see `venv --python 3.13 ...`
# (the upper bound of >=3.10,<3.14 is 3.14, so the highest usable is 3.13).
grep -q '^venv --python 3.13' "$UV_STUB_LOG"
}