whiskers.git / tools / preflight.sh
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