1#!/usr/bin/env bash 2# Check that everything the app talks to is up and behaves, before a demo. 3# 4# tools/preflight.sh <host:port> (or set WHISKERS_SERVICE; there is no default) 5# 6# Prints OK / WARN / FAIL per check and exits non-zero on any FAIL. WARN means the demo 7# will work but degrade (for example the natural voice falls back to the phone's own). 8set -u 9target="${1:-${WHISKERS_SERVICE:-}}" 10[ -n "$target" ] || { echo "usage: tools/preflight.sh <host:port> (or set WHISKERS_SERVICE)" >&2; exit 2; } 11base="http://$target" 12fail=0 13ok() { echo "OK $*"; } 14warn() { echo "WARN $*"; } 15bad() { echo "FAIL $*"; fail=1; } 16 17post() { curl -s -m "${3:-30}" -X POST "$base$1" -H 'content-type: application/json' -d "$2" -w '\n%{http_code}'; } 18 19# 0. Nothing private in the tracked files (a no-op without .private-terms). 20"$(dirname "$0")/check-private.sh" && ok "no private terms in tracked files" || bad "tools/check-private.sh found private terms" 21 22# 1. The guard, both ways round. 23r=$(post /check '{"direction":"FromChild","age":6,"text":"I like my bunny Biscuit."}' 20) 24[[ "$r" == *'"Allow"'* && "$r" == *200 ]] && ok "/check allows an ordinary sentence" || bad "/check on an ordinary sentence: $r" 25r=$(post /check '{"direction":"FromChild","age":6,"text":"My tummy hurts and nobody is home and I am scared."}' 20) 26[[ "$r" == *NeedsAGrownUp* ]] && ok "/check sends an unsafe sentence to a grown-up" || bad "/check on an unsafe sentence: $r" 27 28# 2. The model, through the narrow proxy, with the real persona's shape. 29body='{"model":"claude-haiku-4-5-20251001","max_tokens":120,"system":"You are Whiskers, a friendly robot cat talking to a young child. Reply in one short sentence.","messages":[{"role":"user","content":"Hi! Do you like carrots?"}]}' 30r=$(post /v1/messages "$body" 60) 31[[ "$r" == *'"type":"text"'* && "$r" == *200 ]] && ok "/v1/messages answers (Claude subscription via the gateway)" || bad "/v1/messages: ${r:0:200}" 32r=$(post /v1/messages '{"model":"gpt-4","max_tokens":10,"messages":[]}' 10) 33[[ "$r" == *400 ]] && ok "/v1/messages refuses any other model" || bad "/v1/messages should refuse another model: $r" 34 35# 3. The natural voice. This spends the household's daily voice allowance (a few characters), so it 36# only runs when asked: WHISKERS_PREFLIGHT_SPEAK=1. 37if [ "${WHISKERS_PREFLIGHT_SPEAK:-0}" = 1 ]; then 38 r=$(post /speak '{"text":"Hello there!"}' 30) 39 code="${r##*$'\n'}" 40 case "$code" in 41 200) ok "/speak returns audio (ElevenLabs)" ;; 42 503) warn "/speak: ElevenLabs is not configured (ELEVENLABS_API_KEY / ELEVENLABS_VOICE_ID); the phone will use its own voice" ;; 43 429) warn "/speak: today's character allowance is spent; the phone will use its own voice" ;; 44 *) warn "/speak answered $code: ${r:0:160}; the phone will use its own voice" ;; 45 esac 46else 47 warn "/speak not checked (it spends the voice allowance); WHISKERS_PREFLIGHT_SPEAK=1 to check it" 48fi 49 50[ $fail = 0 ] && echo "ready" || echo "NOT ready" 51exit $fail