#!/bin/bash
|
# Round01 candidate filtering fixture QA. It uses a deterministic snapshot
|
# containing accepted and rejected window records, then verifies that only the
|
# accepted candidates reach the Quick Switch UI.
|
|
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-candidate-filter-fixture-report.json"
|
REPORT_WAIT="${ALIGNER_ROUND1_CANDIDATE_FILTER_REPORT_WAIT:-6.0}"
|
|
fail() {
|
echo "Round01 candidate filter 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"
|
}
|
|
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"candidate filter report did not become loaded within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
assert_report() {
|
/usr/bin/python3 - "$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", {})
|
columns = root.get("waterfallColumns", [])
|
cards = [card for column in columns for card in column.get("cards", [])]
|
def stable_fingerprint(value):
|
text = value or ""
|
hash_value = 0xCBF29CE484222325
|
for byte in text.encode("utf-8"):
|
hash_value ^= byte
|
hash_value = (hash_value * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
|
return f"{hash_value:016x}"
|
|
title_hashes = {card.get("titleHash") for card in cards}
|
expected_titles = {
|
"Standard Candidate",
|
"Minimized Candidate",
|
"Fullscreen Candidate",
|
"Sheet Candidate",
|
"Dialog Candidate",
|
"Candidate Standard Untitled",
|
"CG-only Space-backed Candidate",
|
}
|
rejected_titles = {
|
"Accessory Rejected",
|
"Hidden Rejected",
|
"Tiny Rejected",
|
"Ghost Rejected",
|
"Popover Rejected",
|
"Desktop",
|
"Security Confirmation",
|
"Noninteractive Rejected",
|
}
|
expected_title_hashes = {stable_fingerprint(title) for title in expected_titles}
|
rejected_title_hashes = {stable_fingerprint(title) for title in rejected_titles}
|
|
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
|
require(report.get("windowCount") == len(expected_titles), "candidate fixture must expose only accepted windows")
|
require(len(cards) == len(expected_titles), "Waterfall cards must match accepted candidate count")
|
require(title_hashes == expected_title_hashes, "Waterfall must include exactly the accepted candidate title hashes")
|
require(title_hashes.isdisjoint(rejected_title_hashes), "Waterfall must not include rejected candidate title hashes")
|
require(len(columns) == 1, "candidate fixture should group accepted candidates under one app")
|
require(root.get("screenshotNotRequestedCount") == len(expected_titles), "candidate fixture must disable screenshot refresh")
|
|
by_title_hash = {card.get("titleHash"): card for card in cards}
|
minimized = by_title_hash[stable_fingerprint("Minimized Candidate")]
|
fullscreen = by_title_hash[stable_fingerprint("Fullscreen Candidate")]
|
sheet = by_title_hash[stable_fingerprint("Sheet Candidate")]
|
dialog = by_title_hash[stable_fingerprint("Dialog Candidate")]
|
untitled = by_title_hash[stable_fingerprint("Candidate Standard Untitled")]
|
require(minimized.get("isMinimized") is True, "minimized candidate must remain marked minimized")
|
require(minimized.get("identifierSource") == "syntheticAX", "minimized fixture must expose syntheticAX source")
|
require(fullscreen.get("isFullscreen") is True, "fullscreen candidate must remain marked fullscreen")
|
require("fullscreen" in fullscreen.get("visualStates", []), "fullscreen candidate must expose fullscreen visual state")
|
require(sheet.get("isMinimized") is False, "sheet candidate must be a normal selectable card")
|
require(dialog.get("isMinimized") is False, "dialog candidate must be a normal selectable card")
|
require(untitled.get("titleHash") == stable_fingerprint("Candidate Standard Untitled"), "untitled windows must use App name + Untitled fallback")
|
|
print(json.dumps({
|
"windowCount": report.get("windowCount"),
|
"acceptedTitleHashes": sorted(title_hashes),
|
"rejectedTitleHashesAbsent": sorted(rejected_title_hashes),
|
"notRequested": root.get("screenshotNotRequestedCount"),
|
}, 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-fixture-candidate-filtering \
|
--round01-disable-screenshot-refresh \
|
--round01-quick-switch-report="$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_loaded_report
|
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-quick-switch >&2
|
assert_report
|
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
sleep 0.5
|
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
|
trap - EXIT
|
stop_current_aligner
|