"""Tests de recuperación ante overflow de ventana de contexto. Cubre: detección del error de context-length del proveedor, y el envoltorio del adapter que lo traduce a `ContextOverflowError` (dominio) tanto si salta al iniciar el stream como durante la iteración. """ from __future__ import annotations import asyncio import enum import sys import types import pytest if not hasattr(enum, "StrEnum"): class _CompatStrEnum(str, enum.Enum): pass enum.StrEnum = _CompatStrEnum if "anthropic" not in sys.modules: anthropic_stub = types.ModuleType("anthropic") anthropic_stub.AsyncAnthropic = type("_AsyncAnthropic", (), {}) sys.modules["anthropic"] = anthropic_stub if "openai" not in sys.modules: openai_stub = types.ModuleType("openai") openai_stub.AsyncOpenAI = type("_AsyncOpenAI", (), {}) sys.modules["openai"] = openai_stub from src.adapters.base import ContextOverflowError from src.adapters.openai_adapter import OpenAIAdapter, _is_context_overflow class TestOverflowDetection: def test_detects_by_message(self): assert _is_context_overflow( Exception("This model's maximum context length is 8192 tokens, however you requested 9000") ) assert _is_context_overflow(Exception("context_length_exceeded")) assert _is_context_overflow(Exception("Please reduce the length of the messages")) def test_does_not_flag_unrelated_errors(self): assert not _is_context_overflow(Exception("rate limit exceeded")) assert not _is_context_overflow(Exception("invalid api key")) def test_detects_by_type_name(self): class ContextWindowExceededError(Exception): pass assert _is_context_overflow(ContextWindowExceededError("boom")) class TestStreamWrapperMapsOverflow: def _make_adapter(self): # Saltamos __init__ (no necesitamos el cliente AsyncOpenAI: parcheamos # _stream_impl). Así el test no depende del stub de openai. return OpenAIAdapter.__new__(OpenAIAdapter) def test_overflow_at_stream_init_becomes_domain_error(self, monkeypatch): adapter = self._make_adapter() async def _impl(messages, tools=None, config=None): raise RuntimeError("maximum context length is 32768 tokens") yield # noqa: hace de esto un async generator monkeypatch.setattr(adapter, "_stream_impl", _impl) async def _run(): async for _ in adapter.stream([{"role": "user", "content": "hola"}]): pass with pytest.raises(ContextOverflowError): asyncio.run(_run()) def test_non_overflow_error_propagates_unchanged(self, monkeypatch): adapter = self._make_adapter() async def _impl(messages, tools=None, config=None): raise RuntimeError("connection reset by peer") yield monkeypatch.setattr(adapter, "_stream_impl", _impl) async def _run(): async for _ in adapter.stream([{"role": "user", "content": "hola"}]): pass with pytest.raises(RuntimeError) as exc: asyncio.run(_run()) assert not isinstance(exc.value, ContextOverflowError)