#!/bin/bash # Round01 dual-screen placement QA. It does not rebuild/package; it verifies # the current internal Aligner.app so candidate DMG hashes stay stable. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=build-output-paths.sh source "$SCRIPT_DIR/build-output-paths.sh" APP="$BUILD_CURRENT_APP" REPORT="$BUILD_REPORT_ROOT/round01-dual-screen-placement-report.json" WORK_DIR="${ALIGNER_ROUND1_DUAL_SCREEN_WORK_DIR:-$(mktemp -d /tmp/aligner-round01-dual-screen-placement.XXXXXX)}" ALIGNER_LOG="$WORK_DIR/aligner.log" START_WAIT="${ALIGNER_ROUND1_DUAL_SCREEN_START_WAIT:-1.6}" OPEN_WAIT="${ALIGNER_ROUND1_DUAL_SCREEN_OPEN_WAIT:-0.8}" CLOSE_WAIT="${ALIGNER_ROUND1_DUAL_SCREEN_CLOSE_WAIT:-0.8}" RAPID_RETRIGGER_COUNT="${ALIGNER_ROUND1_DUAL_SCREEN_RAPID_RETRIGGER_COUNT:-5}" RAPID_RETRIGGER_WAIT="${ALIGNER_ROUND1_DUAL_SCREEN_RAPID_RETRIGGER_WAIT:-0.18}" APP_PID="" fail() { echo "Round01 dual-screen placement QA failed: $*" >&2 exit 1 } screen_count() { swift -e 'import AppKit; print(NSScreen.screens.count)' } post_key_with_system_events() { local shortcut="$1" case "$shortcut" in option-tab) osascript -e 'tell application "System Events" to key code 48 using {option down}' ;; escape) osascript -e 'tell application "System Events" to key code 53' ;; *) fail "unsupported System Events shortcut: $shortcut" ;; esac } aligner_pids_for_current_app() { ps -axo pid=,args= | while read -r pid command; do case "$command" in "$APP/Contents/MacOS/Aligner"*) echo "$pid" ;; esac done } stop_current_aligner() { for pid in $(aligner_pids_for_current_app); do kill "$pid" 2>/dev/null || true done for _ in {1..30}; do [ -z "$(aligner_pids_for_current_app)" ] && return sleep 0.1 done fail "current Aligner app did not exit before QA" } wait_for_no_quick_switch() { local timeout="$1" /usr/bin/python3 - "$SCRIPT_DIR" "$timeout" <<'PY' import subprocess import sys import time script_dir = sys.argv[1] timeout = float(sys.argv[2]) deadline = time.monotonic() + timeout last_output = "" while time.monotonic() < deadline: result = subprocess.run( ["swift", f"{script_dir}/window-logic-qa.swift", "--expect-no-quick-switch"], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) last_output = result.stdout if result.returncode == 0: print(last_output[-6000:], end="") sys.exit(0) time.sleep(0.2) print(last_output[-6000:], end="") sys.exit(1) PY } run_window_logic_check() { swift "$SCRIPT_DIR/window-logic-qa.swift" "$@" 2>&1 | head -c 6000 local status="${PIPESTATUS[0]}" return "$status" } cleanup() { post_key_with_system_events escape 2>/dev/null || true if [ -n "${APP_PID:-}" ] && kill -0 "$APP_PID" 2>/dev/null; then kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true fi stop_current_aligner 2>/dev/null || true } [ -x "$APP/Contents/MacOS/Aligner" ] || fail "missing built app: $APP" mkdir -p "$WORK_DIR" SCREENS="$(screen_count)" if [ "$SCREENS" -lt 2 ]; then fail "expected at least 2 screens, got $SCREENS" fi stop_current_aligner rm -f "$REPORT" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-quick-switch-report="$REPORT" >"$ALIGNER_LOG" 2>&1 & APP_PID=$! trap cleanup EXIT sleep "$START_WAIT" for index in $(seq 0 $((SCREENS - 1))); do echo "==> screen-$index" swift "$SCRIPT_DIR/round1-move-mouse-to-screen-center.swift" "$index" 2>&1 | head -c 6000 post_key_with_system_events option-tab sleep "$OPEN_WAIT" run_window_logic_check \ --expect-quick-switch \ --expect-single-aligner-overlay \ --expect-overlay-on-mouse-screen \ --expect-overlay-uses-screen-frame next_index=$(( (index + 1) % SCREENS )) if [ "$next_index" -ne "$index" ]; then echo "==> visible-retrigger-to-screen-$next_index" swift "$SCRIPT_DIR/round1-move-mouse-to-screen-center.swift" "$next_index" 2>&1 | head -c 6000 post_key_with_system_events option-tab sleep "$OPEN_WAIT" run_window_logic_check \ --expect-quick-switch \ --expect-single-aligner-overlay \ --expect-overlay-on-mouse-screen \ --expect-overlay-uses-screen-frame if [ "$index" -eq 0 ]; then echo "==> rapid-visible-retrigger-$RAPID_RETRIGGER_COUNT" for count in $(seq 1 "$RAPID_RETRIGGER_COUNT"); do target_index=$(( count % SCREENS )) swift "$SCRIPT_DIR/round1-move-mouse-to-screen-center.swift" "$target_index" 2>&1 | head -c 6000 post_key_with_system_events option-tab sleep "$RAPID_RETRIGGER_WAIT" run_window_logic_check \ --expect-quick-switch \ --expect-single-aligner-overlay \ --expect-overlay-on-mouse-screen \ --expect-overlay-uses-screen-frame done fi fi post_key_with_system_events escape sleep "$CLOSE_WAIT" wait_for_no_quick_switch 5.0 done if kill -0 "$APP_PID" 2>/dev/null; then kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true fi stop_current_aligner trap - EXIT echo "Round01 dual-screen placement QA passed" echo "Screens covered: $SCREENS" echo "Log directory: $WORK_DIR"