`MoAPresetNotFoundError` subclasses ValueError, so `is_local_validation_error` re-opened the fallback the classifier had just refused (#55933). Only an UNCLASSIFIED (reason=unknown) local error keeps the historical fallback. Test exercises the real exception through settlement, not the classifier bool.
94 lines
3.8 KiB
Python
94 lines
3.8 KiB
Python
"""A 400 for the model's own malformed tool-call JSON must not walk the fallback chain (#12770)."""
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from agent.error_classifier import FailoverReason, classify_api_error
|
|
from agent.turn_api_error import settle_unrecovered_error
|
|
|
|
|
|
class _Err(Exception):
|
|
status_code = 400
|
|
response = None
|
|
|
|
def __init__(self, message, body=None):
|
|
super().__init__(message)
|
|
self.body = body or {"error": {"message": message, "type": "invalid_request_error"}}
|
|
|
|
|
|
@pytest.mark.parametrize("wording", ["invalid tool call arguments", "Invalid function_call arguments"])
|
|
def test_malformed_tool_args_400_is_terminal_without_fallback(wording):
|
|
"""Large session included: the bare-ish message must not read as context overflow either."""
|
|
verdict = classify_api_error(_Err(f"Error code: 400 - {wording}"), provider="ollama",
|
|
approx_tokens=90_000, context_length=128_000, num_messages=120)
|
|
assert verdict.reason is FailoverReason.format_error
|
|
assert (verdict.retryable, verdict.should_compress, verdict.should_fallback) == (False, False, False)
|
|
|
|
# Unrelated request-shape 400s keep their fallback (another provider may accept the request).
|
|
assert classify_api_error(_Err("Unsupported parameter: 'max_tokens'")).should_fallback is True
|
|
|
|
|
|
class _Agent:
|
|
"""Only the fallback seam is real; every other helper the terminal path touches is a no-op."""
|
|
log_prefix = ""
|
|
verbose = False
|
|
provider = "ollama"
|
|
_fallback_chain = [object()]
|
|
_fallback_index = 0
|
|
_credential_pool = None
|
|
|
|
def __init__(self):
|
|
self.activated = []
|
|
|
|
def _has_pending_fallback(self):
|
|
return True
|
|
|
|
def _try_activate_fallback(self, **kwargs):
|
|
self.activated.append(True)
|
|
return True
|
|
|
|
def _summarize_api_error(self, error):
|
|
return str(error)
|
|
|
|
def __getattr__(self, name):
|
|
return lambda *args, **kwargs: None
|
|
|
|
|
|
def _settle(agent, err, provider, status_code):
|
|
retry = SimpleNamespace(copilot_stale_cred_retry_attempted=False, primary_recovery_attempted=False)
|
|
classified = classify_api_error(err, provider=provider)
|
|
with patch("agent.conversation_loop._is_copilot_provider", lambda a: False):
|
|
return settle_unrecovered_error(
|
|
agent, api_error=err, classified=classified, _retry=retry, status_code=status_code, error_msg=str(err),
|
|
is_context_length_error=False, is_rate_limited=False, _is_zai_coding_overload=False,
|
|
_provider=provider, _base="http://127.0.0.1:11434/v1", _model="glm", messages=[], api_messages=[],
|
|
api_kwargs={}, active_system_prompt="", conversation_history=None, approx_tokens=10,
|
|
retry_count=0, max_retries=3, compression_attempts=0, api_call_count=1,
|
|
)
|
|
|
|
|
|
def test_client_error_settlement_skips_fallback_when_classifier_says_so():
|
|
agent = _Agent()
|
|
verdict = _settle(agent, _Err("invalid tool call arguments"), "ollama", 400)
|
|
assert verdict.action == "return"
|
|
assert verdict.result["failure_reason"] == FailoverReason.format_error.value
|
|
assert agent.activated == []
|
|
|
|
|
|
def test_fallback_free_verdict_wins_over_local_valueerror_shape():
|
|
"""A recognised no-fallback verdict raised as a ValueError subclass (MoA preset missing,
|
|
#55933) must not sneak through the unclassified-local-error fallback allowance; a truly
|
|
unclassified ValueError keeps it."""
|
|
from agent.errors import MoAPresetNotFoundError
|
|
|
|
agent = _Agent()
|
|
verdict = _settle(agent, MoAPresetNotFoundError("MoA preset 'old' was not found"), "moa", None)
|
|
assert verdict.action == "return"
|
|
assert agent.activated == []
|
|
|
|
agent = _Agent()
|
|
verdict = _settle(agent, ValueError("some local bug"), "openai", None)
|
|
assert verdict.action == "break"
|
|
assert agent.activated == [True]
|