mirror of
https://github.com/usestrix/strix.git
synced 2026-10-12 03:47:56 +00:00
ContextWindowExceededError carries status_code=400, which matched the _INPUT_REJECTION_CODES guard and triggered image-strip retry logic. Image stripping cannot reduce token count, so the agent wasted up to 3 retry cycles before parking as failed. Add an explicit isinstance check before the status-code guard to detect context-window overflow and park the agent as 'failed' immediately.
151 lines
4.8 KiB
Python
151 lines
4.8 KiB
Python
"""Tests for strix/core/execution.py — Fix 1: context-window bypass."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import AsyncGenerator
|
|
from litellm.exceptions import ContextWindowExceededError
|
|
|
|
from strix.core.execution import _run_cycle
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _context_window_exc() -> ContextWindowExceededError:
|
|
return ContextWindowExceededError(
|
|
message="prompt is too long: 1023797 tokens > 1000000 maximum",
|
|
model="claude-sonnet-4-6",
|
|
llm_provider="anthropic",
|
|
)
|
|
|
|
|
|
def _make_coordinator() -> AsyncMock:
|
|
coordinator = AsyncMock()
|
|
coordinator.is_shutting_down = False
|
|
return coordinator
|
|
|
|
|
|
async def _empty_events() -> AsyncGenerator[None]:
|
|
"""Async generator that yields nothing — simulates a completed stream."""
|
|
for _ in ():
|
|
yield
|
|
|
|
|
|
def _make_stream(exception: Exception | None = None) -> MagicMock:
|
|
stream = MagicMock()
|
|
stream.stream_events.return_value = _empty_events()
|
|
stream.run_loop_exception = exception
|
|
return stream
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fix 1 tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def test_context_window_error_sets_status_failed() -> None:
|
|
"""ContextWindowExceededError must park the agent as 'failed', not retry."""
|
|
exc = _context_window_exc()
|
|
coordinator = _make_coordinator()
|
|
|
|
with (
|
|
patch("strix.core.execution.Runner") as mock_runner,
|
|
patch(
|
|
"strix.core.execution.strip_all_images_from_session", return_value=True
|
|
) as mock_strip,
|
|
):
|
|
mock_runner.run_streamed.return_value = _make_stream(exception=exc)
|
|
|
|
with pytest.raises(ContextWindowExceededError):
|
|
await _run_cycle(
|
|
MagicMock(), # agent
|
|
coordinator,
|
|
"agent-001",
|
|
input_data=[],
|
|
run_config=MagicMock(),
|
|
context={}, # no parent_id → will re-raise
|
|
max_turns=10,
|
|
session=AsyncMock(), # non-None so image-strip guard could trigger
|
|
interactive=True,
|
|
event_sink=None,
|
|
hooks=None,
|
|
)
|
|
|
|
# Image-strip must NOT have been attempted.
|
|
mock_strip.assert_not_called()
|
|
# Agent must be parked as failed.
|
|
coordinator.set_status.assert_called_once_with("agent-001", "failed")
|
|
|
|
|
|
async def test_context_window_error_returns_none_for_child_agent() -> None:
|
|
"""Child agents (parent_id set) must return None rather than re-raising."""
|
|
exc = _context_window_exc()
|
|
coordinator = _make_coordinator()
|
|
|
|
with patch("strix.core.execution.Runner") as mock_runner:
|
|
mock_runner.run_streamed.return_value = _make_stream(exception=exc)
|
|
|
|
result = await _run_cycle(
|
|
MagicMock(),
|
|
coordinator,
|
|
"child-001",
|
|
input_data=[],
|
|
run_config=MagicMock(),
|
|
context={"parent_id": "root-001"},
|
|
max_turns=10,
|
|
session=AsyncMock(),
|
|
interactive=True,
|
|
event_sink=None,
|
|
hooks=None,
|
|
)
|
|
|
|
assert result is None
|
|
coordinator.set_status.assert_called_once_with("child-001", "failed")
|
|
|
|
|
|
async def test_non_context_window_400_still_triggers_image_strip() -> None:
|
|
"""A plain 400 error (not ContextWindowExceededError) must still attempt image stripping."""
|
|
|
|
class _Plain400Error(Exception):
|
|
"""Minimal stand-in for any non-context-window 400 error."""
|
|
|
|
status_code = 400
|
|
|
|
plain_400 = _Plain400Error("some other bad request")
|
|
coordinator = _make_coordinator()
|
|
|
|
with (
|
|
patch("strix.core.execution.Runner") as mock_runner,
|
|
patch(
|
|
"strix.core.execution.strip_all_images_from_session", return_value=False
|
|
) as mock_strip,
|
|
):
|
|
mock_runner.run_streamed.return_value = _make_stream(exception=plain_400)
|
|
|
|
# Not interactive → re-raises immediately without retry when strip returns False.
|
|
with pytest.raises(_Plain400Error):
|
|
await _run_cycle(
|
|
MagicMock(),
|
|
coordinator,
|
|
"agent-002",
|
|
input_data=[],
|
|
run_config=MagicMock(),
|
|
context={},
|
|
max_turns=10,
|
|
session=AsyncMock(),
|
|
interactive=False,
|
|
event_sink=None,
|
|
hooks=None,
|
|
)
|
|
|
|
# strip WAS attempted (it's the right path for plain 400).
|
|
mock_strip.assert_called_once()
|