556217ae7b
prd-number-check / require-numbered-prds (pull_request) Successful in 9s
test / image-input-builds (pull_request) Successful in 46s
test / integration-docker (pull_request) Successful in 1m3s
test / unit (pull_request) Successful in 2m58s
test / coverage (pull_request) Failing after 18s
tracker-policy-pr / check-pr (pull_request) Failing after 14m1s
The first run against the branch's own code confirmed the doctor fix — no traceback, the firecracker probes now report "TAP pool: 0/8" instead of dying on PermissionError — and then failed for a reason the installer cannot fix. The Apple `container` service is per-USER. `container system status` reports an appRoot under ~/Library/Application Support/com.apple.container/, and bbtest sees "container system service: NOT running" while the creating admin's is running. A brand-new account therefore has no backend until it runs `container system start` once, doctor rightly fails, and `test` would be permanently red for host state that install.sh neither creates nor can regress. So stop treating doctor's exit code as one verdict. `test` now fails when the install is broken — no entry point, doctor crashed, or doctor could not report a usable python and config dir — and reports backend readiness as a note. BB_TEST_REQUIRE_BACKEND=1 restores the strict behaviour. A traceback is checked for explicitly rather than inferred from the exit code, because those are the same value. The PermissionError bug exited non-zero exactly like a missing prerequisite does; only the traceback distinguishes a defect from an environment fact, and that distinction is the whole point of this change. The research doc's footprint table had the service as a host-wide launchd service that survives user deletion. It does not. The state lives in the home and goes with it, so the reset is more complete than claimed — but the corollary is that a fresh account cannot run a bottle until the service is started for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
392 lines
17 KiB
Bash
Executable File
392 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Clean-install test harness for the macOS (Apple `container`) path.
|
|
#
|
|
# Exercises install.sh the way a brand-new user would, inside a throwaway
|
|
# macOS account you create and delete from the CLI. install.sh's entire
|
|
# footprint is user-home-local — the pipx venv under ~/.local, or the private
|
|
# venv at ~/.bot-bottle/venv plus a ~/.local/bin symlink, and the ~/.bot-bottle
|
|
# config dir. It writes no shell-profile PATH line, and never installs the
|
|
# backend (see
|
|
# the header of install.sh), so deleting the user is a complete,
|
|
# deterministic reset of everything the installer touched. The Apple
|
|
# `container` runtime is a HOST prerequisite installed once and kept;
|
|
# `deep-reset` is the rare escape hatch that also removes it.
|
|
#
|
|
# Why a throwaway user and not a disposable VM: bot-bottle's default macOS
|
|
# backend is Apple `container`, which runs each container in its own
|
|
# Virtualization.framework microVM. Running that backend inside a macOS
|
|
# guest VM needs nested virtualization, which Apple gates to M3+ silicon.
|
|
# On M1/M2 a separate user account is the only way to get a clean $HOME
|
|
# while still reaching the real host backend. Full rationale in
|
|
# docs/research/testing-clean-install-on-macos.md.
|
|
#
|
|
# Usage:
|
|
# sudo ./scripts/macos-install-test.sh test # up -> run -> status -> down
|
|
# sudo ./scripts/macos-install-test.sh up # create the throwaway user
|
|
# sudo ./scripts/macos-install-test.sh run # run install.sh (+doctor) as it
|
|
# ./scripts/macos-install-test.sh status # user present? backend ready?
|
|
# sudo ./scripts/macos-install-test.sh down # delete user + home (the reset)
|
|
# sudo ./scripts/macos-install-test.sh deep-reset # ALSO uninstall host `container`
|
|
#
|
|
# `test` is the one-shot clean cycle and the command you normally want: it
|
|
# refuses to start if the account already exists (a reused home is not a clean
|
|
# install), and it tears the account down on the way out however it exits, so
|
|
# a failed run never leaves an orphan behind.
|
|
#
|
|
# It exits non-zero if the install fails, if no entry point was installed, if
|
|
# `doctor` crashes, or if doctor can't report a usable python and config dir.
|
|
# install.sh itself exits 0 when doctor reports unmet prerequisites, so `test`
|
|
# is the stricter gate. It does NOT fail on backend readiness, which is host
|
|
# and per-user state the installer neither creates nor can regress — see
|
|
# doctor_as_user. BB_TEST_REQUIRE_BACKEND=1 makes that fatal too.
|
|
#
|
|
# Config via env:
|
|
# BB_TEST_USER account short name (default: bbtest)
|
|
# BB_TEST_FULLNAME account full name (default: "bot-bottle install test")
|
|
# BB_TEST_ADMIN 1=admin (reach container svc), 0=standard (default: 1)
|
|
# BB_TEST_INSTALL_URL curl this install.sh instead of piping the local checkout
|
|
# BB_TEST_KEEP 1=`test` skips its teardown, to poke at a failure
|
|
# BB_TEST_REQUIRE_BACKEND 1=`test` also fails when no backend is ready
|
|
# BB_TEST_REPO_URL https repo the throwaway user clones (default: this one)
|
|
# BOT_BOTTLE_INSTALL_SPEC passed through to install.sh (pip / git spec)
|
|
#
|
|
# Notes:
|
|
# * Run from a normally-booted admin session. Grant Terminal *Full Disk
|
|
# Access* (System Settings -> Privacy & Security) or `down` half-fails
|
|
# with error -14120 and leaves an orphaned account.
|
|
# * `sysadminctl` always exits 0 even on failure, so `up`/`down` verify
|
|
# the result with `dscl` and fail loudly on a mismatch.
|
|
# * The account is created without a password: `run` drives it headlessly
|
|
# via `sudo -u`, which never needs the target's password. The account
|
|
# cannot GUI-login, which this harness does not require.
|
|
# * `run` covers the installer + `bot-bottle doctor`. Actually launching a
|
|
# bottle from the throwaway user may need a full launchd user session
|
|
# (`launchctl asuser`); on M1/M2 the backend can't run under nested virt
|
|
# anyway, so this harness stops at install + doctor.
|
|
|
|
set -euo pipefail
|
|
|
|
USER_NAME="${BB_TEST_USER:-bbtest}"
|
|
FULL_NAME="${BB_TEST_FULLNAME:-bot-bottle install test}"
|
|
ADMIN="${BB_TEST_ADMIN:-1}"
|
|
# https, not the ssh `origin`: the throwaway user has no key of ours.
|
|
BB_TEST_REPO_URL="${BB_TEST_REPO_URL:-https://gitea.dideric.is/didericis/bot-bottle.git}"
|
|
|
|
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
_REPO_ROOT="$(cd "$_SCRIPT_DIR/.." && pwd)"
|
|
|
|
# Set by `test`, which chains the steps itself and so suppresses the
|
|
# "here's the next command to run" hints the individual steps print.
|
|
IN_TEST=0
|
|
|
|
# --- guards ----------------------------------------------------------
|
|
require_macos() {
|
|
[ "$(uname -s)" = "Darwin" ] \
|
|
|| { echo "error: this harness is macOS-only (uname is $(uname -s))" >&2; exit 1; }
|
|
}
|
|
|
|
require_root() {
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "error: '$1' needs root; re-run under sudo" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
user_exists() { dscl . -read "/Users/$USER_NAME" >/dev/null 2>&1; }
|
|
|
|
# Run a shell snippet as the throwaway user in a fresh login shell.
|
|
#
|
|
# The snippet goes in on stdin, never as an argument. `sudo -i` joins its argv
|
|
# into a single string and hands that to the login shell's -c, so quoting in an
|
|
# argument is not preserved and a newline ends the command outright ("sh: -c:
|
|
# line 1: syntax error: unexpected end of file"). Feeding `sh -s` from stdin
|
|
# sidesteps the joining entirely and lets snippets be multi-line.
|
|
run_as_user() { printf '%s\n' "$1" | sudo -u "$USER_NAME" -i sh -s; }
|
|
|
|
# `bot-bottle doctor` as the throwaway user. Non-zero when no entry point was
|
|
# installed at all, or when doctor itself is unhappy.
|
|
#
|
|
# Deliberately does NOT require `bot-bottle` on PATH: install.sh prints the
|
|
# PATH line rather than editing a shell profile, so on a fresh account the
|
|
# entry point is installed and working but not on PATH. Demanding PATH here
|
|
# would fail every run for a reason the installer intends.
|
|
#
|
|
# Doctor's own exit code conflates two unrelated things: whether the install
|
|
# works, and whether this user has a backend ready to run a bottle. Those need
|
|
# different verdicts here. install.sh explicitly does not install a backend,
|
|
# and the Apple `container` service is per-USER — its state lives in
|
|
# ~/Library/Application Support/com.apple.container — so a brand-new account
|
|
# never has one running, no matter how correct the install is. Failing on that
|
|
# would leave `test` permanently red for something the installer cannot fix
|
|
# and cannot regress. So: assert the install, report the backend.
|
|
# BB_TEST_REQUIRE_BACKEND=1 makes backend readiness fatal too.
|
|
doctor_as_user() {
|
|
local out rc=0 bad=0
|
|
out="$(mktemp "${TMPDIR:-/tmp}/bb-doctor.XXXXXX")"
|
|
# shellcheck disable=SC2016 # $HOME/$bb must expand in the *target* user's
|
|
# shell, not in this one — that's the whole point of the single quotes.
|
|
run_as_user '
|
|
for bb in bot-bottle "$HOME/.local/bin/bot-bottle" "$HOME/.bot-bottle/venv/bin/bot-bottle"; do
|
|
if command -v "$bb" >/dev/null 2>&1; then
|
|
case "$bb" in
|
|
bot-bottle) : ;;
|
|
*) echo " (not on PATH — running $bb directly, as install.sh advises)" ;;
|
|
esac
|
|
exec "$bb" doctor
|
|
fi
|
|
done
|
|
echo " no bot-bottle entry point found for this user" >&2
|
|
exit 1
|
|
' >"$out" 2>&1 || rc=$?
|
|
cat "$out"
|
|
|
|
# An unhandled exception is always an install/product defect, never an
|
|
# environment fact — this is exactly how the PermissionError on `ip` showed
|
|
# up, and doctor's non-zero exit alone would not have distinguished it.
|
|
if grep -q 'Traceback (most recent call last)' "$out"; then
|
|
echo " doctor crashed (traceback above) — a defect, not a missing prerequisite" >&2
|
|
bad=1
|
|
fi
|
|
grep -qE '^ok: +python:' "$out" \
|
|
|| { echo " doctor never reported a usable python" >&2; bad=1; }
|
|
grep -qE '^ok: +config:' "$out" \
|
|
|| { echo " doctor never reported a usable config dir" >&2; bad=1; }
|
|
if [ "$rc" -ne 0 ] && ! grep -qE '^(fail|warn): +backend' "$out"; then
|
|
echo " doctor failed for something other than backend readiness" >&2
|
|
bad=1
|
|
fi
|
|
|
|
local backend_ready=1
|
|
grep -qE '^fail: +backend' "$out" && backend_ready=0
|
|
rm -f "$out"
|
|
|
|
[ "$bad" -eq 0 ] || return 1
|
|
if [ "$backend_ready" -eq 0 ]; then
|
|
if [ "${BB_TEST_REQUIRE_BACKEND:-0}" = "1" ]; then
|
|
echo " no backend is ready and BB_TEST_REQUIRE_BACKEND=1" >&2
|
|
return 1
|
|
fi
|
|
echo " note: install is sound; no backend ready for this user."
|
|
echo " Expected on a fresh account — the Apple 'container' service is"
|
|
echo " per-user and needs one 'container system start'. Set"
|
|
echo " BB_TEST_REQUIRE_BACKEND=1 to treat this as a failure."
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# --- commands --------------------------------------------------------
|
|
cmd_up() {
|
|
require_macos
|
|
require_root up
|
|
if user_exists; then
|
|
echo "$USER_NAME already exists; nothing to do (run 'down' first to reset)"
|
|
return 0
|
|
fi
|
|
local admin_flag=()
|
|
[ "$ADMIN" = "1" ] && admin_flag=(-admin)
|
|
# No -password: the account is only ever driven headlessly via `sudo -u`,
|
|
# which doesn't need one. sysadminctl warns about FileVault here; that's
|
|
# irrelevant to a headless test account.
|
|
sysadminctl -addUser "$USER_NAME" -fullName "$FULL_NAME" "${admin_flag[@]}" || true
|
|
# sysadminctl exits 0 regardless of outcome, so confirm the account landed.
|
|
user_exists || { echo "error: failed to create $USER_NAME" >&2; return 1; }
|
|
if [ "$IN_TEST" = 1 ]; then
|
|
echo "created $USER_NAME (admin=$ADMIN)"
|
|
else
|
|
echo "created $USER_NAME (admin=$ADMIN). Install into it with: sudo $0 run"
|
|
fi
|
|
}
|
|
|
|
# The package spec to install. install.sh's own default is the repo's DEFAULT
|
|
# BRANCH, which would mean piping this checkout's installer into the throwaway
|
|
# user and then installing main's code — verifying the PR's install.sh against
|
|
# a package that doesn't contain the PR. Pin to the branch under test instead.
|
|
#
|
|
# The branch has to be pushed: the throwaway user clones over https and cannot
|
|
# read this checkout (mode-700 home, and no SSH key for the ssh remote), so
|
|
# what it gets is whatever the remote has. Say so when that differs from here.
|
|
resolve_install_spec() {
|
|
if [ -n "${BOT_BOTTLE_INSTALL_SPEC:-}" ]; then
|
|
echo "$BOT_BOTTLE_INSTALL_SPEC"
|
|
return 0
|
|
fi
|
|
local branch
|
|
branch="$(git -C "$_REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" || branch=""
|
|
if [ -z "$branch" ] || [ "$branch" = "HEAD" ]; then
|
|
echo "note: not on a named branch; installing the repo default" >&2
|
|
echo "git+${BB_TEST_REPO_URL}"
|
|
return 0
|
|
fi
|
|
if [ -n "$(git -C "$_REPO_ROOT" status --porcelain 2>/dev/null)" ]; then
|
|
echo "warning: working tree is dirty — the throwaway user installs" >&2
|
|
echo " $branch as pushed, not what is on disk here." >&2
|
|
elif ! git -C "$_REPO_ROOT" diff --quiet "@{upstream}" 2>/dev/null; then
|
|
echo "warning: $branch differs from its upstream — push first, or the" >&2
|
|
echo " throwaway user installs stale code." >&2
|
|
fi
|
|
echo "git+${BB_TEST_REPO_URL}@${branch}"
|
|
}
|
|
|
|
cmd_run() {
|
|
require_macos
|
|
require_root run
|
|
user_exists || { echo "error: $USER_NAME does not exist; run 'sudo $0 up' first" >&2; return 1; }
|
|
|
|
local spec
|
|
spec="$(resolve_install_spec)"
|
|
|
|
echo "== installing bot-bottle as $USER_NAME =="
|
|
echo "== spec: $spec =="
|
|
if [ -n "${BB_TEST_INSTALL_URL:-}" ]; then
|
|
run_as_user "BOT_BOTTLE_INSTALL_SPEC='$spec' curl -fsSL '$BB_TEST_INSTALL_URL' | sh"
|
|
else
|
|
# Test THIS checkout's install.sh, not the published one, so a PR is
|
|
# verifiable before it lands. Feed it in on stdin rather than staging a
|
|
# copy somewhere the throwaway user can read: the redirect is opened by
|
|
# root before sudo drops privileges, so the tester's mode-700 home is a
|
|
# non-issue, there's no temp file to leak if the run is interrupted, and
|
|
# `sh -s` is the same shape as the documented `curl … | sh` install.
|
|
#
|
|
# The spec is prepended to the stream as an export rather than passed
|
|
# as an argument, for the same argv-joining reason as run_as_user.
|
|
{
|
|
printf "export BOT_BOTTLE_INSTALL_SPEC='%s'\n" "$spec"
|
|
cat "$_REPO_ROOT/install.sh"
|
|
} | sudo -u "$USER_NAME" -i sh -s
|
|
fi
|
|
[ "$IN_TEST" = 1 ] \
|
|
|| echo "== install.sh runs 'doctor' itself; re-check anytime with: $0 status =="
|
|
}
|
|
|
|
# Informational, with one teeth-bearing case: when it can actually reach
|
|
# doctor (root, account present) its exit status is doctor's, so `test` and
|
|
# any other caller can use it as the post-install assertion.
|
|
cmd_status() {
|
|
require_macos
|
|
local rc=0
|
|
if user_exists; then
|
|
echo "user: $USER_NAME present"
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
echo "doctor (as $USER_NAME):"
|
|
doctor_as_user || rc=1
|
|
else
|
|
echo " (re-run under sudo to run 'bot-bottle doctor' as $USER_NAME)"
|
|
fi
|
|
else
|
|
echo "user: $USER_NAME absent"
|
|
fi
|
|
if command -v container >/dev/null 2>&1; then
|
|
echo "backend: apple 'container' present ($(container --version 2>/dev/null | head -1))"
|
|
else
|
|
echo "backend: apple 'container' NOT on PATH (host prerequisite; install once)"
|
|
fi
|
|
return "$rc"
|
|
}
|
|
|
|
cmd_down() {
|
|
require_macos
|
|
require_root down
|
|
if ! user_exists; then
|
|
echo "$USER_NAME not present; nothing to remove"
|
|
return 0
|
|
fi
|
|
# A plain -deleteUser removes the home dir, which is the whole reset.
|
|
# -secure is a no-op on modern macOS (secure erase of the home folder
|
|
# was removed in Sierra), so it buys nothing here.
|
|
sysadminctl -deleteUser "$USER_NAME" || true
|
|
if user_exists; then
|
|
echo "error: $USER_NAME still present after delete." >&2
|
|
echo " - grant Terminal Full Disk Access (System Settings > Privacy & Security), or" >&2
|
|
echo " - it may hold the last Secure Token (won't happen while another admin exists)" >&2
|
|
return 1
|
|
fi
|
|
echo "removed $USER_NAME and its home — install surface is clean."
|
|
}
|
|
|
|
# Teardown half of `test`, installed as an EXIT trap the moment the account
|
|
# exists so that a failure — or a Ctrl-C — still leaves the machine clean.
|
|
_test_teardown() {
|
|
local rc=$?
|
|
trap - EXIT INT TERM
|
|
if [ "${BB_TEST_KEEP:-0}" = "1" ]; then
|
|
echo
|
|
echo "== [4/4] down: SKIPPED (BB_TEST_KEEP=1) =="
|
|
echo " $USER_NAME is still around; remove it with: sudo $0 down"
|
|
exit "$rc"
|
|
fi
|
|
echo
|
|
echo "== [4/4] down =="
|
|
cmd_down || rc=1
|
|
if [ "$rc" -eq 0 ]; then
|
|
echo
|
|
echo "PASS: a brand-new user can install bot-bottle and pass doctor."
|
|
else
|
|
echo
|
|
echo "FAIL: see above (the throwaway account was torn down regardless)." >&2
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
|
|
cmd_test() {
|
|
require_macos
|
|
require_root test
|
|
# A pre-existing account means a pre-existing home, which is the one thing
|
|
# this harness exists to rule out. Don't silently test a dirty install.
|
|
if user_exists; then
|
|
echo "error: $USER_NAME already exists, so this would not be a clean install." >&2
|
|
echo " reset first: sudo $0 down" >&2
|
|
return 1
|
|
fi
|
|
IN_TEST=1
|
|
|
|
echo "== [1/4] up =="
|
|
cmd_up
|
|
trap _test_teardown EXIT INT TERM
|
|
|
|
echo
|
|
echo "== [2/4] run =="
|
|
cmd_run
|
|
|
|
echo
|
|
echo "== [3/4] status =="
|
|
# install.sh exits 0 even when doctor reports unmet prerequisites, so the
|
|
# install succeeding is not the verdict — this is.
|
|
cmd_status || {
|
|
echo "error: doctor is unhappy for a freshly installed user (see above)." >&2
|
|
echo " re-run with BB_TEST_KEEP=1 to keep $USER_NAME around and dig in." >&2
|
|
return 1
|
|
}
|
|
}
|
|
|
|
cmd_deep_reset() {
|
|
require_macos
|
|
require_root deep-reset
|
|
# Remove the user first (idempotent), then the HOST-level container
|
|
# runtime that a user deletion leaves behind under /usr/local + launchd.
|
|
cmd_down || true
|
|
if command -v container >/dev/null 2>&1; then
|
|
# The service can run in more than one launchd context (the invoking
|
|
# user's and root's), so stop both, best-effort.
|
|
[ -n "${SUDO_USER:-}" ] && sudo -u "$SUDO_USER" container system stop 2>/dev/null || true
|
|
container system stop 2>/dev/null || true
|
|
if [ -x /usr/local/bin/uninstall-container.sh ]; then
|
|
/usr/local/bin/uninstall-container.sh -d || true
|
|
echo "uninstalled the host Apple 'container' runtime"
|
|
else
|
|
echo "note: /usr/local/bin/uninstall-container.sh not found; runtime left as-is" >&2
|
|
fi
|
|
else
|
|
echo "no 'container' runtime on PATH; nothing further to remove"
|
|
fi
|
|
}
|
|
|
|
case "${1:-}" in
|
|
test) cmd_test ;;
|
|
up) cmd_up ;;
|
|
run) cmd_run ;;
|
|
status) cmd_status ;;
|
|
down) cmd_down ;;
|
|
deep-reset) cmd_deep_reset ;;
|
|
*) echo "usage: $0 {test|up|run|status|down|deep-reset}" >&2 ; exit 2 ;;
|
|
esac
|