import os import shutil import json import uuid import sqlite3 import pytest @pytest.fixture def mam_sandbox(tmp_path, monkeypatch): """ Manages a temporary sandboxed directory for .mam/ configuration files and copies the scripts from .agents/skills to prevent contaminating the real codebase. Also overrides environments (HOME, PATH, AGENT_SESSIONS_YAML, etc.) for isolation. """ # 1. Copy the skill scripts to sandboxed tmp_path/skills and tmp_path/.agents/skills src_skills = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".agents", "skills")) shutil.copytree(src_skills, tmp_path / "skills") shutil.copytree(src_skills, tmp_path / ".agents" / "skills") # 2. Create sandboxed directory structure mam_dir = tmp_path / ".mam" mam_dir.mkdir(parents=True, exist_ok=True) yaml_path = mam_dir / "agent-sessions.yaml" # Create empty registry structure yaml_path.write_text("herdr_sessions: []\ndelegation_jobs: []\n") # 3. Setup mock home directory contents (tmp_path / ".claude" / "projects").mkdir(parents=True, exist_ok=True) (tmp_path / ".local" / "bin").mkdir(parents=True, exist_ok=True) # 4. Monkeypatch environment variables to point inside the sandbox monkeypatch.setenv("HOME", str(tmp_path)) monkeypatch.setenv("HOME_DIR", str(tmp_path)) monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path / ".claude" / "projects")) monkeypatch.setenv("LOCAL_BIN", str(tmp_path / ".local" / "bin")) monkeypatch.setenv("AGENT_SESSIONS_YAML", str(yaml_path)) monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path)) import sys monkeypatch.setenv("AGENT_PYTHON_BIN", sys.executable) return tmp_path @pytest.fixture def mock_herdr(mam_sandbox, monkeypatch): """ Generates a mock 'herdr' executable and prepends it to PATH. Allows tests to control and verify state via mock_herdr_state.json. Automatically generates mock session database files upon 'agent start'. """ tmp_path = mam_sandbox state_file = tmp_path / "mock_herdr_state.json" # Initial mock state initial_state = { "workspaces": [ { "workspace_id": "w1", "label": "default", "cwd": str(tmp_path) } ], "agents": {}, "calls": [] } state_file.write_text(json.dumps(initial_state, indent=2)) bin_dir = tmp_path / "bin" bin_dir.mkdir(parents=True, exist_ok=True) herdr_bin = bin_dir / "herdr" # Write Python implementation of mock herdr (using string replacement to avoid f-string escaping issues) # Double escape backslashes for newline (\\\\n) and write correctly code = """#!/usr/bin/env python3 import sys import os import json import uuid import sqlite3 import fcntl state_file = "STATE_FILE_PLACEHOLDER" if not os.path.exists(state_file): sys.exit(0) # Lock the state file exclusively to prevent concurrent race conditions lock_f = open(state_file + ".lock", "w") fcntl.flock(lock_f, fcntl.LOCK_EX) state = {"workspaces": [], "agents": {}, "calls": []} if os.path.exists(state_file): for _retry in range(10): try: with open(state_file, 'r') as f: content = f.read().strip() if content: state = json.loads(content) break except Exception: import time time.sleep(0.05) # Record the command call state["calls"].append(sys.argv[1:]) def save_state(): tmp_state = state_file + f".tmp.{os.getpid()}" with open(tmp_state, 'w') as f: json.dump(state, f, indent=2) f.flush() os.fsync(f.fileno()) os.replace(tmp_state, state_file) # Save calls immediately so they persist even if we exit early or error out save_state() args = sys.argv[1:] while args and args[0] in ("-L", "--server", "-s", "--session"): if len(args) > 1: args = args[2:] else: args = args[1:] if not args: sys.exit(0) cmd1 = args[0] if cmd1 == "workspace": if len(args) < 2: sys.exit(0) cmd2 = args[1] if cmd2 == "list": res = {"workspaces": state.get("workspaces", [])} print(json.dumps({"result": res})) sys.exit(0) elif cmd2 == "create": label = "default" cwd = "." i = 2 while i < len(args): if args[i] == "--label": label = args[i+1] i += 2 elif args[i] == "--cwd": cwd = args[i+1] i += 2 else: i += 1 workspaces = state.get("workspaces", []) if not any(w["label"] == label for w in workspaces): ws_id = f"w{len(workspaces) + 1}" workspaces.append({"workspace_id": ws_id, "label": label, "cwd": cwd}) state["workspaces"] = workspaces save_state() sys.exit(0) elif cmd1 == "agent": if len(args) < 2: sys.exit(0) cmd2 = args[1] if cmd2 == "list": agents_list = [] for name, data in state.get("agents", {}).items(): agents_list.append({ "name": name, "agent": data.get("agent", "claude"), "agent_status": data.get("status", "running"), "cwd": data.get("cwd", ""), "pane_id": data.get("pane_id", "w1:p1"), "workspace_id": data.get("workspace_id", "w1") }) print(json.dumps({"result": {"agents": agents_list}})) sys.exit(0) elif cmd2 == "start": if len(args) < 3: sys.exit(1) name = args[2] ws = "" cwd = "" # Find where -- is try: double_dash_idx = args.index("--") agent_cmd = args[double_dash_idx+1:] opts = args[3:double_dash_idx] except ValueError: agent_cmd = [] opts = args[3:] i = 0 while i < len(opts): if opts[i] == "--workspace": ws = opts[i+1] i += 2 elif opts[i] == "--cwd": cwd = opts[i+1] i += 2 elif opts[i] == "--env": env_val = opts[i+1] if "=" in env_val: k, v = env_val.split("=", 1) os.environ[k] = v i += 2 else: i += 1 # Determine the agent type (claude, agy, hermes, cline) agent_type = "claude" if agent_cmd: if "claude" in agent_cmd[0]: agent_type = "claude" elif "agy" in agent_cmd[0]: agent_type = "agy" elif "hermes" in agent_cmd[0]: agent_type = "hermes" elif "cline" in agent_cmd[0]: agent_type = "cline" else: # guess from name if "claude" in name: agent_type = "claude" elif "agy" in name: agent_type = "agy" elif "hermes" in name: agent_type = "hermes" elif "cline" in name: agent_type = "cline" # TUI Welcome Tokens definition to prevent TUI readiness check timeout buffer_content = { "claude": "Anthropic Claude Ready", "agy": "Antigravity Ready", "hermes": "Hermes Ready", "cline": "Cline Chat Ready" }.get(agent_type, "Ready") agents = state.get("agents", {}) agents[name] = { "agent": agent_type, "status": "running", "cwd": cwd or "TMP_PATH_PLACEHOLDER", "workspace_id": ws or "w1", "pid": 9999, "pane_id": f"w1:p{len(agents)+1}", "command": " ".join(agent_cmd), "buffer": buffer_content } # Generate session/conversation UUID and files to simulate agent startup session_uuid = str(uuid.uuid4()) own_key_map = { "claude": "claude_session_id_own", "agy": "agy_conversation_id_own", "hermes": "hermes_conversation_id_own", "cline": "cline_conversation_id_own" } own_key = own_key_map.get(agent_type) if own_key: agents[name][own_key] = session_uuid # Find isolation directory (e.g. if HOME has agent_homes/) home_dir = os.environ.get("HOME", "TMP_PATH_PLACEHOLDER") ws_abs = os.path.abspath(cwd or "TMP_PATH_PLACEHOLDER") ws_key = ws_abs.replace('/', '-').replace('_', '-') if agent_type == "claude": ccd = os.environ.get("CLAUDE_CONFIG_DIR") if ccd: cp_dir = os.path.join(ccd, "projects") else: cp_dir = os.environ.get("CLAUDE_PROJECT_DIR", f"{home_dir}/.claude/projects") proj_dir = os.path.join(cp_dir, ws_key) os.makedirs(proj_dir, exist_ok=True) jsonl_file = os.path.join(proj_dir, f"{session_uuid}.jsonl") with open(jsonl_file, 'w') as jf: jf.write(json.dumps({"sessionId": session_uuid}) + "\\n") elif agent_type == "agy": db_dir = os.path.join(home_dir, ".gemini", "antigravity-cli", "conversations") os.makedirs(db_dir, exist_ok=True) db_file = os.path.join(db_dir, f"{session_uuid}.db") with open(db_file, 'w') as df: df.write("") lc_dir = os.path.join(home_dir, ".gemini", "antigravity-cli", "cache") os.makedirs(lc_dir, exist_ok=True) lc_file = os.path.join(lc_dir, "last_conversations.json") lc_data = {} if os.path.exists(lc_file): try: with open(lc_file, 'r') as lcf: lc_data = json.load(lcf) except Exception: pass lc_data[ws_abs] = session_uuid with open(lc_file, 'w') as lcf: json.dump(lc_data, lcf) elif agent_type == "hermes": hermes_dir = os.path.join(home_dir, ".hermes") os.makedirs(hermes_dir, exist_ok=True) hdb = os.path.join(hermes_dir, "state.db") conn = sqlite3.connect(hdb) conn.execute("CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, cwd TEXT, started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)") conn.execute("INSERT OR REPLACE INTO sessions (id, cwd) VALUES (?, ?)", (session_uuid, ws_abs)) conn.commit() conn.close() elif agent_type == "cline": clin_base = os.path.join(home_dir, ".cline", "data", "sessions") if "agent_homes" in home_dir: clin_base = os.path.join(home_dir, "sessions") session_dir = os.path.join(clin_base, session_uuid) os.makedirs(session_dir, exist_ok=True) json_file = os.path.join(session_dir, f"{session_uuid}.json") with open(json_file, 'w') as jf: jf.write(json.dumps({"id": session_uuid})) state["agents"] = agents save_state() sys.exit(0) elif cmd2 == "get": if len(args) < 3: sys.exit(1) name = args[2] agents = state.get("agents", {}) sys.stderr.write(f"[mock_herdr] agent get '{name}' — known agents: {list(agents.keys())}\\n") if name in agents: agent_data = agents[name] pane_info = { "pid": agent_data.get("pid", 9999), "cwd": agent_data.get("cwd", ""), "command": agent_data.get("command", ""), "argv": agent_data.get("command", "") } print(json.dumps({ "pane": pane_info, "result": { "agent": { "agent": agent_data.get("agent", "claude"), "status": agent_data.get("status", "running"), "cwd": agent_data.get("cwd", ""), "pane_id": agent_data.get("pane_id", "w1:p1"), "workspace_id": agent_data.get("workspace_id", "w1") } } })) sys.exit(0) else: sys.stderr.write("Agent " + name + " not found\\\\n") sys.exit(1) elif cmd2 == "read": if len(args) < 3: sys.exit(1) name = args[2] agents = state.get("agents", {}) if name in agents: buffer_content = agents[name].get("buffer", "Ready") print(buffer_content) sys.exit(0) else: sys.stderr.write("Agent " + name + " not found\\\\n") sys.exit(1) elif cmd2 == "send": if len(args) < 4: sys.exit(1) name = args[2] text = args[3] agents = state.get("agents", {}) if name in agents: agents[name]["sent_text"] = agents[name].get("sent_text", "") + text if text == "C-m": agents[name]["buffer"] = agents[name].get("buffer", "") + "\\nesc to interrupt" else: agents[name]["buffer"] = agents[name].get("buffer", "") + "\\n" + text if "/exit" in text or "exit" in text or "Exit" in text: agents[name]["status"] = "stopped" state["agents"] = agents save_state() sys.exit(0) else: sys.stderr.write("Agent " + name + " not found\\\\n") sys.exit(1) elif cmd1 == "session": if len(args) < 2: sys.exit(0) cmd2 = args[1] if cmd2 == "list": session_names = ["custom_server", "default", "multi-agent-mux"] session_names.extend(list(state.get("agents", {}).keys())) if os.environ.get("MAM_SESS"): session_names.append(os.environ["MAM_SESS"]) res = {"sessions": [{"name": s, "running": True} for s in set(session_names)]} print(json.dumps(res)) sys.exit(0) if len(args) < 3: sys.exit(1) name = args[2] agents = state.get("agents", {}) if cmd2 == "stop": if name in agents: agents[name]["status"] = "stopped" state["agents"] = agents save_state() sys.exit(0) else: sys.exit(1) elif cmd2 == "delete": if name in agents: del agents[name] state["agents"] = agents save_state() sys.exit(0) else: sys.exit(1) elif cmd1 == "pane": if len(args) < 3: sys.exit(1) cmd2 = args[1] if cmd2 == "send-keys": if len(args) < 4: sys.exit(1) name = args[2] key = args[3] agents = state.get("agents", {}) actual_name = name for a_name, data in agents.items(): if data.get("pane_id") == name: actual_name = a_name break if actual_name in agents: agents[actual_name]["sent_keys"] = agents[actual_name].get("sent_keys", []) + [key] if key in ("Enter", "C-m"): agents[actual_name]["buffer"] = agents[actual_name].get("buffer", "") + "\\nesc to interrupt" state["agents"] = agents save_state() sys.exit(0) else: sys.exit(1) elif cmd2 == "process-info": pane_id = "" if "--pane" in args: pane_id = args[args.index("--pane") + 1] pid = 9999 for name, data in state.get("agents", {}).items(): if data.get("pane_id") == pane_id or pane_id == "w1:p1": pid = data.get("pid", 9999) break res = { "process_info": { "foreground_processes": [{"pid": pid}], "shell_pid": pid } } print(json.dumps({"result": res})) sys.exit(0) elif cmd2 == "close": pane_id = args[2] agents = state.get("agents", {}) to_delete = [] for name, data in agents.items(): if data.get("pane_id") == pane_id or pane_id == "w1:p1": to_delete.append(name) for name in to_delete: del agents[name] state["agents"] = agents save_state() sys.exit(0) elif cmd1 == "list-panes": session_target = "" if "-t" in args: session_target = args[args.index("-t") + 1] agents = state.get("agents", {}) if session_target in agents: data = agents[session_target] pid = data.get("pid", 9999) cwd = data.get("cwd", "TMP_PATH_PLACEHOLDER") cmd = data.get("command", "claude") print(f"{pid}|{cwd}|{cmd}") sys.exit(0) else: sys.exit(1) elif cmd1 == "has-session": sess_target = "" if "-t" in args: sess_target = args[args.index("-t") + 1] elif len(args) > 1: sess_target = args[1] agents = state.get("agents", {}) if sess_target in agents: sys.exit(0) else: sys.exit(1) elif cmd1 == "ls": if "-F" in args: for name, data in state.get("agents", {}).items(): print(name + "|999999") sys.exit(0) agents_list = [] for name, data in state.get("agents", {}).items(): agents_list.append({ "name": name, "agent": data.get("agent", "claude"), "agent_status": data.get("status", "running") }) print(json.dumps({"result": {"agents": agents_list}})) sys.exit(0) sys.exit(0) """.replace("STATE_FILE_PLACEHOLDER", str(state_file)).replace("TMP_PATH_PLACEHOLDER", str(tmp_path)) herdr_bin.write_text(code) herdr_bin.chmod(0o755) # Update PATH using monkeypatch. # Prepend bin_dir, and append standard Homebrew/system paths so lib.sh won't prepend them old_path = os.environ.get("PATH", "") extra_dirs = [ "/home/linuxbrew/.linuxbrew/bin", "/home/linuxbrew/.linuxbrew/sbin", f"{tmp_path}/.local/bin", f"{tmp_path}/.npm-global/bin" ] new_path = old_path for d in extra_dirs: if d not in new_path: new_path = f"{new_path}:{d}" monkeypatch.setenv("PATH", f"{bin_dir}:{new_path}") return state_file @pytest.fixture def mock_agents(mam_sandbox, monkeypatch): """ Generates mock agent binaries (claude, agy, hermes, cline) and places them in the sandboxed bin folder to satisfy preflight checks. """ tmp_path = mam_sandbox bin_dir = tmp_path / "bin" bin_dir.mkdir(parents=True, exist_ok=True) # 1. claude mock claude_bin = bin_dir / "claude" claude_bin.write_text("""#!/usr/bin/env python3 import sys import json args = sys.argv[1:] if len(args) >= 2 and args[0] == "auth" and args[1] == "status": print(json.dumps({"loggedIn": True})) sys.exit(0) sys.exit(0) """) claude_bin.chmod(0o755) # 2. agy mock agy_bin = bin_dir / "agy" agy_bin.write_text("""#!/usr/bin/env python3 import sys args = sys.argv[1:] if len(args) >= 1 and args[0] == "models": print("gemini-1.5-pro\\ngemini-1.5-flash") sys.exit(0) sys.exit(0) """) agy_bin.chmod(0o755) # 3. hermes mock hermes_bin = bin_dir / "hermes" hermes_bin.write_text("""#!/usr/bin/env python3 import sys args = sys.argv[1:] if len(args) >= 1 and args[0] == "status": print("Hermes functional") sys.exit(0) sys.exit(0) """) hermes_bin.chmod(0o755) # 4. cline mock cline_bin = bin_dir / "cline" cline_bin.write_text("""#!/usr/bin/env python3 import sys import json args = sys.argv[1:] if len(args) >= 1 and args[0] == "history": print(json.dumps([])) sys.exit(0) sys.exit(0) """) cline_bin.chmod(0o755) # 5. uuidgen mock to guarantee isolated creation UUIDs uuidgen_bin = bin_dir / "uuidgen" uuidgen_bin.write_text("""#!/usr/bin/env python3 import uuid print(str(uuid.uuid4())) """) uuidgen_bin.chmod(0o755) # Prepend bin directory to PATH old_path = os.environ.get("PATH", "") extra_dirs = [ "/home/linuxbrew/.linuxbrew/bin", "/home/linuxbrew/.linuxbrew/sbin", f"{tmp_path}/.local/bin", f"{tmp_path}/.npm-global/bin" ] new_path = old_path for d in extra_dirs: if d not in new_path: new_path = f"{new_path}:{d}" monkeypatch.setenv("PATH", f"{bin_dir}:{new_path}") return bin_dir