Ariver
2026-06-19 6f2856fa70722dad7f7c69a3addaa05ea9fd6e94
C3.tools/round1-horizontal-waterfall-fixture-qa.sh
@@ -14,14 +14,24 @@
LAYOUT_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-layout-report.json"
KEYBOARD_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-keyboard-report.json"
APP_ENTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-app-enter-report.json"
LETTER_INDEX_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-letter-index-report.json"
WINDOW_SHORTCUT_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-window-shortcut-report.json"
OVERFLOW_APP_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-window-shortcut-overflow-app-report.json"
OVERFLOW_WINDOW_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-window-shortcut-overflow-window-report.json"
ARROW_WRAP_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-arrow-wrap-report.json"
FILTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-filter-report.json"
FIXTURE_APP_COUNT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_FIXTURE_APP_COUNT:-8}"
FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_FIXTURE_WINDOWS_PER_APP:-6}"
HOVER_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_HOVER_APP_INDEX:-7}"
KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_KEY_SEQUENCE:-tab}"
APP_ENTER_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_APP_ENTER_KEY_SEQUENCE:-app:3,enter}"
APP_ENTER_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_APP_ENTER_KEY_SEQUENCE:-physical-index:3,enter}"
APP_ENTER_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_APP_ENTER_TARGET_APP_INDEX:-2}"
LETTER_INDEX_FIXTURE_APP_COUNT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_FIXTURE_APP_COUNT:-12}"
LETTER_INDEX_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_KEY_SEQUENCE:-physical-index:A}"
LETTER_INDEX_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_TARGET_APP_INDEX:-10}"
WINDOW_SHORTCUT_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_KEY_SEQUENCE:-physical-index:3,physical-index:6}"
WINDOW_SHORTCUT_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_TARGET_APP_INDEX:-2}"
WINDOW_SHORTCUT_TARGET_WINDOW_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_TARGET_WINDOW_INDEX:-5}"
ARROW_WRAP_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_ARROW_WRAP_KEY_SEQUENCE:-app:1,down,up,left,down,enter}"
REPORT_WAIT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_REPORT_WAIT:-6.0}"
@@ -267,11 +277,31 @@
import sys
path = sys.argv[1]
key_sequence = [part for part in sys.argv[2].split(",") if part]
raw_key_sequence = [part for part in sys.argv[2].split(",") if part]
target_app_index = int(sys.argv[3])
with open(path, "r", encoding="utf-8") as file:
    report = json.load(file)
def normalize_key_sequence(sequence):
    normalized = []
    physical_symbols = []
    for part in sequence:
        lowered = part.lower()
        matched_prefix = None
        for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"):
            if lowered.startswith(prefix):
                matched_prefix = prefix
                break
        if matched_prefix is None:
            normalized.append(part)
            continue
        symbol = part[len(matched_prefix):].strip().upper()
        physical_symbols.append(symbol)
        normalized.append(f"app:{symbol}")
    return normalized, physical_symbols
key_sequence, physical_symbols = normalize_key_sequence(raw_key_sequence)
def require(condition, message):
    if not condition:
@@ -284,6 +314,9 @@
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("waterfallViewMode") == "horizontalMasonry", "App Enter run must use horizontalMasonry")
require(root.get("keyboardCommandsApplied") == key_sequence, "App Enter keyboard commands must be applied")
require(root.get("appShelfIndexKeyDownCount") == len(physical_symbols), "physical App index keyDown path must be exercised")
if physical_symbols:
    require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last physical App index symbol must be reported")
require(target_app_index < len(columns), "target App index must exist")
target_card = columns[target_app_index].get("cards", [])[0]
require(root.get("hoverTargetKind") == "app", "App index focus must leave hover target on the App icon")
@@ -305,6 +338,188 @@
    "hoverTargetKind": root.get("hoverTargetKind"),
    "targetAppGroupIndex": target_app_index,
    "lastCommittedWindowID": root.get("lastCommittedWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_letter_index_report() {
  /usr/bin/python3 - "$LETTER_INDEX_REPORT" "$LETTER_INDEX_KEY_SEQUENCE" "$LETTER_INDEX_TARGET_APP_INDEX" <<'PY'
import json
import sys
path = sys.argv[1]
raw_key_sequence = [part for part in sys.argv[2].split(",") if part]
target_app_index = int(sys.argv[3])
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)
def normalize_key_sequence(sequence):
    normalized = []
    physical_symbols = []
    for part in sequence:
        lowered = part.lower()
        matched_prefix = None
        for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"):
            if lowered.startswith(prefix):
                matched_prefix = prefix
                break
        if matched_prefix is None:
            normalized.append(part)
            continue
        symbol = part[len(matched_prefix):].strip().upper()
        physical_symbols.append(symbol)
        normalized.append(f"app:{symbol}")
    return normalized, physical_symbols
key_sequence, physical_symbols = normalize_key_sequence(raw_key_sequence)
root = report.get("rootView", {})
columns = root.get("waterfallColumns", [])
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("waterfallViewMode") == "horizontalMasonry", "letter App index run must use horizontalMasonry")
require(root.get("keyboardCommandsApplied") == key_sequence, "letter App index keyboard commands must be applied")
require(root.get("appShelfIndexKeyDownCount") == len(physical_symbols), "letter App index must exercise physical keyDown path")
require(physical_symbols, "letter App index test must contain a physical key")
require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last letter App index symbol must be reported")
require(target_app_index < len(columns), "target letter App index must exist")
target_card = columns[target_app_index].get("cards", [])[0]
require(root.get("hoverTargetKind") == "app", "letter App index focus must leave hover target on App icon")
require(root.get("hoverTargetSource") == "keyboard", "letter App index focus must report keyboard source")
require(root.get("keyboardFocusedAppGroupIndex") == target_app_index, "letter App index must focus the expected App")
require(root.get("keyboardFocusedWindowID") is None, "letter App icon focus must not keep stale window keyboard focus")
require(root.get("selectedAppGroupIndex") == target_app_index, "letter App icon focus must select the target App")
require(root.get("selectedWindowIndex") == 0, "letter App icon focus must select the first window")
require(root.get("horizontalAppDefaultWindowID") == target_card.get("windowID"), "letter App default window must match the first card")
require(root.get("lastCommittedWindowID") is None, "letter App index focus without Enter must not commit")
require(root.get("lastCommitSource") is None, "letter App index focus without Enter must not report commit source")
require(target_card.get("isHorizontalAppDefault") is True, "letter target first card must report app default state")
require("appDefault" in target_card.get("visualStates", []), "letter target first card must expose appDefault visual state")
require("selected" in target_card.get("visualStates", []), "letter target first card must expose selected visual state")
print(json.dumps({
    "commands": root.get("keyboardCommandsApplied"),
    "physicalKeyDownCount": root.get("appShelfIndexKeyDownCount"),
    "lastSymbol": root.get("lastAppShelfIndexKeySymbol"),
    "targetAppGroupIndex": target_app_index,
    "defaultWindowID": root.get("horizontalAppDefaultWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_window_shortcut_report() {
  /usr/bin/python3 - "$WINDOW_SHORTCUT_REPORT" "$WINDOW_SHORTCUT_KEY_SEQUENCE" "$WINDOW_SHORTCUT_TARGET_APP_INDEX" "$WINDOW_SHORTCUT_TARGET_WINDOW_INDEX" <<'PY'
import json
import sys
path = sys.argv[1]
raw_key_sequence = [part for part in sys.argv[2].split(",") if part]
target_app_index = int(sys.argv[3])
target_window_index = int(sys.argv[4])
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)
def physical_symbol(part):
    lowered = part.lower()
    for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"):
        if lowered.startswith(prefix):
            return part[len(prefix):].strip().upper()
    return None
physical_symbols = [symbol for part in raw_key_sequence if (symbol := physical_symbol(part))]
require(len(physical_symbols) == 2, "window shortcut test must use two physical symbols")
expected_code = "".join(physical_symbols)
expected_sequence = [f"app:{physical_symbols[0]}", f"window:{expected_code}"]
root = report.get("rootView", {})
columns = root.get("waterfallColumns", [])
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("waterfallViewMode") == "horizontalMasonry", "window shortcut run must use horizontalMasonry")
require(root.get("keyboardCommandsApplied") == expected_sequence, "two-key window shortcut commands must be applied")
require(root.get("appShelfIndexKeyDownCount") == 2, "two-key window shortcut must exercise two physical keyDown events")
require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last physical symbol must be the window index key")
require(root.get("lastWindowIndexKeyCommand") == f"window:{expected_code}", "last window shortcut command must be reported")
require(root.get("lastWindowIndexKeyCommitCode") == expected_code, "last window shortcut commit code must be reported")
require(root.get("windowIndexKeyPendingAppGroupIndex") is None, "two-key commit must clear pending App")
require(root.get("windowIndexKeyPendingAppSymbol") is None, "two-key commit must clear pending App symbol")
require(target_app_index < len(columns), "target App index must exist")
target_column = columns[target_app_index]
cards = target_column.get("cards", [])
require(target_window_index < len(cards), "target window index must exist")
target_card = cards[target_window_index]
require(target_card.get("windowShortcutCode") == expected_code, "target card must expose the expected two-character shortcut code")
require(target_card.get("primarySpaceLabel") != target_card.get("windowShortcutCode"), "shortcut code must replace the old Space label in the UI meta slot")
require(root.get("lastCommittedAppGroupIndex") == target_app_index, "two-key shortcut must commit target App")
require(root.get("lastCommittedWindowIndex") == target_window_index, "two-key shortcut must commit target window")
require(root.get("lastCommittedWindowID") == target_card.get("windowID"), "two-key shortcut must commit target window ID")
require(root.get("lastCommitSource") == "keyboard", "two-key shortcut commit must be keyboard sourced")
require(root.get("selectedAppGroupIndex") == target_app_index, "two-key shortcut must select target App")
require(root.get("selectedWindowIndex") == target_window_index, "two-key shortcut must select target window")
require(root.get("selectedWindowID") == target_card.get("windowID"), "two-key shortcut selection ID must match target window")
for column in columns:
    for card in column.get("cards", []):
        code = card.get("windowShortcutCode")
        require(code != "No Space" and code != "N...", "window shortcut code must not expose the old No Space label")
print(json.dumps({
    "commands": root.get("keyboardCommandsApplied"),
    "code": expected_code,
    "lastCommittedWindowID": root.get("lastCommittedWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_window_shortcut_overflow_report() {
  /usr/bin/python3 - "$OVERFLOW_APP_REPORT" "$OVERFLOW_WINDOW_REPORT" <<'PY'
import json
import sys
app_path = sys.argv[1]
window_path = sys.argv[2]
with open(app_path, "r", encoding="utf-8") as file:
    app_report = json.load(file)
with open(window_path, "r", encoding="utf-8") as file:
    window_report = json.load(file)
def require(report, 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)
app_root = app_report.get("rootView", {})
app_columns = app_root.get("waterfallColumns", [])
require(app_report, app_report.get("snapshotLoaded") is True, "App overflow report must be loaded")
require(app_report, len(app_columns) == 37, "App overflow fixture must expose 37 columns")
require(app_report, app_columns[35].get("cards", [])[0].get("windowShortcutCode") == "Z1", "36th App must still expose a shortcut code")
require(app_report, app_columns[36].get("cards", [])[0].get("windowShortcutCode") == "", "37th App must not expose a two-key shortcut code")
window_root = window_report.get("rootView", {})
window_columns = window_root.get("waterfallColumns", [])
require(window_report, window_report.get("snapshotLoaded") is True, "Window overflow report must be loaded")
require(window_report, len(window_columns) == 1, "Window overflow fixture must expose one column")
cards = window_columns[0].get("cards", [])
require(window_report, len(cards) == 37, "Window overflow fixture must expose 37 windows")
require(window_report, cards[35].get("windowShortcutCode") == "1Z", "36th window must still expose a shortcut code")
require(window_report, cards[36].get("windowShortcutCode") == "", "37th window must not expose a two-key shortcut code")
print(json.dumps({
    "appOverflowCode": app_columns[36].get("cards", [])[0].get("windowShortcutCode"),
    "windowOverflowCode": cards[36].get("windowShortcutCode")
}, indent=2, ensure_ascii=False))
PY
}
@@ -406,12 +621,29 @@
  local report="$1"
  shift
  run_fixture_with_app_count "$report" "$FIXTURE_APP_COUNT" "$@"
}
run_fixture_with_app_count() {
  local report="$1"
  local app_count="$2"
  shift 2
  run_fixture_with_shape "$report" "$app_count" "$FIXTURE_WINDOWS_PER_APP" "$@"
}
run_fixture_with_shape() {
  local report="$1"
  local app_count="$2"
  local windows_per_app="$3"
  shift 3
  rm -f "$report"
  "$APP/Contents/MacOS/Aligner" \
    --round0-skip-permissions \
    --round01-open-quick-switch \
    --round01-fixture-app-count="$FIXTURE_APP_COUNT" \
    --round01-fixture-windows-per-app="$FIXTURE_WINDOWS_PER_APP" \
    --round01-fixture-app-count="$app_count" \
    --round01-fixture-windows-per-app="$windows_per_app" \
    --round01-disable-screenshot-refresh \
    --round01-waterfall-view-mode=horizontal \
    --round01-quick-switch-report="$report" \
@@ -438,6 +670,9 @@
if [ "$FIXTURE_APP_COUNT" -le "$HOVER_APP_INDEX" ]; then
  fail "hover App index must be inside fixture app count"
fi
if [ "$LETTER_INDEX_FIXTURE_APP_COUNT" -le "$LETTER_INDEX_TARGET_APP_INDEX" ]; then
  fail "letter index target App must be inside letter fixture app count"
fi
if [ "$FIXTURE_WINDOWS_PER_APP" -lt 2 ]; then
  fail "fixture must include at least two windows per App for Tab navigation"
@@ -477,6 +712,33 @@
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture_with_app_count "$LETTER_INDEX_REPORT" "$LETTER_INDEX_FIXTURE_APP_COUNT" --round01-debug-key-sequence="$LETTER_INDEX_KEY_SEQUENCE"
wait_for_loaded_report "$LETTER_INDEX_REPORT"
assert_letter_index_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture "$WINDOW_SHORTCUT_REPORT" --round01-debug-key-sequence="$WINDOW_SHORTCUT_KEY_SEQUENCE"
wait_for_loaded_report "$WINDOW_SHORTCUT_REPORT"
assert_window_shortcut_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture_with_shape "$OVERFLOW_APP_REPORT" 37 1
wait_for_loaded_report "$OVERFLOW_APP_REPORT"
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture_with_shape "$OVERFLOW_WINDOW_REPORT" 1 37
wait_for_loaded_report "$OVERFLOW_WINDOW_REPORT"
assert_window_shortcut_overflow_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture "$ARROW_WRAP_REPORT" --round01-debug-key-sequence="$ARROW_WRAP_KEY_SEQUENCE"
wait_for_loaded_report "$ARROW_WRAP_REPORT"
assert_arrow_wrap_report