151 lines
5.8 KiB
Bash
151 lines
5.8 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
#
|
||
|
|
# Pilot check: go-tell consuming the shared contract.
|
||
|
|
#
|
||
|
|
# go-tell is the hard case. Before the migration it had no CSS custom properties at
|
||
|
|
# all: 340 hardcoded hex values and 153 per-palette override rules, with cyberpunk
|
||
|
|
# baked into the un-scoped base rules. Its stylesheet now reads from the contract, so
|
||
|
|
# this check exists to prove the rewrite works rather than to assert that it was typed:
|
||
|
|
#
|
||
|
|
# - The four palettes go-tell's own rules key on are all published.
|
||
|
|
# - Switching a palette through the shipped control moves the rendered page.
|
||
|
|
# - The terminal palette's global `font-family: ... !important` reaches the
|
||
|
|
# application but not into the controls' shadow roots.
|
||
|
|
# - The appearance axis writes data-mode, which is what gives go-tell the light
|
||
|
|
# mode it never had.
|
||
|
|
#
|
||
|
|
# It does not boot go-tell: the application needs Postgres and a session cookie to
|
||
|
|
# reach its authenticated routes, and none of that is what is being tested. What is
|
||
|
|
# tested is the built shell and the built stylesheet.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
|
TELL="${GO_TELL_DIR:-/home/dnoulet/go/go-tell}"
|
||
|
|
WORK="$(mktemp -d)"
|
||
|
|
PORT="${PORT:-8396}"
|
||
|
|
THEME_PORT="${THEME_PORT:-8395}"
|
||
|
|
CHROME="${CHROME:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || true)}"
|
||
|
|
|
||
|
|
cleanup() {
|
||
|
|
for pid in "${SERVER_PID:-}" "${THEME_PID:-}"; do
|
||
|
|
[[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
|
||
|
|
done
|
||
|
|
rm -rf "$WORK"
|
||
|
|
}
|
||
|
|
trap cleanup EXIT
|
||
|
|
|
||
|
|
[[ -d "$TELL/ui/app" ]] || { echo "go-tell not found at $TELL; set GO_TELL_DIR" >&2; exit 1; }
|
||
|
|
[[ -n "$CHROME" ]] || { echo "no chromium found; set CHROME" >&2; exit 1; }
|
||
|
|
|
||
|
|
echo "==> building the service, the SDK and go-tell's UI"
|
||
|
|
(cd "$ROOT" && go build -o "$WORK/go-bananas" ./cmd/go-bananas)
|
||
|
|
(cd "$ROOT/sdk" && npm run build >/dev/null)
|
||
|
|
(cd "$TELL/ui/app" && npm run build >/dev/null)
|
||
|
|
|
||
|
|
DIST="$TELL/ui/dist"
|
||
|
|
[[ -d "$DIST" ]] || { echo "expected a build at $DIST" >&2; exit 1; }
|
||
|
|
|
||
|
|
mkdir -p "$WORK/site" "$WORK/site/assets"
|
||
|
|
cp "$ROOT"/sdk/dist/*.js "$WORK/site/assets/"
|
||
|
|
cp "$DIST/index.html" "$WORK/site/index.html"
|
||
|
|
cp "$DIST"/assets/* "$WORK/site/" 2>/dev/null || true
|
||
|
|
cp "$ROOT/hack/pilot-go-tell.js" "$WORK/site/pilot.js"
|
||
|
|
|
||
|
|
# The contract is added here rather than served through go-tell's Go handler, because
|
||
|
|
# this check serves the built shell with a plain static server. The mechanism itself is
|
||
|
|
# covered where it lives, by `go test ./internal/web/ -run Theme` in go-tell; this check
|
||
|
|
# is about whether the stylesheet and the controls work together. What is added below is
|
||
|
|
# what applyThemeOrigin adds when CMS_THEME_BASE_URL is set.
|
||
|
|
echo "==> assembling go-tell's built shell"
|
||
|
|
SHELL="$WORK/site/index.html" SITE_ASSETS="$WORK/site" THEME_ORIGIN="http://localhost:$THEME_PORT" \
|
||
|
|
python3 <<'PY'
|
||
|
|
import os, re
|
||
|
|
|
||
|
|
path = os.environ['SHELL']
|
||
|
|
origin = os.environ['THEME_ORIGIN']
|
||
|
|
s = open(path).read()
|
||
|
|
|
||
|
|
# The shell references its own bundle by absolute path; the check serves those files
|
||
|
|
# beside the shell rather than under /assets.
|
||
|
|
s = re.sub(r'(src|href)="/assets/', r'\1="/', s)
|
||
|
|
|
||
|
|
# What go-tell's handler adds when the theme origin is configured: a stylesheet and the
|
||
|
|
# control module, inside the head so the stylesheet parses before the body renders.
|
||
|
|
tag = (
|
||
|
|
'\n <link rel="stylesheet" href="' + origin + '/rt/v1/contract.css" data-theme-contract>'
|
||
|
|
'\n <script type="module" src="' + origin + '/rt/v1/components.js"></script>\n'
|
||
|
|
)
|
||
|
|
if '</head>' in s:
|
||
|
|
s = s.replace('</head>', tag + ' </head>', 1)
|
||
|
|
|
||
|
|
# Transitions are disabled for the check. go-tell transitions `background` over
|
||
|
|
# 300ms, and a headless run under --virtual-time-budget fast-forwards timers without
|
||
|
|
# advancing the style clock, so a transition never leaves its start value: the
|
||
|
|
# untransitioned --bn-bg already reports the new palette while the computed background
|
||
|
|
# still reports the old one. That is an artefact of the harness, not of the
|
||
|
|
# application, and reading through it is the only way to make the assertions
|
||
|
|
# deterministic.
|
||
|
|
s = s.replace(
|
||
|
|
'</head>',
|
||
|
|
'<style>*,*::before,*::after{transition:none !important;animation:none !important}</style>'
|
||
|
|
' </head>',
|
||
|
|
1,
|
||
|
|
)
|
||
|
|
|
||
|
|
# The reporter, appended to the copy. go-tell's own source is never edited.
|
||
|
|
s = s.replace('</body>', ' <script type="module" src="/pilot.js"></script>\n</body>')
|
||
|
|
open(path, 'w').write(s)
|
||
|
|
PY
|
||
|
|
|
||
|
|
echo "==> starting the theme service on :$THEME_PORT"
|
||
|
|
DATABASE_PATH="$WORK/tell.db" \
|
||
|
|
BANANAS_ENVIRONMENT=development \
|
||
|
|
BANANAS_AUTH_MODE=dev \
|
||
|
|
BANANAS_PUBLIC_BASE_URL="http://localhost:$THEME_PORT" \
|
||
|
|
BANANAS_HTTP_ADDR=":$THEME_PORT" \
|
||
|
|
BANANAS_SDK_DIR="$WORK/site/assets" \
|
||
|
|
BANANAS_CORS_ALLOWED_ORIGINS="http://localhost:$PORT" \
|
||
|
|
"$WORK/go-bananas" >"$WORK/theme.log" 2>&1 &
|
||
|
|
THEME_PID=$!
|
||
|
|
|
||
|
|
for _ in $(seq 1 40); do
|
||
|
|
curl -fsS -m 2 "http://localhost:$THEME_PORT/healthz" >/dev/null 2>&1 && break
|
||
|
|
sleep 0.25
|
||
|
|
done
|
||
|
|
|
||
|
|
echo "==> serving go-tell's shell on :$PORT"
|
||
|
|
(cd "$WORK/site" && exec python3 -m http.server "$PORT") >"$WORK/site.log" 2>&1 &
|
||
|
|
SERVER_PID=$!
|
||
|
|
for _ in $(seq 1 40); do
|
||
|
|
curl -fsS -m 2 "http://localhost:$PORT/index.html" >/dev/null 2>&1 && break
|
||
|
|
sleep 0.25
|
||
|
|
done
|
||
|
|
|
||
|
|
echo "==> driving the page"
|
||
|
|
"$CHROME" \
|
||
|
|
--headless --no-sandbox --disable-gpu \
|
||
|
|
--virtual-time-budget=15000 \
|
||
|
|
--dump-dom "http://localhost:$PORT/index.html" \
|
||
|
|
>"$WORK/dom.html" 2>"$WORK/chrome.log"
|
||
|
|
|
||
|
|
result="$(python3 - "$WORK/dom.html" <<'PYEOF'
|
||
|
|
import re, sys
|
||
|
|
dom = open(sys.argv[1]).read()
|
||
|
|
m = re.search(r'id="pilot"[^>]*>(.*?)</pre>', dom, re.S)
|
||
|
|
print(m.group(1).strip() if m else "")
|
||
|
|
PYEOF
|
||
|
|
)"
|
||
|
|
|
||
|
|
if [[ -z "$result" ]]; then
|
||
|
|
echo "PILOT FAIL: the page recorded nothing, so the check did not run." >&2
|
||
|
|
echo "--- browser console ---" >&2
|
||
|
|
grep -iE 'CONSOLE.*(error|Uncaught|violates|refused)' "$WORK/chrome.log" | head -5 >&2 || true
|
||
|
|
echo "--- dom size ---" >&2
|
||
|
|
wc -c <"$WORK/dom.html" >&2 || true
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "$result"
|
||
|
|
[[ "$result" == PILOT\ PASS* ]]
|