feat(cli,registry): introduce --herdr-workspace option and decouple socket fallback chains
This commit is contained in:
@@ -247,8 +247,133 @@ def test_comp_create_herdr_session_yaml_propagation(mam_sandbox, mock_herdr, moc
|
||||
assert "HERDR_SESSION_NAME=isolated-suite-01" in s["kill_command"]
|
||||
|
||||
|
||||
def test_comp_create_herdr_workspace_parsing_and_env_fallback(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T4: Verify --herdr-workspace CLI flag, HERDR_WORKSPACE env fallback, and default bare slug."""
|
||||
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
|
||||
# Flag passed
|
||||
res1 = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--role", "Creator",
|
||||
"--herdr-workspace", "my-explicit-label",
|
||||
"--dry-run"
|
||||
], capture_output=True, text=True)
|
||||
assert res1.returncode == 0
|
||||
assert "herdr_workspace=my-explicit-label" in res1.stdout
|
||||
|
||||
# Env set, flag omitted -> env wins (C-3)
|
||||
res2 = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--role", "Creator",
|
||||
"--dry-run"
|
||||
], capture_output=True, text=True, env={**os.environ, "HERDR_WORKSPACE": "from-env-label"})
|
||||
assert res2.returncode == 0
|
||||
assert "herdr_workspace=from-env-label" in res2.stdout
|
||||
|
||||
# Both flag and env -> flag wins
|
||||
res3 = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--role", "Creator",
|
||||
"--herdr-workspace", "my-explicit-label",
|
||||
"--dry-run"
|
||||
], capture_output=True, text=True, env={**os.environ, "HERDR_WORKSPACE": "from-env-label"})
|
||||
assert res3.returncode == 0
|
||||
assert "herdr_workspace=my-explicit-label" in res3.stdout
|
||||
|
||||
# Neither -> default bare slug (D3), distinct from herdr_session
|
||||
run_env = dict(os.environ)
|
||||
run_env.pop("HERDR_WORKSPACE", None)
|
||||
run_env.pop("HERDR_SESSION_NAME", None)
|
||||
res4 = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--role", "Creator",
|
||||
"--dry-run"
|
||||
], capture_output=True, text=True, env=run_env)
|
||||
assert res4.returncode == 0
|
||||
parent = os.path.basename(os.path.dirname(str(mam_sandbox))).lower().replace('_', '-')
|
||||
work = os.path.basename(str(mam_sandbox)).lower().replace('_', '-')
|
||||
bare = f"{parent}-{work}".replace('_', '-')
|
||||
import re
|
||||
bare = re.sub(r'[^a-zA-Z0-9-]', '', bare).lstrip('-')
|
||||
assert f"herdr_workspace={bare}" in res4.stdout
|
||||
assert f"herdr_session=mam-{bare}" in res4.stdout
|
||||
|
||||
|
||||
def test_comp_create_herdr_workspace_yaml_propagation(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T5: Verify herdr_workspace distinct persistence in YAML and no leakage into commands."""
|
||||
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
res = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--role", "Creator",
|
||||
"--session", "custom-ws-creator-claude",
|
||||
"--herdr-session", "isolated-sock-01",
|
||||
"--herdr-workspace", "distinct-ws-label"
|
||||
], capture_output=True, text=True)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
import yaml
|
||||
with open(yaml_path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
sessions = data.get("herdr_sessions", [])
|
||||
assert len(sessions) == 1
|
||||
s = sessions[0]
|
||||
assert s["name"] == "custom-ws-creator-claude"
|
||||
assert s["herdr_session"] == "isolated-sock-01"
|
||||
assert s["herdr_server"] == "isolated-sock-01"
|
||||
assert s["herdr_workspace"] == "distinct-ws-label"
|
||||
assert "distinct-ws-label" not in s["start_command"]
|
||||
assert "distinct-ws-label" not in s["attach_command"]
|
||||
assert "distinct-ws-label" not in s["kill_command"]
|
||||
|
||||
|
||||
def test_create_does_not_inherit_a_stale_workspace_label(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T9 / D5: Recreating over a terminated row derives label afresh from --workspace."""
|
||||
mutation = """
|
||||
d['herdr_sessions'] = [{
|
||||
'name': 'reuse-creator-claude',
|
||||
'status': 'terminated',
|
||||
'herdr_session': 'old-sock',
|
||||
'herdr_server': 'old-sock',
|
||||
'herdr_workspace': 'old-stale-label',
|
||||
'pane': {'cwd': '/old/place', 'cmd': 'claude'}
|
||||
}]
|
||||
"""
|
||||
run_mutation(mam_sandbox, mutation)
|
||||
|
||||
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
res = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--role", "Creator",
|
||||
"--session", "reuse-creator-claude"
|
||||
], capture_output=True, text=True)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
import yaml
|
||||
with open(yaml_path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
sessions = data.get("herdr_sessions", [])
|
||||
assert len(sessions) == 1
|
||||
s = sessions[0]
|
||||
assert s["status"] == "running"
|
||||
assert s["herdr_workspace"] != "old-stale-label"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# FEATURE 2: Resume Session (6 Test Cases)
|
||||
# FEATURE 2: Resume Session (8 Test Cases)
|
||||
# ==============================================================================
|
||||
|
||||
def test_comp_resume_config_restore(mam_sandbox):
|
||||
@@ -452,6 +577,78 @@ d['herdr_sessions'] = [{{
|
||||
assert "HERDR_SESSION_NAME=NEW-HERDR-SESSION" in s["kill_command"]
|
||||
|
||||
|
||||
def test_comp_resume_herdr_workspace_propagation(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T6: Verify resume_session.sh with --herdr-workspace updates herdr_workspace while preserving herdr_session."""
|
||||
conv_id = "22222222-3333-4444-5555-666666666666"
|
||||
key = str(mam_sandbox).replace('/', '-').replace('_', '-')
|
||||
proj_dir = mam_sandbox / ".claude" / "projects" / key
|
||||
proj_dir.mkdir(parents=True, exist_ok=True)
|
||||
(proj_dir / f"{conv_id}.jsonl").write_text(f'{{"sessionId": "{conv_id}"}}')
|
||||
|
||||
mutation = f"""
|
||||
d['herdr_sessions'] = [{{
|
||||
'name': 'test-proj-ws-creator-claude',
|
||||
'status': 'stopped',
|
||||
'herdr_session': 'PRESERVED-SESSION',
|
||||
'herdr_server': 'PRESERVED-SESSION',
|
||||
'herdr_workspace': 'OLD-WS-LABEL',
|
||||
'claude_session_id_own': '{conv_id}',
|
||||
'pane': {{'cwd': '{str(mam_sandbox)}', 'cmd': 'claude'}}
|
||||
}}]
|
||||
"""
|
||||
run_mutation(mam_sandbox, mutation)
|
||||
|
||||
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
|
||||
res = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--session", "test-proj-ws-creator-claude",
|
||||
"--herdr-workspace", "NEW-WS-LABEL"
|
||||
], capture_output=True, text=True)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
import yaml
|
||||
with open(yaml_path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
sessions = data.get("herdr_sessions", [])
|
||||
assert len(sessions) == 1
|
||||
s = sessions[0]
|
||||
assert s["status"] == "running"
|
||||
assert s["herdr_workspace"] == "NEW-WS-LABEL"
|
||||
assert s["herdr_session"] == "PRESERVED-SESSION"
|
||||
|
||||
|
||||
def test_comp_resume_herdr_workspace_new_row_branch(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T7: Verify update_yaml_resumed.sh creates a new row with herdr_workspace when target is None."""
|
||||
run_mutation(mam_sandbox, "d['herdr_sessions'] = []")
|
||||
|
||||
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "update_yaml_resumed.sh"
|
||||
res = subprocess.run([
|
||||
"bash", str(script),
|
||||
"--workspace", str(mam_sandbox),
|
||||
"--agent", "claude",
|
||||
"--session", "brand-new-resumed-session",
|
||||
"--uuid", "33333333-4444-5555-6666-777777777777",
|
||||
"--herdr-session", "explicit-sock",
|
||||
"--herdr-workspace", "explicit-ws"
|
||||
], capture_output=True, text=True)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
import yaml
|
||||
with open(yaml_path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
sessions = data.get("herdr_sessions", [])
|
||||
assert len(sessions) == 1
|
||||
s = sessions[0]
|
||||
assert s["name"] == "brand-new-resumed-session"
|
||||
assert s["herdr_session"] == "explicit-sock"
|
||||
assert s["herdr_server"] == "explicit-sock"
|
||||
assert s["herdr_workspace"] == "explicit-ws"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# FEATURE 3: Stop Session (7 Test Cases)
|
||||
# ==============================================================================
|
||||
@@ -773,8 +970,38 @@ d['herdr_sessions'] = [{
|
||||
assert session["pane_cwd"] == "/tmp"
|
||||
|
||||
|
||||
def test_comp_status_displays_socket_and_workspace_columns(mam_sandbox):
|
||||
"""T12: Verify status.sh displays distinct SOCKET and WORKSPACE columns."""
|
||||
mutation = """
|
||||
d['herdr_sessions'] = [{
|
||||
'name': 'test-cols-creator-claude',
|
||||
'status': 'running',
|
||||
'herdr_session': 'socket-AAA',
|
||||
'herdr_server': 'socket-AAA',
|
||||
'herdr_workspace': 'label-BBB',
|
||||
'pane': {'cwd': '/tmp', 'cmd': 'claude'}
|
||||
}]
|
||||
"""
|
||||
run_mutation(mam_sandbox, mutation)
|
||||
|
||||
script_path = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-status" / "scripts" / "status.sh"
|
||||
res = subprocess.run(["bash", str(script_path)], capture_output=True, text=True)
|
||||
assert res.returncode == 0
|
||||
assert "SOCKET" in res.stdout
|
||||
assert "WORKSPACE" in res.stdout
|
||||
assert "socket-AAA" in res.stdout
|
||||
assert "label-BBB" in res.stdout
|
||||
|
||||
# Also verify --json has herdr_workspace
|
||||
res_json = subprocess.run(["bash", str(script_path), "--json"], capture_output=True, text=True)
|
||||
assert res_json.returncode == 0
|
||||
data = json.loads(res_json.stdout)
|
||||
assert data["sessions_detail"][0]["herdr_workspace"] == "label-BBB"
|
||||
assert data["sessions_detail"][0]["server"] == "socket-AAA"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# FEATURE 5: Monitor/Reconcile (6 Test Cases)
|
||||
# FEATURE 5: Monitor/Reconcile (7 Test Cases)
|
||||
# ==============================================================================
|
||||
|
||||
def test_comp_monitor_concurrency_lock(mam_sandbox):
|
||||
@@ -940,7 +1167,8 @@ def test_comp_stop_usage_matches_parser(mam_sandbox):
|
||||
("--purge-conversation", ["--purge-conversation"]),
|
||||
("--yes", ["--yes"]),
|
||||
("--agent", ["--agent", "hermes"]),
|
||||
("--herdr-session", ["--herdr-session", "isolated-sess"])):
|
||||
("--herdr-session", ["--herdr-session", "isolated-sess"]),
|
||||
("--herdr-workspace", ["--herdr-workspace", "isolated-ws"])):
|
||||
r = subprocess.run(["bash", str(script), "--session", VALID] + args,
|
||||
capture_output=True, text=True)
|
||||
assert "unknown arg" not in r.stderr, f"usage() advertises {flag} but parser rejects it: {r.stderr}"
|
||||
@@ -958,3 +1186,42 @@ def test_comp_stop_usage_matches_parser(mam_sandbox):
|
||||
head = "".join(script.read_text().splitlines(keepends=True)[:35])
|
||||
assert "--mode soft|hard" not in head
|
||||
|
||||
|
||||
def test_comp_reconcile_drift_b_populates_workspace_and_server(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T11 / S10: Verify drift B auto-registration populates herdr_workspace and herdr_server."""
|
||||
session_name = "canary-test-creator-claude"
|
||||
state = {
|
||||
"workspaces": [{"workspace_id": "w1", "label": "default", "cwd": str(mam_sandbox)}],
|
||||
"agents": {
|
||||
session_name: {
|
||||
"name": session_name,
|
||||
"status": "running",
|
||||
"cwd": str(mam_sandbox),
|
||||
"command": "claude",
|
||||
"pid": 98765
|
||||
}
|
||||
},
|
||||
"calls": []
|
||||
}
|
||||
with open(mock_herdr, "w") as f:
|
||||
json.dump(state, f)
|
||||
|
||||
run_mutation(mam_sandbox, "d['herdr_sessions'] = []")
|
||||
|
||||
reconcile_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
|
||||
res = subprocess.run(["bash", str(reconcile_script)], capture_output=True, text=True, cwd=str(mam_sandbox), env={**os.environ, "WORKSPACE_ROOT": str(mam_sandbox)})
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
import yaml
|
||||
with open(yaml_path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
sessions = data.get("herdr_sessions", [])
|
||||
matching = [s for s in sessions if s.get("name") == session_name]
|
||||
assert len(matching) == 1
|
||||
s = matching[0]
|
||||
assert s["herdr_session"] == "default"
|
||||
assert s["herdr_server"] == "default"
|
||||
assert s["herdr_workspace"] != ""
|
||||
assert s["herdr_workspace"] != "-"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user