Compare commits
10
Commits
e4c7c8fe9b
...
5f6065344f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f6065344f | ||
|
|
a47a4683f3 | ||
|
|
8b829647f4 | ||
|
|
0fc162e8cd | ||
|
|
a6fb39ba9f | ||
|
|
9410913da9 | ||
|
|
bd61ef532f | ||
|
|
015075077c | ||
|
|
263a72483a | ||
|
|
5cd72d2df3 |
No files matched your search
+27
-8
@@ -37,15 +37,26 @@ AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.
|
||||
# (Issue #71). The herdr shim gets this same definition: _init_herdr_isolation
|
||||
# writes it in with `declare -f`. A long name needs its SHA-1 suffix; with no
|
||||
# shasum, sha1sum, openssl or python3 to make it, this fails (stderr, nonzero)
|
||||
# rather than return a name herdr never registered.
|
||||
# rather than return a name herdr never registered. A name that is not ASCII
|
||||
# is lowercased and replaced by python3 (tr/sed differ from str.lower() there),
|
||||
# and fails the same way without it. Line breaks become '-' before sed (which
|
||||
# works line by line) and before $(...) could drop a trailing one.
|
||||
_sanitize_herdr_agent_name() {
|
||||
local n="$1"
|
||||
if [ -z "$n" ]; then
|
||||
printf 'agent\n'
|
||||
return 0
|
||||
fi
|
||||
local s
|
||||
s=$(echo "$n" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g')
|
||||
local s=""
|
||||
if [ -z "$(printf '%s' "$n" | LC_ALL=C tr -d '\000-\177')" ]; then
|
||||
s=$(printf '%s' "$n" | tr '\n' '-' | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g')
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
s=$(python3 -c 'import re,sys; sys.stdout.write(re.sub(r"[^a-z0-9_-]", "-", sys.argv[1].lower()))' "$n") || s=""
|
||||
fi
|
||||
if [ -z "$s" ]; then
|
||||
echo "ERROR: cannot make the herdr name of '$n': it is not ASCII and there is no python3 to convert it" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! "$s" =~ ^[a-z] ]]; then s="x-$s"; fi
|
||||
if [ "${#s}" -gt 32 ]; then
|
||||
local h=""
|
||||
@@ -1580,10 +1591,14 @@ herdr() {
|
||||
# ---------------------------------------------------------------------------
|
||||
# NOTE: Retained as small inline PYEOF block (39 lines) per Plan Rev.2 (Job 98393a97).
|
||||
# Small blocks have minimal overhead and high local cohesion; extraction into lib_py is reserved for large blocks (>200 lines).
|
||||
# `load_state_json strict` (Issue #71) fails (stderr, exit 1) when the YAML or
|
||||
# DB cannot be read, a DB table missing included, instead of printing {} - for
|
||||
# a caller that acts on a session being absent.
|
||||
load_state_json() {
|
||||
env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
|
||||
env_python "$AGENT_SESSIONS_YAML" "MAM_STATE_READ=${1:-}" <<'PYEOF'
|
||||
import os, sys, sqlite3, json, yaml
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
strict = os.environ.get('MAM_STATE_READ') == 'strict'
|
||||
db_path = os.path.splitext(yaml_path)[0] + '.db'
|
||||
d = {}
|
||||
db_sessions = []
|
||||
@@ -1596,20 +1611,24 @@ try:
|
||||
if row:
|
||||
d = json.loads(row[0])
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
if strict:
|
||||
raise
|
||||
try:
|
||||
cursor = conn.execute('SELECT data FROM sessions')
|
||||
for r in cursor.fetchall():
|
||||
db_sessions.append(json.loads(r[0]))
|
||||
d['herdr_sessions'] = db_sessions
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
if strict:
|
||||
raise
|
||||
conn.close()
|
||||
elif os.path.exists(yaml_path):
|
||||
with open(yaml_path) as f:
|
||||
d = yaml.safe_load(f) or {}
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
if strict:
|
||||
print('ERROR: cannot read the registry %s: %s' % (yaml_path, e), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# Clean lone surrogates to prevent UnicodeEncodeError on stdout print
|
||||
def clean_surrogates(obj):
|
||||
if isinstance(obj, str):
|
||||
|
||||
@@ -124,6 +124,17 @@ def atomic_dump_yaml_main():
|
||||
if from_version < registry_format.CURRENT:
|
||||
d, migrated_rows = registry_format.migrate(d)
|
||||
force_yaml = True
|
||||
elif row:
|
||||
# The DB is current; its YAML copy may not be (a migration whose
|
||||
# YAML rewrite failed after the DB commit). Then rewrite it.
|
||||
y = None
|
||||
if os.path.exists(yaml_path):
|
||||
try:
|
||||
with open(yaml_path) as f:
|
||||
y = yaml.safe_load(f)
|
||||
except Exception:
|
||||
y = None
|
||||
force_yaml = registry_format.stored_version(y) < registry_format.CURRENT
|
||||
|
||||
old_sessions = get_all_sessions_status(d)
|
||||
# a mutation that replaces a row the monitor registered for a pane it
|
||||
|
||||
@@ -155,12 +155,12 @@ class HerdrError(Exception):
|
||||
class HerdrSource(object):
|
||||
"""Every herdr and OS read the judgement needs, in one place."""
|
||||
|
||||
def __init__(self, target, agent, session=None, name=None):
|
||||
def __init__(self, target, agent, session=None, *, name):
|
||||
self.target, self.session = target, session or None
|
||||
# `name` is the name herdr knows the agent by (#66), worked out once
|
||||
# by StallWatch (#71); the kind is read off the original name, whose
|
||||
# suffix a long name's hash replaces.
|
||||
self.name = name or target
|
||||
self.name = name
|
||||
self.agent = resolve_agent(agent, target)
|
||||
adapter = get_adapter(self.agent) if self.agent else None
|
||||
self.resident = tuple(adapter.resident_children) if adapter else ()
|
||||
|
||||
@@ -58,7 +58,7 @@ bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
|
||||
| Exit | Meaning |
|
||||
|---|---|
|
||||
| 0 | Stopped; or already `stopped` (no-op, except with `--purge-conversation`, which still purges) |
|
||||
| 1 | From the checks before the stop, nothing was changed: YAML not found, or the session is neither registered nor on the herdr server. **The registry update after the stop also exits 1** (the row disappeared from the YAML during the run) |
|
||||
| 1 | From the checks before the stop, nothing was changed: YAML not found, its row could not be read, or the session is neither registered nor on the herdr server. **The registry update after the stop also exits 1** (the row disappeared from the YAML during the run) |
|
||||
| 2 | Bad arguments (including the retired `--mode` / `--capture-id` / `--graceful`), or `--agent` omitted and not resolvable |
|
||||
| 3 | `--purge-conversation` without `--yes` |
|
||||
| 4 | Purge aborted: the herdr session, or an agent process still holding the conversation, survived the kill chain. Records preserved: the registry row and the conversation files are untouched, though the session itself may already be down |
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = success (or already-stopped no-op; or a session with no row closed)
|
||||
# 1 = YAML not found / not registered and not on the herdr server
|
||||
# 1 = YAML not found / its row unreadable / not registered and not on the herdr server
|
||||
# 2 = invalid args | 3 = interactive confirmation required (--yes 누락)
|
||||
# 4 = purge aborted (herdr session or an agent process holding the conversation survived the kill chain)
|
||||
# 5 = no row, and herdr runs the session outside this workspace (refused, nothing changed)
|
||||
@@ -106,6 +106,7 @@ if [ "$PURGE" = "1" ]; then
|
||||
trap 'rm -f "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"' EXIT
|
||||
fi
|
||||
|
||||
AMBIENT_HERDR_SESSION="${HERDR_SESSION_NAME:-}"
|
||||
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
||||
else
|
||||
@@ -116,12 +117,17 @@ fi
|
||||
# Issue #71: a session with no registry row (its create/resume died before
|
||||
# registering it) is closed here - herdr only, the registry is left alone. It
|
||||
# has no agent kind, so --agent is not needed. Only a session herdr runs
|
||||
# inside this workspace is closed; anything else changes nothing.
|
||||
# inside this workspace is closed; anything else changes nothing. With no row
|
||||
# the server is chosen as create_session.sh chose it: --herdr-session, else
|
||||
# the environment's HERDR_SESSION_NAME, else the workspace's server.
|
||||
stop_rowless() {
|
||||
if [ "$PURGE" = "1" ]; then
|
||||
echo "ERROR: session '$SESSION_NAME' not in $AGENT_SESSIONS_YAML; --purge-conversation needs its row. Nothing changed." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$HERDR_SERVER_OPT" ] && [ -n "$AMBIENT_HERDR_SESSION" ] && [ "$AMBIENT_HERDR_SESSION" != "default" ]; then
|
||||
export HERDR_SESSION_NAME="$AMBIENT_HERDR_SESSION"
|
||||
fi
|
||||
local info cwd
|
||||
info=$(herdr agent get "$SESSION_NAME" 2>/dev/null) || info=""
|
||||
if [ -z "$info" ]; then
|
||||
@@ -160,7 +166,12 @@ sys.exit(0 if cwd == root or cwd.startswith(root + os.sep) else 1)
|
||||
|
||||
# 세션이 YAML 에 있는지 + 해당 row 의 워크스페이스 cwd 및 delegate_job_id 추출.
|
||||
# JSON 으로 emit — cwd 에 '|' 가 들어가도 안전 (review item 7; 기존 cwd|jid 파서 대체).
|
||||
MAPPED_DATA=$(MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$SESSION_NAME" python3 -c "
|
||||
# A registry that cannot be read is not "no row" (Issue #71): stop before herdr.
|
||||
STATE_JSON=$(load_state_json strict) || {
|
||||
echo "ERROR: cannot read $AGENT_SESSIONS_YAML, so whether '$SESSION_NAME' has a row is unknown. Nothing changed." >&2
|
||||
exit 1
|
||||
}
|
||||
MAPPED_DATA=$(MAM_STATE_JSON="$STATE_JSON" SESSION_NAME="$SESSION_NAME" python3 -c "
|
||||
import sys, os, json
|
||||
name = os.environ['SESSION_NAME']
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
@@ -173,7 +184,13 @@ for s in d.get('herdr_sessions', []):
|
||||
'delegate_job_logs_dir': cp.get('delegate_job_logs_dir', '') or ''}))
|
||||
sys.exit(0)
|
||||
sys.exit(7)
|
||||
") || stop_rowless
|
||||
") || MAPPED_RC=$?
|
||||
# 7 = no row; any other failure (registry unreadable) stops as before.
|
||||
[ "${MAPPED_RC:-0}" -ne 7 ] || stop_rowless
|
||||
if [ "${MAPPED_RC:-0}" -ne 0 ]; then
|
||||
echo "ERROR: cannot read the row of '$SESSION_NAME' from $AGENT_SESSIONS_YAML" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
|
||||
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
|
||||
|
||||
@@ -433,7 +433,7 @@
|
||||
"class": "c",
|
||||
"kind": "diagram",
|
||||
"evidence": [
|
||||
"multi-agent-mux-stop/scripts/stop_session.sh:228-242"
|
||||
"multi-agent-mux-stop/scripts/stop_session.sh:245-259"
|
||||
],
|
||||
"mismatch": null,
|
||||
"note": "state machine"
|
||||
@@ -504,8 +504,8 @@
|
||||
"class": "a",
|
||||
"evidence": [
|
||||
"multi-agent-mux-stop/scripts/stop_session.sh:101",
|
||||
"multi-agent-mux-stop/scripts/stop_session.sh:120-176",
|
||||
"multi-agent-mux-stop/scripts/stop_session.sh:228-232"
|
||||
"multi-agent-mux-stop/scripts/stop_session.sh:123-193",
|
||||
"multi-agent-mux-stop/scripts/stop_session.sh:245-249"
|
||||
],
|
||||
"mismatch": [
|
||||
"T1: the doc offered --force-no-yaml, which no script accepts"
|
||||
@@ -601,7 +601,7 @@
|
||||
"other"
|
||||
],
|
||||
"passthrough": {
|
||||
"1": "multi-agent-mux-stop/scripts/stop_session.sh:367"
|
||||
"1": "multi-agent-mux-stop/scripts/stop_session.sh:384"
|
||||
}
|
||||
},
|
||||
"multi-agent-mux-create/scripts/create_session.sh": {
|
||||
|
||||
@@ -303,7 +303,7 @@ def test_s11_this_branch_does_not_change_the_generated_shim(mam_sandbox):
|
||||
too (one sanitize definition written in with `declare -f`, agent attach
|
||||
resolved, no silent wrong name), so the base is that commit."""
|
||||
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
base = os.environ.get("MAM_BASE_SHA", "4243da5")
|
||||
base = os.environ.get("MAM_BASE_SHA", "0fc162e")
|
||||
show = subprocess.run(["git", "show", "%s:.agents/skills/lib.sh" % base],
|
||||
capture_output=True, text=True, cwd=repo)
|
||||
if show.returncode != 0:
|
||||
|
||||
@@ -96,7 +96,7 @@ def test_the_1x_capture_still_reads_empty():
|
||||
|
||||
@pytest.mark.parametrize("name", HOME_SCREENS)
|
||||
def test_stall_watch_sees_the_2x_home_box_empty(name, monkeypatch):
|
||||
src = stall_watch.HerdrSource("t", "opencode")
|
||||
src = stall_watch.HerdrSource("t", "opencode", name="t")
|
||||
monkeypatch.setattr(src, "_herdr", lambda *args: _pane(name))
|
||||
assert src.input_empty() is True
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ from lib_py.agents.sanitize import sanitize_herdr_agent_name # noqa: E402
|
||||
LONG = "lab-issue71-a-very-long-branch-name-creator-claude" # > 32 characters
|
||||
UPPER = "Lab-Upper-creator-claude"
|
||||
PLAIN = "lab-plain-creator-claude" # needs no conversion
|
||||
UNI = "\u0130stanbul-creator-claude" # str.lower() differs from tr here
|
||||
NL = "newline-creator-claude\n" # $(...) would drop the line break
|
||||
STOP = "bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh"
|
||||
|
||||
|
||||
@@ -77,7 +79,7 @@ def _assert_new_form(row, server):
|
||||
assert "kill_command" not in row and "start_command" not in row
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", [LONG, UPPER, PLAIN])
|
||||
@pytest.mark.parametrize("name", [LONG, UPPER, PLAIN, UNI, NL])
|
||||
@pytest.mark.parametrize("server", ["default", "mam-iso71"])
|
||||
def test_create_writes_the_attach_command(mam_sandbox, mock_herdr, mock_agents, name, server):
|
||||
_set_mock(mock_herdr, strict_agent_names=True)
|
||||
@@ -160,7 +162,10 @@ def test_wrapper_attach_calls_herdr_with_the_herdr_name(mam_sandbox, mock_herdr,
|
||||
# --- one sanitize rule ----------------------------------------------------------
|
||||
|
||||
NAMES = ["", "plain", "Upper-Case", "9lead", "-dash", "has space/and.dot", "x" * 32, "y" * 33,
|
||||
LONG, LONG.upper(), "creator-claude-fix-issue-99-long-branch"]
|
||||
LONG, LONG.upper(), "creator-claude-fix-issue-99-long-branch", "-n", "-e", "a\\nb",
|
||||
NL, "a\nb", "\n", "x\r\ny", "Up\nper"]
|
||||
UNI_NAMES = [UNI, "caf\u00e9-creator-claude", "\u00fc" * 40, "kelvin-\u212a", "\u65e5\u672c-creator-claude",
|
||||
"\u0130" * 20 + "-creator-claude", "\u00fc\n"]
|
||||
|
||||
|
||||
def test_lib_sh_has_one_definition():
|
||||
@@ -216,6 +221,26 @@ def test_each_hasher_alone_matches_python(tmp_path, hashers):
|
||||
assert res.stdout == sanitize_herdr_agent_name(name) + "\n", name
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hashers", [["python3"], ["shasum", "python3"]])
|
||||
def test_a_name_that_is_not_ascii_matches_python(tmp_path, hashers):
|
||||
if not shutil.which(hashers[0]):
|
||||
pytest.skip(hashers[0] + " not installed")
|
||||
defn = _definition(tmp_path)
|
||||
d = _tools_dir(tmp_path, ["tr", "sed", "awk", "cut"] + hashers)
|
||||
for name in UNI_NAMES:
|
||||
res = _sanitize_with_path(defn, d, name)
|
||||
assert res.returncode == 0, (name, res.stderr)
|
||||
assert res.stdout == sanitize_herdr_agent_name(name) + "\n", name
|
||||
|
||||
|
||||
def test_a_name_that_is_not_ascii_needs_python3(tmp_path):
|
||||
defn = _definition(tmp_path)
|
||||
d = _tools_dir(tmp_path, ["tr", "sed", "awk", "cut"])
|
||||
for name in UNI_NAMES:
|
||||
res = _sanitize_with_path(defn, d, name)
|
||||
assert res.returncode != 0 and res.stdout == "" and "not ASCII" in res.stderr, (name, res.stderr)
|
||||
|
||||
|
||||
def test_no_hasher_at_all_is_an_error_not_a_name(tmp_path):
|
||||
defn = _definition(tmp_path)
|
||||
d = _tools_dir(tmp_path, ["tr", "sed", "awk", "cut"])
|
||||
@@ -351,6 +376,41 @@ def test_any_write_migrates_and_rewrites_the_yaml(own_workspace_root, tmp_path):
|
||||
_assert_new_form(s, s["herdr_session"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("retry", ["entry-point", "any-write"])
|
||||
def test_a_retry_repairs_the_yaml_a_failed_migration_left_behind(own_workspace_root, tmp_path, monkeypatch, retry):
|
||||
"""The DB is committed before the YAML is replaced; if that fails, the next run rewrites the YAML."""
|
||||
from lib_py import registry_format
|
||||
ws = tmp_path / "ws"
|
||||
yaml_path = _write_old_registry(ws, with_db=True)
|
||||
real_replace, failed = os.replace, []
|
||||
|
||||
def replace_once(src, dst, *a, **kw):
|
||||
if os.path.abspath(str(dst)) == str(yaml_path) and not failed:
|
||||
failed.append(dst)
|
||||
raise OSError("injected: YAML not replaced")
|
||||
return real_replace(src, dst, *a, **kw)
|
||||
|
||||
monkeypatch.setenv("YAML_PATH", "")
|
||||
monkeypatch.setenv("AGENT_SESSIONS_MUTATION", "")
|
||||
monkeypatch.setattr(os, "replace", replace_once)
|
||||
assert registry_format.main(["--yaml", str(yaml_path)]) == 1 and failed
|
||||
monkeypatch.setattr(os, "replace", real_replace)
|
||||
assert _state(ws)["registry_format"] == 2
|
||||
assert registry_format.stored_version(yaml.safe_load(yaml_path.read_text())) == 1
|
||||
|
||||
if retry == "entry-point":
|
||||
res = _entry(yaml_path)
|
||||
else:
|
||||
res = subprocess.run(["bash", "-c", 'source "$1" && atomic_dump_yaml "$2"', "_", str(SKILLS / "lib.sh"), str(yaml_path)],
|
||||
input="pass\n", capture_output=True, text=True, timeout=60)
|
||||
assert res.returncode == 0, res.stderr
|
||||
y = yaml.safe_load(yaml_path.read_text())
|
||||
assert y["registry_format"] == 2
|
||||
assert {s["name"]: s for s in y["herdr_sessions"]} == _db_rows(ws), "YAML and DB agree"
|
||||
for s in y["herdr_sessions"]:
|
||||
_assert_new_form(s, s["herdr_session"])
|
||||
|
||||
|
||||
def test_a_new_registry_is_written_current(own_workspace_root, tmp_path):
|
||||
yaml_path = tmp_path / "fresh" / ".mam" / "agent-sessions.yaml"
|
||||
yaml_path.parent.mkdir(parents=True)
|
||||
@@ -375,10 +435,12 @@ def _registry_bytes(sandbox):
|
||||
return out
|
||||
|
||||
|
||||
def _stop(sandbox, name, *extra):
|
||||
def _stop(sandbox, name, *extra, server="default", env=None):
|
||||
script = sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
||||
return subprocess.run(["bash", str(script), "--session", name, "--herdr-session", "default"] + list(extra),
|
||||
capture_output=True, text=True, cwd=str(sandbox), timeout=120)
|
||||
flag = ["--herdr-session", server] if server else []
|
||||
return subprocess.run(["bash", str(script), "--session", name] + flag + list(extra),
|
||||
capture_output=True, text=True, cwd=str(sandbox), timeout=120,
|
||||
env=dict(os.environ, **(env or {})))
|
||||
|
||||
|
||||
def _agent(cwd, pane):
|
||||
@@ -422,6 +484,63 @@ def test_stop_reports_a_rowless_session_that_is_not_there(mam_sandbox, mock_herd
|
||||
assert _registry_bytes(mam_sandbox) == registry_with_a_row
|
||||
|
||||
|
||||
def test_stop_without_a_row_uses_the_environments_server(mam_sandbox, mock_herdr, registry_with_a_row):
|
||||
"""No --herdr-session: the server is HERDR_SESSION_NAME, as create chose it, not one derived from the workspace."""
|
||||
_set_mock(mock_herdr, agents={"orphan73": _agent(mam_sandbox, "w1:p74")}, calls=[])
|
||||
res = _stop(mam_sandbox, "orphan73", server=None, env={"HERDR_SESSION_NAME": "wanted-env-server"})
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
assert "wanted-env-server" in res.stdout
|
||||
calls = json.loads(Path(mock_herdr).read_text())["calls"]
|
||||
# every call on an agent or pane names that server (`session list` and `config check` name none)
|
||||
targeted = [c for c in calls if "agent" in c[:3] or "pane" in c[:3]]
|
||||
assert targeted and all(c[:2] == ["--session", "wanted-env-server"] for c in targeted), calls
|
||||
assert ["--session", "wanted-env-server", "agent", "get", "orphan73"] in calls
|
||||
assert _registry_bytes(mam_sandbox) == registry_with_a_row
|
||||
|
||||
|
||||
def test_stop_does_not_take_an_unreadable_row_for_no_row(mam_sandbox, mock_herdr):
|
||||
"""Only "no such row" leads to the row-less path; a lookup that fails otherwise stops."""
|
||||
db = mam_sandbox / ".mam" / "agent-sessions.db"
|
||||
if db.exists():
|
||||
db.unlink()
|
||||
(mam_sandbox / ".mam" / "agent-sessions.yaml").write_text("herdr_sessions:\n- 42\n")
|
||||
_set_mock(mock_herdr, agents={"orphan74": _agent(mam_sandbox, "w1:p75")})
|
||||
res = _stop(mam_sandbox, "orphan74")
|
||||
assert res.returncode == 1, res.stdout + res.stderr
|
||||
assert "cannot read the row" in res.stderr
|
||||
assert "orphan74" in json.loads(Path(mock_herdr).read_text())["agents"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("broken", ["yaml-without-db", "db", "no-sessions-table", "no-tables"])
|
||||
def test_stop_does_not_take_an_unreadable_registry_for_no_row(mam_sandbox, mock_herdr, broken):
|
||||
"""load_state_json's {} on a read error is not "no row": a registered session is not closed row-less."""
|
||||
name = "orphan75-creator-claude"
|
||||
_seed(mam_sandbox, [{"name": name, "status": "running", "role": "creator", "agent": "claude",
|
||||
"pane": {"cwd": str(mam_sandbox), "cmd": "claude"}}])
|
||||
mam = mam_sandbox / ".mam"
|
||||
if broken == "yaml-without-db":
|
||||
for f in mam.glob("agent-sessions.db*"):
|
||||
f.unlink()
|
||||
(mam / "agent-sessions.yaml").write_text("herdr_sessions: [\n - {name: " + name + "\n")
|
||||
elif broken == "db":
|
||||
for f in mam.glob("agent-sessions.db-*"):
|
||||
f.unlink()
|
||||
(mam / "agent-sessions.db").write_bytes(b"this is not a database" * 64)
|
||||
else:
|
||||
# a missing table is not an empty registry
|
||||
conn = sqlite3.connect(str(mam / "agent-sessions.db"))
|
||||
conn.execute("DROP TABLE sessions")
|
||||
if broken == "no-tables":
|
||||
conn.execute("DROP TABLE state")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
_set_mock(mock_herdr, agents={name: _agent(mam_sandbox, "w1:p76")})
|
||||
res = _stop(mam_sandbox, name)
|
||||
assert res.returncode == 1, res.stdout + res.stderr
|
||||
assert "cannot read" in res.stderr and "no registry row" not in res.stdout
|
||||
assert name in json.loads(Path(mock_herdr).read_text())["agents"]
|
||||
|
||||
|
||||
def test_stop_without_a_row_purges_nothing(mam_sandbox, mock_herdr, registry_with_a_row):
|
||||
_set_mock(mock_herdr, agents={"orphan72": _agent(mam_sandbox, "w1:p73")})
|
||||
res = _stop(mam_sandbox, "orphan72", "--purge-conversation", "--yes")
|
||||
|
||||
@@ -239,7 +239,8 @@ def test_lib_sh_no_local_in_shim_heredoc():
|
||||
with open(lib_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
start_idx = content.find("cat <<'EOF' > \"$tmp_file\"")
|
||||
# Issue #71: the shim is written as { printf ...; declare -f ...; cat <<'EOF' ... } > "$tmp_file"
|
||||
start_idx = content.find("cat <<'EOF'\n")
|
||||
end_idx = content.find("\nEOF\n", start_idx)
|
||||
assert start_idx != -1 and end_idx != -1
|
||||
|
||||
|
||||
@@ -393,6 +393,10 @@ def test_integration_invalid_arguments_exit_statuses(mam_sandbox, mock_herdr, mo
|
||||
assert "ERROR: --session required" in res.stderr
|
||||
|
||||
# 3. stop_session.sh cannot infer agent from weird session name
|
||||
# (Issue #71: a session with no row needs no agent kind, so the row is
|
||||
# registered here, with no agent, suffix or pane.cmd)
|
||||
(tmp_path / ".mam" / "agent-sessions.yaml").write_text(
|
||||
"herdr_sessions:\n- name: bad-name\n status: running\n pane: {}\n")
|
||||
res = subprocess.run(["bash", str(stop_script), "--session", "bad-name"], capture_output=True, text=True, cwd=str(tmp_path))
|
||||
assert res.returncode == 2
|
||||
assert "ERROR: cannot infer agent" in res.stderr
|
||||
|
||||
Reference in new issue
Block a user