#!/usr/bin/env bash # Dependency-free CLI assertions for box. Run: bash test/cli.sh # # Runnable by a NON-root user with NO Incus installed — that is the whole point. # Anything that needs a real incus daemon (every lifecycle command) is proven the # way rig proves its root-only paths: source the pure function and drive it against # a fixture, or grep the load-bearing line so a deleted guard cannot ship green. # Deliberately no `set -e` — the harness asserts on failing commands. set -u ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PASS=0 FAIL=0 # check # Runs cmd, asserts exit code and (if non-empty) that combined output # contains want_substr. check() { local desc="$1" want="$2" substr="$3"; shift 3 local out rc out="$("$@" 2>&1)"; rc=$? if [ "$rc" -ne "$want" ]; then echo "FAIL: $desc — exit $rc, wanted $want" printf '%s\n' "$out" | sed 's/^/ /' FAIL=$((FAIL + 1)); return fi if [ -n "$substr" ] && ! printf '%s' "$out" | grep -qF -e "$substr"; then echo "FAIL: $desc — output missing '$substr'" printf '%s\n' "$out" | sed 's/^/ /' FAIL=$((FAIL + 1)); return fi echo "ok: $desc"; PASS=$((PASS + 1)) } BOX="$ROOT/bin/box" # --------------------------------------------------------------------------- # The CLI contract: dispatch, help, usage errors. No incus needed — these all # resolve before any daemon call. Exit codes are box's own (0 ok / 1 wrong / # 2 you-asked-wrong), read straight from bin/box and confirmed by running it. # --------------------------------------------------------------------------- # box with no args is 'help' (cmd="${1:-help}"), which prints the general usage # and exits 0 — NOT rig's exit-2 bare-usage. Assert box's actual contract. check "no args → general help, exit 0" 0 "USAGE" "$BOX" check "no args help names the command form" 0 "box " "$BOX" check "--help exits 0" 0 "USAGE" "$BOX" --help check "-h exits 0" 0 "USAGE" "$BOX" -h check "help exits 0" 0 "USAGE" "$BOX" help check "help → that command's usage" 0 "usage: box new" "$BOX" help new check "--version exits 0" 0 "box" "$BOX" --version # Unknown command is a usage error (2), and it says so — the suggester may add a # 'did you mean', but the stem is stable. check "unknown command exits 2" 2 "unknown command" "$BOX" frobnicate check "unknown command points at help" 2 "box help" "$BOX" zzzzzz # Options before the command are the classic mistake; box names the fix. check "option before command exits 2" 2 "options come after the command" "$BOX" --json list # A missing required positional is a usage error carrying that command's synopsis. check "new without --name exits 2" 2 "usage: box new" "$BOX" new check "shell without a box exits 2" 2 "usage: box shell" "$BOX" shell check "restore without arg2 needs a box first" 2 "usage: box restore" "$BOX" restore # An unknown flag is refused, not swallowed as a positional (the --labl bug). check "unknown flag on list exits 2" 2 "unknown option" "$BOX" list --nope # A flag that needs a value and gets none. check "--name with no value exits 2" 2 "--name needs a value" "$BOX" new --name # --------------------------------------------------------------------------- # A shim `id` on PATH: lets us drive install.sh's DEST branch with a canned uid + # group output, exactly the way rig drives assert_runner_repo against fixtures. # --------------------------------------------------------------------------- SHIMDIR="$(mktemp -d)" cat > "$SHIMDIR/id" <<'SHIM' #!/usr/bin/env bash # Fake `id`: -u prints $FAKE_UID, -nG prints $FAKE_GROUPS. Just enough for # install.sh's DEST branch, which only ever asks these two. case "${1:-}" in -u) printf '%s\n' "${FAKE_UID:-1000}" ;; -nG) printf '%s\n' "${FAKE_GROUPS:-}" ;; *) exit 0 ;; esac SHIM chmod +x "$SHIMDIR/id" # --------------------------------------------------------------------------- # install.sh — #71 global/root install. bash -n first, then drive the actual # DEST/BINDIR branch with the shim id (the functional proof the contract asks # for), then grep the root-only pieces that a daemon-free run cannot exercise. # --------------------------------------------------------------------------- check "install.sh is valid bash" 0 "" bash -n "$ROOT/install.sh" # Extract EXACTLY the DEST/BINDIR if/else/fi (the first `id -u -eq 0` block) and # print what it resolved — the same "run the pure block in isolation" trick rig # uses for its embedded dump script. Fail closed: a mangled extraction is caught # by the /opt/box grep below before any resolution is trusted. DBLOCK="$(mktemp)" awk '/id -u.*-eq 0/{f=1} f{print} f&&/^fi$/{exit}' "$ROOT/install.sh" > "$DBLOCK" # The $DEST/$BINDIR here are LITERAL text appended into the extracted block — they # must expand when that block RUNS, not when this printf writes it. Hence single # quotes; SC2016 is the intent. # shellcheck disable=SC2016 printf '\nprintf "DEST=%%s BINDIR=%%s\\n" "$DEST" "$BINDIR"\n' >> "$DBLOCK" check "install.sh: DEST block extracted (guards the awk)" 0 "/opt/box" cat "$DBLOCK" check "install.sh: the extracted DEST block is valid bash" 0 "" bash -n "$DBLOCK" dest() { # dest [extra env assignments...] — resolve DEST/BINDIR local uid="$1"; shift FAKE_UID="$uid" HOME=/home/tester PATH="$SHIMDIR:$PATH" env "$@" bash "$DBLOCK" } # Root: the global path — a system tree other users can read (#71). check "install.sh: root → DEST=/opt/box" 0 "DEST=/opt/box" dest 0 check "install.sh: root → BINDIR=/usr/local/bin" 0 "BINDIR=/usr/local/bin" dest 0 # Non-root: unchanged, the solo path. check "install.sh: non-root → DEST=\$HOME/.local" 0 "DEST=/home/tester/.local/share/box" dest 1000 check "install.sh: non-root → BINDIR=\$HOME/.local" 0 "BINDIR=/home/tester/.local/bin" dest 1000 # BOX_HOME / BOX_BIN still win on BOTH branches — the scripting override. check "install.sh: BOX_HOME overrides the root default" 0 "DEST=/srv/box" dest 0 BOX_HOME=/srv/box check "install.sh: BOX_BIN overrides the root default" 0 "BINDIR=/srv/bin" dest 0 BOX_BIN=/srv/bin check "install.sh: BOX_HOME overrides the non-root default" 0 "DEST=/srv/box" dest 1000 BOX_HOME=/srv/box rm -f "$DBLOCK" # The root-only world-readable chmod (#71): the tree is EXECUTED by other users, # so root must open read+traverse. Grep it, and that it is root-guarded so the # per-user install stays byte-identical to before. # $DEST is a LITERAL in the grep pattern (install.sh's own variable) — single # quotes intended. # shellcheck disable=SC2016 check "install.sh: root makes the tree world-readable (a+rX)" 0 "" \ grep -qF 'chmod -R a+rX "$DEST"' "$ROOT/install.sh" check "install.sh: the a+rX is root-guarded" 0 "" \ bash -c 'grep -B2 "chmod -R a+rX" "'"$ROOT"'/install.sh" | grep -q "id -u.*-eq 0"' # #66's flow, preserved: confirm-before-download, and no-op if already installed. check "install.sh: still confirms before downloading (#66)" 0 "" \ grep -qF 'confirm "Install box from' "$ROOT/install.sh" check "install.sh: still no-ops on an existing install (#66)" 0 "" \ grep -qF 'already installed' "$ROOT/install.sh" # --------------------------------------------------------------------------- # Templates — DYNAMIC over templates/*/ (#68): the loop discovers every # template directory, so a new template cannot ship without passing these (the # old hardcoded blank/claude/codex/grok list let exactly that happen). The # box.env parse is proven against the REAL allowlist: load_template is # extracted from bin/box and DRIVEN against each template — the same # source-the-pure-function trick install.sh's DEST block and box_tier get # below — so an unknown key, a missing BOX_IMAGE/BOX_USER, or a line that is # not KEY="value" fails HERE, not at mint time on a host. # --------------------------------------------------------------------------- TPLFN="$(mktemp)" awk '/^load_template\(\) \{/,/^\}/' "$ROOT/bin/box" > "$TPLFN" check "load_template: extracted from bin/box (guards the awk)" 0 "unknown key" cat "$TPLFN" check "load_template: the extracted function is valid bash" 0 "" bash -n "$TPLFN" # tpl