#!/bin/bash # Real-desktop QA for Obsidian windows that share one process but live on # different fullscreen Spaces. This catches WindowServer "activated" results # that do not actually switch to the clicked window's Space. 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" RUN_ID="$(date +%Y%m%d_%H%M%S)" REPORT_DIR="$BUILD_REPORT_ROOT/round01-obsidian-space-activation-$RUN_ID" fail() { echo "Round01 Obsidian Space activation 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" } set_current_space_for_target() { local target_space_id="$1" swift - "$target_space_id" <<'SWIFT' import Foundation import Darwin typealias MainConnection = @convention(c) () -> UInt32 typealias CopySpaces = @convention(c) (UInt32) -> Unmanaged? typealias SetCurrentSpace = @convention(c) (UInt32, CFString, UInt64) -> Void func symbol(_ name: String, in handle: UnsafeMutableRawPointer) -> T? { guard let raw = dlsym(handle, name) else { return nil } return unsafeBitCast(raw, to: T.self) } func spaceID(_ record: [String: Any]) -> UInt64? { if let number = record["id64"] as? NSNumber { return number.uint64Value } if let number = record["id"] as? NSNumber { return number.uint64Value } return nil } let targetSpaceID = UInt64(CommandLine.arguments[1])! guard let handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY), let mainConnection: MainConnection = symbol("CGSMainConnectionID", in: handle), let copySpaces: CopySpaces = symbol("CGSCopyManagedDisplaySpaces", in: handle), let setCurrentSpace: SetCurrentSpace = symbol("CGSManagedDisplaySetCurrentSpace", in: handle), let displayRecords = copySpaces(mainConnection())?.takeRetainedValue() as? [[String: Any]] else { fputs("SkyLight Space APIs unavailable\n", stderr) exit(2) } for displayRecord in displayRecords { let displayIdentifier = displayRecord["Display Identifier"] as? String ?? "" let spaces = displayRecord["Spaces"] as? [[String: Any]] ?? [] guard spaces.contains(where: { spaceID($0) == targetSpaceID }) else { continue } guard !displayIdentifier.isEmpty else { fputs("display identifier missing for target Space\n", stderr) exit(3) } setCurrentSpace(mainConnection(), displayIdentifier as CFString, targetSpaceID) Thread.sleep(forTimeInterval: 0.5) print(targetSpaceID) exit(0) } fputs("target Space not found: \(targetSpaceID)\n", stderr) exit(4) SWIFT } current_space_for_target_display() { local target_space_id="$1" swift - "$target_space_id" <<'SWIFT' import Foundation import Darwin typealias MainConnection = @convention(c) () -> UInt32 typealias CopySpaces = @convention(c) (UInt32) -> Unmanaged? func symbol(_ name: String, in handle: UnsafeMutableRawPointer) -> T? { guard let raw = dlsym(handle, name) else { return nil } return unsafeBitCast(raw, to: T.self) } func spaceID(_ record: [String: Any]) -> UInt64? { if let number = record["id64"] as? NSNumber { return number.uint64Value } if let number = record["id"] as? NSNumber { return number.uint64Value } return nil } let targetSpaceID = UInt64(CommandLine.arguments[1])! guard let handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY), let mainConnection: MainConnection = symbol("CGSMainConnectionID", in: handle), let copySpaces: CopySpaces = symbol("CGSCopyManagedDisplaySpaces", in: handle), let displayRecords = copySpaces(mainConnection())?.takeRetainedValue() as? [[String: Any]] else { fputs("SkyLight Space APIs unavailable\n", stderr) exit(2) } for displayRecord in displayRecords { let spaces = displayRecord["Spaces"] as? [[String: Any]] ?? [] guard spaces.contains(where: { spaceID($0) == targetSpaceID }) else { continue } guard let currentSpace = displayRecord["Current Space"] as? [String: Any], let currentSpaceID = spaceID(currentSpace) else { fputs("current Space missing for target display\n", stderr) exit(3) } print(currentSpaceID) exit(0) } fputs("target Space not found: \(targetSpaceID)\n", stderr) exit(4) SWIFT } run_report() { local report="$1" local log="$2" stop_current_aligner ALIGNER_DIAGNOSTICS_INCLUDE_SENSITIVE=1 \ "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-disable-screenshot-refresh \ --round01-quick-switch-auto-hide-after=0.8 \ --round01-quick-switch-quit-after=1.2 \ --round01-quick-switch-report="$report" >"$log" 2>&1 } run_click() { local app_group_index="$1" local window_index="$2" local report="$3" local log="$4" stop_current_aligner ALIGNER_DIAGNOSTICS_INCLUDE_SENSITIVE=1 \ "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-disable-screenshot-refresh \ --round01-debug-mouse-sequence="click-card:$app_group_index:$window_index" \ --round01-quick-switch-quit-after=1.5 \ --round01-quick-switch-report="$report" >"$log" 2>&1 } extract_pair() { local report="$1" /usr/bin/python3 - "$report" <<'PY' import json import sys with open(sys.argv[1], "r", encoding="utf-8") as file: report = json.load(file) columns = [ column for column in report.get("rootView", {}).get("waterfallColumns", []) if column.get("bundleIdentifier") == "md.obsidian" ] if not columns: print("Obsidian app group not found", file=sys.stderr) sys.exit(2) cards = [ card for card in columns[0].get("cards", []) if card.get("primarySpaceID") is not None ] fullscreen_cards = [card for card in cards if card.get("isFullscreen") is True] pool = fullscreen_cards if len({card.get("primarySpaceID") for card in fullscreen_cards}) >= 2 else cards for source in pool: for target in pool: if source.get("windowID") == target.get("windowID"): continue if source.get("primarySpaceID") == target.get("primarySpaceID"): continue print(f"SOURCE_WINDOW_ID={source['windowID']}") print(f"SOURCE_SPACE_ID={source['primarySpaceID']}") print(f"TARGET_WINDOW_ID={target['windowID']}") print(f"TARGET_SPACE_ID={target['primarySpaceID']}") sys.exit(0) print("Obsidian needs two candidate windows on distinct Spaces", file=sys.stderr) sys.exit(3) PY } find_card_indices() { local report="$1" local window_id="$2" /usr/bin/python3 - "$report" "$window_id" <<'PY' import json import sys with open(sys.argv[1], "r", encoding="utf-8") as file: report = json.load(file) target_window_id = int(sys.argv[2]) for column in report.get("rootView", {}).get("waterfallColumns", []): if column.get("bundleIdentifier") != "md.obsidian": continue for card in column.get("cards", []): if card.get("windowID") == target_window_id: print(f"APP_GROUP_INDEX={column['appGroupIndex']}") print(f"WINDOW_INDEX={card['windowIndex']}") print(f"WINDOW_SPACE_ID={card['primarySpaceID']}") sys.exit(0) print(f"Obsidian target window not found: {target_window_id}", file=sys.stderr) sys.exit(4) PY } assert_click_report() { local report="$1" local target_window_id="$2" /usr/bin/python3 - "$report" "$target_window_id" <<'PY' import json import sys with open(sys.argv[1], "r", encoding="utf-8") as file: report = json.load(file) target_window_id = int(sys.argv[2]) def require(condition, message): if not condition: print(message, file=sys.stderr) print(json.dumps({ "lastActivationWindowID": report.get("lastActivationWindowID"), "lastActivationResult": report.get("lastActivationResult"), "lastCommittedWindowID": report.get("lastCommittedWindowID"), }, indent=2, ensure_ascii=False), file=sys.stderr) sys.exit(5) require(report.get("lastCommittedWindowID") == target_window_id, "committed window must match target") require(report.get("lastActivationWindowID") == target_window_id, "activation window must match target") require(report.get("lastActivationResult") == "activated", "activation result must be activated") PY } run_direction() { local source_space_id="$1" local target_window_id="$2" local target_space_id="$3" local label="$4" local before_report="$REPORT_DIR/$label-before.json" local before_log="$REPORT_DIR/$label-before.log" local click_report="$REPORT_DIR/$label-click.json" local click_log="$REPORT_DIR/$label-click.log" local current_space_id set_current_space_for_target "$source_space_id" >/dev/null run_report "$before_report" "$before_log" eval "$(find_card_indices "$before_report" "$target_window_id")" [ "$WINDOW_SPACE_ID" = "$target_space_id" ] || fail "$label target card Space changed: expected $target_space_id, got $WINDOW_SPACE_ID" run_click "$APP_GROUP_INDEX" "$WINDOW_INDEX" "$click_report" "$click_log" assert_click_report "$click_report" "$target_window_id" sleep 0.8 current_space_id="$(current_space_for_target_display "$target_space_id")" [ "$current_space_id" = "$target_space_id" ] || fail "$label did not switch to target Space: expected $target_space_id, got $current_space_id" echo "PASS $label targetWindow=$target_window_id targetSpace=$target_space_id" } mkdir -p "$REPORT_DIR" INITIAL_REPORT="$REPORT_DIR/initial.json" INITIAL_LOG="$REPORT_DIR/initial.log" run_report "$INITIAL_REPORT" "$INITIAL_LOG" eval "$(extract_pair "$INITIAL_REPORT")" run_direction "$SOURCE_SPACE_ID" "$TARGET_WINDOW_ID" "$TARGET_SPACE_ID" "forward" run_direction "$TARGET_SPACE_ID" "$SOURCE_WINDOW_ID" "$SOURCE_SPACE_ID" "reverse" stop_current_aligner cat <