cmux/tests_v2/test_command_palette_window_scope.py
Lawrence Chen 710ed9b068 Fix Cmd+P/Cmd+Shift+P window routing (#413)
* Fix command palette shortcuts to stay window-scoped

* Fix cross-window command palette typing focus lock
2026-02-24 14:35:10 -08:00

219 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""
Regression test: command palette should open only in the active window.
Why: if command-palette toggle is broadcast to all windows, inactive windows can
end up with an open palette that steals focus once they become key.
"""
import os
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from cmux import cmux, cmuxError
SOCKET_PATH = os.environ.get("CMUX_SOCKET", "/tmp/cmux-debug.sock")
def _wait_until(predicate, timeout_s: float = 5.0, interval_s: float = 0.05, message: str = "timeout") -> None:
start = time.time()
while time.time() - start < timeout_s:
if predicate():
return
time.sleep(interval_s)
raise cmuxError(message)
def _palette_visible(client: cmux, window_id: str) -> bool:
res = client._call("debug.command_palette.visible", {"window_id": window_id}) or {}
return bool(res.get("visible"))
def _palette_results(client: cmux, window_id: str, limit: int = 20) -> dict:
return client.command_palette_results(window_id=window_id, limit=limit)
def _set_palette_visible(client: cmux, window_id: str, visible: bool) -> None:
if _palette_visible(client, window_id) == visible:
return
client._call("debug.command_palette.toggle", {"window_id": window_id})
_wait_until(
lambda: _palette_visible(client, window_id) == visible,
timeout_s=3.0,
message=f"palette in {window_id} did not become {visible}",
)
def _focus_window(client: cmux, window_id: str) -> None:
client.focus_window(window_id)
client.activate_app()
_wait_until(
lambda: client.current_window().lower() == window_id.lower(),
timeout_s=3.0,
message=f"failed to focus window {window_id}",
)
time.sleep(0.15)
def _assert_shortcut_window_scoped(client: cmux, shortcut: str, w1: str, w2: str) -> None:
_set_palette_visible(client, w1, False)
_set_palette_visible(client, w2, False)
_focus_window(client, w1)
client.simulate_shortcut(shortcut)
_wait_until(
lambda: _palette_visible(client, w1),
timeout_s=3.0,
message=f"{shortcut} did not open palette in window1",
)
if _palette_visible(client, w2):
raise cmuxError(f"{shortcut} in window1 incorrectly opened palette in window2")
_focus_window(client, w2)
client.simulate_shortcut(shortcut)
_wait_until(
lambda: _palette_visible(client, w2),
timeout_s=3.0,
message=f"{shortcut} did not open palette in window2",
)
if not _palette_visible(client, w1):
raise cmuxError(
f"{shortcut} in window2 incorrectly toggled window1 palette off "
"(cross-window routing regression)"
)
client.simulate_shortcut(shortcut)
_wait_until(
lambda: not _palette_visible(client, w2),
timeout_s=3.0,
message=f"second {shortcut} did not close palette in window2",
)
if not _palette_visible(client, w1):
raise cmuxError(
f"second {shortcut} in window2 incorrectly changed window1 palette visibility"
)
_focus_window(client, w1)
client.simulate_shortcut(shortcut)
_wait_until(
lambda: not _palette_visible(client, w1),
timeout_s=3.0,
message=f"second {shortcut} did not close palette in window1",
)
def _assert_cross_window_typing_after_mixed_shortcuts(client: cmux, w1: str, w2: str) -> None:
_set_palette_visible(client, w1, False)
_set_palette_visible(client, w2, False)
_focus_window(client, w1)
client.simulate_shortcut("cmd+shift+p")
_wait_until(
lambda: _palette_visible(client, w1),
timeout_s=3.0,
message="cmd+shift+p did not open palette in window1",
)
_wait_until(
lambda: str(_palette_results(client, w1).get("mode") or "") == "commands",
timeout_s=3.0,
message="window1 palette did not enter commands mode",
)
window1_query_before = str(_palette_results(client, w1).get("query") or "")
_focus_window(client, w2)
client.simulate_shortcut("cmd+p")
_wait_until(
lambda: _palette_visible(client, w2),
timeout_s=3.0,
message="cmd+p did not open palette in window2",
)
_wait_until(
lambda: str(_palette_results(client, w2).get("mode") or "") == "switcher",
timeout_s=3.0,
message="window2 palette did not enter switcher mode",
)
typed = ""
for ch in "crosswindow":
typed += ch
client.simulate_type(ch)
_wait_until(
lambda expected=typed: str(_palette_results(client, w2).get("query") or "").lower() == expected,
timeout_s=1.8,
message=(
"typing into window2 palette did not accumulate query text "
f"(expected {typed!r})"
),
)
window1_query_now = str(_palette_results(client, w1).get("query") or "")
if window1_query_now != window1_query_before:
raise cmuxError(
"typing in window2 changed window1 command-palette query "
f"(before={window1_query_before!r}, now={window1_query_now!r})"
)
def main() -> int:
with cmux(SOCKET_PATH) as client:
client.activate_app()
time.sleep(0.2)
w1 = client.current_window()
w2 = client.new_window()
time.sleep(0.25)
_ = client.new_workspace(window_id=w1)
_ = client.new_workspace(window_id=w2)
time.sleep(0.25)
_set_palette_visible(client, w1, False)
_set_palette_visible(client, w2, False)
# Open palette in window1 and verify window2 remains untouched.
client._call("debug.command_palette.toggle", {"window_id": w1})
_wait_until(
lambda: _palette_visible(client, w1),
timeout_s=3.0,
message="window1 command palette did not open",
)
if _palette_visible(client, w2):
raise cmuxError("window2 palette became visible when toggling window1")
# Closing window1 palette should not affect window2.
client._call("debug.command_palette.toggle", {"window_id": w1})
_wait_until(
lambda: not _palette_visible(client, w1),
timeout_s=3.0,
message="window1 command palette did not close",
)
# Mirror the same check in the other direction.
client._call("debug.command_palette.toggle", {"window_id": w2})
_wait_until(
lambda: _palette_visible(client, w2),
timeout_s=3.0,
message="window2 command palette did not open",
)
if _palette_visible(client, w1):
raise cmuxError("window1 palette became visible when toggling window2")
client._call("debug.command_palette.toggle", {"window_id": w2})
_wait_until(
lambda: not _palette_visible(client, w2),
timeout_s=3.0,
message="window2 command palette did not close",
)
# Reproduce keyboard-shortcut window-scoping path:
# opening from window2 must not jump back and toggle window1.
_assert_shortcut_window_scoped(client, "cmd+shift+p", w1, w2)
_assert_shortcut_window_scoped(client, "cmd+p", w1, w2)
_assert_cross_window_typing_after_mixed_shortcuts(client, w1, w2)
print("PASS: command palette is scoped to active window")
return 0
if __name__ == "__main__":
raise SystemExit(main())