fix(resume): no guess with two same-type rows in a cwd; refuse another row's id before spawning; the running branch uses the same decision (Issue #63)
This commit is contained in:
1 parent
3a2219454e
commit
fa93cc5622
4 files changed
+290
-27
No files matched your search
@@ -389,3 +389,227 @@ def test_c2_purge_never_deletes_another_rows_conversation(mam_sandbox, mock_herd
|
||||
assert rows[name_a]["agy_conversation_id_own"] == UA
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- Done Criteria 3, 4, 7 & rev 2 (resume)
|
||||
|
||||
def _run_resume(sandbox: Path, ws: Path, session: str, agent: str = "agy",
|
||||
dry_run: bool = False) -> subprocess.CompletedProcess:
|
||||
script = sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
|
||||
cmd = ["bash", str(script), "--workspace", str(ws), "--agent", agent, "--session", session]
|
||||
if dry_run:
|
||||
cmd.append("--dry-run")
|
||||
return subprocess.run(cmd, capture_output=True, text=True, cwd=str(sandbox), timeout=120)
|
||||
|
||||
|
||||
def test_c3_resume_unpinned_second_agy_row_starts_fresh_and_running_branch_keeps_epoch(
|
||||
mam_sandbox, mock_herdr, mock_agents
|
||||
):
|
||||
"""Done Criterion 3: two stopped agy rows in one cwd (A=UA, B=None):
|
||||
dry-run and real resume of B spawn without --conversation UA and emit NOTE;
|
||||
when B's herdr is already alive without an id, resume does not record UA and
|
||||
preserves B's herdr_session_epoch."""
|
||||
ws = mam_sandbox / "same-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
epoch_a = int(time.time()) - 300
|
||||
epoch_b = int(time.time()) - 200
|
||||
_agy_full_artifacts(mam_sandbox, UA)
|
||||
_agy_cache(mam_sandbox, {ws: UA})
|
||||
|
||||
name_a, name_b = "same-ws-creator-agy", "same-ws-reviewer-agy"
|
||||
_write_rows(mam_sandbox, [
|
||||
_row(name_a, "agy", ws, epoch_a, status="stopped", agy_conversation_id_own=UA,
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
_row(name_b, "agy", ws, epoch_b, status="stopped", agy_conversation_id_own=None),
|
||||
])
|
||||
|
||||
dry = _run_resume(mam_sandbox, ws, name_b, "agy", dry_run=True)
|
||||
assert dry.returncode == 0, dry.stdout + dry.stderr
|
||||
assert "--conversation" not in dry.stdout
|
||||
assert f"NOTE: {name_b} has no recorded agy id and 2 agy sessions share" in dry.stderr
|
||||
|
||||
real = _run_resume(mam_sandbox, ws, name_b, "agy", dry_run=False)
|
||||
assert real.returncode == 0, real.stdout + real.stderr
|
||||
rows = _read_rows(mam_sandbox)
|
||||
assert rows[name_b]["status"] == "running"
|
||||
assert rows[name_b]["agy_conversation_id_own"] is None
|
||||
assert "--conversation" not in rows[name_b]["pane"]["cmd_full"]
|
||||
|
||||
# Running branch: B is already alive in herdr without an own id; epoch must stay fixed.
|
||||
fixed_epoch = 1700000123
|
||||
_write_rows(mam_sandbox, [
|
||||
_row(name_a, "agy", ws, epoch_a, status="stopped", agy_conversation_id_own=UA,
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
_row(name_b, "agy", ws, fixed_epoch, status="running", agy_conversation_id_own=None),
|
||||
])
|
||||
_mock_herdr_agent(mock_herdr, name_b, ws, 9999, fixed_epoch, pane_id="w1:p_b")
|
||||
|
||||
alive_res = _run_resume(mam_sandbox, ws, name_b, "agy", dry_run=False)
|
||||
assert alive_res.returncode == 0, alive_res.stdout + alive_res.stderr
|
||||
rows = _read_rows(mam_sandbox)
|
||||
assert rows[name_b]["agy_conversation_id_own"] is None
|
||||
assert rows[name_b]["herdr_session_epoch"] == fixed_epoch
|
||||
assert rows[name_b]["last_visible_status"].startswith("already running; no id recorded at ")
|
||||
|
||||
|
||||
def test_c4_pinned_post_discovered_and_preassigned_agents_unchanged(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""Done Criterion 4: agy/opencode rows with own_id keep it on stop and resume with it;
|
||||
claude/grok stop/resume and find_workspace_uuid behave as before."""
|
||||
ws = mam_sandbox / "c4-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
epoch = int(time.time()) - 100
|
||||
_agy_full_artifacts(mam_sandbox, UA)
|
||||
_claude_transcript(mam_sandbox / ".claude" / "projects", ws, UB)
|
||||
|
||||
# Seed opencode DB for ses_c4
|
||||
oc_db = mam_sandbox / ".local" / "share" / "opencode" / "opencode.db"
|
||||
oc_db.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(oc_db))
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS session_v2 (id TEXT PRIMARY KEY, parent_id TEXT, directory TEXT, time_created INTEGER)")
|
||||
conn.execute("INSERT OR REPLACE INTO session_v2 VALUES (?, NULL, ?, ?)", ("ses_c4", str(ws), epoch * 1000))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
_write_rows(mam_sandbox, [
|
||||
_row("c4-creator-agy", "agy", ws, epoch, status="running", agy_conversation_id_own=UA,
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
_row("c4-creator-opencode", "opencode", ws, epoch, status="running", opencode_session_id_own="ses_c4",
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
_row("c4-creator-claude", "claude", ws, epoch, status="running", claude_session_id_own=UB,
|
||||
session_id_source="assigned", session_id_verified=True),
|
||||
])
|
||||
|
||||
for sname, agent in (("c4-creator-agy", "agy"), ("c4-creator-opencode", "opencode"), ("c4-creator-claude", "claude")):
|
||||
st = _run_stop(mam_sandbox, sname, agent)
|
||||
assert st.returncode == 0, st.stdout + st.stderr
|
||||
|
||||
rows = _read_rows(mam_sandbox)
|
||||
assert rows["c4-creator-agy"]["agy_conversation_id_own"] == UA and rows["c4-creator-agy"]["resumable"] is True
|
||||
assert rows["c4-creator-opencode"]["opencode_session_id_own"] == "ses_c4" and rows["c4-creator-opencode"]["resumable"] is True
|
||||
assert rows["c4-creator-claude"]["claude_session_id_own"] == UB and rows["c4-creator-claude"]["resumable"] is True
|
||||
|
||||
r_agy = _run_resume(mam_sandbox, ws, "c4-creator-agy", "agy", dry_run=True)
|
||||
assert r_agy.returncode == 0 and f"--conversation {UA}" in r_agy.stdout
|
||||
r_oc = _run_resume(mam_sandbox, ws, "c4-creator-opencode", "opencode", dry_run=True)
|
||||
assert r_oc.returncode == 0 and "--session ses_c4" in r_oc.stdout
|
||||
r_cl = _run_resume(mam_sandbox, ws, "c4-creator-claude", "claude", dry_run=True)
|
||||
assert r_cl.returncode == 0 and f"-r {UB}" in r_cl.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize("db_exists", [True, False])
|
||||
def test_c7_resume_refuses_when_recorded_id_is_claimed_by_another_row(
|
||||
mam_sandbox, mock_herdr, mock_agents, db_exists
|
||||
):
|
||||
"""Done Criterion 7 & rev 2 (A): when both A and B record UA (whether UA.db
|
||||
exists on disk or is missing), resume B refuses with exit 1 before spawning,
|
||||
names A on stderr, and emits no fresh-spawn NOTE."""
|
||||
ws = mam_sandbox / "c7-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
epoch = int(time.time()) - 100
|
||||
if db_exists:
|
||||
_agy_full_artifacts(mam_sandbox, UA)
|
||||
_agy_cache(mam_sandbox, {ws: UA})
|
||||
|
||||
name_a, name_b = "c7-ws-creator-agy", "c7-ws-reviewer-agy"
|
||||
_write_rows(mam_sandbox, [
|
||||
_row(name_a, "agy", ws, epoch, status="stopped", agy_conversation_id_own=UA,
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
_row(name_b, "agy", ws, epoch, status="stopped", agy_conversation_id_own=UA,
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
])
|
||||
before = _read_rows(mam_sandbox)
|
||||
|
||||
res = _run_resume(mam_sandbox, ws, name_b, "agy", dry_run=False)
|
||||
assert res.returncode == 1, res.stdout + res.stderr
|
||||
assert name_a in res.stderr and UA in res.stderr
|
||||
assert "NOTE:" not in res.stderr
|
||||
state = json.loads(Path(mock_herdr).read_text())
|
||||
assert name_b not in state.get("agents", {})
|
||||
assert _read_rows(mam_sandbox) == before
|
||||
|
||||
|
||||
def test_rev2_stale_own_id_single_row_never_overwrites_with_cache_and_clears_on_resume(
|
||||
mam_sandbox, mock_herdr, mock_agents
|
||||
):
|
||||
"""rev 2 (A)-3: single agy row with stale own_id UX (UX.db missing) while cache
|
||||
and disk hold UC. Stop keeps UX; resume starts a fresh conversation without UC,
|
||||
emits NOTE, and clears agy_conversation_id_own to None so #40 can pin later."""
|
||||
ws = mam_sandbox / "stale-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
epoch = int(time.time()) - 100
|
||||
_agy_full_artifacts(mam_sandbox, UC)
|
||||
_agy_cache(mam_sandbox, {ws: UC})
|
||||
|
||||
name = "stale-ws-creator-agy"
|
||||
_write_rows(mam_sandbox, [
|
||||
_row(name, "agy", ws, epoch, status="running", agy_conversation_id_own=UX,
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
])
|
||||
|
||||
st = _run_stop(mam_sandbox, name, "agy")
|
||||
assert st.returncode == 0, st.stdout + st.stderr
|
||||
row_after_stop = _read_rows(mam_sandbox)[name]
|
||||
assert row_after_stop["agy_conversation_id_own"] == UX
|
||||
assert "no longer verifies; kept as is" in row_after_stop["last_visible_status"]
|
||||
|
||||
res = _run_resume(mam_sandbox, ws, name, "agy", dry_run=False)
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
assert f"NOTE: {name}'s recorded agy id {UX} no longer verifies" in res.stderr
|
||||
row_after_resume = _read_rows(mam_sandbox)[name]
|
||||
assert row_after_resume["agy_conversation_id_own"] is None
|
||||
assert row_after_resume["session_id_source"] == "pending-discovery"
|
||||
assert row_after_resume["session_id_verified"] is False
|
||||
assert f"recorded id {UX} no longer verified" in row_after_resume["last_visible_status"]
|
||||
assert UC not in row_after_resume["pane"]["cmd_full"]
|
||||
|
||||
|
||||
def test_rev2_drop_stale_id_refuses_to_clear_id_claimed_by_another_row(mam_sandbox):
|
||||
"""rev 2: update_yaml_resumed.sh --drop-stale-id keeps own_id if another row also claims it."""
|
||||
ws = mam_sandbox / "dup-drop-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
epoch = int(time.time()) - 100
|
||||
name_a, name_b = "dup-creator-agy", "dup-reviewer-agy"
|
||||
_write_rows(mam_sandbox, [
|
||||
_row(name_a, "agy", ws, epoch, status="stopped", agy_conversation_id_own=UA),
|
||||
_row(name_b, "agy", ws, epoch, status="stopped", agy_conversation_id_own=UA),
|
||||
])
|
||||
upd = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "update_yaml_resumed.sh"
|
||||
res = subprocess.run(
|
||||
["bash", str(upd), "--session", name_b, "--uuid", "", "--agent", "agy",
|
||||
"--workspace", str(ws), "--drop-stale-id"],
|
||||
capture_output=True, text=True, cwd=str(mam_sandbox), timeout=60,
|
||||
)
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
assert _read_rows(mam_sandbox)[name_b]["agy_conversation_id_own"] == UA
|
||||
|
||||
|
||||
def test_rev2_claude_unpinned_sibling_row_skips_creator_claimed_id(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""rev 2 (B): ws-creator-claude is stopped with UA; ws-reviewer-claude has no
|
||||
own_id (--wrapper row shape); disk has UA.jsonl and UB.jsonl. Resume and stop
|
||||
of ws-reviewer-claude pick UB instead of creator's UA."""
|
||||
ws = mam_sandbox / "claude-sib-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
now = time.time()
|
||||
epoch = int(now) - 100
|
||||
cdir = mam_sandbox / ".claude" / "projects"
|
||||
_claude_transcript(cdir, ws, UA, mtime=now)
|
||||
_claude_transcript(cdir, ws, UB, mtime=now - 10)
|
||||
|
||||
name_a, name_b = "claude-sib-ws-creator-claude", "claude-sib-ws-reviewer-claude"
|
||||
_write_rows(mam_sandbox, [
|
||||
_row(name_a, "claude", ws, epoch, status="stopped", claude_session_id_own=UA,
|
||||
session_id_source="discovered", session_id_verified=True),
|
||||
_row(name_b, "claude", ws, epoch, status="running", claude_session_id_own=None),
|
||||
])
|
||||
|
||||
dry = _run_resume(mam_sandbox, ws, name_b, "claude", dry_run=True)
|
||||
assert dry.returncode == 0, dry.stdout + dry.stderr
|
||||
|
||||
st = _run_stop(mam_sandbox, name_b, "claude")
|
||||
assert st.returncode == 0, st.stdout + st.stderr
|
||||
assert _read_rows(mam_sandbox)[name_b]["claude_session_id_own"] == UB
|
||||
|
||||
dry_after = _run_resume(mam_sandbox, ws, name_b, "claude", dry_run=True)
|
||||
assert dry_after.returncode == 0, dry_after.stdout + dry_after.stderr
|
||||
assert f"-r {UB}" in dry_after.stdout
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user