Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
run: brew install shellcheck

- name: shellcheck
run: shellcheck -S warning install.sh qbraid-code statusline.sh tests/statusline.sh
run: shellcheck -S warning install.sh qbraid-code statusline.sh tests/statusline.sh tests/extractors.sh

- name: syntax
run: |
Expand All @@ -29,6 +29,9 @@ jobs:
- name: statusline tests
run: tests/statusline.sh

- name: extractor tests
run: tests/extractors.sh

- name: installer --help does not touch the machine
run: bash install.sh --help

Expand Down
37 changes: 28 additions & 9 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,40 @@ fi

# ------------------------------------------------------------- 3. credential

json_str() { # json_str <blob> <key>
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 <blob> <key> -> the value, or empty
(
set +o pipefail
printf '%s' "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//; s/\"$//"
)
}

json_num() { # json_num <blob> <key> -> the value, or empty
(
set +o pipefail
printf '%s' "$1" | grep -o "\"$2\":-\?[0-9.]*" | head -1 | sed 's/.*://'
)
}

HTTP_STATUS=""
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 <url> <key>
local url="$1" key="$2" tmp
api_get() { # api_get <url> <key> [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"
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion qbraid-code
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
56 changes: 56 additions & 0 deletions tests/extractors.sh
Original file line number Diff line number Diff line change
@@ -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 <name>
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 <name> <script>
if bash -c "set -euo pipefail; $JSON_STR_SRC; $JSON_NUM_SRC; $2" >/dev/null 2>&1; then
pass=$((pass + 1)); printf ' ok %s\n' "$1"
else
fail=$((fail + 1)); printf ' FAIL %s (extractor killed a set -e shell)\n' "$1"
fi
}

ERR='{"success":false,"message":"Organization context required"}'
OKB='{"data":{"name":"qBraid","qbraidCredits":42.5,"organizationId":"abc"}}'

check "json_str on a missing field survives set -e" \
"x=\$(json_str '$ERR' name); [ -z \"\$x\" ]"
check "json_num on a missing field survives set -e" \
"x=\$(json_num '$ERR' qbraidCredits); [ -z \"\$x\" ]"
check "json_str still extracts a present field" \
"x=\$(json_str '$OKB' name); [ \"\$x\" = qBraid ]"
check "json_num still extracts a present number" \
"x=\$(json_num '$OKB' qbraidCredits); [ \"\$x\" = 42.5 ]"
check "json_str on an empty body survives set -e" \
"x=\$(json_str '' name); [ -z \"\$x\" ]"

printf '\n%d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
Loading