#!/usr/bin/env bash # Produce pass/fail Bevy screenshot evidence without manual window interaction. set -euo pipefail root=$(cd "$(dirname "$0")/.." && pwd) kind=dark output="" build=1 seconds=60 usage() { cat <<'EOF' usage: tools/bevy-headless.sh [shot-kind] [--output PATH] [--no-build] [--timeout SECONDS] Runs a MISALIGNED_SHOT harness, verifies its PNG, prints its SHA-256, and exits nonzero on any failure. Material-mode shots additionally require fog-audit success. The running frontend reports its actual staged dialect; this wrapper does not duplicate the screenshot-kind-to-view mapping. EOF } while [ $# -gt 0 ]; do case "$1" in --output) shift; output=${1:-}; shift || true ;; --no-build) build=0; shift ;; --timeout) shift; seconds=${1:-60}; shift || true ;; -h|--help) usage; exit 0 ;; -*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;; *) kind=$1; shift ;; esac done case "$seconds" in *[!0-9]*|"") echo "timeout must be seconds" >&2; exit 2 ;; esac case "$kind" in zoomin|zoomout) echo "FAIL: shot '$kind' is an interaction probe, not screenshot evidence" >&2 exit 2 ;; esac cd "$root" if [ "$build" -eq 1 ]; then cargo build --quiet -p misaligned-bevy --bin misaligned-bevy fi bin="$root/target/debug/misaligned-bevy" [ -x "$bin" ] || { echo "FAIL: Bevy binary missing: $bin" >&2; exit 1; } tmp=$(mktemp -d "${TMPDIR:-/tmp}/misaligned-bevy.XXXXXX") trap 'rm -rf "$tmp"' EXIT [ -n "$output" ] || output="$tmp/${kind}.png" case "$output" in /*) ;; *) output="$root/$output" ;; esac mkdir -p "$(dirname "$output")" python3 - "$bin" "$kind" "$output" "$seconds" <<'PY' import hashlib, os, pathlib, shutil, subprocess, sys binary, kind, output, seconds = sys.argv[1], sys.argv[2], pathlib.Path(sys.argv[3]), int(sys.argv[4]) env = os.environ.copy() env.update({ "MISALIGNED_SHOT": kind, "MISALIGNED_SHOT_PATH": str(output), "MISALIGNED_WAKE": "off", }) command = [binary] if sys.platform.startswith("linux") and not (env.get("DISPLAY") or env.get("WAYLAND_DISPLAY")): xvfb = shutil.which("xvfb-run") if not xvfb: print("FAIL: no display and xvfb-run is unavailable", file=sys.stderr) sys.exit(1) command = [xvfb, "-a", *command] try: result = subprocess.run( command, env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=seconds, check=False, ) except subprocess.TimeoutExpired as exc: print(f"FAIL: Bevy evidence timed out after {seconds}s", file=sys.stderr) if exc.stdout: print(exc.stdout, file=sys.stderr) sys.exit(1) print(result.stdout, end="") if result.returncode: print(f"FAIL: Bevy evidence exited {result.returncode}", file=sys.stderr) sys.exit(result.returncode) prefix = f"bevy shot ready: kind={kind} dialect=" dialect_lines = [line.removeprefix(prefix) for line in result.stdout.splitlines() if line.startswith(prefix)] if len(dialect_lines) != 1 or dialect_lines[0] not in {"DIGITAL", "REAL"}: print(f"FAIL: Bevy evidence did not report one valid staged dialect for shot '{kind}'", file=sys.stderr) sys.exit(1) dialect = dialect_lines[0] if dialect == "REAL" and "fog audit OK" not in result.stdout: print("FAIL: Bevy evidence did not report fog audit OK", file=sys.stderr) sys.exit(1) if not output.is_file() or output.stat().st_size < 8: print(f"FAIL: screenshot missing or empty: {output}", file=sys.stderr) sys.exit(1) data = output.read_bytes() if data[:8] != b"\x89PNG\r\n\x1a\n": print(f"FAIL: screenshot is not a PNG: {output}", file=sys.stderr) sys.exit(1) print(f"bevy evidence: OK shot={kind} dialect={dialect.lower()} path={output} sha256={hashlib.sha256(data).hexdigest()}") PY