From 532f2c63fa5e326e79140f1516ae6297e5d5c9fa Mon Sep 17 00:00:00 2001 From: BeLazy167 Date: Thu, 20 Aug 2026 16:07:53 -0500 Subject: [PATCH 1/2] Fix the silent exit right after 'key accepted' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first real install died with no message the moment the key was accepted. Two causes, found by reproducing it: - install.sh runs under `set -euo pipefail`, and the JSON extractors are grep pipelines. A grep that matches nothing makes the pipeline non-zero, so an absent field — a normal outcome, e.g. an error body with no `name` — killed the whole script via set -e, silently. The extractors now disable pipefail in their own subshell and return empty instead. - The /organizations/current lookup lost its X-Organization-Id header in an earlier refactor, so it returned an error body and handed the extractor exactly that absent field. tests/extractors.sh pins the fix by pulling the real function definitions out of install.sh (not a copy that can drift) and calling them on bodies missing the field, under the same shell options. Wired into CI on both platforms. --- install.sh | 37 ++++++++++++++++++++++-------- qbraid-code | 3 ++- tests/extractors.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 10 deletions(-) create mode 100755 tests/extractors.sh diff --git a/install.sh b/install.sh index 53f5a72..2b8ec20 100755 --- a/install.sh +++ b/install.sh @@ -158,8 +158,22 @@ fi # ------------------------------------------------------------- 3. credential -json_str() { # json_str - printf '%s' "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//; s/\"$//" +# `set -o pipefail` is on, so a grep that matches NOTHING makes the whole +# pipeline non-zero, and `set -e` then kills the installer with no message at +# all. An absent field is a normal outcome here (an error body has no `name`), +# not an error, so pipefail is disabled inside the extracting subshell. +json_str() { # json_str -> the value, or empty + ( + set +o pipefail + printf '%s' "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//; s/\"$//" + ) +} + +json_num() { # json_num -> the value, or empty + ( + set +o pipefail + printf '%s' "$1" | grep -o "\"$2\":-\?[0-9.]*" | head -1 | sed 's/.*://' + ) } HTTP_STATUS="" @@ -167,12 +181,17 @@ API_BODY="" # Sets API_BODY and HTTP_STATUS in the CALLER (000 = could not connect). # It must not print the body: `x=$(api_get ...)` would run this in a subshell # and the status would never make it back, so every failure read as "rejected". -api_get() { # api_get - local url="$1" key="$2" tmp +api_get() { # api_get [org-id] + local url="$1" key="$2" org="${3:-}" tmp API_BODY="" tmp=$(mktemp) || { HTTP_STATUS="000"; return 0; } - HTTP_STATUS=$(curl -sS -m 25 -o "$tmp" -w '%{http_code}' "$url" -H "X-API-Key: $key" 2>/dev/null) \ - || HTTP_STATUS="000" + if [ -n "$org" ]; then + HTTP_STATUS=$(curl -sS -m 25 -o "$tmp" -w '%{http_code}' "$url" \ + -H "X-API-Key: $key" -H "X-Organization-Id: $org" 2>/dev/null) || HTTP_STATUS="000" + else + HTTP_STATUS=$(curl -sS -m 25 -o "$tmp" -w '%{http_code}' "$url" \ + -H "X-API-Key: $key" 2>/dev/null) || HTTP_STATUS="000" + fi API_BODY=$(cat "$tmp") rm -f "$tmp" } @@ -265,7 +284,7 @@ esac # ------------------------------------------------------- 4. organization check ORG_ID=$(json_str "$BALANCE" organizationId) -CREDITS_RAW=$(printf '%s' "$BALANCE" | grep -o '"qbraidCredits":-\?[0-9.]*' | head -1 | sed 's/.*://') +CREDITS_RAW=$(json_num "$BALANCE" qbraidCredits) if [ -n "$CREDITS_RAW" ]; then CREDITS=$(awk -v c="$CREDITS_RAW" 'BEGIN { printf "%.0f", c }' 2>/dev/null) || CREDITS="$CREDITS_RAW" else @@ -279,7 +298,7 @@ fi # alongside so a bad parse cannot quietly point someone at the wrong org. ORG_NAME="" if [ -n "$ORG_ID" ]; then - api_get "$API_BASE/organizations/current" "$API_KEY" + api_get "$API_BASE/organizations/current" "$API_KEY" "$ORG_ID" ORG_DATA=$(printf '%s' "$API_BODY" | sed 's/.*"data"[[:space:]]*:[[:space:]]*{//') ORG_NAME=$(json_str "$ORG_DATA" name) fi @@ -314,7 +333,7 @@ MODEL="${QBRAID_CODE_MODEL:-}" if [ -z "$MODEL" ]; then # The list is fetched live so new gateway models appear without a release here. api_get "$GATEWAY_URL/v1/models" "$API_KEY" - MODEL_IDS=$(printf '%s' "$API_BODY" | grep -o '"id":"[^"]*"' | sed 's/"id":"//; s/"$//') + MODEL_IDS=$(set +o pipefail; printf '%s' "$API_BODY" | grep -o '"id":"[^"]*"' | sed 's/"id":"//; s/"$//') if [ -z "$MODEL_IDS" ]; then warn "could not list models — defaulting to claude-sonnet-4-6" MODEL="claude-sonnet-4-6" diff --git a/qbraid-code b/qbraid-code index 649c5a6..7df53d0 100755 --- a/qbraid-code +++ b/qbraid-code @@ -55,7 +55,8 @@ case "${1:-}" in BODY=$(cat "$TMP"); rm -f "$TMP" case "$STATUS" in 200) - RAW=$(printf '%s' "$BODY" | grep -o '"qbraidCredits":-\?[0-9.]*' | head -1 | sed 's/.*://') + # pipefail + set -e would kill --doctor silently if the field is absent. + RAW=$(set +o pipefail; printf '%s' "$BODY" | grep -o '"qbraidCredits":-\?[0-9.]*' | head -1 | sed 's/.*://') CREDITS=$(awk -v c="$RAW" 'BEGIN { printf "%.0f", c }' 2>/dev/null) || CREDITS="$RAW" echo "key: valid" echo "credits: ${CREDITS:-unknown}" diff --git a/tests/extractors.sh b/tests/extractors.sh new file mode 100755 index 0000000..b6719f0 --- /dev/null +++ b/tests/extractors.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Regression test for the silent-exit bug found by the first real install. +# +# install.sh runs under `set -euo pipefail`. Its JSON extractors are grep +# pipelines, and a grep that matches NOTHING makes the pipeline non-zero — +# so an absent field (a normal outcome, e.g. an error body with no `name`) +# killed the installer with no message at all, right after "key accepted". +# +# This test extracts the REAL function definitions from install.sh — not a +# copy that could drift — and calls them on bodies missing the field, under +# the same shell options install.sh uses. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 + +pass=0; fail=0 + +# Pull each function's source out of install.sh by its opening line. +extract_fn() { # extract_fn + awk -v fn="$1" ' + $0 ~ "^"fn"\\(\\) \\{" { inside = 1 } + inside { print } + inside && /^\}$/ { exit } + ' install.sh +} + +JSON_STR_SRC=$(extract_fn json_str) +JSON_NUM_SRC=$(extract_fn json_num) +if [ -z "$JSON_STR_SRC" ] || [ -z "$JSON_NUM_SRC" ]; then + echo " FAIL could not extract json_str/json_num from install.sh" + exit 1 +fi + +check() { # check