fix(issue-17): follow-ups - a failed decision ends the wrapper with 1; resume closes herdr 0.9.3's placeholder (v4.8.7) #52

Merged
Godopu merged 9 commits from fix/issue17-followups into main 2026-10-08 02:32:57 +00:00
2 changed files with 59 additions and 3 deletions
Showing only changes of commit f19f94099f - Show all commits

No files matched your search

+17 -3
View File
@@ -3,7 +3,8 @@
With restore-on-restart off (lib_py/herdr_config.py) a restarted MAM server With restore-on-restart off (lib_py/herdr_config.py) a restarted MAM server
still brings every saved pane back - as an empty shell in its old cwd. When still brings every saved pane back - as an empty shell in its old cwd. When
that directory is gone, herdr 0.9.1 opens the shell in $HOME and herdr 0.9.3 that directory is gone, herdr 0.9.1 opens the shell in $HOME and herdr 0.9.3
keeps the old path; nothing below looks at the cwd. A pane keeps the keeps the old path with no shell in it (see 4.); nothing below looks at the
cwd. A pane keeps the
`agent_session` its agent `agent_session` its agent
reported before the server died. resume_session.sh calls this once, right reported before the server died. resume_session.sh calls this once, right
before it spawns, with the id it is about to resume; a pane is closed only when before it spawns, with the id it is about to resume; a pane is closed only when
@@ -13,7 +14,10 @@ all of these hold:
2. herdr sees no live agent in it; 2. herdr sees no live agent in it;
3. its agent_session value is that id - so a user's pane, another session's 3. its agent_session value is that id - so a user's pane, another session's
pane and a freshly split pane (no agent_session yet) are never candidates; pane and a freshly split pane (no agent_session yet) are never candidates;
4. its foreground process is the shell itself (nothing running in it). 4. its foreground process is the shell itself (nothing running in it) - or
it has no process at all: herdr 0.9.3 restores a pane whose directory is
gone as a placeholder (`restore_error` set) that `pane process-info`
reports as pane_not_found.
The registry is not consulted: a session whose server died stays `running` The registry is not consulted: a session whose server died stays `running`
there, so "running" cannot tell a live pane from a restored one. there, so "running" cannot tell a live pane from a restored one.
@@ -51,6 +55,16 @@ def _shell_in_foreground(herdr: str, session: str, pane_id: str) -> bool:
return _is_pid(shell) and _is_pid(fg) and fg == shell return _is_pid(shell) and _is_pid(fg) and fg == shell
def _nothing_running(herdr: str, session: str, pane: dict) -> bool:
try:
return _shell_in_foreground(herdr, session, pane["pane_id"])
except subprocess.CalledProcessError as e:
# Only herdr's own placeholder counts: any other failure still stops the run.
if pane.get("restore_error") and "pane_not_found" in (e.stderr or "") + (e.stdout or ""):
return True
raise
def _is_pid(v) -> bool: def _is_pid(v) -> bool:
return isinstance(v, int) and not isinstance(v, bool) and v > 0 return isinstance(v, int) and not isinstance(v, bool) and v > 0
@@ -68,7 +82,7 @@ def close_restored_shells(session_id: str, herdr: str = "herdr",
continue continue
if (p.get("agent_session") or {}).get("value") != session_id: if (p.get("agent_session") or {}).get("value") != session_id:
continue continue
if not _shell_in_foreground(herdr, session, pane_id): if not _nothing_running(herdr, session, p):
continue continue
_herdr(herdr, session, "pane", "close", pane_id) _herdr(herdr, session, "pane", "close", pane_id)
print(f"closed restored pane {pane_id} (cwd={p.get('cwd', '')}, agent_session={session_id})", print(f"closed restored pane {pane_id} (cwd={p.get('cwd', '')}, agent_session={session_id})",
+42
View File
@@ -4,6 +4,7 @@ null, 0, negative, bool, string - must leave the pane alone. herdr is a stub
here; the real-server behaviour is tests/test_issue45_restore.py.""" here; the real-server behaviour is tests/test_issue45_restore.py."""
from __future__ import annotations from __future__ import annotations
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -54,3 +55,44 @@ def test_a_busy_pane_is_left_alone(monkeypatch):
calls = _stub(monkeypatch, {"shell_pid": 4242, "foreground_process_group_id": 4300}) calls = _stub(monkeypatch, {"shell_pid": 4242, "foreground_process_group_id": 4300})
assert hr.close_restored_shells(SID, session="m45-review-probe") == [] assert hr.close_restored_shells(SID, session="m45-review-probe") == []
assert ("pane", "close", "probe-pane") not in calls assert ("pane", "close", "probe-pane") not in calls
# herdr 0.9.3: a pane whose saved directory is gone is restored as a placeholder
# with no process; process-info answers pane_not_found (real-server check:
# test_issue45_restore.py test_b_only_the_resumed_sessions_restored_shell_is_closed).
PLACEHOLDER_ERR = '{"error":{"code":"pane_not_found","message":"pane not found"},"id":"cli:pane:process_info"}'
def _stub_failing(monkeypatch, restore_error, stderr):
calls = []
def fake(herdr, session, *args):
calls.append(args)
if args[:2] == ("pane", "list"):
pane = {"pane_id": "probe-pane", "agent": None, "agent_session": {"value": SID}}
if restore_error:
pane["restore_error"] = restore_error
return {"panes": [pane]}
if args[:2] == ("pane", "process-info"):
raise subprocess.CalledProcessError(1, ["herdr", *args], output="", stderr=stderr)
return {}
monkeypatch.setattr(hr, "_herdr", fake)
return calls
def test_a_placeholder_for_a_missing_directory_is_closed(monkeypatch):
calls = _stub_failing(monkeypatch, "Saved directory is unavailable. Restore the directory and restart "
"this session.", PLACEHOLDER_ERR)
assert hr.close_restored_shells(SID, session="m45-review-probe") == ["probe-pane"]
assert ("pane", "close", "probe-pane") in calls
@pytest.mark.parametrize("restore_error,stderr", [
(None, PLACEHOLDER_ERR), # pane_not_found without herdr's marker
("Saved directory is unavailable.", '{"error":{"code":"server_error"}}'), # another failure
], ids=["no-restore-error", "other-error"])
def test_any_other_process_info_failure_still_stops_the_run(monkeypatch, restore_error, stderr):
calls = _stub_failing(monkeypatch, restore_error, stderr)
with pytest.raises(subprocess.CalledProcessError):
hr.close_restored_shells(SID, session="m45-review-probe")
assert ("pane", "close", "probe-pane") not in calls