lumbda/examples/cuda-fanout/smoke-bend-http.sh
russell@unturf.com 88e16c0ce2
bend: dual-port worker (8320 wire + 8321 http) — playground onramp
Each gpu-worker.lsp now listens on both wire-TCP (existing :8320) and
HTTP/1.1+CORS (new :8321), sharing one handle-request dispatcher. Lets
a tab on https://lumbda.com/playground/ POST to its own machine via
http://localhost:8321/ — browsers permit localhost from HTTPS origins
without TLS, so no proxy, no cert, no fox-owned infra required for the
decentralized run-your-own-bend story.

main() forks at startup: child runs http-run-loop on :8321, parent
keeps existing run-loop on :8320. Adding a new op-head to handle-request
exposes it over both transports automatically. Binary modes
(BSHK/BCGB/BSCP/BSRT/BSB3) stay wire-only — they exist for native
callers who already cache the binary locally; browser callers send
S-expression recipes the worker dispatches the same way.

Two latent defects fixed to make CPU-only and Python-tier hosts work:
- vram-used-mib now file-exists? guards /usr/bin/nvidia-smi. Python
  tier's spawn-process-stdio raises FileNotFoundError on missing
  binary, not returning #f as the prior code expected, which crashed
  every worker on a CPU-only laptop.
- fork-self return discriminated via (number? pid) not (eq? pid 0).
  Python tier's (eq? 0 #f) returns #t because == conflates int 0
  with bool False; pre-existing run-loop has the same risk but
  C/asm tier (identity eq?) masks it for the production case.

Phase 2 (server-side factory ops: compile uploaded .lsp recipes into
.bin before bending — the foxhop champion-circuit workflow) deferred
until authentication lands; today a worker on the public internet
would let any caller occupy our GPU.

Operational Caddy + DNS proposals in plans/bend-http-deploy.md cover
the personal-remote-access endpoint chain (proxy.unturf.com edge →
ai.foxhop.net Caddy → 3090-ai:8321) gated by trusted-IP allowlist —
applied separately.

Also codifies the playground "CSS Grid only, never flexbox" rule in
CLAUDE.md: all www/ and wasm/ stylesheets are already grid-only;
documenting the invariant so future edits don't drift.

Tests: smoke-bend-http.sh — (ping)→(ok pong), unknown-op fallback,
OPTIONS CORS preflight — all PASS. Wire path unchanged, verified
round-trip via 8-digit-prefix framing.
2026-06-14 17:32:46 -04:00

105 lines
3.7 KiB
Bash
Executable file

#!/bin/bash
# smoke-bend-http.sh — exercise gpu-worker.lsp's HTTP/8321 path.
#
# Boots gpu-worker on a test port pair (avoids prod 8320/8321), then:
# 1. curl -X POST '(ping)' → expect "(ok pong)"
# 2. curl -X POST '(echo "hello")' → expect "(echo \"hello\")"
# (current handle-request has no 'echo head, returns unknown-op —
# so we expect (error (unknown-op echo)) instead, which still
# round-trips the wire correctly)
# 3. curl -X OPTIONS preflight → expect 204 + CORS headers
#
# Memory-discipline pattern lifted from lumbda CLAUDE.md (asm heap
# never shrinks): kernel cap, trap cleanup, wall-clock timeout,
# explicit PID kill, straggler verify.
set -e
ulimit -v 524288 # 512 MB kernel cap — process gets SIGKILL at cap
# Test ports — high so they never collide with prod 8320/8321
WIRE_PORT=18320
HTTP_PORT=18321
# Pick tier: env override > default Python (always available, has GC)
LUMBDA="${LUMBDA:-$(cd "$(dirname "$0")/../.." && pwd)/lumbda.py}"
case "$LUMBDA" in
*.py) LUMBDA_RUN="python3 $LUMBDA" ;;
*) LUMBDA_RUN="$LUMBDA" ;;
esac
trap 'kill -9 $WORKER_PID 2>/dev/null || true;
pkill -9 -u "$USER" -f "gpu-worker.lsp --port $WIRE_PORT" 2>/dev/null || true' \
EXIT INT TERM
cd "$(dirname "$0")"
echo "=== smoke-bend-http (tier: $LUMBDA) ==="
# Boot worker — script-mode .lsp that loads gpu-worker.lsp + calls (main)
cat > /tmp/smoke-bend-http-boot.lsp <<EOF
(load "gpu-worker.lsp")
(main)
EOF
# Background the worker (foreground would block the test).
timeout 30 $LUMBDA_RUN /tmp/smoke-bend-http-boot.lsp \
--port $WIRE_PORT --http-port $HTTP_PORT > /tmp/smoke-bend-http-worker.log 2>&1 &
WORKER_PID=$!
# Wait up to 5s for HTTP port to come up
for i in 1 2 3 4 5 6 7 8 9 10; do
if exec 3<>/dev/tcp/127.0.0.1/$HTTP_PORT 2>/dev/null; then
exec 3<&-; exec 3>&-
break
fi
sleep 0.5
done
# ---- test 1: ping -----------------------------------------------
echo "[1/3] POST (ping) → expect (ok pong) ..."
PING_RESP=$(curl -sS -X POST -H 'Content-Type: text/plain' \
--data-binary '(ping)' "http://127.0.0.1:$HTTP_PORT/" --max-time 5)
echo " got: $PING_RESP"
if [ "$PING_RESP" != "(ok pong)" ]; then
echo " FAIL: expected (ok pong), got [$PING_RESP]"
echo " worker log:"
cat /tmp/smoke-bend-http-worker.log
exit 1
fi
echo " PASS"
# ---- test 2: unknown-op gracefully ------------------------------
echo "[2/3] POST (asdf 1 2) → expect (error (unknown-op asdf)) ..."
UNK_RESP=$(curl -sS -X POST -H 'Content-Type: text/plain' \
--data-binary '(asdf 1 2)' "http://127.0.0.1:$HTTP_PORT/" --max-time 5)
echo " got: $UNK_RESP"
case "$UNK_RESP" in
*unknown-op*) echo " PASS" ;;
*) echo " FAIL: expected unknown-op error, got [$UNK_RESP]"; exit 1 ;;
esac
# ---- test 3: OPTIONS preflight + CORS headers -------------------
echo "[3/3] OPTIONS / → expect 204 + CORS headers ..."
PREFLIGHT=$(curl -sSI -X OPTIONS "http://127.0.0.1:$HTTP_PORT/" --max-time 5)
echo "$PREFLIGHT" | head -5 | sed 's/^/ /'
case "$PREFLIGHT" in
*"204 No Content"*) ;;
*) echo " FAIL: expected 204, got [$PREFLIGHT]"; exit 1 ;;
esac
case "$PREFLIGHT" in
*"Access-Control-Allow-Origin"*) echo " PASS (CORS present)" ;;
*) echo " FAIL: missing CORS headers"; exit 1 ;;
esac
# Clean teardown
kill -9 $WORKER_PID 2>/dev/null || true
wait $WORKER_PID 2>/dev/null || true
# Straggler check — verify nothing left behind before reporting success
if pgrep -u "$USER" -f "gpu-worker.lsp --port $WIRE_PORT" >/dev/null; then
echo "STRAGGLER: gpu-worker on port $WIRE_PORT still running"
exit 1
fi
rm -f /tmp/smoke-bend-http-boot.lsp /tmp/smoke-bend-http-worker.log
echo "=== smoke-bend-http: PASS ==="