feat(cli,registry): introduce --herdr-workspace option and decouple socket fallback chains
This commit is contained in:
+1
-1
@@ -112,7 +112,7 @@ if os.path.exists(state_file):
|
||||
time.sleep(0.02)
|
||||
|
||||
# Record the command call
|
||||
state["calls"].append(sys.argv[1:])
|
||||
state.setdefault("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")
|
||||
|
||||
@@ -78,21 +78,103 @@ def test_create_validate_env_key(mam_sandbox):
|
||||
# ==============================================================================
|
||||
|
||||
def test_resume_resolve_herdr_session_default(mam_sandbox):
|
||||
"""Test resolve_herdr_workspace fallback behavior when session is not in YAML."""
|
||||
res = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "non-existent-session")
|
||||
"""Test resolve_herdr_session fallback behavior when session is not in YAML."""
|
||||
res = run_lib_func(mam_sandbox, "resolve_herdr_session", "non-existent-session")
|
||||
assert res.returncode == 0
|
||||
assert res.stdout.strip() != ""
|
||||
|
||||
def test_resume_resolve_herdr_session_env(mam_sandbox):
|
||||
"""Test resolve_herdr_workspace fallback to HERDR_SESSION_NAME or HERDR_SERVER_NAME env var."""
|
||||
res = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "non-existent-session", env={"HERDR_SESSION_NAME": "custom_session"})
|
||||
"""Test resolve_herdr_session fallback to HERDR_SESSION_NAME or HERDR_SERVER_NAME env var."""
|
||||
res = run_lib_func(mam_sandbox, "resolve_herdr_session", "non-existent-session", env={"HERDR_SESSION_NAME": "custom_session"})
|
||||
assert res.returncode == 0
|
||||
assert res.stdout.strip() == "custom_session"
|
||||
|
||||
res_legacy = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "non-existent-session", env={"HERDR_SERVER_NAME": "custom_server"})
|
||||
res_legacy = run_lib_func(mam_sandbox, "resolve_herdr_session", "non-existent-session", env={"HERDR_SERVER_NAME": "custom_server"})
|
||||
assert res_legacy.returncode == 0
|
||||
assert res_legacy.stdout.strip() == "custom_server"
|
||||
|
||||
def test_resolvers_are_decoupled(mam_sandbox):
|
||||
"""소켓과 워크스페이스 라벨이 다른 행에서 두 함수가 서로 다른 값을 낸다."""
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
yaml_path.write_text("""herdr_sessions:
|
||||
- name: d-creator-claude
|
||||
status: running
|
||||
herdr_session: socket-A
|
||||
herdr_server: socket-A
|
||||
herdr_workspace: label-B
|
||||
pane:
|
||||
cwd: /tmp
|
||||
""")
|
||||
s = run_lib_func(mam_sandbox, "resolve_herdr_session", "d-creator-claude")
|
||||
w = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "d-creator-claude")
|
||||
assert s.stdout.strip() == "socket-A"
|
||||
assert w.stdout.strip() == "label-B"
|
||||
|
||||
def test_workspace_label_never_resolves_as_socket(mam_sandbox):
|
||||
"""B-22: herdr_session 이 없는 행에서도 herdr_workspace 는 소켓 이름이 되지 않는다."""
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
yaml_path.write_text("""herdr_sessions:
|
||||
- name: legacy-creator-claude
|
||||
status: running
|
||||
herdr_workspace: my-label
|
||||
pane:
|
||||
cwd: /tmp
|
||||
""")
|
||||
s = run_lib_func(mam_sandbox, "resolve_herdr_session", "legacy-creator-claude")
|
||||
assert s.stdout.strip() != "my-label"
|
||||
|
||||
def test_socket_resolver_fallback_chain(mam_sandbox):
|
||||
"""herdr_server 만 있는 행 -> herdr_server 반환, 둘 다 없으면 기본/슬러그 fallback."""
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
yaml_path.write_text("""herdr_sessions:
|
||||
- name: srv-only-creator-claude
|
||||
status: running
|
||||
herdr_server: socket-from-srv
|
||||
pane:
|
||||
cwd: /tmp
|
||||
""")
|
||||
s = run_lib_func(mam_sandbox, "resolve_herdr_session", "srv-only-creator-claude")
|
||||
assert s.stdout.strip() == "socket-from-srv"
|
||||
|
||||
def test_workspace_resolver_prefers_the_row_over_the_caller_argument(mam_sandbox):
|
||||
"""C-1: 등록된 행에는 herdr_workspace 가 없지만 pane.cwd 가 있다.
|
||||
호출자가 '다른' 워크스페이스를 넘겨도 행의 cwd 가 이긴다."""
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
yaml_path.write_text("""herdr_sessions:
|
||||
- name: pa-creator-claude
|
||||
status: running
|
||||
pane:
|
||||
cwd: /path/to/project_a
|
||||
""")
|
||||
r = run_lib_func(mam_sandbox, "resolve_herdr_workspace",
|
||||
"pa-creator-claude", "/path/to/project_b")
|
||||
assert r.stdout.strip() == "to-project-a"
|
||||
|
||||
def test_workspace_resolver_uses_the_argument_only_when_unregistered(mam_sandbox):
|
||||
"""③ 분기가 살아 있음을 확인 — 미등록 세션에서는 인자가 쓰인다."""
|
||||
r = run_lib_func(mam_sandbox, "resolve_herdr_workspace",
|
||||
"not-registered", "/path/to/project_b")
|
||||
assert r.stdout.strip() == "to-project-b"
|
||||
|
||||
@pytest.mark.parametrize("path", ["/tmp", "/", "/a/My_Proj.v2", "/private/var/folders/q_/x"])
|
||||
def test_slug_parity_between_bash_and_python(mam_sandbox, path):
|
||||
"""D5 는 두 슬러그 구현의 일치에 의존한다 (lib.sh derive_workspace_slug 와
|
||||
resolve_herdr_workspace / reconcile.sh 의 인라인 slug())."""
|
||||
b = run_lib_func(mam_sandbox, "derive_workspace_slug", path).stdout.strip()
|
||||
p = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "not-registered", path).stdout.strip()
|
||||
assert b.removeprefix("mam-") == p
|
||||
|
||||
def test_no_socket_lookup_falls_back_to_workspace_label(mam_sandbox):
|
||||
"""B-22 구조 가드: 소켓 lookup 표현식에 herdr_workspace 가 다시 끼어들지 못한다."""
|
||||
import re
|
||||
pat = re.compile(r"herdr_session'\)\s*or\s*.*herdr_workspace")
|
||||
lib_sh = mam_sandbox / "skills" / "lib.sh"
|
||||
reconcile_sh = mam_sandbox / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
|
||||
status_sh = mam_sandbox / "skills" / "multi-agent-mux-status" / "scripts" / "status.sh"
|
||||
for f in (lib_sh, reconcile_sh, status_sh):
|
||||
for i, line in enumerate(f.read_text().splitlines(), 1):
|
||||
assert not pat.search(line), f"{f.name}:{i} — socket lookup falls back to workspace label:\n{line}"
|
||||
|
||||
def test_resume_find_workspace_uuid_empty(mam_sandbox):
|
||||
"""Test find_workspace_uuid returns empty string for non-existent workspace."""
|
||||
res = run_lib_func(mam_sandbox, "find_workspace_uuid", "/non/existent/path", "claude")
|
||||
|
||||
@@ -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