fix(lib): finalize herdr 0.7.4 contract refactor and layout policy
This commit is contained in:
+255
-63
@@ -36,6 +36,8 @@ def mam_sandbox(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("LOCAL_BIN", str(tmp_path / ".local" / "bin"))
|
||||
monkeypatch.setenv("AGENT_SESSIONS_YAML", str(yaml_path))
|
||||
monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))
|
||||
monkeypatch.delenv("HERDR_SESSION_NAME", raising=False)
|
||||
monkeypatch.delenv("HERDR_SERVER_NAME", raising=False)
|
||||
import sys
|
||||
monkeypatch.setenv("AGENT_PYTHON_BIN", sys.executable)
|
||||
|
||||
@@ -105,9 +107,21 @@ if os.path.exists(state_file):
|
||||
|
||||
# Record the command call
|
||||
state["calls"].append(sys.argv[1:])
|
||||
try:
|
||||
with open(state_file + ".trace", "a") as tf:
|
||||
tf.write(f"PID {os.getpid()} ARGS: {sys.argv[1:]}\\n")
|
||||
tf.write(f" AGENTS: {list(state.get('agents', {}).keys())}\\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def save_state():
|
||||
disk_state = {"workspaces": [], "agents": {}, "calls": []}
|
||||
# LOCK INVARIANT (W17):
|
||||
# lock_f (state_file + .lock) acquired at entry is deliberately NOT closed inside save_state().
|
||||
# This exclusive global process lock is the LOAD-BEARING guarantee of consistency for
|
||||
# overwriting disk_state["agents"] and disk_state["workspaces"].
|
||||
# If lock scope is ever narrowed or made fine-grained, state merging MUST switch to
|
||||
# explicit key-by-key dictionary diff merging first before removing the global lock.
|
||||
disk_state = {"workspaces": [], "agents": {}, "calls": [], "panes": []}
|
||||
if os.path.exists(state_file):
|
||||
for _retry in range(50):
|
||||
try:
|
||||
@@ -120,8 +134,18 @@ def save_state():
|
||||
pass
|
||||
time.sleep(0.02)
|
||||
disk_state["agents"] = state.get("agents", {})
|
||||
state["agents"] = disk_state["agents"]
|
||||
|
||||
if "workspaces" in state:
|
||||
disk_state["workspaces"] = state["workspaces"]
|
||||
disk_ws = disk_state.setdefault("workspaces", [])
|
||||
for w in state["workspaces"]:
|
||||
if not any(dw.get("workspace_id") == w.get("workspace_id") for dw in disk_ws):
|
||||
disk_ws.append(w)
|
||||
if "panes" in state:
|
||||
disk_panes = disk_state.setdefault("panes", [])
|
||||
for p in state["panes"]:
|
||||
if not any(dp.get("pane_id") == p.get("pane_id") for dp in disk_panes):
|
||||
disk_panes.append(p)
|
||||
disk_calls = disk_state.setdefault("calls", [])
|
||||
if sys.argv[1:] and (not disk_calls or disk_calls[-1] != sys.argv[1:]):
|
||||
disk_calls.append(sys.argv[1:])
|
||||
@@ -151,7 +175,11 @@ if cmd1 == "workspace":
|
||||
sys.exit(0)
|
||||
cmd2 = args[1]
|
||||
if cmd2 == "list":
|
||||
res = {"workspaces": state.get("workspaces", [])}
|
||||
# W10: WorkspaceInfo has NO cwd key
|
||||
wss = []
|
||||
for w in state.get("workspaces", []):
|
||||
wss.append({"workspace_id": w["workspace_id"], "label": w.get("label", "default")})
|
||||
res = {"workspaces": wss}
|
||||
print(json.dumps({"result": res}))
|
||||
sys.exit(0)
|
||||
elif cmd2 == "create":
|
||||
@@ -168,11 +196,135 @@ if cmd1 == "workspace":
|
||||
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
|
||||
ws_id = f"w{len(workspaces) + 1}"
|
||||
workspaces.append({"workspace_id": ws_id, "label": label, "cwd": cwd})
|
||||
state["workspaces"] = workspaces
|
||||
|
||||
# W10: workspace create returns full workspace_created object
|
||||
root_pane_id = f"{ws_id}:p1"
|
||||
panes = state.get("panes", [])
|
||||
panes.append({"pane_id": root_pane_id, "workspace_id": ws_id, "cwd": cwd, "tab_id": f"{ws_id}:t1"})
|
||||
state["panes"] = panes
|
||||
save_state()
|
||||
print(json.dumps({
|
||||
"result": {
|
||||
"type": "workspace_created",
|
||||
"workspace": {"workspace_id": ws_id, "label": label},
|
||||
"tab": {"tab_id": f"{ws_id}:t1"},
|
||||
"root_pane": {"pane_id": root_pane_id}
|
||||
}
|
||||
}))
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd1 == "pane":
|
||||
if len(args) < 2:
|
||||
sys.exit(0)
|
||||
cmd2 = args[1]
|
||||
if cmd2 == "list":
|
||||
target_ws = ""
|
||||
i = 2
|
||||
while i < len(args):
|
||||
if args[i] == "--workspace":
|
||||
target_ws = args[i+1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
panes_list = []
|
||||
# Build panes from agents or state["panes"]
|
||||
for name, data in state.get("agents", {}).items():
|
||||
ws_id = data.get("workspace_id", "w1")
|
||||
if target_ws and ws_id != target_ws:
|
||||
continue
|
||||
panes_list.append({
|
||||
"pane_id": data.get("pane_id", f"{ws_id}:p1"),
|
||||
"workspace_id": ws_id,
|
||||
"cwd": data.get("cwd", "."),
|
||||
"tab_id": f"{ws_id}:t1",
|
||||
"agent": data.get("agent", "claude")
|
||||
})
|
||||
if not panes_list:
|
||||
for p in state.get("panes", []):
|
||||
if target_ws and p.get("workspace_id") != target_ws:
|
||||
continue
|
||||
panes_list.append(p)
|
||||
print(json.dumps({"result": {"panes": panes_list}}))
|
||||
sys.exit(0)
|
||||
elif cmd2 == "split":
|
||||
# W11: pane split handler
|
||||
print(json.dumps({"result": {"type": "pane_split", "pane": {"pane_id": "w1:p_split"}}}))
|
||||
sys.exit(0)
|
||||
elif cmd2 == "layout":
|
||||
# W11: pane layout handler returning area, panes, focused_pane_id
|
||||
focused = "w1:p1"
|
||||
panes_list = []
|
||||
agents = state.get("agents", {})
|
||||
count = max(len(agents), 1)
|
||||
w_per_pane = 184 // count
|
||||
idx = 0
|
||||
for name, data in agents.items():
|
||||
panes_list.append({
|
||||
"pane_id": data.get("pane_id", f"w1:p{idx+1}"),
|
||||
"rect": {"width": w_per_pane, "height": 78}
|
||||
})
|
||||
idx += 1
|
||||
if not panes_list:
|
||||
panes_list = [{"pane_id": "w1:p1", "rect": {"width": 184, "height": 78}}]
|
||||
print(json.dumps({
|
||||
"result": {
|
||||
"area": {"width": 184, "height": 78},
|
||||
"focused_pane_id": panes_list[0]["pane_id"],
|
||||
"panes": panes_list
|
||||
}
|
||||
}))
|
||||
sys.exit(0)
|
||||
elif 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 or a_name == 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", "") + "\\\\n\\\\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" or name == pane_id:
|
||||
to_delete.append(name)
|
||||
for name in to_delete:
|
||||
del agents[name]
|
||||
state["agents"] = agents
|
||||
save_state()
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd1 == "agent":
|
||||
@@ -207,22 +359,36 @@ elif cmd1 == "agent":
|
||||
agent_cmd = []
|
||||
opts = args[3:]
|
||||
|
||||
# W8: Whitelist herdr 0.7.4 allowed flags
|
||||
whitelist = {"--cwd", "--workspace", "--tab", "--split", "--env", "--focus", "--no-focus"}
|
||||
i = 0
|
||||
unknown_flags = []
|
||||
while i < len(opts):
|
||||
if opts[i] == "--workspace":
|
||||
opt = opts[i]
|
||||
if opt not in whitelist:
|
||||
unknown_flags.append(opt)
|
||||
i += 1
|
||||
continue
|
||||
if opt == "--workspace":
|
||||
ws = opts[i+1]
|
||||
i += 2
|
||||
elif opts[i] == "--cwd":
|
||||
elif opt == "--cwd":
|
||||
cwd = opts[i+1]
|
||||
i += 2
|
||||
elif opts[i] == "--env":
|
||||
elif opt == "--env":
|
||||
env_val = opts[i+1]
|
||||
if "=" in env_val:
|
||||
k, v = env_val.split("=", 1)
|
||||
os.environ[k] = v
|
||||
i += 2
|
||||
elif opt in ("--split", "--tab"):
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if unknown_flags:
|
||||
print(f"usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--env KEY=VALUE] [--focus|--no-focus] -- <argv...> (unknown flag: {unknown_flags[0]})")
|
||||
sys.exit(0)
|
||||
|
||||
# Determine the agent type (claude, agy, hermes, cline)
|
||||
agent_type = "claude"
|
||||
@@ -344,13 +510,24 @@ elif cmd1 == "agent":
|
||||
|
||||
state["agents"] = agents
|
||||
save_state()
|
||||
print(json.dumps({
|
||||
"result": {
|
||||
"type": "agent_started",
|
||||
"agent": {
|
||||
"name": name,
|
||||
"workspace_id": ws or "w1",
|
||||
"pane_id": f"w1:p_{name}",
|
||||
"cwd": cwd or "TMP_PATH_PLACEHOLDER"
|
||||
},
|
||||
"argv": agent_cmd
|
||||
}
|
||||
}))
|
||||
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 = {
|
||||
@@ -396,9 +573,9 @@ elif cmd1 == "agent":
|
||||
if name in agents:
|
||||
agents[name]["sent_text"] = agents[name].get("sent_text", "") + text
|
||||
if text in ("C-m", "Enter"):
|
||||
agents[name]["buffer"] = agents[name].get("buffer", "") + "\\nesc to interrupt"
|
||||
agents[name]["buffer"] = agents[name].get("buffer", "") + "\\\\n\\\\nesc to interrupt"
|
||||
else:
|
||||
agents[name]["buffer"] = agents[name].get("buffer", "") + "\\n" + text
|
||||
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
|
||||
@@ -441,59 +618,53 @@ elif cmd1 == "session":
|
||||
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)
|
||||
elif cmd1 == "send-keys":
|
||||
target = ""
|
||||
key = ""
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] in ("-t", "--target") and i + 1 < len(args):
|
||||
target = args[i+1]
|
||||
i += 2
|
||||
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]
|
||||
key = args[i]
|
||||
i += 1
|
||||
agents = state.get("agents", {})
|
||||
actual_name = target
|
||||
for a_name, data in agents.items():
|
||||
if data.get("pane_id") == target or a_name == target:
|
||||
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", "") + "\\\\n\\\\nesc to interrupt"
|
||||
state["agents"] = agents
|
||||
save_state()
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd1 == "capture-pane":
|
||||
target = ""
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] in ("-t", "--target") and i + 1 < len(args):
|
||||
target = args[i+1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
agents = state.get("agents", {})
|
||||
actual_name = target
|
||||
for a_name, data in agents.items():
|
||||
if data.get("pane_id") == target or a_name == target:
|
||||
actual_name = a_name
|
||||
break
|
||||
if actual_name in agents:
|
||||
print(agents[actual_name].get("buffer", "Ready"))
|
||||
else:
|
||||
print("Ready")
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd1 == "list-panes":
|
||||
session_target = ""
|
||||
@@ -505,7 +676,15 @@ elif cmd1 == "list-panes":
|
||||
pid = data.get("pid", 9999)
|
||||
cwd = data.get("cwd", "TMP_PATH_PLACEHOLDER")
|
||||
cmd = data.get("command", "claude")
|
||||
print(f"{pid}|{cwd}|{cmd}")
|
||||
if "-F" in args:
|
||||
f_idx = args.index("-F")
|
||||
fmt = args[f_idx + 1] if f_idx + 1 < len(args) else ""
|
||||
if "pane_current_path" in fmt or "|" in fmt:
|
||||
print(f"{pid}|{cwd}|{cmd}")
|
||||
else:
|
||||
print(f"{pid}")
|
||||
else:
|
||||
print(f"{pid}|{cwd}|{cmd}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
@@ -522,6 +701,19 @@ elif cmd1 == "has-session":
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif cmd1 == "kill-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:
|
||||
del agents[sess_target]
|
||||
state["agents"] = agents
|
||||
save_state()
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd1 == "ls":
|
||||
if "-F" in args:
|
||||
# Real POSIX seconds, not a sentinel: a below-floor value silently
|
||||
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"WorkspaceInfo": {
|
||||
"properties": [
|
||||
"active_tab_id",
|
||||
"agent_status",
|
||||
"focused",
|
||||
"label",
|
||||
"number",
|
||||
"pane_count",
|
||||
"tab_count",
|
||||
"tokens",
|
||||
"workspace_id",
|
||||
"worktree"
|
||||
]
|
||||
},
|
||||
"PaneInfo": {
|
||||
"properties": [
|
||||
"agent",
|
||||
"cwd",
|
||||
"foreground_cwd",
|
||||
"pane_id",
|
||||
"tab_id",
|
||||
"workspace_id"
|
||||
]
|
||||
},
|
||||
"AgentStartFlags": [
|
||||
"--cwd",
|
||||
"--workspace",
|
||||
"--tab",
|
||||
"--split",
|
||||
"--env",
|
||||
"--focus",
|
||||
"--no-focus"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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 absolute path preservation
|
||||
cmd_str = f"""
|
||||
source {lib_path}
|
||||
_init_herdr_isolation
|
||||
herdr new-session -d -s "test-h1-sess" -c "{tmp_path}" "/usr/bin/python3 -c 'print(1)'"
|
||||
"""
|
||||
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 = {"--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 no un-whitelisted flags like --kind or --pane
|
||||
for i in range(len(opts)):
|
||||
if opts[i].startswith("--"):
|
||||
assert opts[i] in whitelist, f"Forbidden flag in agent start: {opts[i]}"
|
||||
|
||||
# H-4: Check executable absolute path preserved
|
||||
if argv:
|
||||
assert argv[0] == "/usr/bin/python3", f"Executable path mutated: {argv[0]}"
|
||||
|
||||
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-sess" -c "{tmp_path}" "python3 -c 'print(1)'"
|
||||
"""
|
||||
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}}", "--cwd", "/tmp", "--", "claude"])
|
||||
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
|
||||
Reference in New Issue
Block a user