Phase 1 Step 2 of architecture roadmap. The OCaml HTTP server is now generic — all sx_docs-specific values (layout components, path prefix, title, warmup paths, handler prefixes, CSS/JS, client libs) move into sx/sx/app-config.sx as a __app-config dict. Server reads config at startup with hardcoded defaults as fallback, so it works with no config, partial config, or full config. Removed: 9 demo data stubs, stepper cookie cache logic, page-functions.sx directory heuristic. Added: 29-test server config test suite covering standard, custom, no-config, and minimal-config scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
256 lines
8.3 KiB
Bash
Executable File
256 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Test suite for SX HTTP server app config handling.
|
|
# Starts the server with different __app-config values and verifies behavior.
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
PROJECT_DIR="$(pwd)"
|
|
SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
|
|
BASE_PORT=8090
|
|
PASS=0
|
|
FAIL=0
|
|
ERRORS=""
|
|
SERVER_PID=""
|
|
CURRENT_PORT=""
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
TMP_DIR=$(mktemp -d)
|
|
|
|
cleanup() {
|
|
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
kill "$SERVER_PID" 2>/dev/null
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
fi
|
|
rm -rf "$TMP_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
start_server() {
|
|
local port="$1"
|
|
local extra_env="${2:-}"
|
|
CURRENT_PORT="$port"
|
|
# Kill anything on this port
|
|
kill $(lsof -ti :"$port") 2>/dev/null || true
|
|
sleep 1
|
|
# Start server
|
|
eval "$extra_env SX_PROJECT_DIR=$PROJECT_DIR $SERVER --http $port" \
|
|
> "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr" &
|
|
SERVER_PID=$!
|
|
# Wait for "Listening" message
|
|
local tries=0
|
|
while ! grep -q "Listening" "$TMP_DIR/stderr" 2>/dev/null; do
|
|
sleep 1
|
|
tries=$((tries + 1))
|
|
if [ $tries -gt 120 ]; then
|
|
echo "TIMEOUT waiting for server on port $port"
|
|
cat "$TMP_DIR/stderr"
|
|
return 1
|
|
fi
|
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
echo "Server died during startup on port $port"
|
|
cat "$TMP_DIR/stderr"
|
|
return 1
|
|
fi
|
|
done
|
|
echo " (server ready on port $port)"
|
|
}
|
|
|
|
stop_server() {
|
|
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
kill "$SERVER_PID" 2>/dev/null
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
SERVER_PID=""
|
|
}
|
|
|
|
url() { echo "http://localhost:$CURRENT_PORT$1"; }
|
|
|
|
assert_status() {
|
|
local desc="$1" path="$2" expected="$3"
|
|
shift 3
|
|
local actual
|
|
actual=$(curl -s -o /dev/null -w "%{http_code}" "$@" "$(url "$path")" 2>/dev/null)
|
|
if [ "$actual" = "$expected" ]; then
|
|
PASS=$((PASS + 1))
|
|
echo " PASS: $desc"
|
|
else
|
|
FAIL=$((FAIL + 1))
|
|
ERRORS="$ERRORS\n FAIL: $desc — expected $expected, got $actual"
|
|
echo " FAIL: $desc — expected $expected, got $actual"
|
|
fi
|
|
}
|
|
|
|
assert_redirect() {
|
|
local desc="$1" path="$2" expected_path="$3"
|
|
local actual
|
|
actual=$(curl -s -o /dev/null -w "%{redirect_url}" "$(url "$path")" 2>/dev/null)
|
|
local expected="http://localhost:$CURRENT_PORT$expected_path"
|
|
if [ "$actual" = "$expected" ]; then
|
|
PASS=$((PASS + 1))
|
|
echo " PASS: $desc"
|
|
else
|
|
FAIL=$((FAIL + 1))
|
|
ERRORS="$ERRORS\n FAIL: $desc — expected → $expected, got → $actual"
|
|
echo " FAIL: $desc — expected → $expected_path, got → $actual"
|
|
fi
|
|
}
|
|
|
|
assert_body_contains() {
|
|
local desc="$1" path="$2" expected="$3"
|
|
shift 3
|
|
curl -s "$@" "$(url "$path")" > "$TMP_DIR/body" 2>/dev/null
|
|
if grep -qF "$expected" "$TMP_DIR/body"; then
|
|
PASS=$((PASS + 1))
|
|
echo " PASS: $desc"
|
|
else
|
|
FAIL=$((FAIL + 1))
|
|
local size=$(wc -c < "$TMP_DIR/body")
|
|
ERRORS="$ERRORS\n FAIL: $desc — body ($size bytes) missing '$expected'"
|
|
echo " FAIL: $desc — body ($size bytes) missing '$expected'"
|
|
fi
|
|
}
|
|
|
|
assert_stderr_contains() {
|
|
local desc="$1" expected="$2"
|
|
if grep -qF "$expected" "$TMP_DIR/stderr" 2>/dev/null; then
|
|
PASS=$((PASS + 1))
|
|
echo " PASS: $desc"
|
|
else
|
|
FAIL=$((FAIL + 1))
|
|
ERRORS="$ERRORS\n FAIL: $desc — stderr missing '$expected'"
|
|
echo " FAIL: $desc — stderr missing '$expected'"
|
|
fi
|
|
}
|
|
|
|
assert_stderr_not_contains() {
|
|
local desc="$1" expected="$2"
|
|
if grep -qF "$expected" "$TMP_DIR/stderr" 2>/dev/null; then
|
|
FAIL=$((FAIL + 1))
|
|
ERRORS="$ERRORS\n FAIL: $desc — stderr should NOT contain '$expected'"
|
|
echo " FAIL: $desc — stderr should NOT contain '$expected'"
|
|
else
|
|
PASS=$((PASS + 1))
|
|
echo " PASS: $desc"
|
|
fi
|
|
}
|
|
|
|
# ── Setup ────────────────────────────────────────────────────────────
|
|
|
|
if [ ! -x "$SERVER" ]; then
|
|
echo "ERROR: Server binary not found at $SERVER"
|
|
echo "Run: cd hosts/ocaml && eval \$(opam env) && dune build"
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== SX HTTP Server Config Tests ==="
|
|
echo ""
|
|
|
|
# ── Test Group 1: Default config (standard app-config.sx) ───────────
|
|
|
|
echo "── Group 1: Standard config ──"
|
|
start_server $((BASE_PORT + 1))
|
|
|
|
assert_stderr_contains "config loaded" "App config loaded: title=SX prefix=/sx/"
|
|
assert_stderr_contains "warmup ran 9 pages" "Pre-warmed 9 pages"
|
|
assert_stderr_not_contains "no stepper cookie refs" "sx-home-stepper"
|
|
|
|
assert_status "homepage 200" "/sx/" 200
|
|
assert_status "AJAX homepage 200" "/sx/" 200 -H "sx-request: true"
|
|
assert_status "geography 200" "/sx/(geography)" 200
|
|
assert_status "language 200" "/sx/(language)" 200
|
|
assert_status "applications 200" "/sx/(applications)" 200
|
|
assert_redirect "root → /sx/" "/" "/sx/"
|
|
assert_status "handler 200" "/sx/(api.spec-detail)?name=render-to-html" 200
|
|
assert_status "static 200" "/static/styles/tw.css" 200
|
|
assert_status "debug 200" "/sx/_debug/eval?expr=(%2B%201%202)" 200
|
|
assert_status "unknown 404" "/nope" 404
|
|
|
|
assert_body_contains "title is SX" "/sx/" "<title>SX</title>"
|
|
assert_body_contains "AJAX returns SX wire format" "/sx/" "sx-swap-oob" -H "sx-request: true"
|
|
assert_body_contains "debug eval result" "/sx/_debug/eval?expr=(%2B%201%202)" "3"
|
|
|
|
stop_server
|
|
echo ""
|
|
|
|
# ── Test Group 2: Custom title + minimal warmup ─────────────────────
|
|
|
|
echo "── Group 2: Custom title ──"
|
|
mkdir -p "$TMP_DIR/custom-sx"
|
|
cp -r "$PROJECT_DIR/sx/sx/"* "$TMP_DIR/custom-sx/"
|
|
cat > "$TMP_DIR/custom-sx/app-config.sx" << 'SXEOF'
|
|
(define __app-config
|
|
{:title "My Custom App"
|
|
:path-prefix "/sx/"
|
|
:home-path "/sx/"
|
|
:inner-layout "~layouts/doc"
|
|
:outer-layout "~shared:layout/app-body"
|
|
:shell "~shared:shell/sx-page-shell"
|
|
:client-libs (list "tw-layout.sx" "tw-type.sx" "tw.sx")
|
|
:css-files (list "basics.css" "tw.css")
|
|
:batchable-helpers (list "highlight" "component-source")
|
|
:handler-prefixes (list "handler:ex-" "handler:reactive-" "handler:")
|
|
:warmup-paths (list "/sx/")
|
|
:init-script :default})
|
|
SXEOF
|
|
|
|
start_server $((BASE_PORT + 2)) "SX_COMPONENTS_DIR=$TMP_DIR/custom-sx"
|
|
|
|
assert_stderr_contains "custom title in log" "App config loaded: title=My Custom App"
|
|
assert_stderr_contains "warmup 1 page" "Pre-warmed 1 pages"
|
|
assert_status "homepage works" "/sx/" 200
|
|
assert_body_contains "custom title in HTML" "/sx/" "<title>My Custom App</title>"
|
|
|
|
stop_server
|
|
echo ""
|
|
|
|
# ── Test Group 3: No config (missing __app-config) ──────────────────
|
|
|
|
echo "── Group 3: No app config (defaults) ──"
|
|
mkdir -p "$TMP_DIR/noconfig-sx"
|
|
cp -r "$PROJECT_DIR/sx/sx/"* "$TMP_DIR/noconfig-sx/"
|
|
rm -f "$TMP_DIR/noconfig-sx/app-config.sx"
|
|
|
|
start_server $((BASE_PORT + 3)) "SX_COMPONENTS_DIR=$TMP_DIR/noconfig-sx"
|
|
|
|
assert_stderr_contains "defaults fallback" "No __app-config found, using defaults"
|
|
assert_status "homepage without config" "/sx/" 200
|
|
assert_redirect "root redirect without config" "/" "/sx/"
|
|
assert_body_contains "default title" "/sx/" "<title>SX</title>"
|
|
|
|
stop_server
|
|
echo ""
|
|
|
|
# ── Test Group 4: Minimal config (only title) ───────────────────────
|
|
|
|
echo "── Group 4: Minimal config ──"
|
|
mkdir -p "$TMP_DIR/minimal-sx"
|
|
cp -r "$PROJECT_DIR/sx/sx/"* "$TMP_DIR/minimal-sx/"
|
|
cat > "$TMP_DIR/minimal-sx/app-config.sx" << 'SXEOF'
|
|
(define __app-config {:title "Bare"})
|
|
SXEOF
|
|
|
|
start_server $((BASE_PORT + 4)) "SX_COMPONENTS_DIR=$TMP_DIR/minimal-sx"
|
|
|
|
assert_stderr_contains "minimal config log" "App config loaded: title=Bare"
|
|
assert_status "homepage with minimal config" "/sx/" 200
|
|
assert_body_contains "bare title" "/sx/" "<title>Bare</title>"
|
|
# Defaults should still work for everything not specified
|
|
assert_redirect "default redirect" "/" "/sx/"
|
|
assert_status "default debug" "/sx/_debug/eval?expr=1" 200
|
|
|
|
stop_server
|
|
echo ""
|
|
|
|
# ── Summary ──────────────────────────────────────────────────────────
|
|
|
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
if [ $FAIL -gt 0 ]; then
|
|
echo -e "\nFailures:$ERRORS"
|
|
exit 1
|
|
else
|
|
echo "All tests passed."
|
|
exit 0
|
|
fi
|