A tag is a moving pointer, and three things conspired to make the base immutable-in-practice while mutable-in-theory: image_hash() hashed the string "ubuntu:24.04" rather than what it resolves to, ensure_image() returns early when the tag exists, and podman build's pull policy is "missing" -- so even 'build --rebuild' rebuilt on the cached base. A Canonical patch simply never arrived, and nothing said so. Now the tag is resolved to a registry digest (skopeo, falling back to a pull unless AISB_BASE_PULL_FALLBACK=0), recorded per-ref under $AISB_CACHE_DIR/base/ at 0600, and used in FROM *and* in image_hash(): refreshing the base produces a new image instead of mutating one a sandbox may be running. Freshness is a TTL (AISB_BASE_TTL_DAYS=7, 0 every build, -1 never) plus an explicit 'update' / 'update --check'. Two rules keep it usable rather than merely strict. A stale pin beats a mutable tag: offline or with AISB_BASE_AUTO_UPDATE=0 the recorded digest is still used, and only a build with no digest at all is allowed -- announced as UNPINNED, labelled that way, warned about by doctor. And preview modes stay deterministic: --print-* never reach the network, inspect is the surface that asks the machine now. Both images carry aisb.base-digest / .node / .pi-package / .pi-version labels plus /etc/aisb/manifest.txt of resolved apt versions, which answers "which squid was enforcing this?" after the build log is gone. Tags are named after the base instead of the hardcoded u24- prefix. Bug found on the way: POL[base_digest]="$(base_pin ...)" silently produced unpinned labels, because an assignment inside a command substitution is discarded with the subshell. Helpers return text; callers assign. Also fixed: the manifest step used jq on npm's dependency objects as if they were strings, and a missing Node checksum used to skip verification quietly -- it now warns. 311 assertions pass (unit + e2e), shellcheck clean. LLM-Generated: true
94 lines
3.5 KiB
Bash
Executable File
94 lines
3.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# tests/run.sh -- the ai-sandbox test runner.
|
|
#
|
|
# ./tests/run.sh unit tests (no containers started, no network)
|
|
# ./tests/run.sh --e2e everything, including real containers and egress tests
|
|
# ./tests/run.sh --only 04 just the files matching 04
|
|
# ./tests/run.sh --verbose show each test file's raw output as it runs
|
|
#
|
|
# Every test file runs with its own XDG state so nothing here touches the real
|
|
# machine's config, state or cache directories.
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT="$(dirname "$HERE")"
|
|
AISB="${AISB:-$ROOT/bin/ai-sandbox}"
|
|
export AISB
|
|
|
|
WITH_E2E=0 ONLY="" VERBOSE=0
|
|
while (($#)); do
|
|
case "$1" in
|
|
--e2e|-e) WITH_E2E=1; shift ;;
|
|
--only) ONLY="${2:-}"; shift 2 ;;
|
|
--verbose|-v) export VERBOSE=1; shift ;; # lib.sh reads VERBOSE
|
|
--help|-h) sed -n '2,12p' "$0"; exit 0 ;;
|
|
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
command -v podman >/dev/null || { echo "podman is required to run the tests" >&2; exit 2; }
|
|
|
|
# Syntax-check every shell file in the project before running anything.
|
|
SYNTAX_BAD=0
|
|
for f in "$AISB" "$HERE"/*.sh "$HERE"/unit/*.sh "$HERE"/e2e/*.sh "$ROOT"/install.sh; do
|
|
[[ -f "$f" ]] || continue
|
|
if ! syn="$(bash -n "$f" 2>&1)"; then
|
|
echo "SYNTAX: $f" >&2; printf '%s\n' "$syn" | sed 's/^/ /' >&2; SYNTAX_BAD=1
|
|
fi
|
|
done
|
|
(( SYNTAX_BAD )) && exit 2
|
|
|
|
FILES=( "$HERE"/unit/*.sh )
|
|
(( WITH_E2E )) && FILES+=( "$HERE"/e2e/*.sh )
|
|
if [[ -n "$ONLY" ]]; then
|
|
mapfile -t FILES < <(printf '%s\n' "${FILES[@]}" | grep "/[^/]*$ONLY")
|
|
fi
|
|
|
|
BASE="$(mktemp -d "${TMPDIR:-/tmp}/ai-sandbox-tests.XXXXXX")"
|
|
COUNTS="$BASE/counts"
|
|
: > "$COUNTS"
|
|
trap 'chmod -R u+rwX "$BASE" 2>/dev/null; rm -rf "$BASE" 2>/dev/null' EXIT
|
|
|
|
echo "ai-sandbox test suite"
|
|
echo " script : $AISB"
|
|
echo " mode : $([[ $WITH_E2E == 1 ]] && echo 'unit + e2e (real containers)' || echo 'unit only (add --e2e for containers)')"
|
|
echo " files : ${#FILES[@]}"
|
|
echo
|
|
|
|
FAILED_FILES=()
|
|
for f in "${FILES[@]}"; do
|
|
name="$(basename "$f")"
|
|
W="$BASE/$name"; mkdir -p "$W"/{state,cache,cfg}
|
|
echo "=================================================================="
|
|
echo "== $name"
|
|
echo "=================================================================="
|
|
# shellcheck disable=SC1090
|
|
(
|
|
export AISB_STATE_DIR="$W/state" AISB_CACHE_DIR="$W/cache" AISB_CONFIG_DIR="$W/cfg"
|
|
export WORK="$W" TESTS="$HERE" AISB_TEST_COUNTS="$COUNTS"
|
|
# Unit files must not touch a registry: pins come from the seeded cache and the
|
|
# digest lookup is a stub. e2e files keep the real behaviour.
|
|
case "$f" in
|
|
*/unit/*) export AISB_BASE_AUTO_UPDATE="${AISB_BASE_AUTO_UPDATE:-0}" \
|
|
AISB_BASE_PULL_FALLBACK="${AISB_BASE_PULL_FALLBACK:-0}" ;;
|
|
esac
|
|
# NOTE: HOME is deliberately not overridden -- podman storage lives there.
|
|
unset AISB_ALLOW AISB_ENV AISB_MOUNT AISB_INSTALL AISB_NET AISB_EXTRA_ARGS AISB_SESSION_OVERRIDE
|
|
source "$f"
|
|
summary
|
|
) || FAILED_FILES+=("$name")
|
|
echo
|
|
done
|
|
|
|
TOTAL_P=0 TOTAL_F=0
|
|
while read -r p fl; do TOTAL_P=$((TOTAL_P + p)); TOTAL_F=$((TOTAL_F + fl)); done < "$COUNTS"
|
|
|
|
echo "=================================================================="
|
|
if ((${#FAILED_FILES[@]})); then
|
|
printf '\033[31mFAILED\033[0m %d passed, %d failed across %d file(s)\n' "$TOTAL_P" "$TOTAL_F" "${#FAILED_FILES[@]}"
|
|
printf ' failing files: %s\n' "${FAILED_FILES[*]}"
|
|
exit 1
|
|
fi
|
|
printf '\033[32mALL PASS\033[0m %d assertions, %d file(s), %s mode\n' \
|
|
"$TOTAL_P" "${#FILES[@]}" "$([[ $WITH_E2E == 1 ]] && echo unit+e2e || echo unit)"
|