| 1 | #!/usr/bin/env bash |
| 2 | # Drive the real editor in a real browser: build the app, run it on a |
| 3 | # scratch database, sign in for real, and put a headless Chrome through |
| 4 | # fixtures/browser-drive.mjs. Exits non-zero if any check fails. |
| 5 | # |
| 6 | # Not in .amadan/ci.d/10-gate yet, deliberately, and the reason is |
| 7 | # bigger than "the runner needs a Chrome installed". |
| 8 | # |
| 9 | # Chromium does not start inside the agent sandbox as it is configured |
| 10 | # by default — it fails early on something it cannot write or bind. |
| 11 | # The exact symptom moves: it has been "socket() failed: Operation not |
| 12 | # permitted" and it has been "Failed to create a unique user data |
| 13 | # directory for headless". Neither is quoted here as the diagnosis, |
| 14 | # because quoting one symptom as though it were the reason is part of |
| 15 | # what made the old version of this paragraph read as final. |
| 16 | # |
| 17 | # BUT IT RUNS FINE WITH THE SANDBOX OFF, and this header said otherwise |
| 18 | # for long enough to do damage. It read "cannot start at all", full |
| 19 | # stop — and a session that believed it merged a branch describing |
| 20 | # itself as impossible to browser-verify, having never tried. A real |
| 21 | # WCAG 2.2 SC 1.4.10 failure and four blinded checks went to main behind |
| 22 | # that sentence. The sandbox is a setting, not a property of the |
| 23 | # machine; a session that needs a browser turns it off for this one |
| 24 | # command and gets one. |
| 25 | # |
| 26 | # So the open question for the gate is smaller than this file used to |
| 27 | # claim. Not "can this run here" — it can — but "may a CI step run |
| 28 | # unsandboxed", which is a real decision about what CI is allowed to do |
| 29 | # and belongs to whoever owns the runner rather than to the branch that |
| 30 | # wrote the script. |
| 31 | # |
| 32 | # Until that is answered it is run by hand, which is worth far more than |
| 33 | # not having it: its first run caught an editor that silently discarded |
| 34 | # every edit, and the run after this file's own header was corrected |
| 35 | # caught five regressions a green gate had passed. |
| 36 | # |
| 37 | # $DOCS_CHROME overrides which browser to use. |
| 38 | set -euo pipefail |
| 39 | cd "$(dirname "$0")/.." |
| 40 | |
| 41 | chrome="${DOCS_CHROME:-}" |
| 42 | if [ -z "$chrome" ]; then |
| 43 | for candidate in chromium chromium-browser google-chrome google-chrome-stable; do |
| 44 | if command -v "$candidate" >/dev/null 2>&1; then chrome="$candidate"; break; fi |
| 45 | done |
| 46 | fi |
| 47 | [ -n "$chrome" ] || { echo "browser-drive: no Chrome found; set \$DOCS_CHROME" >&2; exit 2; } |
| 48 | command -v node >/dev/null || { echo "browser-drive: node not on PATH" >&2; exit 2; } |
| 49 | |
| 50 | port="${DOCS_DRIVE_PORT:-8188}" |
| 51 | debug_port="${DOCS_DRIVE_DEBUG_PORT:-9288}" |
| 52 | addr="127.0.0.1:${port}" |
| 53 | scratch="$(mktemp -d "${TMPDIR:-/tmp}/docs-drive.XXXXXX")" |
| 54 | |
| 55 | server_pid="" |
| 56 | chrome_pid="" |
| 57 | cleanup() { |
| 58 | [ -n "$chrome_pid" ] && kill "$chrome_pid" 2>/dev/null || true |
| 59 | [ -n "$server_pid" ] && kill "$server_pid" 2>/dev/null || true |
| 60 | rm -rf "$scratch" 2>/dev/null || true |
| 61 | } |
| 62 | trap cleanup EXIT |
| 63 | |
| 64 | export CGO_ENABLED=0 |
| 65 | echo "browser-drive: building" |
| 66 | go build -o "${scratch}/docs" ./cmd/docs |
| 67 | |
| 68 | # -seed so there is a document to open; the same development-only path |
| 69 | # scripts/run-dev.sh uses, against a database that exists for this run |
| 70 | # only. |
| 71 | "${scratch}/docs" -env development -seed -addr "$addr" -db "${scratch}/docs.db" > "${scratch}/server.log" 2>&1 & |
| 72 | server_pid=$! |
| 73 | |
| 74 | for _ in $(seq 1 40); do |
| 75 | if curl -fsS -o /dev/null "http://${addr}/signin" 2>/dev/null; then break; fi |
| 76 | sleep 0.25 |
| 77 | done |
| 78 | curl -fsS -o /dev/null "http://${addr}/signin" || { echo "browser-drive: server did not start" >&2; cat "${scratch}/server.log" >&2; exit 1; } |
| 79 | |
| 80 | # Sign in the way a person does. In development the magic link is |
| 81 | # logged instead of mailed (internal/docs/auth.go), so the link is read |
| 82 | # back out of this run's own log — no stand-in identity, no test-only |
| 83 | # door into the app. |
| 84 | curl -fsS -X POST -H 'Sec-Fetch-Site: same-origin' \ |
| 85 | --data-urlencode 'address=dev@example.com' -o /dev/null "http://${addr}/signin" |
| 86 | link="$(grep -o "http://${addr}/auth/verify?token=[A-Za-z0-9_-]*" "${scratch}/server.log" | tail -1)" |
| 87 | [ -n "$link" ] || { echo "browser-drive: no sign-in link in the log" >&2; exit 1; } |
| 88 | |
| 89 | # The link opens a confirm door that spends nothing; the drive presses |
| 90 | # its Sign in button (browser-drive.mjs), which is what leaves the |
| 91 | # session cookie in the profile it then uses. |
| 92 | |
| 93 | "$chrome" --headless=new --no-sandbox --disable-gpu --disable-crash-reporter \ |
| 94 | --remote-debugging-port="${debug_port}" --user-data-dir="${scratch}/profile" \ |
| 95 | --window-size=1280,900 \ |
| 96 | about:blank > "${scratch}/chrome.log" 2>&1 & |
| 97 | chrome_pid=$! |
| 98 | |
| 99 | target="" |
| 100 | for _ in $(seq 1 40); do |
| 101 | target="$(curl -fsS "http://127.0.0.1:${debug_port}/json/list" 2>/dev/null \ |
| 102 | | tr ',' '\n' | grep -o 'ws://[^"]*devtools/page/[A-Za-z0-9]*' | head -1 || true)" |
| 103 | [ -n "$target" ] && break |
| 104 | sleep 0.25 |
| 105 | done |
| 106 | [ -n "$target" ] || { echo "browser-drive: Chrome did not expose a debugging target" >&2; exit 1; } |
| 107 | |
| 108 | echo "browser-drive: driving http://${addr}" |
| 109 | node fixtures/browser-drive.mjs "$target" "http://${addr}" "$link" |
| 110 | |