#!/bin/bash # Round01 real trigger QA. It launches Aligner normally, posts Option+Tab through # CoreGraphics, verifies the Quick Switch overlay, then posts Esc and verifies # clean close. 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-trigger-session-report.json" START_WAIT="${ALIGNER_ROUND1_TRIGGER_START_WAIT:-1.6}" OPEN_WAIT="${ALIGNER_ROUND1_TRIGGER_OPEN_WAIT:-0.8}" CLOSE_WAIT="${ALIGNER_ROUND1_TRIGGER_CLOSE_WAIT:-0.8}" FORCE_QUIT_WAIT="${ALIGNER_ROUND1_TRIGGER_FORCE_QUIT_WAIT:-0.8}" REPORT_WAIT="${ALIGNER_ROUND1_TRIGGER_REPORT_WAIT:-5.0}" fail() { echo "Round01 trigger session QA failed: $*" >&2 exit 1 } aligner_pids_for_current_app() { ps -axo pid=,args= | while read -r pid command; do if [[ "$command" == "$APP/Contents/MacOS/Aligner"* ]] \ || [[ "$command" == "/Applications/Aligner.app/Contents/MacOS/Aligner"* ]] \ || [[ "$command" == "$HOME/Applications/Aligner.app/Contents/MacOS/Aligner"* ]]; then echo "$pid" fi 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" } stop_launched_aligner() { 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 } dismiss_force_quit_windows() { osascript <<'APPLESCRIPT' >/dev/null 2>&1 || true tell application "System Events" tell process "loginwindow" repeat with targetWindow in windows try click button 2 of targetWindow end try end repeat end tell end tell APPLESCRIPT } cleanup() { post_key escape 2>/dev/null || true dismiss_force_quit_windows stop_launched_aligner } 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, end="") sys.exit(0) time.sleep(0.2) print(last_output, end="") sys.exit(1) PY } wait_for_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-quick-switch", "--expect-single-aligner-overlay", "--expect-overlay-on-mouse-screen", "--expect-overlay-uses-screen-frame", ], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) last_output = result.stdout if result.returncode == 0: print(last_output, end="") sys.exit(0) time.sleep(0.2) print(last_output, end="") sys.exit(1) PY } open_quick_switch_with_retry() { swift "$SCRIPT_DIR/round1-move-mouse-to-screen-center.swift" 0 >/dev/null post_key option-tab if wait_for_quick_switch "$OPEN_WAIT"; then return fi if ! swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >/dev/null 2>&1; then fail "Quick Switch appeared after first Option+Tab but did not satisfy overlay assertions" fi post_key option-tab wait_for_quick_switch "$OPEN_WAIT" } wait_for_system_critical_close_report() { /usr/bin/python3 - "$REPORT" "$REPORT_WAIT" <<'PY' import json import sys import time path = sys.argv[1] timeout = float(sys.argv[2]) deadline = time.monotonic() + timeout last_report = None while time.monotonic() < deadline: try: with open(path, "r", encoding="utf-8") as file: report = json.load(file) last_report = report if ( report.get("quickSwitchVisible") is False and report.get("lastSystemCriticalAction") == "closeOverlay" and report.get("lastDismissReason") == "systemCriticalWindow" and report.get("lastSystemCriticalWindowCount", 0) > 0 ): print(json.dumps({ "quickSwitchVisible": report.get("quickSwitchVisible"), "lastSystemCriticalAction": report.get("lastSystemCriticalAction"), "lastDismissReason": report.get("lastDismissReason"), "lastSystemCriticalWindowOwners": report.get("lastSystemCriticalWindowOwners"), "lastSystemCriticalWindowTitleHashes": report.get("lastSystemCriticalWindowTitleHashes"), "lastSystemCriticalWindowTitleLengths": report.get("lastSystemCriticalWindowTitleLengths"), }, indent=2, ensure_ascii=False)) sys.exit(0) except FileNotFoundError: pass except json.JSONDecodeError: pass time.sleep(0.1) if last_report is not None: print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr) print(f"system-critical close report did not become ready within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } post_key() { local shortcut="$1" case "$shortcut" in option-tab) swift "$SCRIPT_DIR/post-round1-key.swift" --option-tab ;; command-option-escape) osascript -e 'tell application "System Events" to key code 53 using {command down, option down}' ;; escape) osascript -e 'tell application "System Events" to key code 53' ;; *) fail "unsupported shortcut: $shortcut" ;; esac } stop_current_aligner "$SCRIPT_DIR/package-app.sh" >&2 rm -f "$REPORT" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-quick-switch-report="$REPORT" & APP_PID=$! trap cleanup EXIT sleep "$START_WAIT" open_quick_switch_with_retry post_key escape sleep "$CLOSE_WAIT" wait_for_no_quick_switch 5.0 open_quick_switch_with_retry post_key command-option-escape sleep "$FORCE_QUIT_WAIT" wait_for_system_critical_close_report wait_for_no_quick_switch 5.0 post_key escape dismiss_force_quit_windows wait_for_no_quick_switch 5.0 sleep "$CLOSE_WAIT" wait_for_no_quick_switch 5.0 stop_launched_aligner trap - EXIT