#!/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"
|