#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" UPDATE_SCRIPT="$ROOT_DIR/ods-update.sh" fail() { echo "[FAIL] $*"; exit 1; } pass() { echo "[PASS] $*"; } command -v jq >/dev/null 2>&1 || fail "jq is required" command -v git >/dev/null 2>&1 || fail "git is required" [[ -f "$UPDATE_SCRIPT" ]] || fail "ods-update.sh not found" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT INSTALL_DIR="$TMP_DIR/ods" BIN_DIR="$TMP_DIR/bin" mkdir -p "$INSTALL_DIR/data" "$INSTALL_DIR/config/litellm" "$BIN_DIR" cp "$UPDATE_SCRIPT" "$INSTALL_DIR/ods-update.sh" chmod +x "$INSTALL_DIR/ods-update.sh" cat > "$INSTALL_DIR/.env" <<'EOF' ODS_MODE=local GPU_BACKEND=cpu GPU_COUNT=1 TIER=1 DASHBOARD_API_PORT=3002 OLLAMA_PORT=8080 EOF cat > "$INSTALL_DIR/.version" <<'EOF' {"version":"test-runtime"} EOF cat > "$INSTALL_DIR/docker-compose.base.yml" <<'EOF' services: dashboard-api: image: example/dashboard-api:test litellm: image: example/litellm:test EOF cat > "$INSTALL_DIR/docker-compose.cpu.yml" <<'EOF' services: llama-server: image: example/llama:test EOF printf '%s\n' '-f docker-compose.base.yml -f docker-compose.cpu.yml' > "$INSTALL_DIR/.compose-flags" DOCKER_LOG="$TMP_DIR/docker-args.log" export DOCKER_LOG cat > "$BIN_DIR/docker" <<'SH' #!/usr/bin/env bash printf '%s\n' "$*" >> "${DOCKER_LOG:?}" if [[ "${1:-}" == "info" ]]; then exit 0 fi if [[ "${1:-}" == "compose" && "${2:-}" == "version" ]]; then exit 0 fi if [[ "${1:-}" == "compose" ]]; then shift fi args=("$@") for ((i = 0; i < ${#args[@]}; i++)); do if [[ "${args[$i]}" == "ps" ]]; then next="${args[$((i + 1))]:-}" if [[ "$next" == "--services" ]]; then printf '%s\n' dashboard-api litellm exit 0 fi if [[ "$next" == "--format" ]]; then printf '%s\n' '{"State":"running"}' exit 0 fi fi done exit 0 SH chmod +x "$BIN_DIR/docker" cat > "$BIN_DIR/curl" <<'SH' #!/usr/bin/env bash if [[ "$*" == *"api.github.com/repos/"*"releases/latest"* ]]; then printf '%s\n' '{"tag_name":"v9.9.9"}' fi exit 0 SH chmod +x "$BIN_DIR/curl" PATH="$BIN_DIR:$PATH" bash "$INSTALL_DIR/ods-update.sh" health > "$TMP_DIR/health.out" 2>&1 \ || { cat "$TMP_DIR/health.out"; fail "health should pass with saved compose flags"; } grep -q "Service dashboard-api: running" "$TMP_DIR/health.out" \ || { cat "$TMP_DIR/health.out"; fail "health did not inspect dashboard-api"; } grep -q -- "-f docker-compose.base.yml -f docker-compose.cpu.yml ps --services" "$DOCKER_LOG" \ || { cat "$DOCKER_LOG"; fail "health did not pass saved compose flags to docker compose ps"; } if grep -q "No services defined in docker-compose" "$TMP_DIR/health.out"; then cat "$TMP_DIR/health.out" fail "health emitted stale bare-compose warning" fi pass "ods-update health uses saved compose flags in runtime installs" cat > "$INSTALL_DIR/.version" <<'EOF' {"version":"1.0.0"} EOF PATH="$BIN_DIR:$PATH" bash "$INSTALL_DIR/ods-update.sh" check > "$TMP_DIR/check.out" 2>&1 \ || { cat "$TMP_DIR/check.out"; fail "check should pass with mocked release API"; } grep -q "ods update" "$TMP_DIR/check.out" \ || { cat "$TMP_DIR/check.out"; fail "check did not recommend runtime update command"; } if grep -q "Run 'ods-update.sh update' to update" "$TMP_DIR/check.out"; then cat "$TMP_DIR/check.out" fail "check still recommends direct source updater for runtime installs" fi pass "ods-update check recommends runtime update command" SOURCE_BIN_DIR="$TMP_DIR/source-bin" SOURCE_PARENT="$TMP_DIR/source-parent" SOURCE_INSTALL="$SOURCE_PARENT/ods" mkdir -p "$SOURCE_BIN_DIR" "$SOURCE_INSTALL" cp "$UPDATE_SCRIPT" "$SOURCE_INSTALL/ods-update.sh" chmod +x "$SOURCE_INSTALL/ods-update.sh" cat > "$SOURCE_BIN_DIR/curl" <<'SH' #!/usr/bin/env bash if [[ "$*" == *"api.github.com/repos/"*"releases/latest"* ]]; then printf '%s\n' '{"tag_name":"v9.9.9"}' fi exit 0 SH chmod +x "$SOURCE_BIN_DIR/curl" git -C "$SOURCE_PARENT" init -q git -C "$SOURCE_PARENT" add ods/ods-update.sh PATH="$SOURCE_BIN_DIR:$PATH" bash "$SOURCE_INSTALL/ods-update.sh" check > "$TMP_DIR/source-check.out" 2>&1 \ || { cat "$TMP_DIR/source-check.out"; fail "check should pass in nested source checkout"; } grep -q "Source checkout detected" "$TMP_DIR/source-check.out" \ || { cat "$TMP_DIR/source-check.out"; fail "nested source checkout was not recognized"; } pass "ods-update recognizes nested source checkout layout" : > "$DOCKER_LOG" set +e PATH="$BIN_DIR:$PATH" bash "$INSTALL_DIR/ods-update.sh" update > "$TMP_DIR/update.out" 2>&1 update_exit=$? set -e [[ "$update_exit" -ne 0 ]] || fail "update without .git should fail" grep -q "git-backed ODS source checkout" "$TMP_DIR/update.out" \ || { cat "$TMP_DIR/update.out"; fail "missing non-git install diagnosis"; } grep -q "./ods-cli update" "$TMP_DIR/update.out" \ || { cat "$TMP_DIR/update.out"; fail "missing runtime update guidance"; } if find "$INSTALL_DIR/data/backups" -mindepth 1 -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null | grep -q .; then find "$INSTALL_DIR/data/backups" -mindepth 1 -maxdepth 1 -type d -name 'pre-update-*' fail "non-git update created a rollback snapshot before preflight" fi [[ ! -s "$DOCKER_LOG" ]] || { cat "$DOCKER_LOG"; fail "non-git update invoked Docker"; } pass "ods-update fails cleanly before mutating non-git runtime installs"