fix(herdr): ensure unique agent name via sha1 truncation and align mock errors
- Replace legacy s[:16]-s[-15:] truncation with 8-char SHA-1 hash suffix in sanitize.py and lib.sh to prevent workspace name collisions - Align mock Herdr error messages in tests/conftest.py to real Herdr 0.8.0 output format - Add missing required/invalid_agent_name to early abort regex in lib.sh - Remove legacy heuristics in lib.sh has-session and unify mock agent lookups - Add unit tests in tests/test_sanitize_and_mock_errors.py (256/256 passed)
This commit is contained in:
+46
-13
@@ -1,10 +1,16 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import json
|
||||
import uuid
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
SKILLS_DIR = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
|
||||
if SKILLS_DIR not in sys.path:
|
||||
sys.path.insert(0, SKILLS_DIR)
|
||||
|
||||
@pytest.fixture
|
||||
def mam_sandbox(tmp_path, monkeypatch):
|
||||
"""
|
||||
@@ -160,6 +166,24 @@ def save_state():
|
||||
# Save calls immediately so they persist even if we exit early or error out
|
||||
save_state()
|
||||
|
||||
def _sanitize_agent_name(name):
|
||||
if not name:
|
||||
return "agent"
|
||||
s = str(name).lower()
|
||||
import re, hashlib
|
||||
s = re.sub(r'[^a-z0-9_-]', '-', s)
|
||||
if not s or not s[0].isalpha():
|
||||
s = "x-" + s
|
||||
if len(s) > 32:
|
||||
h = hashlib.sha1(s.encode('utf-8')).hexdigest()[:8]
|
||||
s = f"{s[:23]}-{h}"
|
||||
return s
|
||||
|
||||
def _match_agent(k, target):
|
||||
if not k or not target:
|
||||
return False
|
||||
return k == target or _sanitize_agent_name(k) == target or _sanitize_agent_name(target) == k or _sanitize_agent_name(k) == _sanitize_agent_name(target)
|
||||
|
||||
args = sys.argv[1:]
|
||||
while args and args[0] in ("-L", "--server", "-s", "--session"):
|
||||
if len(args) > 1:
|
||||
@@ -399,17 +423,26 @@ elif cmd1 == "agent":
|
||||
i += 1
|
||||
|
||||
if unknown_flags:
|
||||
sys.stderr.write("error: unexpected argument '" + str(unknown_flags[0]) + "' found\\\\n\\\\nUsage: herdr agent start <NAME> --kind <KIND> --pane <ID> [OPTIONS] [-- [AGENT_ARG]...]\\\\n")
|
||||
sys.stderr.write("unknown option: " + str(unknown_flags[0]) + "\\n")
|
||||
sys.exit(1)
|
||||
|
||||
if not agent_type or not pane:
|
||||
sys.stderr.write("error: the following required arguments were not provided:\\\\n --kind <KIND>\\\\n --pane <ID>\\\\n")
|
||||
missing_arg = "--pane" if not pane else "--kind"
|
||||
sys.stderr.write("missing required " + missing_arg + "\\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Name validation: ^[a-z][a-z0-9_-]{0,31}$
|
||||
import re
|
||||
if not re.match(r'^[a-z][a-z0-9_-]{0,31}$', name):
|
||||
print(f"error: invalid value '{name}' for '<NAME>': agent name must start with a lowercase letter and contain only lowercase letters, digits, '-' or '_' (1-32 characters)", file=sys.stderr)
|
||||
import json
|
||||
err_payload = {
|
||||
"id": "cli:agent:start",
|
||||
"error": {
|
||||
"code": "invalid_agent_name",
|
||||
"message": "agent name must start with a lowercase letter and contain only lowercase letters, digits, '-' or '_' (1-32 characters)"
|
||||
}
|
||||
}
|
||||
sys.stderr.write(json.dumps(err_payload) + "\\n")
|
||||
sys.exit(1)
|
||||
|
||||
# TUI Welcome Tokens definition to prevent TUI readiness check timeout
|
||||
@@ -540,7 +573,7 @@ elif cmd1 == "agent":
|
||||
agents = state.get("agents", {})
|
||||
matched_k = None
|
||||
for k in agents:
|
||||
if k == name or (len(k) > 32 and f"{k[:16]}-{k[-15:]}"[:32] == name) or (len(name) > 32 and f"{name[:16]}-{name[-15:]}"[:32] == k):
|
||||
if _match_agent(k, name):
|
||||
matched_k = k
|
||||
break
|
||||
if matched_k:
|
||||
@@ -565,7 +598,7 @@ elif cmd1 == "agent":
|
||||
}))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.stderr.write("Agent " + name + " not found\\\\n")
|
||||
sys.stderr.write("Agent " + name + " not found\\n")
|
||||
sys.exit(1)
|
||||
elif cmd2 == "read":
|
||||
if len(args) < 3:
|
||||
@@ -574,7 +607,7 @@ elif cmd1 == "agent":
|
||||
agents = state.get("agents", {})
|
||||
matched_k = None
|
||||
for k in agents:
|
||||
if k == name or (len(k) > 32 and f"{k[:16]}-{k[-15:]}"[:32] == name) or (len(name) > 32 and f"{name[:16]}-{name[-15:]}"[:32] == k):
|
||||
if _match_agent(k, name):
|
||||
matched_k = k
|
||||
break
|
||||
if matched_k:
|
||||
@@ -582,7 +615,7 @@ elif cmd1 == "agent":
|
||||
print(buffer_content)
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.stderr.write("Agent " + name + " not found\\\\n")
|
||||
sys.stderr.write("Agent " + name + " not found\\n")
|
||||
sys.exit(1)
|
||||
elif cmd2 == "prompt":
|
||||
if len(args) < 4:
|
||||
@@ -592,12 +625,12 @@ elif cmd1 == "agent":
|
||||
agents = state.get("agents", {})
|
||||
matched_k = None
|
||||
for k in agents:
|
||||
if k == name or (len(k) > 32 and f"{k[:16]}-{k[-15:]}"[:32] == name) or (len(name) > 32 and f"{name[:16]}-{name[-15:]}"[:32] == k):
|
||||
if _match_agent(k, name):
|
||||
matched_k = k
|
||||
break
|
||||
if matched_k:
|
||||
agents[matched_k]["sent_text"] = agents[matched_k].get("sent_text", "") + text
|
||||
agents[matched_k]["buffer"] = agents[matched_k].get("buffer", "") + "\\\\n" + text + "\\\\n\\\\nesc to interrupt"
|
||||
agents[matched_k]["buffer"] = agents[matched_k].get("buffer", "") + "\\n" + text + "\\n\\nesc to interrupt"
|
||||
state["agents"] = agents
|
||||
save_state()
|
||||
print(json.dumps({"id": "cli:agent:prompt", "result": {"type": "ok"}}))
|
||||
@@ -610,7 +643,7 @@ elif cmd1 == "agent":
|
||||
agents = state.get("agents", {})
|
||||
matched_k = None
|
||||
for k in agents:
|
||||
if k == name or (len(k) > 32 and f"{k[:16]}-{k[-15:]}"[:32] == name) or (len(name) > 32 and f"{name[:16]}-{name[-15:]}"[:32] == k):
|
||||
if _match_agent(k, name):
|
||||
matched_k = k
|
||||
break
|
||||
if matched_k:
|
||||
@@ -716,7 +749,7 @@ elif cmd1 == "list-panes":
|
||||
agents = state.get("agents", {})
|
||||
matched_k = None
|
||||
for k in agents:
|
||||
if k == session_target or (len(k) > 32 and f"{k[:16]}-{k[-15:]}"[:32] == session_target) or (len(session_target) > 32 and f"{session_target[:16]}-{session_target[-15:]}"[:32] == k):
|
||||
if _match_agent(k, session_target):
|
||||
matched_k = k
|
||||
break
|
||||
if matched_k:
|
||||
@@ -744,7 +777,7 @@ elif cmd1 == "has-session":
|
||||
elif len(args) > 1:
|
||||
sess_target = args[1]
|
||||
agents = state.get("agents", {})
|
||||
if any(k == sess_target or (len(k) > 32 and f"{k[:16]}-{k[-15:]}"[:32] == sess_target) or (len(sess_target) > 32 and f"{sess_target[:16]}-{sess_target[-15:]}"[:32] == k) for k in agents):
|
||||
if any(_match_agent(k, sess_target) for k in agents):
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
@@ -756,7 +789,7 @@ elif cmd1 == "kill-session":
|
||||
elif len(args) > 1:
|
||||
sess_target = args[1]
|
||||
agents = state.get("agents", {})
|
||||
to_del = [k for k in agents if k == sess_target or (len(k) > 32 and f"{k[:16]}-{k[-15:]}"[:32] == sess_target) or (len(sess_target) > 32 and f"{sess_target[:16]}-{sess_target[-15:]}"[:32] == k)]
|
||||
to_del = [k for k in agents if _match_agent(k, sess_target)]
|
||||
for k in to_del:
|
||||
del agents[k]
|
||||
state["agents"] = agents
|
||||
|
||||
Reference in New Issue
Block a user