diff --git a/tests/test_spawner.py b/tests/test_spawner.py index 6eabe8c..f376d6c 100644 --- a/tests/test_spawner.py +++ b/tests/test_spawner.py @@ -414,6 +414,10 @@ class TestAgentBusyErrorClassification: "state": {"status": "idle", "lock_pid_alive": True, "lock_expired": False}, "expected": "session_locked", }, + { + "state": {"status": "running", "lock_pid_alive": True, "lock_expired": False}, + "expected": "session_running", + }, { "state": {"status": "idle", "lock_pid_alive": False, "recent_compact": True}, "expected": "session_compacting", @@ -450,3 +454,92 @@ class TestAgentBusyErrorClassification: assert err.detail["blockers"][0][0] == "session_locked" assert "my-agent" in str(err) assert "session_locked" in str(err) + + +# --------------------------------------------------------------------------- +# 司马懿评审补充:Phase 2.5 + session_stuck(v2.8 #07.1 v1.1 兜底) +# --------------------------------------------------------------------------- + +class TestPhase25AndStuck: + """司马懿评审遗漏 #1 + session_stuck 遗漏补充""" + + def test_phase25_stuck_fallback(self, spawner): + """Phase 0 不触发(status 非 running),Phase 2 检测到假死 → revive + + Phase 2.5 是 #07 v1.1 加的兜底:Phase 0 时 session 正常(idle), + 但 Phase 2 检查时变为 running + lock PID 死。Phase 2.5 应 revive。 + """ + call_count = [0] + + def mock_check(agent_id): + call_count[0] += 1 + if call_count[0] <= 1: + # Phase 0: 正常 idle,不触发 revive + return {"status": "idle", "lock_pid_alive": False} + # Phase 2: 假死(Phase 0 和 Phase 2 之间进程变 stuck) + return {"status": "running", "lock_pid_alive": False} + + spawner._check_session_state = mock_check + + revive_called = [False] + original_revive = spawner._revive_session + + def mock_revive(agent_id): + revive_called[0] = True + return True + + spawner._revive_session = mock_revive + + # Phase 2.5 应触发 revive + session_id = asyncio.run( + spawner.spawn_full_agent( + "test-agent", "task", + task_id="t1", use_main_session=True, + ) + ) + assert revive_called[0], "Phase 2.5 should have revived stuck session" + + def test_session_stuck_after_failed_revive(self, spawner): + """Phase 2.5 revive 失败 → session_stuck + + 假死 revive 后 status 仍为 running → AgentBusyError(session_stuck)。 + """ + call_count = [0] + + def mock_check(agent_id): + call_count[0] += 1 + if call_count[0] <= 1: + return {"status": "idle", "lock_pid_alive": False} + # Phase 2: 假死 + return {"status": "running", "lock_pid_alive": False} + + spawner._check_session_state = mock_check + + # revive 后 session 仍 stuck + def mock_revive(agent_id): + return True # revive 成功但 mock_check 不变,下次仍返回 running + + # 让 revive 后 check 返回 stuck + revive_and_check_count = [0] + original_check = mock_check + + def mock_check_v2(agent_id): + revive_and_check_count[0] += 1 + if revive_and_check_count[0] <= 1: + return {"status": "idle", "lock_pid_alive": False} + if revive_and_check_count[0] == 2: + return {"status": "running", "lock_pid_alive": False} + # revive 后重检:仍 stuck + return {"status": "running", "lock_pid_alive": False} + + spawner._check_session_state = mock_check_v2 + spawner._revive_session = mock_revive + + with pytest.raises(AgentBusyError) as exc_info: + asyncio.run( + spawner.spawn_full_agent( + "test-agent", "task", + task_id="t1", use_main_session=True, + ) + ) + assert "stuck" in exc_info.value.reason