#!/usr/bin/env python3 """ Layout/flash regression tests for cmux splits. Goals: 1) Ensure programmatic splits don't transiently render EmptyPanelView (visible flash). 2) Validate selected panel bounds are non-zero and aligned with bonsplit pane bounds. """ 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 _rect_area(r: dict) -> float: return max(0.0, float(r.get("width", 0.0))) * max(0.0, float(r.get("height", 0.0))) def _rect_intersection_area(a: dict, b: dict) -> float: ax1 = float(a["x"]) ay1 = float(a["y"]) ax2 = ax1 + float(a["width"]) ay2 = ay1 + float(a["height"]) bx1 = float(b["x"]) by1 = float(b["y"]) bx2 = bx1 + float(b["width"]) by2 = by1 + float(b["height"]) ix1 = max(ax1, bx1) iy1 = max(ay1, by1) ix2 = min(ax2, bx2) iy2 = min(ay2, by2) if ix2 <= ix1 or iy2 <= iy1: return 0.0 return (ix2 - ix1) * (iy2 - iy1) def _assert_selected_panels_healthy(payload: dict, *, min_wh: float = 80.0) -> None: selected = payload.get("selectedPanels") or [] if not selected: raise cmuxError("layout_debug returned no selectedPanels") for i, row in enumerate(selected): pane_id = row.get("paneId") pane_frame = row.get("paneFrame") view_frame = row.get("viewFrame") panel_id = row.get("panelId") if not panel_id: raise cmuxError(f"selectedPanels[{i}] missing panelId (pane={pane_id})") if row.get("inWindow") is not True: raise cmuxError(f"selectedPanels[{i}] panel not in window (pane={pane_id}, panel={panel_id})") if row.get("hidden") is True: raise cmuxError(f"selectedPanels[{i}] panel is hidden (pane={pane_id}, panel={panel_id})") if not view_frame: raise cmuxError(f"selectedPanels[{i}] missing viewFrame (pane={pane_id}, panel={panel_id})") if float(view_frame.get("width", 0.0)) < min_wh or float(view_frame.get("height", 0.0)) < min_wh: raise cmuxError( f"selectedPanels[{i}] viewFrame too small: {view_frame} (pane={pane_id}, panel={panel_id})" ) # Coordinate sanity: selected panel should substantially overlap its pane. # This implicitly verifies we're measuring in a consistent coordinate space. if pane_frame: inter = _rect_intersection_area(pane_frame, view_frame) denom = min(_rect_area(pane_frame), _rect_area(view_frame)) ratio = inter / denom if denom > 0 else 0.0 if ratio < 0.50: raise cmuxError( f"selectedPanels[{i}] bounds mismatch (overlap={ratio:.2f}). " f"pane={pane_frame} view={view_frame} pane_id={pane_id} panel={panel_id}" ) def _assert_no_transient_detach_or_hide( c: cmux, *, duration_s: float = 1.0, cadence_s: float = 0.005, max_false_samples: int = 2, ) -> None: false_in_window: dict[str, int] = {} hidden_true: dict[str, int] = {} deadline = time.time() + duration_s while time.time() < deadline: rows = c.surface_health() for row in rows: if row.get("type") != "terminal": continue panel_id = (row.get("id") or "").lower() if not panel_id: continue if row.get("in_window") is False: false_in_window[panel_id] = false_in_window.get(panel_id, 0) + 1 if row.get("hidden") is True: hidden_true[panel_id] = hidden_true.get(panel_id, 0) + 1 time.sleep(cadence_s) detached = {k: v for k, v in false_in_window.items() if v > max_false_samples} hidden = {k: v for k, v in hidden_true.items() if v > max_false_samples} if detached or hidden: raise cmuxError( f"Transient detach/hide during split exceeds tolerance " f"(detached={detached}, hidden={hidden})" ) def main() -> int: with cmux(SOCKET_PATH) as c: # Run on a fresh workspace to avoid state carry-over from restored sessions. test_workspace = c.new_workspace() c.select_workspace(test_workspace) time.sleep(0.2) # Baseline: a fresh counter, no flashes just from connecting. c.reset_empty_panel_count() base = c.layout_debug() _assert_selected_panels_healthy(base) # Programmatic split should not show EmptyPanelView even briefly. c.reset_empty_panel_count() c.new_split("right") time.sleep(0.3) flashes = c.empty_panel_count() if flashes != 0: raise cmuxError(f"EmptyPanelView appeared during split (count={flashes})") after = c.layout_debug() # Expect at least 2 panes after split (exact count can vary if user already has splits). panes = after.get("layout", {}).get("panes") or [] if len(panes) < 2: raise cmuxError(f"Expected >= 2 panes after split, got {len(panes)}") _assert_selected_panels_healthy(after) # Drag-to-split from a single-surface pane should also avoid EmptyPanelView flashes. drag_workspace = c.new_workspace() c.select_workspace(drag_workspace) time.sleep(0.2) drag_before = c.layout_debug() _assert_selected_panels_healthy(drag_before) drag_selected = drag_before.get("selectedPanels") or [] if not drag_selected: raise cmuxError("layout_debug returned no selectedPanels for drag split setup") drag_panel_id = drag_selected[0].get("panelId") if not drag_panel_id: raise cmuxError("drag split setup selected panel has no panelId") drag_panes_before = len(drag_before.get("layout", {}).get("panes") or []) c.reset_empty_panel_count() c.drag_surface_to_split(drag_panel_id, "right") _assert_no_transient_detach_or_hide(c) time.sleep(0.4) flashes = c.empty_panel_count() if flashes != 0: raise cmuxError(f"EmptyPanelView appeared during drag split (count={flashes})") drag_after = c.layout_debug() drag_panes_after = len(drag_after.get("layout", {}).get("panes") or []) if drag_panes_after < drag_panes_before + 1: raise cmuxError( f"Expected drag split to add a pane: before={drag_panes_before} after={drag_panes_after}" ) _assert_selected_panels_healthy(drag_after) # Browser split should also avoid EmptyPanelView flashes. c.reset_empty_panel_count() _browser_id = c.open_browser("https://example.com") time.sleep(0.4) flashes = c.empty_panel_count() if flashes != 0: raise cmuxError(f"EmptyPanelView appeared during browser split (count={flashes})") after_browser = c.layout_debug() _assert_selected_panels_healthy(after_browser) c.close_workspace(test_workspace) time.sleep(0.1) print("PASS: split flash + layout bounds checks") return 0 if __name__ == "__main__": raise SystemExit(main())