Ariver
2026-06-13 6983a4ed2137bf1a8377e5d1cf82183e02e617f8
Fix multi-page targeting and split view tile QA
9 files modified
1 files added
555 ■■■■■ changed files
C1.source/Resources/Aligner-Info.plist 4 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/AlignerApplicationDelegate.swift 4 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift 197 ●●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchRootView.swift 45 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchSessionController.swift 2 ●●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchSnapshotLoader.swift 53 ●●●●● patch | view | raw | blame | history
C3.tools/round1-app-column-alignment-fixture-qa.sh 8 ●●●●● patch | view | raw | blame | history
C3.tools/round1-main-ui-qa.sh 4 ●●● patch | view | raw | blame | history
C3.tools/round1-multi-page-identity-fixture-qa.sh 233 ●●●●● patch | view | raw | blame | history
C3.tools/round1-space-lane-split-view-fixture-qa.sh 5 ●●●●● patch | view | raw | blame | history
C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>0.0.62</string>
    <string>0.0.63</string>
    <key>CFBundleVersion</key>
    <string>20260612.2049</string>
    <string>20260613.1401</string>
    <key>LSMinimumSystemVersion</key>
    <string>26.0</string>
    <key>NSHighResolutionCapable</key>
C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
@@ -666,6 +666,10 @@
            return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .splitView)
        }
        if round1QuickSwitchOptions.fixtureMultiPageIdentity {
            return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .multiPageIdentity)
        }
        if let fixtureAppCount = round1QuickSwitchOptions.fixtureAppCount {
            return FixtureQuickSwitchSnapshotLoader(
                appCount: fixtureAppCount,
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -162,16 +162,12 @@
        let privateActivationOutcome = activateViaPrivateWindowServerAPI(window)
        let didPrivatelyActivate = privateActivationOutcome?.succeeded == true
        let axWindow = AXWindowMetadataReader
            .metadata(appCategorizer: appCategorizer)
            .first { metadata in
                if let windowID = metadata.windowID {
                    return windowID == window.id
                }
                return metadata.processIdentifier == window.app.processIdentifier
                    && metadata.title == window.title
            }
        let axWindow = axWindow(
            for: window,
            in: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer),
            operation: "windowActivation",
            matchAttempt: "initial"
        )
        DevelopmentDiagnostics.log("windowActivation.activate.axLookup", [
            "windowID": window.id,
@@ -273,6 +269,15 @@
        var matchedAXWindow = axWindow(for: window, in: axMetadata)
        var finalAXMetadata = axMetadata
        let shouldPreferFocusedPageClose = matchedAXWindow?.windowID != window.id
        if shouldPreferFocusedPageClose,
           let focusedPageCloseResult = closeFocusedPageViaCommandW(
            window: window,
            reason: matchedAXWindow == nil ? "axMissing" : "nonDirectAXMatch"
           ) {
            return focusedPageCloseResult
        }
        if matchedAXWindow == nil {
            Thread.sleep(forTimeInterval: 0.12)
            finalAXMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
@@ -289,6 +294,14 @@
                "matchingSizeCandidateCount": matchingAXSizeCandidateCount(for: window, in: finalAXMetadata)
            ])
            return .windowNotFound
        }
        if matchedAXWindow.windowID != window.id,
           let focusedPageCloseResult = closeFocusedPageViaCommandW(
            window: window,
            reason: "retryNonDirectAXMatch"
           ) {
            return focusedPageCloseResult
        }
        guard let closeButton = closeButton(for: matchedAXWindow) else {
@@ -364,13 +377,82 @@
        }
    }
    private func closeFocusedPageViaCommandW(
        window: AlignerWindow,
        reason: String
    ) -> WindowCloseResult? {
        guard window.identifierSource == .cgWindow,
              let processIdentifier = window.app.processIdentifier
        else {
            DevelopmentDiagnostics.log("windowClose.commandW.skipped", [
                "windowID": window.id,
                "reason": reason,
                "identifierSource": String(describing: window.identifierSource),
                "hasPID": window.app.processIdentifier != nil
            ])
            return nil
        }
        guard let activationOutcome = activateViaPrivateWindowServerAPI(window),
              activationOutcome.succeeded
        else {
            DevelopmentDiagnostics.log("windowClose.commandW.activationFailed", [
                "windowID": window.id,
                "pid": processIdentifier,
                "reason": reason
            ])
            return nil
        }
        Thread.sleep(forTimeInterval: 0.08)
        guard postCommandW(to: processIdentifier) else {
            DevelopmentDiagnostics.log("windowClose.commandW.postFailed", [
                "windowID": window.id,
                "pid": processIdentifier,
                "reason": reason
            ])
            return .failed("commandWPostFailed")
        }
        DevelopmentDiagnostics.log("windowClose.commandW.requested", [
            "windowID": window.id,
            "pid": processIdentifier,
            "reason": reason
        ])
        return .requested
    }
    private func postCommandW(to processIdentifier: Int32) -> Bool {
        let source = CGEventSource(stateID: .combinedSessionState)
        guard let keyDown = CGEvent(
            keyboardEventSource: source,
            virtualKey: Self.commandWVirtualKeyCode,
            keyDown: true
        ),
              let keyUp = CGEvent(
                keyboardEventSource: source,
                virtualKey: Self.commandWVirtualKeyCode,
                keyDown: false
              )
        else {
            return false
        }
        keyDown.flags = .maskCommand
        keyUp.flags = .maskCommand
        keyDown.postToPid(processIdentifier)
        keyUp.postToPid(processIdentifier)
        return true
    }
    private func axWindow(
        for window: AlignerWindow,
        in axMetadata: [AXWindowMetadata],
        operation: String = "windowClose",
        matchAttempt: String = "initial"
    ) -> AXWindowMetadata? {
        if let directMatch = axMetadata.first(where: { $0.windowID == window.id }) {
            DevelopmentDiagnostics.log("windowClose.axMatch.directWindowID", [
            DevelopmentDiagnostics.log("\(operation).axMatch.directWindowID", [
                "windowID": window.id,
                "pid": window.app.processIdentifier,
                "attempt": matchAttempt
@@ -382,12 +464,35 @@
            return nil
        }
        if let geometryFingerprint = axWindowGeometryFingerprint(for: window) {
            let geometryMatches = axMetadata.filter { metadata in
                AXWindowGeometryFingerprint(metadata) == geometryFingerprint
            }
            if geometryMatches.count == 1 {
                DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprint", [
                    "windowID": window.id,
                    "pid": processIdentifier,
                    "attempt": matchAttempt
                ])
                return geometryMatches[0]
            }
            if geometryMatches.count > 1 {
                DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprintAmbiguous", [
                    "windowID": window.id,
                    "pid": processIdentifier,
                    "candidateCount": geometryMatches.count,
                    "attempt": matchAttempt
                ])
            }
        }
        if let cgFingerprint = cgWindowFingerprint(for: window) {
            let fingerprintMatches = axMetadata.filter { metadata in
                AXWindowFingerprint(metadata) == cgFingerprint
            }
            if fingerprintMatches.count == 1 {
                DevelopmentDiagnostics.log("windowClose.axMatch.fingerprint", [
                DevelopmentDiagnostics.log("\(operation).axMatch.fingerprint", [
                    "windowID": window.id,
                    "pid": processIdentifier,
                    "attempt": matchAttempt
@@ -396,10 +501,11 @@
            }
            if fingerprintMatches.count > 1 {
                DevelopmentDiagnostics.log("windowClose.axMatch.fingerprintAmbiguous", [
                DevelopmentDiagnostics.log("\(operation).axMatch.fingerprintAmbiguous", [
                    "windowID": window.id,
                    "pid": processIdentifier,
                    "candidateCount": fingerprintMatches.count
                    "candidateCount": fingerprintMatches.count,
                    "attempt": matchAttempt
                ])
            }
        }
@@ -409,8 +515,8 @@
            metadata.processIdentifier == processIdentifier
                && normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
        }
        if titleMatches.count == 1 {
            DevelopmentDiagnostics.log("windowClose.axMatch.normalizedTitle", [
        if !normalizedTitle.isEmpty, titleMatches.count == 1 {
            DevelopmentDiagnostics.log("\(operation).axMatch.normalizedTitle", [
                "windowID": window.id,
                "pid": processIdentifier,
                "attempt": matchAttempt
@@ -424,7 +530,7 @@
            processIdentifier: processIdentifier,
            in: axMetadata
           ) {
            DevelopmentDiagnostics.log("windowClose.axMatch.finderFallback", [
            DevelopmentDiagnostics.log("\(operation).axMatch.finderFallback", [
                "windowID": window.id,
                "pid": processIdentifier,
                "attempt": matchAttempt
@@ -433,10 +539,11 @@
        }
        if titleMatches.count > 1 {
            DevelopmentDiagnostics.log("windowClose.axMatch.titleAmbiguous", [
            DevelopmentDiagnostics.log("\(operation).axMatch.titleAmbiguous", [
                "windowID": window.id,
                "pid": processIdentifier,
                "candidateCount": titleMatches.count
                "candidateCount": titleMatches.count,
                "attempt": matchAttempt
            ])
        }
@@ -475,6 +582,29 @@
            title: normalizedWindowTitle(title(rawWindow: rawWindow, axWindow: nil)),
            size: bounds(rawWindow[kCGWindowBounds as String]).size
        )
    }
    private func axWindowGeometryFingerprint(for window: AlignerWindow) -> AXWindowGeometryFingerprint? {
        guard let processIdentifier = window.app.processIdentifier else { return nil }
        let frame = cgWindowFrame(for: window) ?? window.frame
        return AXWindowGeometryFingerprint(
            processIdentifier: processIdentifier,
            title: normalizedWindowTitle(window.title),
            frame: frame
        )
    }
    private func cgWindowFrame(for window: AlignerWindow) -> CGRect? {
        guard let rawWindows = CGWindowListCopyWindowInfo(
            [.optionIncludingWindow],
            CGWindowID(window.id)
        ) as? [[String: Any]],
              let rawWindow = rawWindows.first
        else {
            return nil
        }
        return bounds(rawWindow[kCGWindowBounds as String])
    }
    private func matchingAXTitleCandidateCount(
@@ -984,6 +1114,7 @@
        }
    }
    private static let commandWVirtualKeyCode: CGKeyCode = 13
}
private struct AXWindowMetadata {
@@ -1053,6 +1184,34 @@
    }
}
private struct AXWindowGeometryFingerprint: Hashable {
    let processIdentifier: Int32
    let title: String
    let x: Int
    let y: Int
    let width: Int
    let height: Int
    init?(processIdentifier: Int32, title: String, frame: CGRect?) {
        guard let frame else { return nil }
        self.processIdentifier = processIdentifier
        self.title = title
        self.x = Int(frame.origin.x.rounded())
        self.y = Int(frame.origin.y.rounded())
        self.width = Int(frame.size.width.rounded())
        self.height = Int(frame.size.height.rounded())
    }
    init?(_ metadata: AXWindowMetadata) {
        self.init(
            processIdentifier: metadata.processIdentifier,
            title: normalizedWindowTitle(metadata.title ?? ""),
            frame: metadata.frame
        )
    }
}
private struct RestorableAXWindow {
    let processIdentifier: Int32
    let element: AXUIElement
C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -3087,13 +3087,16 @@
    private func layoutSpaceLaneSegmentContents(_ segment: SpaceLaneSegmentLayers) {
        let bounds = segment.containerLayer.bounds
        let splitAppNames = splitViewAppNames(for: segment.space)
        let isSplitView = splitAppNames.count >= 2
        segment.labelLayer.frame = CGRect(x: 10, y: bounds.height - 29, width: max(1, bounds.width - 48), height: 20)
        segment.countLayer.frame = CGRect(x: bounds.width - 34, y: bounds.height - 29, width: 24, height: 20)
        segment.countLayer.string = isSplitView ? "" : "\(segment.space.windowCount)"
        segment.countLayer.isHidden = isSplitView
        if segment.space.type == .fullscreen {
            let markerFrame = fullscreenMarkerFrame(in: bounds)
            let splitAppNames = splitViewAppNames(for: segment.space)
            segment.appLayer.isHidden = splitAppNames.count >= 2
            if splitAppNames.count >= 2 {
            segment.appLayer.isHidden = isSplitView
            if isSplitView {
                let labelY = markerFrame.midY - 8.5
                let halfWidth = markerFrame.width / 2
                for (index, splitAppLayer) in segment.splitAppLayers.enumerated() {
@@ -3128,7 +3131,7 @@
            layoutFullscreenMarkerLayer(
                fullscreenMarkerLayer,
                in: bounds,
                isSplitView: splitViewAppNames(for: segment.space).count >= 2
                isSplitView: isSplitView
            )
        }
@@ -4740,17 +4743,11 @@
        let bounds = shapeLayer.bounds
        if isSplitView {
            let insetBounds = bounds.insetBy(dx: 0.6, dy: 0.6)
            let radius = min(7, max(5, insetBounds.height * 0.22))
            let path = CGMutablePath()
            path.addRoundedRect(
                in: insetBounds,
                cornerWidth: radius,
                cornerHeight: radius
            )
            path.move(to: CGPoint(x: bounds.midX, y: insetBounds.minY + 1.5))
            path.addLine(to: CGPoint(x: bounds.midX, y: insetBounds.maxY - 1.5))
            path.move(to: CGPoint(x: bounds.midX, y: bounds.minY))
            path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
            shapeLayer.path = path
            shapeLayer.lineCap = .butt
            return
        }
@@ -4774,6 +4771,7 @@
        path.addLine(to: CGPoint(x: 0, y: bounds.maxY - cornerLength))
        shapeLayer.path = path
        shapeLayer.lineCap = .round
    }
    private func fullscreenMarkerFrame(in segmentBounds: CGRect) -> CGRect {
@@ -5176,6 +5174,10 @@
                        markerLayer: segment.fullscreenMarkerLayer,
                        splitAppNames: splitAppNames
                    ),
                    "fullscreenMarkerPathStyle": fullscreenMarkerPathStyle(
                        markerLayer: segment.fullscreenMarkerLayer,
                        splitAppNames: splitAppNames
                    ),
                    "fullscreenMarkerFrame": fullscreenMarkerFrame.map { dictionary(from: $0) } ?? NSNull(),
                    "fullscreenMarkerCenterRatio": fullscreenMarkerCenterRatio ?? NSNull(),
                    "splitViewAppNames": splitAppNames,
@@ -5187,6 +5189,8 @@
                    "windowBlockColors": segment.windowBlocks.map { colorDictionary(from: $0.backgroundColor) },
                    "windowBlockFrames": segment.windowBlocks.map { dictionary(from: $0.frame) },
                    "labelColor": colorDictionary(from: segment.labelLayer.foregroundColor),
                    "countText": string(from: segment.countLayer.string),
                    "countVisible": !segment.countLayer.isHidden && segment.countLayer.opacity > 0,
                    "countColor": colorDictionary(from: segment.countLayer.foregroundColor),
                    "appColor": colorDictionary(from: segment.appLayer.foregroundColor),
                    "fullscreenMarkerStrokeColor": colorDictionary(
@@ -5229,6 +5233,11 @@
    private func fullscreenMarkerKind(markerLayer: CALayer?, splitAppNames: [String]) -> String {
        guard markerLayer != nil else { return "none" }
        return splitAppNames.count >= 2 ? "splitViewPair" : "cornerBracket"
    }
    private func fullscreenMarkerPathStyle(markerLayer: CALayer?, splitAppNames: [String]) -> String {
        guard markerLayer != nil else { return "none" }
        return splitAppNames.count >= 2 ? "centerDividerOnly" : "cornerBracket"
    }
    private func visualStates(for space: QuickSwitchSpaceViewModel) -> [String] {
@@ -5282,6 +5291,16 @@
        ]
    }
    private func string(from textLayerValue: Any?) -> String {
        if let string = textLayerValue as? String {
            return string
        }
        if let attributedString = textLayerValue as? NSAttributedString {
            return attributedString.string
        }
        return ""
    }
    private func colorDictionary(from color: CGColor?) -> [String: Double] {
        guard let color,
              let nsColor = NSColor(cgColor: color)?.usingColorSpace(.deviceRGB)
C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -1479,6 +1479,7 @@
    let fixtureActivation: Bool
    let fixtureSpaceFilter: Bool
    let fixtureSplitView: Bool
    let fixtureMultiPageIdentity: Bool
    let debugHoveredAppGroupIndex: Int?
    let debugOverlayWidth: CGFloat?
    let debugKeySequence: [String]
@@ -1508,6 +1509,7 @@
            fixtureActivation: arguments.contains("--round01-fixture-window-activation"),
            fixtureSpaceFilter: arguments.contains("--round01-fixture-space-filter"),
            fixtureSplitView: arguments.contains("--round01-fixture-split-view"),
            fixtureMultiPageIdentity: arguments.contains("--round01-fixture-multi-page-identity"),
            debugHoveredAppGroupIndex: intValue(for: "--round01-debug-hover-app-index", in: arguments),
            debugOverlayWidth: cgFloatValue(for: "--round01-debug-overlay-width", in: arguments),
            debugKeySequence: stringListValue(for: "--round01-debug-key-sequence", in: arguments),
C1.source/Sources/Aligner/QuickSwitchSnapshotLoader.swift
@@ -17,6 +17,7 @@
    case activation
    case spaceFilter
    case splitView
    case multiPageIdentity
}
final class LiveQuickSwitchSnapshotLoader: QuickSwitchSnapshotLoading {
@@ -91,6 +92,9 @@
        }
        if mode == .splitView {
            return splitViewSnapshot()
        }
        if mode == .multiPageIdentity {
            return multiPageIdentitySnapshot()
        }
        return layoutSnapshot()
@@ -578,4 +582,53 @@
            currentSpaceIDs: [1]
        )
    }
    private func multiPageIdentitySnapshot() -> QuickSwitchSnapshotLoad {
        let displayUUID = "fixture-display-a"
        let displays = [
            AlignerDisplay(
                uuid: displayUUID,
                physical: true,
                spaces: [
                    AlignerSpace(id: 1, type: .user, displayUUID: displayUUID, index: 1)
                ]
            )
        ]
        let app = AlignerApp(
            bundleIdentifier: "com.example.multipage.identity",
            name: "Multi Page App",
            category: .generic,
            processIdentifier: 70_001
        )
        let windows = [
            AlignerWindow(
                id: 70_101,
                app: app,
                title: "Shared Page",
                frame: CGRect(x: 120, y: 120, width: 920, height: 640),
                spaceIDs: [1]
            ),
            AlignerWindow(
                id: 70_102,
                app: app,
                title: "Shared Page",
                frame: CGRect(x: 160, y: 140, width: 920, height: 640),
                spaceIDs: [1]
            ),
            AlignerWindow(
                id: 70_103,
                app: app,
                title: "Shared Page",
                frame: CGRect(x: 200, y: 160, width: 920, height: 640),
                spaceIDs: [1]
            )
        ]
        return QuickSwitchSnapshotLoad(
            snapshot: QuickSwitchSnapshotBuilder.snapshot(displays: displays, windows: windows),
            currentSpaceIDs: [1]
        )
    }
}
C3.tools/round1-app-column-alignment-fixture-qa.sh
@@ -28,9 +28,11 @@
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
    if [[ "$command" == "$APP/Contents/MacOS/Aligner"* ]] \
      || [[ "$command" == "/Applications/Aligner.app/Contents/MacOS/Aligner"* ]] \
      || [[ "$command" == "$HOME/Applications/Aligner.app/Contents/MacOS/Aligner"* ]]; then
      echo "$pid"
    fi
  done
}
C3.tools/round1-main-ui-qa.sh
@@ -110,6 +110,7 @@
run_step screenshot-session "$SCRIPT_DIR/round1-screenshot-session-qa.sh"
run_step keyboard-navigation "$SCRIPT_DIR/round1-keyboard-navigation-fixture-qa.sh"
run_step window-activation "$SCRIPT_DIR/round1-window-activation-fixture-qa.sh"
run_step multi-page-identity "$SCRIPT_DIR/round1-multi-page-identity-fixture-qa.sh"
run_step mouse-interaction "$SCRIPT_DIR/round1-mouse-interaction-fixture-qa.sh"
run_step app-column-alignment "$SCRIPT_DIR/round1-app-column-alignment-fixture-qa.sh"
run_step horizontal-waterfall "$SCRIPT_DIR/round1-horizontal-waterfall-fixture-qa.sh"
@@ -124,5 +125,6 @@
Log directory: $LOG_DIR
Covered: trigger, overlay uniqueness, keyboard navigation, candidate filtering,
screenshots/fallbacks, minimized-window restore/activation, mouse activation,
App column alignment, Split View Space Lane rendering, and final no-residual check.
multi-page precise activation/close targeting, App column alignment,
Split View Space Lane rendering, and final no-residual check.
EOF
C3.tools/round1-multi-page-identity-fixture-qa.sh
New file
@@ -0,0 +1,233 @@
#!/bin/bash
# Round01 multi-page identity fixture QA. It verifies that Quick Switch keeps
# same-App/same-title pages distinct for activation and close requests.
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"
BUNDLE_ID="com.ar.Aligner"
CONFIRM_DEFAULTS_KEY="QuickSwitchConfirmBeforeClose"
REPORT_WAIT="${ALIGNER_ROUND1_MULTI_PAGE_REPORT_WAIT:-7.0}"
ACTIVATION_REPORT="$BUILD_REPORT_ROOT/round01-multi-page-activation-report.json"
CLOSE_REPORT="$BUILD_REPORT_ROOT/round01-multi-page-close-report.json"
fail() {
  echo "Round01 multi-page identity 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"
}
reset_close_confirmation_preference() {
  defaults delete "$BUNDLE_ID" "$CONFIRM_DEFAULTS_KEY" >/dev/null 2>&1 || true
}
wait_for_report() {
  local report="$1"
  local mode="$2"
  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$mode" <<'PY'
import json
import sys
import time
path = sys.argv[1]
timeout = float(sys.argv[2])
mode = sys.argv[3]
deadline = time.monotonic() + timeout
last_report = None
def ready(report):
    root = report.get("rootView", {})
    if report.get("snapshotLoaded") is not True:
        return False
    if mode == "activated":
        return (
            report.get("quickSwitchVisible") is False
            and report.get("lastActivationResult") is not None
        )
    if mode == "verifiedFailure":
        return (
            report.get("quickSwitchVisible") is True
            and report.get("lastCloseResult") is not None
            and root.get("closeFeedbackKind") == "failure"
        )
    return False
while time.monotonic() < deadline:
    try:
        with open(path, "r", encoding="utf-8") as file:
            report = json.load(file)
        last_report = report
        if ready(report):
            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"multi-page identity report {path} did not reach mode={mode} within {timeout:.1f}s", file=sys.stderr)
sys.exit(1)
PY
}
run_case() {
  local report="$1"
  local sequence="$2"
  local mode="$3"
  local activation_flag="${4:-}"
  local close_flag="${5:-}"
  stop_current_aligner
  reset_close_confirmation_preference
  rm -f "$report"
  local args=(
    --round0-skip-permissions
    --round01-open-quick-switch
    --round01-waterfall-view-mode=vertical
    --round01-fixture-multi-page-identity
    --round01-disable-screenshot-refresh
    --round01-debug-mouse-sequence="$sequence"
    --round01-quick-switch-report="$report"
  )
  if [ -n "$activation_flag" ]; then
    args+=(--round01-debug-window-activation)
  fi
  if [ -n "$close_flag" ]; then
    args+=(--round01-debug-window-close)
  fi
  "$APP/Contents/MacOS/Aligner" "${args[@]}" &
  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_report "$report" "$mode"
  kill "$APP_PID" 2>/dev/null || true
  wait "$APP_PID" 2>/dev/null || true
  trap - EXIT
  stop_current_aligner
}
assert_activation_report() {
  /usr/bin/python3 - "$ACTIVATION_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 = columns[0].get("cards", []) if columns else []
ids = [card.get("windowID") for card in cards]
require(report.get("snapshotLoaded") is True, "activation fixture must load")
require(report.get("quickSwitchVisible") is False, "activation must close Quick Switch")
require(report.get("appCount") == 1, "multi-page fixture must expose one App")
require(report.get("windowCount") == 3, "multi-page fixture must expose three pages")
require(ids == [70101, 70102, 70103], "same-title pages must keep stable windowID order")
require(report.get("lastCommittedAppGroupIndex") == 0, "activation must target App group 0")
require(report.get("lastCommittedWindowIndex") == 1, "activation must target second page index")
require(report.get("lastCommittedWindowID") == 70102, "activation commit must target page windowID 70102")
require(report.get("lastActivationWindowID") == 70102, "activation service must receive exact page windowID 70102")
require(report.get("lastActivationResult") == "activated", "debug activation must report activated")
require(root.get("selectedWindowID") == 70102, "selectedWindowID must track exact activated page")
print(json.dumps({
    "case": "multiPageActivation",
    "windowIDs": ids,
    "lastActivationWindowID": report.get("lastActivationWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_close_report() {
  /usr/bin/python3 - "$CLOSE_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 = columns[0].get("cards", []) if columns else []
ids = [card.get("windowID") for card in cards]
require(report.get("snapshotLoaded") is True, "close fixture must load")
require(report.get("quickSwitchVisible") is True, "close verification failure must keep Quick Switch visible")
require(report.get("appCount") == 1, "multi-page close fixture must expose one App")
require(report.get("windowCount") == 3, "fixture must still expose all three pages when debug close does not remove them")
require(ids == [70101, 70102, 70103], "close must not reorder or fake-remove same-title pages")
require(report.get("lastCloseTargetKind") == "window", "close must target a window/page")
require(report.get("lastCloseAppGroupIndex") == 0, "close must target App group 0")
require(report.get("lastCloseWindowID") == 70103, "close service must receive exact page windowID 70103")
require(report.get("lastCloseResult") == "requested", "debug close must report requested")
require(report.get("suppressedCloseTargetCount") == 0, "requested-but-still-present must not suppress the card")
require(root.get("closeFeedbackKind") == "failure", "still-present debug close must show verification failure")
print(json.dumps({
    "case": "multiPageClose",
    "windowIDs": ids,
    "lastCloseWindowID": report.get("lastCloseWindowID"),
    "closeFeedbackKind": root.get("closeFeedbackKind")
}, indent=2, ensure_ascii=False))
PY
}
stop_current_aligner
"$SCRIPT_DIR/package-app.sh" >&2
run_case "$ACTIVATION_REPORT" "click-card:0:1" "activated" "activation" ""
assert_activation_report
run_case "$CLOSE_REPORT" "click-card-close:0:2,confirm-close" "verifiedFailure" "" "close"
assert_close_report
echo "Round01 multi-page identity fixture QA passed."
C3.tools/round1-space-lane-split-view-fixture-qa.sh
@@ -116,8 +116,11 @@
require(split.get("type") == "fullscreen", "Split View tile must still be a fullscreen Space")
require(split.get("windowCount") == 2, "Split View tile must count both fullscreen windows")
require(split.get("appCount") == 2, "Split View tile must count both Split View apps")
require(split.get("countVisible") is False, "Split View tile must hide the top-right count label")
require(split.get("countText") == "", "Split View tile must clear the top-right count text")
require(split.get("windowBlockCount") == 0, "Split View tile must not draw normal window blocks")
require("splitView" in split.get("visualStates", []), "Split View tile must expose splitView visual state")
require(split.get("fullscreenMarkerPathStyle") == "centerDividerOnly", "Split View marker must draw only the center divider")
require(
    split.get("splitViewAppNames") == ["左侧应用", "右侧应用"],
    "Split View app names must be ordered by physical window frame from left to right"
@@ -153,6 +156,8 @@
    "splitSpace": split.get("label"),
    "splitNames": split.get("splitViewAppNames"),
    "markerKind": split.get("fullscreenMarkerKind"),
    "markerPathStyle": split.get("fullscreenMarkerPathStyle"),
    "countVisible": split.get("countVisible"),
    "singleFullscreenLabels": [segment.get("label") for segment in single_fullscreen],
}, indent=2, ensure_ascii=False))
PY