#!/bin/bash # Round01 Quick Switch screenshot refill QA. It opens the real Quick Switch # session and waits until screenshot-eligible cards are resolved to either real # screenshots or explicit skeleton fallbacks. 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-screenshot-session-report.json" OPEN_WAIT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_OPEN_WAIT:-1.2}" REPORT_WAIT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_REPORT_WAIT:-8.0}" SCREENSHOT_WAIT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_WAIT:-45.0}" SKIP_OVERLAY_FRAME_CHECK="${ALIGNER_ROUND1_SCREENSHOT_SESSION_SKIP_OVERLAY_FRAME_CHECK:-0}" REQUIRE_REAL_SCREENSHOT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_REQUIRE_REAL:-1}" fail() { echo "Round01 screenshot session 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" } wait_for_loaded_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("snapshotLoaded") is True: 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"snapshot report did not become loaded within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } wait_for_screenshots() { /usr/bin/python3 - "$REPORT" "$SCREENSHOT_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 root = report.get("rootView", {}) eligible = root.get("screenshotEligibleCount", 0) resolved = root.get("screenshotResolvedCount", 0) pending = root.get("screenshotPendingCount", 0) if report.get("snapshotLoaded") is True and eligible > 0 and resolved == eligible and pending == 0: sys.exit(0) except FileNotFoundError: pass except json.JSONDecodeError: pass time.sleep(0.25) if last_report is not None: root = last_report.get("rootView", {}) print(json.dumps({ "snapshotLoaded": last_report.get("snapshotLoaded"), "windowCount": last_report.get("windowCount"), "screenshotEligibleCount": root.get("screenshotEligibleCount"), "screenshotResolvedCount": root.get("screenshotResolvedCount"), "screenshotPendingCount": root.get("screenshotPendingCount"), "realScreenshotCount": root.get("realScreenshotCount"), "skeletonFallbackCount": root.get("skeletonFallbackCount"), }, indent=2, ensure_ascii=False), file=sys.stderr) print(f"screenshot refill did not resolve eligible cards within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } assert_report() { /usr/bin/python3 - "$REPORT" "$REQUIRE_REAL_SCREENSHOT" <<'PY' import json import os import sys path = sys.argv[1] require_real_screenshot = sys.argv[2] == "1" with open(path, "r", encoding="utf-8") as file: report = json.load(file) def stable_fingerprint(value): hash_value = 0xCBF29CE484222325 for byte in (value or "").encode("utf-8"): hash_value ^= byte hash_value = (hash_value * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF return f"{hash_value:016x}" def summary(): root = report.get("rootView", {}) return { "reportBasename": os.path.basename(path), "reportPathHash": stable_fingerprint(path), "snapshotLoaded": report.get("snapshotLoaded"), "quickSwitchVisible": report.get("quickSwitchVisible"), "windowCount": report.get("windowCount"), "eligible": root.get("screenshotEligibleCount"), "resolved": root.get("screenshotResolvedCount"), "pending": root.get("screenshotPendingCount"), "real": root.get("realScreenshotCount"), "fallback": root.get("skeletonFallbackCount"), "notRequested": root.get("screenshotNotRequestedCount"), } def require(condition, message): if not condition: print(message, file=sys.stderr) print(json.dumps(summary(), indent=2, ensure_ascii=False), file=sys.stderr) sys.exit(1) root = report.get("rootView", {}) columns = root.get("waterfallColumns", []) cards = [card for column in columns for card in column.get("cards", [])] eligible = root.get("screenshotEligibleCount") resolved = root.get("screenshotResolvedCount") pending = root.get("screenshotPendingCount") not_requested = root.get("screenshotNotRequestedCount") real = root.get("realScreenshotCount") fallback = root.get("skeletonFallbackCount") require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") require(report.get("quickSwitchVisible") is True, "Quick Switch must stay visible during screenshot QA") require(cards, "screenshot QA requires at least one Window Card") require(isinstance(eligible, int) and eligible > 0, "screenshot QA requires at least one eligible card") require(isinstance(resolved, int) and resolved == eligible, "all eligible screenshots must be resolved") require(pending == 0, "no screenshot-eligible card may remain pending") require(isinstance(not_requested, int), "root view must expose screenshotNotRequestedCount") require(eligible + not_requested == len(cards), "eligible + not-requested cards must cover all cards") if require_real_screenshot: require(isinstance(real, int) and real >= 1, "Screen Recording authorized QA must produce at least one real screenshot") else: require(isinstance(real, int), "root view must expose realScreenshotCount") require(isinstance(fallback, int), "root view must expose skeletonFallbackCount") require(real + fallback == resolved, "real + fallback screenshots must equal resolved screenshots") for card in cards: source = card.get("screenshotSource") strategy = card.get("thumbnailStrategy") require(source in {"realScreenshot", "skeletonFallback", "notRequested"}, "screenshot source must be resolved or explicitly not requested") require(source != "pending", "no card may remain pending after refill wait") if source == "realScreenshot": require(strategy == "realScreenshot", "real screenshot cards must expose realScreenshot thumbnail strategy") if source == "skeletonFallback": reason = card.get("screenshotFallbackReason") require(reason is not None, "skeleton fallback cards must expose fallback reason") if card.get("identifierSource") == "syntheticAX": require("syntheticWindowID" in reason, "synthetic AX cards must fallback with syntheticWindowID reason") if source == "notRequested": reason = card.get("screenshotNotRequestedReason") require(reason is not None, "not-requested cards must expose a not-requested reason") require(strategy in {"skeleton", "skeletonPreferred"}, "not-requested cards must remain skeleton based") print(json.dumps({ "windowCount": report.get("windowCount"), "eligible": eligible, "resolved": resolved, "real": real, "fallback": fallback, "notRequested": not_requested, }, indent=2, ensure_ascii=False)) PY } stop_current_aligner "$SCRIPT_DIR/package-app.sh" >&2 rm -f "$REPORT" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-quick-switch-report="$REPORT" & APP_PID=$! cleanup() { if kill -0 "$APP_PID" 2>/dev/null; then kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true fi stop_current_aligner } trap cleanup EXIT sleep "$OPEN_WAIT" wait_for_loaded_report if [ "$SKIP_OVERLAY_FRAME_CHECK" = "1" ]; then swift "$SCRIPT_DIR/window-logic-qa.swift" \ --expect-quick-switch \ --expect-single-aligner-overlay else swift "$SCRIPT_DIR/window-logic-qa.swift" \ --expect-quick-switch \ --expect-single-aligner-overlay \ --expect-overlay-on-mouse-screen \ --expect-overlay-uses-screen-frame fi wait_for_screenshots assert_report cleanup trap - EXIT swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch