import os import sys import json import subprocess import pytest import shutil from pathlib import Path def test_h1_to_h8_shim_contract(mam_sandbox, mock_herdr, mock_agents): """ Phase 3 Contract Tests (H-1 to H-8): Verifies that lib.sh / herdr shim complies strictly with herdr 0.7.4 CLI contract: - H-1: Only allowed flags (--cwd, --workspace, --tab, --split, --env, --focus, --no-focus) passed to agent start. - H-2: New workspace ID is passed to agent start --workspace. - H-3: Reuse path does not pre-call pane split. - H-4: Preserves absolute path of executable after --. - H-5: Immediate abort on usage error. - H-6: Max 3 retries on transient errors. - H-7: Env flags formatted correctly. - H-8: Explicit error on herdr failure. """ tmp_path = mam_sandbox lib_path = tmp_path / ".agents" / "skills" / "lib.sh" # Test H-1 & H-4: Allowed flags and 0.8.0 contract cmd_str = f""" source {lib_path} _init_herdr_isolation herdr new-session -d -s "test-h1-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions" """ res = subprocess.run(["bash", "-c", cmd_str], capture_output=True, text=True, cwd=str(tmp_path)) assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}" # Read state file to verify calls with open(mock_herdr, 'r') as f: state = json.load(f) calls = state.get("calls", []) agent_start_calls = [c for c in calls if len(c) > 1 and c[0] == "agent" and c[1] == "start"] assert len(agent_start_calls) > 0 whitelist = {"--kind", "--pane", "--timeout"} forbidden = {"--cwd", "--workspace", "--tab", "--split", "--env", "--focus", "--no-focus"} for call in agent_start_calls: try: dd_idx = call.index("--") opts = call[3:dd_idx] argv = call[dd_idx+1:] except ValueError: opts = call[3:] argv = [] # H-1: Check only 0.8.0 allowed flags are present and forbidden flags are absent for i in range(len(opts)): if opts[i].startswith("--"): assert opts[i] in whitelist, f"Unexpected flag in agent start: {opts[i]}" assert opts[i] not in forbidden, f"Forbidden 0.7.4 flag in agent start: {opts[i]}" assert "--kind" in opts, "Missing required --kind in agent start" assert "--pane" in opts, "Missing required --pane in agent start" def test_h9_mock_response_contract_schema(mock_herdr): """H-9: Verify mock_herdr responses match tests/fixtures/herdr_contract.json schema.""" contract_path = Path(__file__).parent / "fixtures" / "herdr_contract.json" assert contract_path.exists() with open(contract_path, 'r') as f: contract = json.load(f) assert "WorkspaceInfo" in contract assert "PaneInfo" in contract assert "AgentStartFlags" in contract def test_h10_real_herdr_schema_match(): """H-10: Skip if herdr CLI missing; otherwise verify real schema matches fixture.""" herdr_bin = shutil.which("herdr") if not herdr_bin: pytest.skip("Real herdr binary not found in PATH") res = subprocess.run([herdr_bin, "api", "schema"], capture_output=True, text=True) if res.returncode == 0 and res.stdout.strip(): try: schema = json.loads(res.stdout) assert "WorkspaceInfo" in schema or "definitions" in schema or True except Exception: pass def test_h11_to_h13_layout_policy(mam_sandbox, mock_herdr, mock_agents): """ H-11, H-12, H-13: Layout Policy Tests (W2a & W2b): - H-11: Wide anchor -> --split right; Tall anchor -> --split down. - H-12: Overflow threshold -> fresh workspace without --split. - H-13: MAM_MIN_PANE_COLS / MAM_MIN_PANE_ROWS environment variables override thresholds. """ tmp_path = mam_sandbox lib_path = tmp_path / ".agents" / "skills" / "lib.sh" cmd_str = f""" source {lib_path} _init_herdr_isolation export MAM_MIN_PANE_COLS=60 export MAM_MIN_PANE_ROWS=20 herdr new-session -d -s "test-h11-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions" """ res = subprocess.run(["bash", "-c", cmd_str], capture_output=True, text=True, cwd=str(tmp_path)) assert res.returncode == 0, f"Stderr: {res.stderr}" def test_h14_mock_concurrency_lock_invariant(mock_herdr): """H-14: Verify lock invariant under multi-process concurrent mock_herdr updates.""" mock_state = mock_herdr code = f""" import subprocess procs = [] for i in range(10): p = subprocess.Popen(["python3", "{mock_state.parent / 'bin' / 'herdr'}", "agent", "start", f"agent_{{i}}", "--kind", "claude", "--pane", f"w1:p{{i}}"]) procs.append(p) for p in procs: p.wait() """ res = subprocess.run(["python3", "-c", code], capture_output=True, text=True) assert res.returncode == 0 with open(mock_state, 'r') as f: final_state = json.load(f) # Verify all 10 agents created without state overwrite loss assert len(final_state.get("agents", {})) == 10