import os import json import subprocess import sys import pytest from lib_py.layout import ( compute_2xk_layout, LayoutDecision, ) 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_right(): 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 == "right" assert not d.is_overflow assert d.reason == "new_column_right" 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_fill_left_column_down(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 80}}, {"pane_id": "p2", "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 == "p1" assert d.direction == "down" assert not d.is_overflow assert d.reason == "fill_column" 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 assert d.reason == "fill_column" def test_4_panes_to_5_panes_overflows_at_capacity(): 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.direction == "overflow" assert d.is_overflow assert d.reason == "grid_capacity_reached" 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(): p1 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}]}} assert compute_2xk_layout(p1).direction == "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}} ]}} d2 = compute_2xk_layout(p2) assert d2.direction == "down" assert d2.target_pane_id == "p1" 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}} ]}} d3 = compute_2xk_layout(p3) assert d3.direction == "down" assert d3.target_pane_id == "p2" 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}} ]}} d4 = compute_2xk_layout(p4) assert d4.direction == "overflow" assert d4.is_overflow 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() == "right 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"] == "right" 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 layout invocation 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:-15}}" --min-rows "${{MAM_MIN_PANE_ROWS:-0}}" --max-cols "${{MAM_MAX_PANE_COLS:-2}}" --max-rows "${{MAM_MAX_PANE_ROWS:-2}}" --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=right" 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 def _four_panes_two_columns(): """GUI payload: 2 full columns x 2 rows (4 panes). Shared by the max-cols tests.""" return { "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}}, ] } } def test_cli_max_cols_flag_triggers_overflow(): """CLI --max-cols reaches compute_2xk_layout (the lib.sh-facing path).""" payload = json.dumps(_four_panes_two_columns()) skills_dir = os.path.abspath(".agents/skills") env = {**os.environ, "PYTHONPATH": skills_dir} res = subprocess.run( [sys.executable, "-m", "lib_py.layout", "--min-cols", "30", "--min-rows", "20", "--max-cols", "2", "--json"], input=payload, capture_output=True, text=True, env=env) assert res.returncode == 0, res.stderr d = json.loads(res.stdout) assert d["direction"] == "overflow" and d["is_overflow"] assert d["reason"] == "grid_capacity_reached" def test_env_max_cols_applies_without_flag(): """MAM_MAX_PANE_COLS is honoured with no --max-cols flag.""" payload = json.dumps(_four_panes_two_columns()) skills_dir = os.path.abspath(".agents/skills") env = {**os.environ, "PYTHONPATH": skills_dir, "MAM_MAX_PANE_COLS": "2"} res = subprocess.run( [sys.executable, "-m", "lib_py.layout", "--min-cols", "30", "--min-rows", "20", "--json"], input=payload, capture_output=True, text=True, env=env) assert res.returncode == 0, res.stderr assert json.loads(res.stdout)["reason"] == "grid_capacity_reached" def _headless(n): return {"result": {"panes": [ {"pane_id": f"p{i}", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}} for i in range(1, n + 1)]}} def test_headless_max_columns_growth_guard(): """Headless capacity: n=1 right, n=2/3 down, n>=4 overflow at max_columns=2, max_rows=2.""" d4 = compute_2xk_layout(_headless(4), max_columns=2, max_rows=2) assert d4.is_overflow and d4.direction == "overflow" assert d4.reason == "grid_capacity_reached" d2 = compute_2xk_layout(_headless(2), max_columns=2, max_rows=2) assert d2.direction == "down" and d2.target_pane_id == "p1" and not d2.is_overflow d3 = compute_2xk_layout(_headless(3), max_columns=2, max_rows=2) assert d3.direction == "down" and d3.target_pane_id == "p2" and not d3.is_overflow d5 = compute_2xk_layout(_headless(5), max_columns=2, max_rows=2) assert d5.is_overflow and d5.reason == "grid_capacity_reached" # Default cap is 2 even when the caller omits max_columns. assert compute_2xk_layout(_headless(4)).direction == "overflow" _LAYOUT_ENV_VARS = ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", "MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS", "MAM_MAX_COLS", "MAM_MAX_PANE_COLS", "MAM_MAX_ROWS", "MAM_MAX_PANE_ROWS") def _run_layout(payload, args=(), env_extra=None): env = {**os.environ, "PYTHONPATH": os.path.abspath(".agents/skills")} for k in _LAYOUT_ENV_VARS: env.pop(k, None) # 호출자 셸의 오염 차단 env.update(env_extra or {}) res = subprocess.run([sys.executable, "-m", "lib_py.layout", "--json", *args], input=json.dumps(payload), capture_output=True, text=True, env=env) assert res.returncode == 0, res.stderr return json.loads(res.stdout) # height//2 = 15 < min_rows(20) 로 제약 분기 진입, width//2 = 25 가 min_cols 와 비교됨. _ZERO_TRAP = {"result": {"panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 30}}]}} def test_j1_env_zero_min_cols_matches_flag_zero(): """J-1: MAM_MIN_PANE_COLS=0 must mean 0, not fall through to the 15 default.""" flag = _run_layout(_ZERO_TRAP, ("--min-cols", "0", "--min-rows", "20")) assert flag["direction"] == "right" and flag["reason"] == "new_column_right" for var in ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS"): assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {var: "0"}) == flag, var def test_j1_env_zero_min_rows_matches_flag_zero(): flag = _run_layout(_ZERO_TRAP, ("--min-rows", "0")) assert flag["direction"] == "right" and flag["reason"] == "new_column_right" for var in ("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS"): assert _run_layout(_ZERO_TRAP, (), {var: "0"}) == flag, var def test_j1_nonzero_and_malformed_env_behaviour_unchanged(): """Behaviour neutrality: non-zero env still applies, and a lone typo still lands on the documented default instead of crashing on a None comparison.""" assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25", "MAM_MIN_PANE_ROWS": "20"}) == \ _run_layout(_ZERO_TRAP, ("--min-cols", "25", "--min-rows", "20")) assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "abc"}) == _run_layout(_ZERO_TRAP) def test_j1b_invalid_alias_does_not_shadow_the_documented_var(): """C-2: MAM_MIN_COLS is a legacy alias checked first; MAM_MIN_PANE_COLS is the name .mam.env.example documents. An unparsable value in the alias must be skipped, not abort the search and discard the documented setting. Empty values already fell through (`if raw:`); this makes invalid values behave the same way. When every candidate is unusable, `default` still wins. """ good = _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_PANE_COLS": "25"}) assert good["direction"] == "right" # 별칭이 깨져 있어도 문서화된 변수가 적용된다 assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_COLS": "foo", "MAM_MIN_PANE_COLS": "25"}) == good # 0 도 마찬가지 (J-1 과의 상호작용) assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_COLS": "foo", "MAM_MIN_PANE_COLS": "0"}) == \ _run_layout(_ZERO_TRAP, ("--min-cols", "0", "--min-rows", "20")) # 모든 후보가 무효면 문서화된 기본값으로 흡수 (Rev.1 불변식 보존) assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo", "MAM_MIN_PANE_COLS": "bar"}) == _run_layout(_ZERO_TRAP) def test_default_min_cols_is_15_and_min_rows_is_0(): """Default min_cols=15, min_rows=0. N=1 opens a column with right.""" p1_54x23 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}} d1 = compute_2xk_layout(p1_54x23) assert d1.direction == "right" assert not d1.is_overflow assert d1.reason == "new_column_right" payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 40}}, ] } } decision = compute_2xk_layout(payload) assert decision.direction == "right" assert not decision.is_overflow assert decision.reason == "new_column_right" def test_30_col_2_column_splitting_boundary(): """N=1: width >= 30 allows a right split (min_cols=15); 29 overflows.""" payload_30 = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 40}}, ] } } d30 = compute_2xk_layout(payload_30) assert d30.direction == "right" assert not d30.is_overflow payload_29 = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 29, "height": 40}}, ] } } d29 = compute_2xk_layout(payload_29) assert d29.direction == "overflow" assert d29.is_overflow assert d29.reason == "column_width_overflow" def test_54x23_compact_viewport_single_workspace_multi_pane_tiling(): """1→2 right, 2→3 down left, 3→4 down right, 4→5 overflow at capacity.""" p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}} d1 = compute_2xk_layout(p1_layout) assert d1.direction == "right" assert d1.target_pane_id == "p1" assert not d1.is_overflow p2_layout = {"result": {"panes": [ {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 23}}, {"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}}, ]}} d2 = compute_2xk_layout(p2_layout) assert d2.direction == "down" assert d2.target_pane_id == "p1" assert not d2.is_overflow p3_layout = {"result": {"panes": [ {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}}, {"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}}, {"pane_id": "p3", "rect": {"x": 26, "y": 12, "width": 27, "height": 12}}, ]}} d3 = compute_2xk_layout(p3_layout) assert d3.direction == "down" assert d3.target_pane_id == "p2" assert not d3.is_overflow p4_layout = {"result": {"panes": [ {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}}, {"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 11}}, {"pane_id": "p3", "rect": {"x": 26, "y": 12, "width": 27, "height": 12}}, {"pane_id": "p4", "rect": {"x": 53, "y": 12, "width": 27, "height": 12}}, ]}} d4 = compute_2xk_layout(p4_layout) assert d4.direction == "overflow" assert d4.is_overflow assert d4.reason == "grid_capacity_reached" def _payload(panes): return {"result": {"panes": [ {"pane_id": p["id"], "rect": {"x": p["x"], "y": p["y"], "width": p["w"], "height": p["h"]}} for p in panes ]}} def _bsp_split(panes, tid, direction): """herdr contract: bisect only the target pane's rect.""" out = [] next_id = f"p{len(panes) + 1}" for p in panes: if p["id"] != tid: out.append(dict(p)) continue if direction == "right": w1 = p["w"] // 2 w2 = p["w"] - w1 out.append({**p, "w": w1}) out.append({"id": next_id, "x": p["x"] + w1, "y": p["y"], "w": w2, "h": p["h"]}) elif direction == "down": h1 = p["h"] // 2 h2 = p["h"] - h1 out.append({**p, "h": h1}) out.append({"id": next_id, "x": p["x"], "y": p["y"] + h1, "w": p["w"], "h": h2}) else: raise AssertionError(f"unexpected split {direction}") return out def test_four_panes_form_clean_2x2(): panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] for _ in range(3): d = compute_2xk_layout(_payload(panes)) assert not d.is_overflow, d panes = _bsp_split(panes, d.target_pane_id, d.direction) assert len(panes) == 4 assert max(p["w"] for p in panes) - min(p["w"] for p in panes) <= 2 assert max(p["h"] for p in panes) - min(p["h"] for p in panes) <= 2 def test_fifth_pane_overflows(): panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] for _ in range(3): d = compute_2xk_layout(_payload(panes)) assert not d.is_overflow panes = _bsp_split(panes, d.target_pane_id, d.direction) d = compute_2xk_layout(_payload(panes)) assert d.is_overflow and d.direction == "overflow" assert d.reason == "grid_capacity_reached" @pytest.mark.parametrize("n,expected", [(1, "right"), (2, "down"), (3, "down"), (4, "overflow")]) def test_headless_matches_gui_decision(n, expected): hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) assert hl.direction == expected, f"headless n={n}" def test_headless_gui_direction_parity_full_sequence(): panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] for n in range(1, 5): gui = compute_2xk_layout(_payload(panes), max_columns=2, max_rows=2) hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) assert gui.direction == hl.direction, f"divergence at n={n}" if gui.is_overflow: break panes = _bsp_split(panes, gui.target_pane_id, gui.direction) def test_headless_fills_alternating_columns(): assert compute_2xk_layout(_headless(2), max_columns=2, max_rows=2).target_pane_id == "p1" assert compute_2xk_layout(_headless(3), max_columns=2, max_rows=2).target_pane_id == "p2" def test_headless_ignores_sample_pane_anchor(): d = compute_2xk_layout(_headless(3), max_columns=2, max_rows=2, default_anchor_id="p1") assert d.target_pane_id == "p2" def test_never_splits_right_on_partial_height_pane(): payload = { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 277, "height": 39}}, {"pane_id": "p2", "rect": {"x": 0, "y": 39, "width": 277, "height": 39}}, ] } } d = compute_2xk_layout(payload, max_columns=2, max_rows=2) assert d.direction != "right" def test_max_cols_default_reaches_cli_path(): payload = json.dumps(_four_panes_two_columns()) skills_dir = os.path.abspath(".agents/skills") env = {**os.environ, "PYTHONPATH": skills_dir} for k in _LAYOUT_ENV_VARS: env.pop(k, None) res = subprocess.run( [sys.executable, "-m", "lib_py.layout", "--json"], input=payload, capture_output=True, text=True, env=env) assert res.returncode == 0, res.stderr d = json.loads(res.stdout) assert d["direction"] == "overflow" assert d["reason"] == "grid_capacity_reached" def test_lib_sh_passes_max_cols_and_rows(): lib_path = os.path.abspath(".agents/skills/lib.sh") content = open(lib_path, encoding="utf-8").read() assert '--max-cols "${MAM_MAX_PANE_COLS:-2}"' in content assert '--max-rows "${MAM_MAX_PANE_ROWS:-2}"' in content