#!/bin/bash # Round01 AltTab conflict QA. It starts AltTab when available, verifies Aligner # still receives Option+Tab, then verifies the same path after AltTab exits. # It does not rebuild/package, 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-alttab-conflict-report.json" ALTTAB_APP="${ALIGNER_ROUND1_ALTTAB_APP:-/Applications/AltTab.app}" START_WAIT="${ALIGNER_ROUND1_ALTTAB_START_WAIT:-1.6}" OPEN_WAIT="${ALIGNER_ROUND1_ALTTAB_OPEN_WAIT:-0.8}" CLOSE_WAIT="${ALIGNER_ROUND1_ALTTAB_CLOSE_WAIT:-0.8}" SAMPLER_DURATION="${ALIGNER_ROUND1_ALTTAB_SAMPLE_DURATION:-1.4}" SAMPLER_INTERVAL="${ALIGNER_ROUND1_ALTTAB_SAMPLE_INTERVAL:-0.03}" WORK_DIR="${ALIGNER_ROUND1_ALTTAB_WORK_DIR:-$(mktemp -d /tmp/aligner-round01-alttab-conflict.XXXXXX)}" ORIGINAL_ALTTAB_RUNNING=0 ALIGNER_PID="" ACTIVE_SAMPLER_PID="" fail() { echo "Round01 AltTab conflict QA failed: $*" >&2 exit 1 } is_alttab_running() { pgrep -x AltTab >/dev/null 2>&1 } start_alttab() { [ -d "$ALTTAB_APP" ] || fail "AltTab app not found: $ALTTAB_APP" open "$ALTTAB_APP" for _ in {1..50}; do if is_alttab_running; then return 0 fi sleep 0.1 done fail "AltTab did not start" } stop_alttab() { if ! is_alttab_running; then return fi osascript -e 'tell application "AltTab" to quit' >/dev/null 2>&1 || true for _ in {1..30}; do if ! is_alttab_running; then return 0 fi sleep 0.1 done pkill -x AltTab >/dev/null 2>&1 || true for _ in {1..30}; do if ! is_alttab_running; then return 0 fi sleep 0.1 done fail "AltTab did not exit" } 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}' >/dev/null ;; escape) osascript -e 'tell application "System Events" to key code 53' >/dev/null ;; *) 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" } run_window_logic_check() { swift "$SCRIPT_DIR/window-logic-qa.swift" "$@" 2>&1 | head -c 6000 local status="${PIPESTATUS[0]}" return "$status" } 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_aligner_trigger_case() { local label="$1" local aligner_log="$WORK_DIR/aligner-$label.log" local sampler_stdout="$WORK_DIR/alttab-visible-$label.stdout" local sampler_report="$WORK_DIR/alttab-visible-$label.report" local sampler_pid="" rm -f "$REPORT" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-quick-switch-report="$REPORT" >"$aligner_log" 2>&1 & ALIGNER_PID=$! sleep "$START_WAIT" if ! kill -0 "$ALIGNER_PID" 2>/dev/null; then tail -c 6000 "$aligner_log" >&2 || true fail "Aligner exited before $label trigger" fi swift "$SCRIPT_DIR/round1-move-mouse-to-screen-center.swift" 0 2>&1 | head -c 6000 if is_alttab_running; then swift "$SCRIPT_DIR/round1-visible-owner-window-sampler.swift" \ "AltTab" "$SAMPLER_DURATION" "$SAMPLER_INTERVAL" "$sampler_report" >"$sampler_stdout" 2>&1 & sampler_pid=$! ACTIVE_SAMPLER_PID="$sampler_pid" sleep 0.05 fi post_key_with_system_events option-tab sleep "$OPEN_WAIT" echo "==> $label" echo "AltTab running: $(is_alttab_running && echo yes || echo no)" run_window_logic_check \ --expect-quick-switch \ --expect-single-aligner-overlay \ --expect-overlay-on-mouse-screen \ --expect-overlay-uses-screen-frame if [ -n "$sampler_pid" ]; then if ! wait "$sampler_pid"; then ACTIVE_SAMPLER_PID="" tail -c 6000 "$sampler_stdout" >&2 || true tail -c 6000 "$sampler_report" >&2 || true fail "visible AltTab window appeared during $label" fi ACTIVE_SAMPLER_PID="" tail -c 6000 "$sampler_stdout" fi post_key_with_system_events escape sleep "$CLOSE_WAIT" wait_for_no_quick_switch 5.0 kill "$ALIGNER_PID" 2>/dev/null || true wait "$ALIGNER_PID" 2>/dev/null || true ALIGNER_PID="" } cleanup_runtime_only() { post_key_with_system_events escape 2>/dev/null || true if [ -n "$ACTIVE_SAMPLER_PID" ] && kill -0 "$ACTIVE_SAMPLER_PID" 2>/dev/null; then kill "$ACTIVE_SAMPLER_PID" 2>/dev/null || true wait "$ACTIVE_SAMPLER_PID" 2>/dev/null || true fi ACTIVE_SAMPLER_PID="" if [ -n "${ALIGNER_PID:-}" ] && kill -0 "$ALIGNER_PID" 2>/dev/null; then kill "$ALIGNER_PID" 2>/dev/null || true wait "$ALIGNER_PID" 2>/dev/null || true fi ALIGNER_PID="" } restore_original_alttab_state() { if [ "$ORIGINAL_ALTTAB_RUNNING" -eq 1 ]; then start_alttab else stop_alttab fi } assert_original_alttab_state() { local final_running=0 if is_alttab_running; then final_running=1 fi echo "Original AltTab running: $([ "$ORIGINAL_ALTTAB_RUNNING" -eq 1 ] && echo yes || echo no)" echo "Final AltTab running: $([ "$final_running" -eq 1 ] && echo yes || echo no)" if [ "$final_running" -ne "$ORIGINAL_ALTTAB_RUNNING" ]; then fail "AltTab final state did not match original state" fi } cleanup() { cleanup_runtime_only restore_original_alttab_state >/dev/null 2>&1 || true } trap cleanup EXIT [ -x "$APP/Contents/MacOS/Aligner" ] || fail "missing built app: $APP" if is_alttab_running; then ORIGINAL_ALTTAB_RUNNING=1 fi stop_current_aligner start_alttab run_aligner_trigger_case "alttab-running" stop_alttab run_aligner_trigger_case "alttab-stopped" cleanup_runtime_only restore_original_alttab_state assert_original_alttab_state trap - EXIT echo "Log directory: $WORK_DIR" echo "Round01 AltTab conflict QA passed"