| | |
| | | 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() { |
| | |
| | | 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() { |
| | |
| | | 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") |
| | |
| | | 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" |
| | | rm -f "$MINIMIZED_REPORT" "$NORMAL_REPORT" "$APP_ONLY_REPORT" "$FAILURE_REPORT" |
| | | |
| | | "$APP/Contents/MacOS/Aligner" \ |
| | | --round0-skip-permissions \ |
| | |
| | | |
| | | 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 |