Ariver
2026-06-04 3ef24f71a70fc47eba8bf23f6827a60a4f34a07a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#!/bin/bash
# Round01 three-zone linkage fixture QA. It verifies Space Lane focus drives
# App Shelf highlights/badges and Waterfall positioning.
 
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_DIR="$BUILD_REPORT_ROOT"
FIXTURE_APP_COUNT="${ALIGNER_ROUND1_LINKAGE_FIXTURE_APP_COUNT:-8}"
FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_LINKAGE_FIXTURE_WINDOWS_PER_APP:-8}"
REPORT_WAIT="${ALIGNER_ROUND1_LINKAGE_REPORT_WAIT:-6.0}"
OVERLAY_WIDTH="${ALIGNER_ROUND1_LINKAGE_OVERLAY_WIDTH:-1180}"
LOCK_DIR="$BUILD_LOCK_ROOT/round1-three-zone-linkage-fixture-qa.lock"
APP_PID=""
LOCK_HELD=0
 
fail() {
  echo "Round01 three-zone linkage fixture QA failed: $*" >&2
  exit 1
}
 
REMOTE_APP_INDEX=$((FIXTURE_APP_COUNT - 1))
if [ "$FIXTURE_APP_COUNT" -lt 8 ]; then
  fail "fixture app count must be at least 8 to verify right-side remote App linkage"
fi
if [ "$FIXTURE_WINDOWS_PER_APP" -lt 8 ]; then
  fail "fixture windows per app must be at least 8 so every fixture App has a Space A1 window"
fi
 
acquire_lock() {
  if ! mkdir "$LOCK_DIR" 2>/dev/null; then
    fail "another round1-three-zone-linkage-fixture-qa.sh appears to be running; run packaging QA scripts serially"
  fi
  LOCK_HELD=1
}
 
release_lock() {
  if [ "$LOCK_HELD" -eq 1 ]; then
    rmdir "$LOCK_DIR" 2>/dev/null || true
    LOCK_HELD=0
  fi
}
 
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"
}
 
cleanup() {
  if [ -n "${APP_PID:-}" ]; then
    kill "$APP_PID" 2>/dev/null || true
    wait "$APP_PID" 2>/dev/null || true
    APP_PID=""
  fi
  stop_current_aligner
  release_lock
}
 
wait_for_report() {
  local report="$1"
  local expected_last_mouse="$2"
 
  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_last_mouse" <<'PY'
import json
import sys
import time
 
path = sys.argv[1]
timeout = float(sys.argv[2])
expected_last_mouse = sys.argv[3]
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", {})
        if (
            report.get("snapshotLoaded") is True
            and root.get("lastMouseCommand") == expected_last_mouse
        ):
            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"report did not reach expected mouse command {expected_last_mouse!r} within {timeout:.1f}s", file=sys.stderr)
sys.exit(1)
PY
}
 
run_report() {
  local report="$1"
  local sequence="$2"
  local expected_last_mouse="$3"
  local app_count="${4:-$FIXTURE_APP_COUNT}"
  local windows_per_app="${5:-$FIXTURE_WINDOWS_PER_APP}"
 
  rm -f "$report"
  "$APP/Contents/MacOS/Aligner" \
    --round0-skip-permissions \
    --round01-open-quick-switch \
    --round01-fixture-app-count="$app_count" \
    --round01-fixture-windows-per-app="$windows_per_app" \
    --round01-debug-overlay-width="$OVERLAY_WIDTH" \
    --round01-disable-screenshot-refresh \
    --round01-debug-mouse-sequence="$sequence" \
    --round01-quick-switch-report="$report" &
 
  APP_PID=$!
 
  wait_for_report "$report" "$expected_last_mouse"
  swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-quick-switch >&2
 
  kill "$APP_PID" 2>/dev/null || true
  wait "$APP_PID" 2>/dev/null || true
  APP_PID=""
  sleep 0.3
  stop_current_aligner
}
 
assert_focus_report() {
  /usr/bin/python3 - "$1" "$FIXTURE_APP_COUNT" <<'PY'
import json
import sys
 
path = sys.argv[1]
fixture_app_count = int(sys.argv[2])
 
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", {})
items = root.get("appShelfItems", [])
columns = root.get("waterfallColumns", [])
segments = root.get("spaceLaneSegments", [])
 
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("activeSpaceFocusID") == 1, "Space Lane focus must set activeSpaceFocusID")
require(root.get("spaceLaneFocusedSpaceID") == 1, "click-space must persist focused Space")
require(root.get("lastSpaceLaneClickSpaceID") == 1, "last clicked Space must be recorded")
require(root.get("lastSpaceFocusedAppGroupIndex") == 0, "first related App column must be appGroupIndex 0")
require(root.get("spaceFocusAppGroupIndexes") == list(range(fixture_app_count)), "all fixture apps must be marked related to focused Space")
require(root.get("waterfallScrollOffset") == 0, "first related App column should remain visible at offset zero")
 
focused_segments = [segment for segment in segments if segment.get("isFocused") is True]
require(len(focused_segments) == 1, "exactly one Space Lane segment must be focused")
require(focused_segments[0].get("label") == "A1", "focused Space should be A1")
require("focused" in focused_segments[0].get("visualStates", []), "focused Space segment must expose focused visual state")
 
require(len(items) == fixture_app_count, "App Shelf fixture item count must match app count")
require(all(item.get("isSpaceFocused") is True for item in items), "all fixture apps must be highlighted for Space A1")
require(all("spaceFocused" in item.get("visualStates", []) for item in items), "App Shelf items must expose spaceFocused visual state")
require(all(item.get("spaceFocusWindowCount", 0) > 0 for item in items), "focused Space badge count must be positive for every fixture app")
require(all(item.get("badgeVisible") is True for item in items), "focused Space badges must be visible")
require(items[0].get("spaceFocusDirection") == "visible", "first related App must be visible")
require(items[-1].get("spaceFocusDirection") == "right", "last fixture App must be reported to the right before remote click")
require(str(items[-1].get("badgeText", "")).endswith(">"), "right-side remote App badge must include right direction marker")
 
require(len(columns) == fixture_app_count, "Waterfall column reports must match app count")
require(columns[0].get("isSpaceFocused") is True, "first Waterfall column must be marked focused for Space")
require(columns[0].get("spaceFocusDirection") == "visible", "first Waterfall column must be visible")
require(columns[-1].get("isSpaceFocused") is True, "last Waterfall column must be marked focused for Space")
require(columns[-1].get("spaceFocusDirection") == "right", "last Waterfall column must report right direction before remote click")
 
print(json.dumps({
    "activeSpaceFocusID": root.get("activeSpaceFocusID"),
    "focusedApps": root.get("spaceFocusAppGroupIndexes"),
    "lastRemoteDirection": items[-1].get("spaceFocusDirection"),
    "lastRemoteBadgeText": items[-1].get("badgeText")
}, indent=2, ensure_ascii=False))
PY
}
 
assert_remote_click_report() {
  /usr/bin/python3 - "$1" "$FIXTURE_APP_COUNT" <<'PY'
import json
import sys
 
path = sys.argv[1]
fixture_app_count = int(sys.argv[2])
 
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", {})
items = root.get("appShelfItems", [])
columns = root.get("waterfallColumns", [])
last_index = fixture_app_count - 1
 
require(root.get("activeSpaceFocusID") == 1, "Space focus must persist after clicking App Shelf item")
require(root.get("lastClickedAppGroupIndex") == last_index, "remote App click must be recorded")
require(root.get("lastAppShelfClickChangedSelection") is True, "App Shelf click must move keyboard selection to the remote App column")
require(root.get("selectedAppGroupIndex") == last_index, "remote App click must focus the remote App column")
require(root.get("lastCommittedWindowID") is None, "App Shelf click must not commit a window")
require(root.get("waterfallScrollOffset", 0) > 0, "remote App click must scroll Waterfall horizontally")
require(items[last_index].get("spaceFocusDirection") == "visible", "remote App must become visible after click")
require(columns[last_index].get("spaceFocusDirection") == "visible", "remote Waterfall column must become visible after click")
require(items[last_index].get("badgeVisible") is True, "focused Space badge must remain visible after remote click")
 
print(json.dumps({
    "activeSpaceFocusID": root.get("activeSpaceFocusID"),
    "lastClickedAppGroupIndex": root.get("lastClickedAppGroupIndex"),
    "waterfallScrollOffset": root.get("waterfallScrollOffset"),
    "remoteDirectionAfterClick": items[last_index].get("spaceFocusDirection")
}, indent=2, ensure_ascii=False))
PY
}
 
assert_app_shelf_scroll_report() {
  /usr/bin/python3 - "$1" "$1" <<'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", {})
items = root.get("appShelfItems", [])
columns = root.get("waterfallColumns", [])
synced = root.get("lastAppShelfScrollSyncedAppGroupIndex")
 
require(root.get("appShelfScrollable") is True, "App Shelf must be scrollable in the scroll linkage fixture")
require(root.get("appShelfScrollOffset", 0) > 0, "debug App Shelf scroll must move App Shelf")
require(isinstance(synced, int), "App Shelf scroll must record the synced App group")
require(root.get("waterfallScrollOffset", 0) > 0, "App Shelf scroll must move Waterfall to the synced App column")
require(0 <= synced < len(items), "synced App group must point at a real App Shelf item")
require(columns[synced].get("spaceFocusDirection") == "none", "App Shelf scroll QA should not depend on Space focus state")
 
visible = columns[synced].get("visibleFrame", {})
require(visible.get("x", -10_000) < root.get("waterfallVisibleWidth", 0), "synced Waterfall column must enter the visible viewport")
require(visible.get("x", 10_000) + visible.get("width", 0) > 0, "synced Waterfall column must not remain fully offscreen")
 
print(json.dumps({
    "appShelfScrollOffset": root.get("appShelfScrollOffset"),
    "syncedAppGroupIndex": synced,
    "waterfallScrollOffset": root.get("waterfallScrollOffset")
}, indent=2, ensure_ascii=False))
PY
}
 
trap cleanup EXIT
 
acquire_lock
stop_current_aligner
"$SCRIPT_DIR/package-app.sh" >&2
 
FOCUS_REPORT="$REPORT_DIR/round01-three-zone-linkage-focus-report.json"
REMOTE_REPORT="$REPORT_DIR/round01-three-zone-linkage-remote-report.json"
SCROLL_REPORT="$REPORT_DIR/round01-three-zone-linkage-scroll-report.json"
 
run_report "$FOCUS_REPORT" "click-space:1" "click-space:1"
assert_focus_report "$FOCUS_REPORT"
 
run_report "$REMOTE_REPORT" "click-space:1,click-app:$REMOTE_APP_INDEX" "click-app:$REMOTE_APP_INDEX"
assert_remote_click_report "$REMOTE_REPORT"
 
run_report "$SCROLL_REPORT" "scroll-app-shelf:99999" "scroll-app-shelf:99999" 60 1
assert_app_shelf_scroll_report "$SCROLL_REPORT"
 
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
stop_current_aligner
release_lock
trap - EXIT