import os import json import subprocess import sys import pytest from lib_py.layout import ( compute_2xk_layout, extract_panes_and_focus, LayoutDecision, PaneInfo, ) def test_empty_or_malformed_json_fallback(): d = compute_2xk_layout({}, default_anchor_id="pane-123") assert d.target_pane_id == "pane-123" assert d.direction == "right" assert not d.is_overflow def test_1_pane_split_down(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 120, "height": 80}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.target_pane_id == "p1" assert d.direction == "down" assert not d.is_overflow def test_1_pane_height_constrained_splits_right(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 160, "height": 30}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.target_pane_id == "p1" assert d.direction == "right" assert not d.is_overflow def test_1_pane_overflow(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 30}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.target_pane_id == "p1" assert d.direction == "overflow" assert d.is_overflow def test_2_panes_to_3_panes_new_column_right(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 160, "height": 40}}, {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 160, "height": 40}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.target_pane_id == "p1" assert d.direction == "right" assert not d.is_overflow def test_3_panes_to_4_panes_fill_singleton(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}}, {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}}, {"pane_id": "p3", "rect": {"x": 80, "y": 0, "width": 80, "height": 80}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.target_pane_id == "p3" assert d.direction == "down" assert not d.is_overflow def test_4_panes_to_5_panes_new_column(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 120, "height": 40}}, {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 120, "height": 40}}, {"pane_id": "p3", "rect": {"x": 120, "y": 0, "width": 120, "height": 40}}, {"pane_id": "p4", "rect": {"x": 120, "y": 40, "width": 120, "height": 40}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.target_pane_id == "p3" assert d.direction == "right" assert not d.is_overflow def test_4_panes_overflow_when_width_constrained(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 60, "height": 40}}, {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 60, "height": 40}}, {"pane_id": "p3", "rect": {"x": 60, "y": 0, "width": 60, "height": 40}}, {"pane_id": "p4", "rect": {"x": 60, "y": 40, "width": 60, "height": 40}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.direction == "overflow" assert d.is_overflow def test_max_columns_limit(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 40}}, {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 100, "height": 40}}, {"pane_id": "p3", "rect": {"x": 100, "y": 0, "width": 100, "height": 40}}, {"pane_id": "p4", "rect": {"x": 100, "y": 40, "width": 100, "height": 40}} ] } } d = compute_2xk_layout(payload, min_cols=30, min_rows=20, max_columns=2) assert d.direction == "overflow" assert d.is_overflow def test_headless_0x0_transitions(): # N=1 -> down p1 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}]}} assert compute_2xk_layout(p1).direction == "down" # N=2 -> right p2 = {"result": {"panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}, {"pane_id": "p2", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}} ]}} assert compute_2xk_layout(p2).direction == "right" # N=3 -> down p3 = {"result": {"panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}, {"pane_id": "p2", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}, {"pane_id": "p3", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}} ]}} assert compute_2xk_layout(p3).direction == "down" # N=4 -> right p4 = {"result": {"panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}, {"pane_id": "p2", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}, {"pane_id": "p3", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}, {"pane_id": "p4", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}} ]}} assert compute_2xk_layout(p4).direction == "right" def test_real_herdr_080_nested_layout_format(): payload = { "result": { "layout": { "area": {"height": 78, "width": 120, "x": 26, "y": 1}, "focused_pane_id": "wK:p1", "panes": [ {"pane_id": "wK:p1", "rect": {"height": 39, "width": 60, "x": 26, "y": 1, "focused": True}}, {"pane_id": "wK:p2", "rect": {"height": 39, "width": 60, "x": 26, "y": 40, "focused": False}}, {"pane_id": "wK:p3", "rect": {"height": 78, "width": 60, "x": 86, "y": 1, "focused": False}} ], "splits": [{"direction": "right", "id": "split_0_root", "ratio": 0.5}], "workspace_id": "wK", "tab_id": "wK:t1", "zoomed": False }, "type": "pane_layout" } } d = compute_2xk_layout(payload, min_cols=30, min_rows=20) assert d.target_pane_id == "wK:p3" assert d.direction == "down" def test_cli_invocation_pipe(): payload = json.dumps({ "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 120, "height": 80}} ] } }) skills_dir = os.path.abspath(".agents/skills") env = {**os.environ, "PYTHONPATH": skills_dir} res = subprocess.run( [sys.executable, "-m", "lib_py.layout", "--min-cols", "60", "--min-rows", "20"], input=payload, capture_output=True, text=True, env=env ) assert res.returncode == 0 assert res.stdout.strip() == "down p1" res_json = subprocess.run( [sys.executable, "-m", "lib_py.layout", "--json"], input=payload, capture_output=True, text=True, env=env ) assert res_json.returncode == 0 data = json.loads(res_json.stdout) assert data["target_pane_id"] == "p1" assert data["direction"] == "down" assert not data["is_overflow"] def test_lib_sh_no_local_in_shim_heredoc(): """Verify F-1: No 'local' declarations inside the top-level shim heredoc dispatcher.""" import os lib_path = os.path.abspath(".agents/skills/lib.sh") with open(lib_path, "r", encoding="utf-8") as f: content = f.read() start_idx = content.find("cat <<'EOF' > \"$tmp_file\"") end_idx = content.find("\nEOF\n", start_idx) assert start_idx != -1 and end_idx != -1 heredoc = content[start_idx:end_idx] # Check specifically in the layout block layout_idx = heredoc.find('split_dir=""') assert layout_idx != -1 layout_block = heredoc[layout_idx:layout_idx + 800] assert "local " not in layout_block, f"Forbidden 'local' found in top-level shim heredoc:\n{layout_block}" def test_5_panes_to_6_panes_fill_singleton_in_3rd_column(): """Verify 5 panes (2x2 full + 1 singleton in 3rd col) -> splits 3rd col singleton down to make 2x3 grid.""" payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}}, {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}}, {"pane_id": "p3", "rect": {"x": 80, "y": 0, "width": 80, "height": 40}}, {"pane_id": "p4", "rect": {"x": 80, "y": 40, "width": 80, "height": 40}}, {"pane_id": "p5", "rect": {"x": 160, "y": 0, "width": 80, "height": 80}} ] } } d = compute_2xk_layout(payload, min_cols=60, min_rows=20) assert d.target_pane_id == "p5" assert d.direction == "down" assert not d.is_overflow def test_lib_sh_layout_split_in_set_e_subshell(tmp_path): """Verify F-1 & F-2: lib.sh layout split block executes cleanly in set -euo pipefail top-level script.""" import os skills_dir = os.path.abspath(".agents/skills") script = f"""#!/usr/bin/env bash set -euo pipefail export PYTHONPATH="{skills_dir}" _real_herdr() {{ if [ "${{1:-}}" = "pane" ] && [ "${{2:-}}" = "layout" ]; then echo '{{"result": {{"panes": [{{"pane_id": "p1", "rect": {{"x": 0, "y": 0, "width": 120, "height": 80}}}}]}}}}' return 0 fi return 1 }} sample_pane="p1" split_dir="" # Exact snippet from lib.sh:429-435 if [ -n "$sample_pane" ]; then layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "") read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${{MAM_MIN_PANE_COLS:-60}}" --min-rows "${{MAM_MIN_PANE_ROWS:-20}}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane") split_dir="${{split_dir:-right}}" sample_pane="${{split_target:-$sample_pane}}" fi echo "SPLIT_DIR=$split_dir" echo "SAMPLE_PANE=$sample_pane" """ res = subprocess.run(["bash", "-c", script], capture_output=True, text=True) assert res.returncode == 0, f"Script failed with code {res.returncode}. Stderr: {res.stderr}" assert "SPLIT_DIR=down" in res.stdout assert "SAMPLE_PANE=p1" in res.stdout def test_real_generated_shim_layout_split(tmp_path): """Verify generated shim executes layout.py without command not found or local aborts.""" import os skills_dir = os.path.abspath(".agents/skills") ws_dir = str(tmp_path / "ws") os.makedirs(ws_dir, exist_ok=True) test_script = f"""#!/usr/bin/env bash set -euo pipefail export WORKSPACE_ROOT="{ws_dir}" export SKILL_DIR="{skills_dir}" source "{skills_dir}/lib.sh" _init_herdr_isolation shim_path="$WORKSPACE_ROOT/.mam/shim/herdr" if [ ! -x "$shim_path" ]; then echo "ERROR: shim not generated or not executable" >&2 exit 1 fi # Verify no 'local ' inside the shim heredoc body if grep -E '^[[:space:]]*local layout_' "$shim_path"; then echo "ERROR: 'local layout_' found in generated shim" >&2 exit 1 fi echo "SHIM_OK" """ res = subprocess.run(["bash", "-c", test_script], capture_output=True, text=True) assert res.returncode == 0, f"Shim test failed: {res.stderr}" assert "SHIM_OK" in res.stdout