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