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