mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 21:27:41 +02:00
124 lines
5.6 KiB
Python
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"]
|