#!/bin/bash
|
# Round01 window activation fixture QA. It verifies that Enter and mouse
|
# commits call the activation service, close Quick Switch, and distinguish
|
# normal activation from minimized-window restore.
|
|
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"
|
SOURCE_ROOT="$OUTPUT_ROOT/C1.source"
|
APP="$BUILD_CURRENT_APP"
|
MINIMIZED_REPORT="$BUILD_REPORT_ROOT/round01-window-activation-minimized-report.json"
|
NORMAL_REPORT="$BUILD_REPORT_ROOT/round01-window-activation-normal-report.json"
|
APP_ONLY_REPORT="$BUILD_REPORT_ROOT/round01-window-activation-app-only-report.json"
|
FAILURE_REPORT="$BUILD_REPORT_ROOT/round01-window-activation-failure-report.json"
|
REPORT_WAIT="${ALIGNER_ROUND1_ACTIVATION_REPORT_WAIT:-6.0}"
|
|
fail() {
|
echo "Round01 window activation fixture QA failed: $*" >&2
|
exit 1
|
}
|
|
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"
|
}
|
|
assert_activation_budget_source() {
|
/usr/bin/python3 - "$SOURCE_ROOT/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift" "$SOURCE_ROOT/Sources/Aligner/QuickSwitchSessionController.swift" <<'PY'
|
import re
|
import sys
|
|
window_service_path, session_path = sys.argv[1:3]
|
window_service = open(window_service_path, "r", encoding="utf-8").read()
|
session = open(session_path, "r", encoding="utf-8").read()
|
|
def fail(message):
|
print(message, file=sys.stderr)
|
sys.exit(1)
|
|
cycle_match = re.search(r"finderTabCycleMaximumAttempts\s*=\s*(\d+)", window_service)
|
if not cycle_match:
|
fail("Finder tab cycling attempt limit is missing")
|
if int(cycle_match.group(1)) != 5:
|
fail(f"Finder tab cycling attempt limit must be 5, got {cycle_match.group(1)}")
|
|
budget_match = re.search(r"finderTabActivationBudgetLimit:\s*TimeInterval\s*=\s*([0-9.]+)", window_service)
|
if not budget_match:
|
fail("Finder tab activation budget limit is missing")
|
if float(budget_match.group(1)) > 1.5:
|
fail(f"Finder tab activation budget must stay at or below 1.5s, got {budget_match.group(1)}")
|
|
if "lastActivationDurationMilliseconds" not in session:
|
fail("Quick Switch activation report must expose lastActivationDurationMilliseconds")
|
|
print("Finder activation source guardrails passed")
|
PY
|
}
|
|
wait_for_activation_report() {
|
local report="$1"
|
/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("snapshotLoaded") is True
|
and report.get("quickSwitchVisible") is False
|
and report.get("lastActivationResult") is not None
|
):
|
sys.exit(0)
|
except FileNotFoundError:
|
pass
|
except json.JSONDecodeError:
|
pass
|
time.sleep(0.2)
|
|
if last_report is not None:
|
print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
|
print(f"activation report did not become hidden+activated within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
assert_activation_report() {
|
local report="$1"
|
local expected_source="$2"
|
local expected_result="$3"
|
local expected_app_group_index="$4"
|
local expected_window_id="$5"
|
local expected_minimized="$6"
|
|
/usr/bin/python3 - "$report" "$expected_source" "$expected_result" "$expected_app_group_index" "$expected_window_id" "$expected_minimized" <<'PY'
|
import json
|
import sys
|
|
path = sys.argv[1]
|
expected_source = sys.argv[2]
|
expected_result = sys.argv[3]
|
expected_app_group_index = int(sys.argv[4])
|
expected_window_id = int(sys.argv[5])
|
expected_minimized = sys.argv[6] == "true"
|
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
|
def require(condition, message):
|
if not condition:
|
print(message, file=sys.stderr)
|
print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
|
root = report.get("rootView", {})
|
cards = [
|
card
|
for column in root.get("waterfallColumns", [])
|
for card in column.get("cards", [])
|
]
|
selected_cards = [card for card in cards if card.get("isSelected") is True]
|
|
require(report.get("snapshotLoaded") is True, "activation snapshotLoaded must be true")
|
require(report.get("quickSwitchVisible") is False, "activation commit must close Quick Switch")
|
require(report.get("appCount") == 2, "activation fixture must expose two Apps")
|
require(report.get("windowCount") == 3, "activation fixture must expose three windows")
|
require(root.get("selectedAppGroupIndex") == expected_app_group_index, "selected App group must match activation target")
|
require(root.get("selectedWindowID") == expected_window_id, "selectedWindowID must match activation target")
|
require(report.get("lastCommittedWindowID") == expected_window_id, "commit window must match activation target")
|
require(report.get("lastCommitSource") == expected_source, "commit source must match expected path")
|
require(report.get("lastActivationWindowID") == expected_window_id, "activation window must match selected target")
|
require(report.get("lastActivationResult") == expected_result, "activation result must match expected path")
|
require(report.get("lastActivationError") is None, "debug activation should not record an error")
|
duration = report.get("lastActivationDurationMilliseconds")
|
require(isinstance(duration, (int, float)) and duration >= 0, "activation report must include non-negative duration")
|
require(len(selected_cards) == 1, "activation fixture must expose exactly one selected card")
|
require(selected_cards[0].get("windowID") == expected_window_id, "selected card must match activation target")
|
require(selected_cards[0].get("isMinimized") is expected_minimized, "selected card minimized flag must match activation target")
|
|
print(json.dumps({
|
"commitSource": report.get("lastCommitSource"),
|
"activationResult": report.get("lastActivationResult"),
|
"activationDurationMilliseconds": report.get("lastActivationDurationMilliseconds"),
|
"selectedWindowID": root.get("selectedWindowID"),
|
"quickSwitchVisible": report.get("quickSwitchVisible"),
|
"selectedIsMinimized": selected_cards[0].get("isMinimized")
|
}, indent=2, ensure_ascii=False))
|
PY
|
}
|
|
assert_activation_failure_suppression_report() {
|
/usr/bin/python3 - "$FAILURE_REPORT" <<'PY'
|
import json
|
import sys
|
|
path = sys.argv[1]
|
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
|
def require(condition, message):
|
if not condition:
|
print(message, file=sys.stderr)
|
print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
|
root = report.get("rootView", {})
|
cards = [
|
card
|
for column in root.get("waterfallColumns", [])
|
for card in column.get("cards", [])
|
]
|
visible_window_ids = [card.get("windowID") for card in cards]
|
|
require(report.get("snapshotLoaded") is True, "activation failure snapshotLoaded must be true")
|
require(report.get("quickSwitchVisible") is False, "activation failure still hides Quick Switch after commit")
|
require(report.get("lastActivationResult") == "activationFailed", "forced activation failure must be reported")
|
require(report.get("lastActivationWindowID") == 40100, "forced activation failure must target the clicked window")
|
require(report.get("suppressedActivationWindowCount") == 1, "activation failure must suppress exactly one target")
|
require(40100 in report.get("suppressedActivationWindowIDs", []), "failed windowID must be recorded as suppressed")
|
require(40100 not in visible_window_ids, "failed activation target must be removed from the projected Waterfall")
|
require(report.get("windowCount") == 2, "projected view must keep remaining windows only")
|
require(report.get("suppressedCloseTargetCount") == 0, "activation suppression must not use close suppression")
|
|
print(json.dumps({
|
"activationResult": report.get("lastActivationResult"),
|
"lastActivationWindowID": report.get("lastActivationWindowID"),
|
"suppressedActivationWindowIDs": report.get("suppressedActivationWindowIDs"),
|
"visibleWindowIDs": visible_window_ids
|
}, indent=2, ensure_ascii=False))
|
PY
|
}
|
|
assert_activation_budget_source
|
stop_current_aligner
|
"$SCRIPT_DIR/package-app.sh" >&2
|
rm -f "$MINIMIZED_REPORT" "$NORMAL_REPORT" "$APP_ONLY_REPORT" "$FAILURE_REPORT"
|
|
"$APP/Contents/MacOS/Aligner" \
|
--round0-skip-permissions \
|
--round01-open-quick-switch \
|
--round01-waterfall-view-mode=vertical \
|
--round01-fixture-window-activation \
|
--round01-disable-screenshot-refresh \
|
--round01-debug-window-activation \
|
--round01-debug-key-sequence="2,down,enter" \
|
--round01-quick-switch-report="$MINIMIZED_REPORT" &
|
|
APP_PID=$!
|
|
cleanup() {
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
stop_current_aligner
|
}
|
trap cleanup EXIT
|
|
wait_for_activation_report "$MINIMIZED_REPORT"
|
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
|
assert_activation_report "$MINIMIZED_REPORT" "keyboard" "restored" 1 40200 true
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
stop_current_aligner
|
|
"$APP/Contents/MacOS/Aligner" \
|
--round0-skip-permissions \
|
--round01-open-quick-switch \
|
--round01-waterfall-view-mode=vertical \
|
--round01-fixture-window-activation \
|
--round01-disable-screenshot-refresh \
|
--round01-debug-window-activation \
|
--round01-debug-mouse-sequence="click-card:0:1" \
|
--round01-quick-switch-report="$NORMAL_REPORT" &
|
|
APP_PID=$!
|
wait_for_activation_report "$NORMAL_REPORT"
|
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
|
assert_activation_report "$NORMAL_REPORT" "mouse" "activated" 0 40100 false
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
stop_current_aligner
|
|
"$APP/Contents/MacOS/Aligner" \
|
--round0-skip-permissions \
|
--round01-open-quick-switch \
|
--round01-waterfall-view-mode=vertical \
|
--round01-fixture-window-activation \
|
--round01-disable-screenshot-refresh \
|
--round01-debug-window-activation \
|
--round01-debug-mouse-sequence="click-card:0:0" \
|
--round01-quick-switch-report="$APP_ONLY_REPORT" &
|
|
APP_PID=$!
|
wait_for_activation_report "$APP_ONLY_REPORT"
|
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
|
assert_activation_report "$APP_ONLY_REPORT" "mouse" "appActivatedOnly" 0 40110 false
|
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
stop_current_aligner
|
|
ALIGNER_DEBUG_WINDOW_ACTIVATION_RESULT=activationFailed "$APP/Contents/MacOS/Aligner" \
|
--round0-skip-permissions \
|
--round01-open-quick-switch \
|
--round01-waterfall-view-mode=vertical \
|
--round01-fixture-window-activation \
|
--round01-disable-screenshot-refresh \
|
--round01-debug-window-activation \
|
--round01-debug-mouse-sequence="click-card:0:1" \
|
--round01-quick-switch-report="$FAILURE_REPORT" &
|
|
APP_PID=$!
|
wait_for_activation_report "$FAILURE_REPORT"
|
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
|
assert_activation_failure_suppression_report
|
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
trap - EXIT
|
stop_current_aligner
|