Files
omnigent-ai--omnigent/tests/test_goose_executor_interrupt.py
Anas Khan 07c46bc35e fix(goose): reset ACP state after subprocess interrupt (#1928)
When Goose interruption falls back to terminating the ACP subprocess, clear the cached session, prompt, initialization, and capability state. This ensures the replacement process performs a fresh handshake and session/new instead of reusing state owned by the terminated process.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
2026-07-14 11:57:06 -07:00

129 lines
4.4 KiB
Python

"""Focused tests for GooseExecutor.interrupt_session (#1748).
Verifies that the web Stop button (interrupt_session) correctly:
1. Returns False when there is no live process.
2. Falls back to SIGTERM when no ACP session has been established yet.
3. Sends ACP session/cancel when a session_id is known.
4. Falls back to SIGTERM when session/cancel errors out.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
from omnigent.inner.goose_executor import GooseExecutor
def _make_executor() -> GooseExecutor:
"""Return a GooseExecutor with no real subprocess wired."""
return GooseExecutor(cwd="/tmp", goose_path="/usr/bin/goose")
def _live_proc(returncode: int | None = None) -> MagicMock:
"""Return a mock subprocess with the given returncode."""
proc = MagicMock()
proc.returncode = returncode
proc.terminate = MagicMock()
return proc
async def test_interrupt_no_process_returns_false() -> None:
"""interrupt_session returns False when no subprocess is running."""
ex = _make_executor()
assert ex._proc is None
result = await ex.interrupt_session("s1")
assert result is False
async def test_interrupt_dead_process_returns_false() -> None:
"""interrupt_session returns False when the process has already exited."""
ex = _make_executor()
ex._proc = _live_proc(returncode=0)
result = await ex.interrupt_session("s1")
assert result is False
async def test_interrupt_no_session_id_sends_sigterm() -> None:
"""Without a session_id, interrupt falls back to SIGTERM immediately."""
ex = _make_executor()
ex._proc = _live_proc() # returncode=None → live
ex._system_prompt_sent = True
ex._initialized = True
ex._image_supported = True
ex._session_id = None # no ACP session established yet
result = await ex.interrupt_session("s1")
assert result is True
ex._proc.terminate.assert_called_once()
assert ex._session_id is None
assert ex._system_prompt_sent is False
assert ex._initialized is False
assert ex._image_supported is False
async def test_interrupt_with_session_sends_cancel_notification() -> None:
"""With a session_id, interrupt sends the session/cancel notification.
``session/cancel`` is an ACP notification: it goes out via ``_send`` with no
``id`` and no reply is expected (Goose ends the in-flight prompt with a
cancelled stop reason). Asserting on ``_send`` exercises that real contract.
"""
ex = _make_executor()
ex._proc = _live_proc()
ex._session_id = "goose_session_42"
with patch.object(ex, "_send", new_callable=AsyncMock) as mock_send:
result = await ex.interrupt_session("s1")
assert result is True
mock_send.assert_awaited_once()
sent = mock_send.call_args.args[0]
assert sent["method"] == "session/cancel"
assert sent["params"]["sessionId"] == "goose_session_42"
# A notification carries no id — the agent never responds to it.
assert "id" not in sent
# SIGTERM should NOT have been sent — the clean cancel was enough.
ex._proc.terminate.assert_not_called()
async def test_interrupt_cancel_send_error_falls_back_to_sigterm() -> None:
"""When the session/cancel send errors, interrupt falls back to SIGTERM."""
ex = _make_executor()
ex._proc = _live_proc()
ex._session_id = "goose_session_99"
ex._system_prompt_sent = True
ex._initialized = True
ex._image_supported = True
with patch.object(
ex, "_send", new_callable=AsyncMock, side_effect=RuntimeError("broken pipe")
):
result = await ex.interrupt_session("s1")
assert result is True
ex._proc.terminate.assert_called_once()
assert ex._session_id is None
assert ex._system_prompt_sent is False
assert ex._initialized is False
assert ex._image_supported is False
async def test_interrupt_process_lookup_error_returns_false() -> None:
"""ProcessLookupError on terminate → process vanished → returns False."""
ex = _make_executor()
proc = _live_proc()
proc.terminate.side_effect = ProcessLookupError
ex._proc = proc
ex._session_id = "stale-session"
ex._system_prompt_sent = True
ex._initialized = True
ex._image_supported = True
result = await ex.interrupt_session("s1")
assert result is False
assert ex._session_id is None
assert ex._system_prompt_sent is False
assert ex._initialized is False
assert ex._image_supported is False