Files
OpenJarvis/tests/agents/test_errors.py
T
0b140110d8 Make OpenAI-compat and Ollama engine streaming truly async (#626)
The OpenAI-compat and Ollama engines exposed stream()/stream_full() as async def but iterated a synchronous httpx.Client.iter_lines() internally, blocking the single event loop on every inter-token read (serializing concurrent chats; one wedged upstream read froze the whole API). Convert both to a shared AsyncHTTPEngineMixin using httpx.AsyncClient + aiter_lines() with a per-event-loop pooled client and the configured timeout applied; map mid-stream transport errors (RemoteProtocolError/ReadError) to EngineConnectionError via a deliberately narrow set that keeps CancelledError/GeneratorExit propagating; handle non-2xx explicitly (incl. 3xx and a typed EngineContextLengthError for context-window overflow 400s); switch litellm streaming to acompletion; and offload the blocking non-streaming handlers and websocket generate() to asyncio.to_thread. No public API change. Strong MockTransport-based tests, including a pin that the async path never touches the sync client. Complements #618.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:10:22 -07:00

86 lines
3.0 KiB
Python

"""Tests for agent error classification."""
from __future__ import annotations
class TestErrorClassification:
def test_retryable_error(self):
from openjarvis.agents.errors import RetryableError
err = RetryableError("rate limit hit")
assert err.retryable is True
assert str(err) == "rate limit hit"
def test_fatal_error(self):
from openjarvis.agents.errors import FatalError
err = FatalError("invalid API key")
assert err.retryable is False
def test_escalate_error(self):
from openjarvis.agents.errors import EscalateError
err = EscalateError("agent uncertain about next step")
assert err.retryable is False
assert err.needs_human is True
def test_classify_rate_limit(self):
from openjarvis.agents.errors import classify_error
result = classify_error(Exception("rate limit exceeded"))
assert result.retryable is True
def test_classify_timeout(self):
from openjarvis.agents.errors import classify_error
result = classify_error(TimeoutError("connection timed out"))
assert result.retryable is True
def test_classify_permission(self):
from openjarvis.agents.errors import classify_error
result = classify_error(PermissionError("access denied"))
assert result.retryable is False
def test_classify_unknown_defaults_retryable(self):
from openjarvis.agents.errors import classify_error
result = classify_error(ValueError("something weird"))
assert result.retryable is True
def test_retry_delay_exponential(self):
from openjarvis.agents.errors import retry_delay
assert retry_delay(0) == 10
assert retry_delay(1) == 20
assert retry_delay(2) == 40
# Capped at 300 seconds
assert retry_delay(10) == 300
def test_classify_context_length_is_fatal(self):
# A context-window overflow is deterministic — retrying the identical
# over-length request can never succeed, so it must NOT be classified
# retryable (which would burn ~30s of backoff on guaranteed failures).
from openjarvis.agents.errors import classify_error
from openjarvis.engine._base import EngineContextLengthError
typed = classify_error(
EngineContextLengthError(
"The conversation is too long for the model's context window."
)
)
assert typed.retryable is False
# Same for untyped errors whose message reads like a context overflow
# (e.g. raw vendor errors from engines without the typed mapping).
untyped = classify_error(
Exception("This model's maximum context length is 4096 tokens.")
)
assert untyped.retryable is False
def test_suggest_action_context_length(self):
from openjarvis.agents.errors import FatalError, suggest_action
action = suggest_action(FatalError("prompt exceeds the model's context window"))
assert "context window" in action or "too long" in action.lower()