fix(issue-9): translate only a structured server_not_running error in the shim's ls

The ls translation searched stderr and the whole stdout for the substring
server_not_running, so a successful `agent list` that merely carried the text
(an agent named server_not_running-codex, or metadata) made the shim report
"no server running". reconcile.sh reads that as a confirmed-empty server, and
drift A would then mark every live row on it terminated (codex review of
0918917).

- keep the RPC exit code and translate only a failed call whose error
  envelope (the whole stream or any line of it) has
  error.code == "server_not_running"; every other failure keeps the unknown
  path (nonzero exit, herdr_confirmed False) as before
- tests: the real herdr 0.9 envelope, a successful list carrying the
  substring in a name and in metadata, other failures staying unknown, and
  reconcile keeping both live rows with no drift A; the new cases fail on
  0918917
- delegate wrapper: log a non-pending status seen during the inject ack
  grace as "registry advanced" instead of attributing it to the agent
- VERSIONS.md v4.4.2: describe the structured check and count 16 new tests

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-13 00:32:57 +09:00
1 parent 091891756b
commit 7c40bf4930
5 files changed
+150 -22

No files matched your search

+109 -11
View File
@@ -15,6 +15,8 @@ import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
RECONCILE_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
STATUS_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-status" / "scripts" / "status.sh"
@@ -54,6 +56,24 @@ KEPT_CONFIG = {
"MAM_CUSTOM_SETTING": "kept",
}
# What herdr 0.9 writes to stderr (exit 1) for `agent list` against a stopped
# session server.
REAL_DOWN_ENVELOPE = {
"id": "cli:agent:list",
"error": {
"code": "server_not_running",
"message": "no herdr server is running at /tmp/sessions/down-srv/herdr.sock; "
"run `herdr session attach down-srv` to start or attach it",
},
}
# A successful list in which the down-server code appears only as data: in an
# agent name (the sanitizer allows underscores) and in unrelated metadata.
LISTING_WITH_SUBSTRING = {"result": {"agents": [
{"name": "server_not_running-codex", "agent": "codex"},
{"name": "creator-claude-01", "agent": "claude", "title": "server_not_running"},
]}}
def _install_fake_herdr(sandbox: Path) -> dict:
fakebin = sandbox / "fakebin"
@@ -72,6 +92,12 @@ def _install_fake_herdr(sandbox: Path) -> dict:
return env
def _fake_agent_list(sandbox: Path, body: str) -> None:
"""Replace the fake's fallback answer (what `agent list` gets) with `body`."""
fake = sandbox / "fakebin" / "herdr"
fake.write_text(FAKE_HERDR.replace('print(json.dumps({"result": {"agents": []}}))', body))
def _calls(env: dict) -> list:
log = Path(env["FAKE_HERDR_LOG"])
if not log.exists():
@@ -90,6 +116,15 @@ def _run_through_shim(sandbox: Path, env: dict, session: str) -> subprocess.Comp
)
def _shim_ls(sandbox: Path, env: dict, server: str) -> subprocess.CompletedProcess:
"""The exact `ls` query reconcile.sh sends for each registered server."""
lib = sandbox / ".agents" / "skills" / "lib.sh"
return subprocess.run(
["bash", "-c", f'source "{lib}" >/dev/null 2>&1; herdr -L {server} ls -F "#{{session_name}}|#{{session_created}}"'],
capture_output=True, text=True, cwd=str(sandbox), env=env, timeout=60,
)
def test_bootstrap_strips_agent_runtime_markers(mam_sandbox):
env = _install_fake_herdr(mam_sandbox)
env.update(RUNTIME_MARKERS)
@@ -160,22 +195,85 @@ def test_reconcile_and_status_opt_out_of_autostart_statically():
f"{script.name} must opt out of the shim's server bootstrap"
def test_ls_reports_down_server_as_no_server_running(mam_sandbox):
@pytest.mark.parametrize("envelope", [
{"error": {"code": "server_not_running"}},
REAL_DOWN_ENVELOPE,
], ids=["minimal", "herdr-0.9"])
def test_ls_reports_down_server_as_no_server_running(mam_sandbox, envelope):
"""reconcile.sh treats a nonzero `ls` as 'empty' only when stderr says
'no server running'; anything else flips herdr_confirmed=False for every
server. The shim must therefore translate server_not_running explicitly."""
env = _install_fake_herdr(mam_sandbox)
env["MAM_NO_AUTOSTART"] = "1"
fake = mam_sandbox / "fakebin" / "herdr"
fake.write_text(FAKE_HERDR.replace(
'print(json.dumps({"result": {"agents": []}}))',
'sys.stderr.write(json.dumps({"error": {"code": "server_not_running"}}) + "\\n"); sys.exit(1)',
))
lib = mam_sandbox / ".agents" / "skills" / "lib.sh"
res = subprocess.run(
["bash", "-c", f'source "{lib}" >/dev/null 2>&1; herdr -L down-srv ls -F "#{{session_name}}|#{{session_created}}"'],
capture_output=True, text=True, cwd=str(mam_sandbox), env=env, timeout=60,
)
_fake_agent_list(mam_sandbox,
f'sys.stderr.write({json.dumps(json.dumps(envelope))} + "\\n"); sys.exit(1)')
res = _shim_ls(mam_sandbox, env, "down-srv")
assert res.returncode != 0
assert "no server running" in res.stderr, res.stderr
assert res.stdout.strip() == ""
def test_ls_lists_agents_whose_names_contain_server_not_running(mam_sandbox):
"""A successful list is never a down server, whatever text it carries.
Matching the bare substring let one agent named server_not_running-codex
hide every agent its server returned."""
env = _install_fake_herdr(mam_sandbox)
env["MAM_NO_AUTOSTART"] = "1"
_fake_agent_list(mam_sandbox, f"print({json.dumps(json.dumps(LISTING_WITH_SUBSTRING))})")
res = _shim_ls(mam_sandbox, env, "live-srv")
assert res.returncode == 0, res.stderr
assert "no server running" not in res.stderr, res.stderr
names = [line.split("|", 1)[0] for line in res.stdout.splitlines() if line.strip()]
assert names == ["server_not_running-codex", "creator-claude-01"], res.stdout
@pytest.mark.parametrize("stderr_text", [
json.dumps({"error": {"code": "transport_error",
"message": "socket reset while probing server_not_running"}}),
"warning: server_not_running cache entry is stale",
"",
], ids=["other-code", "plain-text", "empty"])
def test_ls_leaves_other_failures_unknown(mam_sandbox, stderr_text):
"""Only the structured code means "down". Any other failure keeps the
unknown path (nonzero exit without 'no server running'), so reconcile
drops herdr_confirmed instead of reading a confirmed-empty server."""
env = _install_fake_herdr(mam_sandbox)
env["MAM_NO_AUTOSTART"] = "1"
_fake_agent_list(mam_sandbox,
f'sys.stderr.write({json.dumps(stderr_text)} + "\\n"); sys.exit(1)')
res = _shim_ls(mam_sandbox, env, "flaky-srv")
assert res.returncode != 0
assert "no server running" not in res.stderr, res.stderr
assert res.stdout.strip() == ""
def test_reconcile_keeps_live_rows_when_a_name_contains_server_not_running(mam_sandbox):
"""End to end through reconcile.sh: the server answers a successful list in
which the down-server code appears as data. Both registered rows must stay
alive with herdr_confirmed intact, so drift A cannot mark them terminated."""
env = _install_fake_herdr(mam_sandbox)
_fake_agent_list(mam_sandbox, f"print({json.dumps(json.dumps(LISTING_WITH_SUBSTRING))})")
rows = "".join(
f" - name: {name}\n"
" status: running\n"
" herdr_session: live-srv\n"
" pane:\n"
" cwd: /tmp/nonexistent-workspace\n"
for name in ("server_not_running-codex", "creator-claude-01")
)
(mam_sandbox / ".mam" / "agent-sessions.yaml").write_text("herdr_sessions:\n" + rows)
reconcile = mam_sandbox / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
res = subprocess.run(["bash", str(reconcile), "--once", "--emit-diff", "--dry-run"],
capture_output=True, text=True, cwd=str(mam_sandbox), env=env, timeout=120)
assert res.returncode == 0, res.stderr
report = json.loads(res.stdout)
assert report["herdr_confirmed"] is True, report
alive = set(report["herdr_sessions_alive"])
assert {"server_not_running-codex|live-srv", "creator-claude-01|live-srv"} <= alive, alive
drift_a = [d for d in report["drifts"] if d.get("class") == "A"]
assert not drift_a, drift_a