Files
openswarm/backend/tests/test_crash_auto_resume.py
T

124 lines
5.6 KiB
Python

"""Pins the crash auto-resume contract (the last silent-stop door): a turn cut off by a dirty
death resumes itself exactly once; consecutive crash implications trip the breaker to the manual
chip (hermes #30719 pairing); a clean quit or a finished stream never auto-resumes; and a real
user message forgives the crash history."""
import asyncio
import json
import os
import pytest
from backend.apps.agents import agent_manager as p_am_module
def p_write_session(tmp_path, monkeypatch, sid: str, status: str, msgs, count: int = 0):
data = {
"id": sid, "name": "t", "status": status, "provider": "anthropic", "model": "sonnet",
"mode": "agent", "messages": msgs, "active_branch_id": "main",
"crash_interrupt_count": count, "closed_at": None,
}
path = os.path.join(str(tmp_path), f"{sid}.json")
with open(path, "w") as f:
json.dump(data, f)
return data
def p_msg(role, text="x"):
return {"role": role, "content": text, "branch_id": "main", "id": f"m-{role}-{text[:6]}"}
@pytest.fixture()
def manager(tmp_path, monkeypatch):
monkeypatch.setattr(p_am_module, "SESSIONS_DIR", str(tmp_path))
mgr = p_am_module.AgentManager()
return mgr
def test_crash_cut_turn_queues_for_auto_resume(manager, tmp_path, monkeypatch):
p_write_session(tmp_path, monkeypatch, "s-cut", "running", [p_msg("user", "do work"), p_msg("tool_call")])
asyncio.run(manager.reconcile_on_startup())
assert manager.crash_resume_queue == ["s-cut"]
with open(os.path.join(str(tmp_path), "s-cut.json")) as f:
assert json.load(f)["crash_interrupt_count"] == 1
def test_finished_stream_and_clean_quit_never_queue(manager, tmp_path, monkeypatch):
p_write_session(tmp_path, monkeypatch, "s-done", "running", [p_msg("user"), p_msg("assistant", "answer")])
p_write_session(tmp_path, monkeypatch, "s-clean", "stopped", [p_msg("user"), p_msg("tool_call")])
asyncio.run(manager.reconcile_on_startup())
assert manager.crash_resume_queue == []
def test_second_consecutive_crash_trips_the_breaker(manager, tmp_path, monkeypatch):
p_write_session(tmp_path, monkeypatch, "s-loop", "running", [p_msg("user"), p_msg("tool_call")], count=1)
asyncio.run(manager.reconcile_on_startup())
assert manager.crash_resume_queue == [], "a session mid-turn at 2 consecutive dirty deaths must NOT auto-resume"
with open(os.path.join(str(tmp_path), "s-loop.json")) as f:
assert json.load(f)["crash_interrupt_count"] == 2
def p_act_like_a_real_app(monkeypatch) -> None:
"""Auto-resume refuses to dispatch under pytest, because it sends REAL turns and the suite shares
the developer's data root (ENG-388). These tests are about the production path, so they opt out."""
import backend.apps.agents.manager.session.SessionPersistence as p_sp
monkeypatch.setattr(p_sp, "auto_resume_held_because", lambda: None)
def test_waiting_approval_never_auto_resumes(manager, tmp_path, monkeypatch):
p_write_session(tmp_path, monkeypatch, "s-appr", "waiting_approval", [p_msg("user"), p_msg("tool_call")])
asyncio.run(manager.reconcile_on_startup())
assert manager.crash_resume_queue == []
def test_auto_resume_sends_one_hidden_continuation(manager, tmp_path, monkeypatch):
p_act_like_a_real_app(monkeypatch)
p_write_session(tmp_path, monkeypatch, "s-cut", "running", [p_msg("user"), p_msg("tool_call")])
asyncio.run(manager.reconcile_on_startup())
sent = []
async def p_fake_send(sid, prompt, **kw):
sent.append((sid, prompt, kw.get("hidden")))
monkeypatch.setattr(manager, "send_message", p_fake_send)
asyncio.run(manager.auto_resume_crashed_turns())
assert len(sent) == 1
sid, prompt, hidden = sent[0]
assert sid == "s-cut" and hidden is True
assert "restarted while you were mid-task" in prompt, "the attribution prefix is added once, at the send chokepoint"
assert manager.crash_resume_queue == []
def test_resume_failure_is_per_session_and_non_fatal(manager, tmp_path, monkeypatch):
p_act_like_a_real_app(monkeypatch)
p_write_session(tmp_path, monkeypatch, "s-a", "running", [p_msg("user"), p_msg("tool_call")])
p_write_session(tmp_path, monkeypatch, "s-b", "running", [p_msg("user"), p_msg("tool_call")])
asyncio.run(manager.reconcile_on_startup())
sent = []
async def p_flaky_send(sid, prompt, **kw):
if sid == sorted(["s-a", "s-b"])[0]:
raise RuntimeError("boom")
sent.append(sid)
monkeypatch.setattr(manager, "send_message", p_flaky_send)
asyncio.run(manager.auto_resume_crashed_turns())
assert len(sent) == 1
def test_a_terminal_system_card_tail_is_not_owed(manager, tmp_path, monkeypatch):
"""ENG-366: an overflow card, an exhausted note or a notice is the END of that ask; a dirty death
after it must not poke a finished chat back to life with a hidden continue."""
p_write_session(tmp_path, monkeypatch, "s-card", "running",
[p_msg("user", "do work"), p_msg("tool_call"), p_msg("system", "This chat exceeded the model's context window")])
asyncio.run(manager.reconcile_on_startup())
assert manager.crash_resume_queue == []
def test_a_system_notice_with_an_armed_continuation_is_owed(manager, tmp_path, monkeypatch):
data = p_write_session(tmp_path, monkeypatch, "s-wait", "running",
[p_msg("user", "do work"), p_msg("system", "token rotated, retrying in a minute")])
data["pending_continuation"] = True
with open(os.path.join(str(tmp_path), "s-wait.json"), "w") as f:
json.dump(data, f)
asyncio.run(manager.reconcile_on_startup())
assert manager.crash_resume_queue == ["s-wait"]