fix(stop): record and purge only the row's own or a verified id; a dead herdr records none (Issue #63)
This commit is contained in:
1 parent
b8a30d5b93
commit
3a2219454e
2 files changed
+178
-20
No files matched your search
@@ -248,3 +248,144 @@ def test_session_ids_own_id_priority_rules_unit(tmp_path):
|
||||
s_stale = decide_stop(state_stale, "ws-creator-agy", "agy", str(ws), False, purge=False, home=str(home))
|
||||
assert s_stale["captured"] == "" and "no longer verifies; kept as is" in s_stale["note"]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- Done Criteria 1 & 2 (stop & purge)
|
||||
|
||||
def _fake_lsof_multi(bindir: Path, mapping: dict) -> None:
|
||||
"""Fake lsof that prints the pid from `mapping` ({uuid_substr: pid}) for any matching path."""
|
||||
cases = "\n".join(f' *{k}*) echo {v}; found=1 ;;' for k, v in mapping.items())
|
||||
exe = Path(bindir) / "lsof"
|
||||
exe.write_text(
|
||||
"#!/bin/sh\n"
|
||||
"found=0\n"
|
||||
'for a in "$@"; do\n'
|
||||
' case "$a" in\n'
|
||||
f"{cases}\n"
|
||||
" esac\n"
|
||||
"done\n"
|
||||
'[ "$found" = "1" ] && exit 0\n'
|
||||
"exit 1\n"
|
||||
)
|
||||
exe.chmod(0o755)
|
||||
|
||||
|
||||
def _mock_herdr_agent(state_file: Path, name: str, cwd: Path, pid: int, epoch: int,
|
||||
agent: str = "agy", pane_id: str = "") -> None:
|
||||
state = json.loads(Path(state_file).read_text())
|
||||
pid_str = pane_id or f"w1:p_{name}"
|
||||
state.setdefault("agents", {})[name] = {
|
||||
"pid": pid,
|
||||
"cwd": str(cwd),
|
||||
"command": agent,
|
||||
"agent": agent,
|
||||
"pane_id": pid_str,
|
||||
"workspace_id": "w1",
|
||||
"created": epoch,
|
||||
"buffer": f"Antigravity {cwd}\n>",
|
||||
}
|
||||
Path(state_file).write_text(json.dumps(state))
|
||||
|
||||
|
||||
def _write_rows(sandbox: Path, rows: list) -> Path:
|
||||
lib = sandbox / ".agents" / "skills" / "lib.sh"
|
||||
yaml_path = sandbox / ".mam" / "agent-sessions.yaml"
|
||||
mutation = f"d['herdr_sessions'] = {repr(rows)}\n"
|
||||
res = subprocess.run(
|
||||
["bash", "-c", f'source "{lib}" && atomic_dump_yaml "{yaml_path}"'],
|
||||
input=mutation, capture_output=True, text=True, cwd=str(sandbox), timeout=60,
|
||||
)
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
return yaml_path
|
||||
|
||||
|
||||
def _read_rows(sandbox: Path) -> dict:
|
||||
yaml_path = sandbox / ".mam" / "agent-sessions.yaml"
|
||||
data = yaml.safe_load(yaml_path.read_text()) or {}
|
||||
return {r["name"]: r for r in data.get("herdr_sessions") or []}
|
||||
|
||||
|
||||
def _run_stop(sandbox: Path, session: str, agent: str = "agy", purge: bool = False) -> subprocess.CompletedProcess:
|
||||
script = sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
||||
cmd = ["bash", str(script), "--session", session, "--agent", agent]
|
||||
if purge:
|
||||
cmd += ["--purge-conversation", "--yes"]
|
||||
env = dict(os.environ, SKS_QUIESCENT_TRIES="0", SKS_DIALOG_TIMEOUT="0", SKS_BUSY_TIMEOUT="0")
|
||||
return subprocess.run(cmd, capture_output=True, text=True, cwd=str(sandbox), env=env, timeout=120)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("order", [("a_then_b", True), ("b_then_a", True), ("both_dead", False)])
|
||||
def test_c1_stop_same_cwd_agy_sessions_records_only_verified_owner(mam_sandbox, mock_herdr, order):
|
||||
"""Done Criterion 1: two agy sessions in one cwd; cache points to A's UA.
|
||||
Stop A->B and B->A with herdr alive records UA on A and None on B; when
|
||||
herdr is already dead for both, neither records an id and both leave a reason."""
|
||||
label, alive = order
|
||||
ws = mam_sandbox / "same-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
now = time.time()
|
||||
epoch = int(now) - 100
|
||||
_agy_full_artifacts(mam_sandbox, UA)
|
||||
_agy_full_artifacts(mam_sandbox, UB)
|
||||
_agy_cache(mam_sandbox, {ws: UA})
|
||||
|
||||
proc_a = subprocess.Popen(["sleep", "120"])
|
||||
proc_b = subprocess.Popen(["sleep", "120"])
|
||||
try:
|
||||
_fake_lsof_multi(mam_sandbox / "bin", {UA: proc_a.pid, UB: proc_b.pid})
|
||||
name_a, name_b = "same-ws-creator-agy", "same-ws-reviewer-agy"
|
||||
_write_rows(mam_sandbox, [
|
||||
_row(name_a, "agy", ws, epoch, pid=proc_a.pid),
|
||||
_row(name_b, "agy", ws, epoch, pid=proc_b.pid),
|
||||
])
|
||||
if alive:
|
||||
_mock_herdr_agent(mock_herdr, name_a, ws, proc_a.pid, epoch, pane_id="w1:p_a")
|
||||
_mock_herdr_agent(mock_herdr, name_b, ws, proc_b.pid, epoch, pane_id="w1:p_b")
|
||||
|
||||
seq = [name_a, name_b] if label != "b_then_a" else [name_b, name_a]
|
||||
for sname in seq:
|
||||
res = _run_stop(mam_sandbox, sname, "agy")
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
|
||||
rows = _read_rows(mam_sandbox)
|
||||
if alive:
|
||||
assert rows[name_a]["agy_conversation_id_own"] == UA
|
||||
assert rows[name_a]["resumable"] is True
|
||||
assert rows[name_b]["agy_conversation_id_own"] is None
|
||||
assert rows[name_b]["last_visible_status"].startswith("stop: ")
|
||||
else:
|
||||
assert rows[name_a]["agy_conversation_id_own"] is None
|
||||
assert rows[name_b]["agy_conversation_id_own"] is None
|
||||
assert "herdr session was already gone" in rows[name_a]["last_visible_status"]
|
||||
assert "herdr session was already gone" in rows[name_b]["last_visible_status"]
|
||||
finally:
|
||||
for p in (proc_a, proc_b):
|
||||
p.kill()
|
||||
p.wait()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("broken_dup", [False, True])
|
||||
def test_c2_purge_never_deletes_another_rows_conversation(mam_sandbox, mock_herdr, broken_dup):
|
||||
"""Done Criterion 2: A is stopped with UA; B has no own id (or also has UA in a
|
||||
broken registry) and herdr is dead. Purging B leaves A's conversation intact."""
|
||||
ws = mam_sandbox / "same-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
epoch = int(time.time()) - 100
|
||||
a_files = _agy_full_artifacts(mam_sandbox, UA)
|
||||
_agy_cache(mam_sandbox, {ws: UA})
|
||||
|
||||
name_a, name_b = "same-ws-creator-agy", "same-ws-reviewer-agy"
|
||||
row_a = _row(name_a, "agy", ws, epoch, status="stopped", agy_conversation_id_own=UA,
|
||||
session_id_source="discovered", session_id_verified=True)
|
||||
row_b = _row(name_b, "agy", ws, epoch, status="running",
|
||||
agy_conversation_id_own=(UA if broken_dup else None))
|
||||
_write_rows(mam_sandbox, [row_a, row_b])
|
||||
|
||||
res = _run_stop(mam_sandbox, name_b, "agy", purge=True)
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
assert "nothing purged" in res.stdout
|
||||
for p in a_files:
|
||||
assert p.exists(), f"A's artifact was deleted by B's purge: {p}"
|
||||
rows = _read_rows(mam_sandbox)
|
||||
assert name_b not in rows
|
||||
assert rows[name_a]["agy_conversation_id_own"] == UA
|
||||
|
||||
|
||||
Reference in new issue
Block a user