fix(resume): allow fresh spawn fallback and refresh epoch on 0-turn Class A resume
- Branch on Class A agents (agy, hermes, opencode) in resume_session.sh to fall back to clean spawn (CMD_FULL) when stopped before first turn (empty UUID) - Update update_yaml_resumed.sh to support empty UUID with --cmd-full and refresh herdr_session_epoch on fresh spawn resume to prevent stale transcript capture - Strictly preserve Class B (claude, grok) assigned UUID escape hatch and test_t8 contract - Add regression tests test_t14, test_t15, test_t16 (Closes #3 Item 2)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
import pytest
|
||||
import subprocess
|
||||
@@ -389,3 +390,125 @@ def test_t13_mam_workspace_key_equivalence(mam_sandbox):
|
||||
abs_path = os.path.realpath(v)
|
||||
py_key = abs_path.replace("/", "-").replace("_", "-")
|
||||
assert sh_key == py_key, f"Mismatch for {v}: shell={sh_key}, py={py_key}"
|
||||
|
||||
|
||||
def test_t14_resume_class_a_fresh_spawn_fallback(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T-14 (Issue #3): agy (Class A) stopped at 0-turns has no conversation id to
|
||||
resolve; resume_session.sh must fall back to a fresh spawn (RC=0, plain spawn
|
||||
command) rather than hard-failing with 'No saved session'."""
|
||||
ws = str(mam_sandbox / "resume_class_a")
|
||||
os.makedirs(ws, exist_ok=True)
|
||||
|
||||
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
||||
resume_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
|
||||
|
||||
cmd = [
|
||||
"bash", str(create_script),
|
||||
"--workspace", ws,
|
||||
"--agent", "agy",
|
||||
"--role", "creator",
|
||||
"--session", "test-t14-agy",
|
||||
"--no-onboard"
|
||||
]
|
||||
subprocess.run(cmd, check=True, cwd=str(mam_sandbox))
|
||||
|
||||
# Stop the session without ever sending a prompt (0-turn stop).
|
||||
subprocess.run(
|
||||
["bash", str(stop_script), "--session", "test-t14-agy", "--agent", "agy"],
|
||||
check=True, cwd=str(mam_sandbox)
|
||||
)
|
||||
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
with open(yaml_path) as f:
|
||||
d = yaml.safe_load(f)
|
||||
session = [s for s in d["herdr_sessions"] if s["name"] == "test-t14-agy"][0]
|
||||
assert session["status"] == "stopped"
|
||||
assert not session.get("agy_conversation_id_own")
|
||||
|
||||
res = subprocess.run([
|
||||
"bash", str(resume_script),
|
||||
"--workspace", ws,
|
||||
"--agent", "agy",
|
||||
"--session", "test-t14-agy",
|
||||
"--dry-run"
|
||||
], capture_output=True, text=True, cwd=str(mam_sandbox))
|
||||
assert res.returncode == 0, f"stderr: {res.stderr}"
|
||||
assert "would spawn:" in res.stdout
|
||||
assert "--dangerously-skip-permissions" in res.stdout
|
||||
assert "--conversation" not in res.stdout
|
||||
|
||||
|
||||
def test_t15_resume_class_b_still_hard_fails_when_truly_unresolvable(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T-15 (Issue #3 regression guard): claude/grok (Class B) must NOT receive the
|
||||
Class A fresh-spawn fallback - a claude session with no resolvable id at all
|
||||
(never created) still hard-fails exactly as before."""
|
||||
ws = str(mam_sandbox / "resume_class_b_missing")
|
||||
os.makedirs(ws, exist_ok=True)
|
||||
resume_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
|
||||
|
||||
res = subprocess.run([
|
||||
"bash", str(resume_script),
|
||||
"--workspace", ws,
|
||||
"--agent", "claude",
|
||||
"--session", "never-created-claude",
|
||||
"--dry-run"
|
||||
], capture_output=True, text=True, cwd=str(mam_sandbox))
|
||||
assert res.returncode == 1
|
||||
assert "ERROR: No saved session for" in res.stderr
|
||||
|
||||
|
||||
def test_t16_resume_class_a_fresh_spawn_resets_discovery_epoch(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""T-16 (Issue #3 challenge refinement): a Class A (agy) 0-turn resume must reset
|
||||
herdr_session_epoch to the resume timestamp so reconcile.sh does not auto-capture
|
||||
stale conversations created between the original create and the resume."""
|
||||
ws = str(mam_sandbox / "resume_class_a_epoch")
|
||||
os.makedirs(ws, exist_ok=True)
|
||||
|
||||
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
||||
resume_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
|
||||
|
||||
cmd = [
|
||||
"bash", str(create_script),
|
||||
"--workspace", ws,
|
||||
"--agent", "agy",
|
||||
"--role", "creator",
|
||||
"--session", "test-t16-agy",
|
||||
"--no-onboard"
|
||||
]
|
||||
subprocess.run(cmd, check=True, cwd=str(mam_sandbox))
|
||||
|
||||
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||
with open(yaml_path) as f:
|
||||
d = yaml.safe_load(f)
|
||||
initial_epoch = [s for s in d["herdr_sessions"] if s["name"] == "test-t16-agy"][0]["herdr_session_epoch"]
|
||||
|
||||
# Stop at 0-turns.
|
||||
subprocess.run(
|
||||
["bash", str(stop_script), "--session", "test-t16-agy", "--agent", "agy"],
|
||||
check=True, cwd=str(mam_sandbox)
|
||||
)
|
||||
|
||||
# Wait to guarantee the epoch advances by at least 1 second.
|
||||
time.sleep(1.1)
|
||||
|
||||
# Resume the session (live execution, updating YAML).
|
||||
res = subprocess.run([
|
||||
"bash", str(resume_script),
|
||||
"--workspace", ws,
|
||||
"--agent", "agy",
|
||||
"--session", "test-t16-agy"
|
||||
], capture_output=True, text=True, cwd=str(mam_sandbox))
|
||||
assert res.returncode == 0, f"stderr: {res.stderr}\nstdout: {res.stdout}"
|
||||
|
||||
with open(yaml_path) as f:
|
||||
d = yaml.safe_load(f)
|
||||
session = [s for s in d["herdr_sessions"] if s["name"] == "test-t16-agy"][0]
|
||||
assert session["status"] == "running"
|
||||
assert session["herdr_session_epoch"] > initial_epoch, (
|
||||
f"herdr_session_epoch was not refreshed: {session['herdr_session_epoch']} <= {initial_epoch}"
|
||||
)
|
||||
assert session.get("session_id_source") == "pending-discovery"
|
||||
assert session.get("session_id_verified") is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user