Files
club-3090/scripts/quality-test.sh
noonghunna 83bf73d3ec feat(quality-test): auto-set BENCHLOCAL_HERMES_RESOLVE_LOCALHOST=1 for localhost URLs
When the user runs quality-test.sh with a localhost-style URL
(default `http://localhost:8020`, or any `localhost`/`127.x`/`[::1]`
variant), auto-export `BENCHLOCAL_HERMES_RESOLVE_LOCALHOST=1` so
benchlocal-cli rewrites the hermes-agent's outbound model endpoint
from `localhost:<port>` to `host.docker.internal:<port>` inside the
Docker sandbox container.

Without this, the hermes-agent inside the sandbox can't reach the
host's vLLM (localhost resolves to the container itself) and every
scenario fails with `"API call failed after 3 retries: Connection
error."` — produced spurious 0/20 grades on this rig prior to the
benchlocal-cli runner.py 9c1566f fix.

Skips the auto-set when:
  - User already set the env var (explicit override)
  - URL points at a non-loopback host (real LAN IP, k8s service name,
    host.docker.internal already) — no rewrite needed

Emits a stderr breadcrumb when the auto-set fires so users can see
what changed.
2026-05-10 20:29:49 +00:00

267 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Quality-test wrapper around `benchlocal-cli` — measures behavioral quality
# (tool-call correctness, instruction-following, structured output, etc) of
# the running compose. Sits in the test pipeline between bench.sh and
# soak-test.sh:
#
# verify.sh — fast smoke ("does it serve")
# verify-full.sh — functional ("does everything work")
# verify-stress.sh — boundary ("does it survive stress")
# bench.sh — throughput ("what's the TPS")
# quality-test.sh — behavioral ("does it produce useful output") ← THIS
# soak-test.sh — stability ("does it stay healthy over time")
#
# Catches what the operational tests miss: a compose can pass all 5 layers
# of operational testing and still ship with degraded tool-call accuracy or
# instruction-follow drift from quantization or Genesis env-var changes.
#
# Reference: docs/QUALITY_TEST.md
#
# Prereq: benchlocal-cli installed (see "Install" below) + a running compose.
set -euo pipefail
# ---- usage / help ------------------------------------------------------------
usage() {
cat <<'EOF'
quality-test.sh — behavioral quality bench against a running compose
USAGE
bash scripts/quality-test.sh [MODE | --pack PACK_ID] [OPTIONS]
MODES
--quick 2 packs: toolcall-15, instructfollow-15
~5-10 min, no Docker required
--medium 5 packs: + structoutput-15, dataextract-15, reasonmath-15 (DEFAULT)
~15-25 min, no Docker required
--full 8 packs: + bugfind-15, hermesagent-20, cli-40
~25-40 min, requires Docker (auto-starts sandbox containers)
--pack PACK_ID Run a single pack (overrides mode flag).
Available IDs:
toolcall-15 instructfollow-15 structoutput-15
dataextract-15 reasonmath-15
bugfind-15 cli-40 hermesagent-20 (require Docker)
OPTIONS
-h, --help Show this help and exit
--list-packs List available packs and exit
--no-sandboxed On --full, skip the Docker sandbox packs (= --medium scope)
--sandboxed-only Run only the 3 sandbox packs (bugfind-15, cli-40, hermesagent-20).
Skips the deterministic packs — useful when iterating on
sandbox verifiers without paying the deterministic-pack cost.
ENV VARS
URL Endpoint base URL (default: auto-detected via preflight,
falls back to http://localhost:8020)
MODEL Served model name (default: auto-detected from /v1/models;
override only if you have a non-standard served-model-name)
TIMEOUT_PER_CASE Per-scenario HTTP timeout in seconds (default: 60)
EXAMPLES
bash scripts/quality-test.sh # --medium against running compose
bash scripts/quality-test.sh --quick # quicker, 2 packs only
bash scripts/quality-test.sh --full # everything, needs Docker
bash scripts/quality-test.sh --pack toolcall-15 # just the tool-call pack
URL=http://localhost:8030 bash scripts/quality-test.sh # against a different port
INSTALL benchlocal-cli (one-time)
pip install git+https://github.com/noonghunna/benchlocal-cli.git
# OR for development from source:
pip install -e /path/to/benchlocal-cli
OUTPUT
- Markdown table to stdout (paste-ready for BENCHMARKS quality rows)
- JSON blob to results/quality/quality-<timestamp>.json (full detail)
- Compact one-liner for the compose `Quality:` profile field
EOF
}
# ---- preamble ----------------------------------------------------------------
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ -f "${ROOT_DIR}/scripts/preflight.sh" ]]; then
# shellcheck source=preflight.sh
source "${ROOT_DIR}/scripts/preflight.sh"
preflight_autodetect_endpoint
fi
URL="${URL:-http://localhost:8020}"
MODEL="${MODEL:-qwen3.6-27b-autoround}"
TIMEOUT_PER_CASE="${TIMEOUT_PER_CASE:-60}"
# ---- arg parsing -------------------------------------------------------------
MODE="--medium" # default
PACK=""
NO_SANDBOX=0
SANDBOXED_ONLY=0
LIST_PACKS=0
while [[ $# -gt 0 ]]; do
case "$1" in
--quick|--medium|--full)
MODE="$1"
shift
;;
--pack)
PACK="${2:-}"
if [[ -z "$PACK" ]]; then
echo "✗ --pack requires a pack id" >&2
exit 2
fi
shift 2
;;
--no-sandboxed)
NO_SANDBOX=1
shift
;;
--sandboxed-only)
SANDBOXED_ONLY=1
shift
;;
--list-packs)
LIST_PACKS=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "✗ unknown argument: $1" >&2
echo " run 'bash scripts/quality-test.sh --help' for usage." >&2
exit 2
;;
esac
done
# ---- prerequisite checks -----------------------------------------------------
if ! command -v benchlocal-cli >/dev/null 2>&1; then
cat >&2 <<EOF
✗ benchlocal-cli not found on \$PATH
Install it (one-time):
pip install git+https://github.com/noonghunna/benchlocal-cli.git
Or from a local checkout:
pip install -e /path/to/benchlocal-cli
See docs/QUALITY_TEST.md for full setup.
EOF
exit 127
fi
if [[ "$LIST_PACKS" == "1" ]]; then
benchlocal-cli list
exit 0
fi
if ! curl -sf -m 5 "${URL}/v1/models" >/dev/null 2>&1; then
echo "✗ endpoint ${URL}/v1/models not responding" >&2
echo " bring up a compose first: bash scripts/launch.sh" >&2
exit 1
fi
# Resolve actual served model id from the running endpoint, in case MODEL was wrong
DETECTED_MODEL=$(curl -sf -m 5 "${URL}/v1/models" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data'][0]['id'])" 2>/dev/null \
|| echo "")
if [[ -n "$DETECTED_MODEL" && "$DETECTED_MODEL" != "$MODEL" ]]; then
echo "[quality-test] model id from endpoint: ${DETECTED_MODEL} (overriding MODEL=${MODEL})" >&2
MODEL="$DETECTED_MODEL"
fi
# hermesagent-20 runs its agent inside a Docker sandbox container. Localhost-style
# URLs (localhost/127.x/[::1]) inside the container resolve to the container itself,
# not the host's vLLM. Auto-set BENCHLOCAL_HERMES_RESOLVE_LOCALHOST=1 so benchlocal-cli
# (a) adds --add-host=host.docker.internal:host-gateway to the sandbox container, and
# (b) rewrites the model endpoint URL to use host.docker.internal:<port> for the
# hermes-agent's outbound API calls. Skip if already set (user override) or if URL
# already uses host.docker.internal / a non-loopback host (real LAN IP, k8s service).
if [[ -z "${BENCHLOCAL_HERMES_RESOLVE_LOCALHOST:-}" ]] \
&& [[ "$URL" =~ ^https?://(localhost|127\.|\[::1\]) ]]; then
export BENCHLOCAL_HERMES_RESOLVE_LOCALHOST=1
echo "[quality-test] localhost URL detected — auto-set BENCHLOCAL_HERMES_RESOLVE_LOCALHOST=1 for hermes sandbox endpoint rewrite" >&2
fi
# ---- run benchlocal-cli ------------------------------------------------------
RESULTS_DIR="${ROOT_DIR}/results/quality"
mkdir -p "$RESULTS_DIR"
TS=$(date +%Y-%m-%dT%H-%M-%S)
JSON_OUT="${RESULTS_DIR}/quality-${TS}.json"
if [[ -n "$PACK" ]]; then
echo "[quality-test] pack=${PACK} endpoint=${URL} model=${MODEL} timeout=${TIMEOUT_PER_CASE}s"
else
echo "[quality-test] mode=${MODE} endpoint=${URL} model=${MODEL} timeout=${TIMEOUT_PER_CASE}s"
fi
echo "[quality-test] results JSON → ${JSON_OUT}"
echo
# Build CLI args
CLI_ARGS=(
run
--endpoint "${URL}"
--model "${MODEL}"
--timeout-per-case "${TIMEOUT_PER_CASE}"
--output markdown
--save-json "${JSON_OUT}"
)
if [[ "$SANDBOXED_ONLY" == "1" ]]; then
CLI_ARGS+=(--sandboxed-only)
elif [[ -n "$PACK" ]]; then
CLI_ARGS+=(--pack "$PACK")
else
CLI_ARGS+=("$MODE")
fi
if [[ "$NO_SANDBOX" == "1" && "$SANDBOXED_ONLY" != "1" ]]; then
CLI_ARGS+=(--no-sandboxed-packs)
fi
# Run; capture exit code so we can also try to emit the compact one-liner
benchlocal-cli "${CLI_ARGS[@]}" || RC=$?
RC="${RC:-0}"
# ---- emit compact one-liner suitable for compose Quality: schema field -------
if [[ -f "$JSON_OUT" ]]; then
echo
echo "=========================================================================="
echo "Quality: line for compose schema field (paste into compose YAML header):"
echo "=========================================================================="
python3 - "$JSON_OUT" "${PACK:-$MODE}" <<'PYEOF'
import json, sys, datetime
path, mode = sys.argv[1], sys.argv[2]
with open(path) as f:
d = json.load(f)
date = datetime.date.today().isoformat()
mode_short = mode.lstrip("-")
parts = []
for p in d.get("packs", []):
if p.get("status") == "stubbed" and p.get("total", 0) == 0:
continue
pid = p["pack_id"]
pa = p["passed"]
pt = p["total"]
pct = round(100 * p["score"]) if pt else 0
parts.append(f"{pid} {pa}/{pt} ({pct}%)")
suffix = f" (--{mode_short}, {date})"
if parts:
print("Quality: " + " · ".join(parts) + suffix)
else:
print("Quality: (no scoreable packs ran)")
PYEOF
fi
echo
exit "$RC"