test(deploy): add opt-in display-device scenarios to the split smoke
CI / verify (pull_request) Successful in 53s
CI / infrastructure-smoke (pull_request) Successful in 13s
CI / migration-smoke (pull_request) Successful in 1m48s
CI / image-build (pull_request) Successful in 20s
CI / deploy-smoke (same-origin) (pull_request) Successful in 48s
CI / deploy-smoke (split) (pull_request) Successful in 55s

Reproduction harness for what the first production Signage deployment
found. With EKKLESIA_SMOKE_DISPLAY_DEVICE=1, the split smoke starts a
second headless Chromium (the "kiosk") whose profile directory is a
Docker volume. It pairs that kiosk through the real flow, closes it
cleanly, restarts it on the same profile, and then runs injected-failure
scenarios: two pages on one credential, a connectivity blip, one refused
viewer-session request, one refused realtime connection, a cross-origin
image-caching probe, a full realtime recycle, and revocation.

Every DevTools command and page-side request has its own deadline, and
each step has a budget. A step that overruns prints the exact call it
was waiting for. The proxy's "aborting with incomplete response" lines
are listed afterwards by UTC time and path only.

Off by default: several scenarios expect Signage and fail on today's
v0.1.1, by design.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-05 18:25:12 -05:00
co-authored by Claude Opus 5.5
parent b2efb4b029
commit ff8a3e8097
2 changed files with 1066 additions and 0 deletions
+87
View File
@@ -15,6 +15,12 @@
# With EKKLESIA_SMOKE_IMAGE, the given image is used as is in either topology; a split run then
# exercises whichever of its modules publishes UI and a display.
#
# Opt-in, split topology only: EKKLESIA_SMOKE_DISPLAY_DEVICE=1 adds the display-device scenarios in
# scripts/lib/deploy-smoke-display.mjs (pairing a kiosk browser with a persistent profile, a clean
# restart, two pages, injected network failures, revocation), each with its own time budget.
# EKKLESIA_SMOKE_DISPLAY_SCENARIOS selects a subset; EKKLESIA_SMOKE_WATCH_MS and
# EKKLESIA_SMOKE_RECYCLE_WATCH_MS change how long they watch. Several scenarios expect Signage.
#
# TEST-ONLY: the proxy runs with Caddy's internal CA (`local_certs`) instead of ACME, because a test
# run has no public hostname. Never use that option in an installation.
#
@@ -141,6 +147,12 @@ cleanup() {
if [[ -n "$browser_container" ]]; then
docker rm --force "$browser_container" >/dev/null 2>&1 || true
fi
if [[ -n "${kiosk_container:-}" ]]; then
docker ps --all --quiet --filter "name=${project}-display-" | xargs -r docker rm --force \
>/dev/null 2>&1 || true
docker rm --force "$kiosk_container" >/dev/null 2>&1 || true
docker volume rm --force "$kiosk_volume" >/dev/null 2>&1 || true
fi
if [[ -f "$work/compose.env" ]]; then
"${compose[@]}" --profile ops down --volumes --remove-orphans --timeout 10 >/dev/null 2>&1 || status=1
fi
@@ -404,6 +416,81 @@ if [[ "$topology" == split ]]; then
-e SMOKE_OWNER_EMAIL="$owner_email" -e SMOKE_OWNER_PASSWORD="$owner_password" \
-v "$daemon_work/browser.mjs:/smoke/browser.mjs:ro" \
--entrypoint node "$image" /smoke/browser.mjs || fail "browser checks failed."
if [[ -n "${EKKLESIA_SMOKE_DISPLAY_DEVICE:-}" ]]; then
# A kiosk: a second browser whose profile directory is a volume, so it outlives the container
# exactly as a kiosk's --user-data-dir outlives a browser restart.
kiosk_container="${project}-kiosk"
kiosk_volume="${project}-kiosk-profile"
kiosk_address="10.249.${octet}.250"
docker volume create "$kiosk_volume" >/dev/null
start_kiosk() {
docker run --detach --rm --name "$kiosk_container" \
--cap-drop ALL --security-opt no-new-privileges --tmpfs /tmp \
--network "$edge_network" --ip "$kiosk_address" "${host_mappings[@]}" \
-v "${kiosk_volume}:/profile" \
"$browser_image" \
"--ignore-certificate-errors-spki-list=${spki}" \
--user-data-dir=/profile >/dev/null || fail "the kiosk browser did not start."
}
wait_kiosk_exit() {
for _ in $(seq 1 60); do
docker inspect "$kiosk_container" >/dev/null 2>&1 || return 0
sleep 0.5
done
fail "the kiosk browser did not exit after a clean close."
}
cp scripts/lib/deploy-smoke-display.mjs "$work/display.mjs"
stage display.mjs
# display_driver <budget seconds> <step>: one scenario with a hard budget. The driver enforces it
# itself and names whatever call was still waiting; `timeout` is the backstop if it cannot.
display_driver() {
local budget="$1" step_name="$2" driver="${project}-display-${2}"
timeout --signal=TERM "$((budget + 30))" \
docker run --rm --name "$driver" --read-only --cap-drop ALL --security-opt no-new-privileges \
--network "$edge_network" \
-e SMOKE_CDP_ADDRESS="${browser_address}:9222" \
-e SMOKE_KIOSK_CDP_ADDRESS="${kiosk_address}:9222" \
-e SMOKE_STEP_BUDGET_MS="$((budget * 1000))" \
-e SMOKE_WATCH_MS="${EKKLESIA_SMOKE_WATCH_MS:-120000}" \
-e SMOKE_RECYCLE_WATCH_MS="${EKKLESIA_SMOKE_RECYCLE_WATCH_MS:-330000}" \
-e SMOKE_HOSTNAME="$hostname" -e SMOKE_DASHBOARD_HOSTNAME="$dashboard_hostname" \
-e SMOKE_OWNER_EMAIL="$owner_email" -e SMOKE_OWNER_PASSWORD="$owner_password" \
-v "$daemon_work/display.mjs:/smoke/display.mjs:ro" \
--entrypoint node "$image" /smoke/display.mjs "$step_name"
local status=$?
if ((status == 124)); then
echo "FAIL ${step_name}: the driver did not stop within its budget plus 30s; removed it"
docker rm --force "$driver" >/dev/null 2>&1 || true
fi
return "$status"
}
display_status=0
step "Pairing a kiosk display, then closing its browser cleanly"
start_kiosk
display_driver 180 pair || display_status=1
wait_kiosk_exit
step "Restarting the kiosk browser on the same profile"
start_kiosk
display_driver 120 restart || display_status=1
for scenario in ${EKKLESIA_SMOKE_DISPLAY_SCENARIOS:-two-pages connectivity-blip session-blip stream-failure caching-probe stream-recycle revoke}; do
case "$scenario" in
two-pages) budget=240 ;;
stream-recycle) budget=420 ;;
revoke) budget=210 ;;
*) budget=150 ;;
esac
step "Display scenario: ${scenario} (budget ${budget}s)"
display_driver "$budget" "$scenario" || display_status=1
done
step "Proxy: streams aborted with an incomplete response (UTC time, path only)"
# Captured, never printed: only each line's time and path (no query, so no signature) leave it.
proxy_logs="$("${compose[@]}" logs --no-color proxy 2>/dev/null)" || proxy_logs=""
grep -o '{.*"aborting with incomplete response".*}' <<<"$proxy_logs" |
sed -E 's/.*"ts":([0-9]+)[^,]*,.*"uri":"([^"?]*)[^"]*".*/\1 \2/' |
while read -r ts uri; do echo " $(date -u -d "@${ts}" +%T) ${uri}"; done || true
((display_status == 0)) || fail "display-device checks failed."
fi
fi
step "Proving the API's database connection is certificate-verified TLS"