diff --git a/cli.py b/cli.py index 56819bb315..1e4d5a5d9b 100644 --- a/cli.py +++ b/cli.py @@ -4137,8 +4137,10 @@ _TRANSIENT_PROVIDER_REASONS = frozenset({ # ``KANBAN_TERMINAL_PROVIDER_EXIT_CODE`` so the dispatcher parks the card after ONE spawn with # the provider's words as the reason, instead of re-spawning into the same wall until # ``kanban.failure_limit`` is spent. ``billing`` stays transient: credit comes back. +# ``upstream_blocked`` (a WAF/CDN refusing the SDK's User-Agent) is terminal too: only a +# header change heals it, never a retry. _TERMINAL_PROVIDER_REASONS = frozenset({ - "auth", "auth_permanent", "model_not_found", "ssl_cert_verification", + "auth", "auth_permanent", "model_not_found", "ssl_cert_verification", "upstream_blocked", }) diff --git a/tests/hermes_cli/test_single_query_exit_contract.py b/tests/hermes_cli/test_single_query_exit_contract.py index 79ac9748d5..d49831977c 100644 --- a/tests/hermes_cli/test_single_query_exit_contract.py +++ b/tests/hermes_cli/test_single_query_exit_contract.py @@ -53,16 +53,27 @@ def test_dispatcher_spawned_worker_signals_a_provider_outage_not_a_protocol_viol assert code == KANBAN_RATE_LIMIT_EXIT_CODE -@pytest.mark.parametrize("reason", ["auth", "auth_permanent", "model_not_found", "ssl_cert_verification"]) +@pytest.mark.parametrize( + "reason", ["auth", "auth_permanent", "model_not_found", "ssl_cert_verification", "upstream_blocked"] +) def test_dispatcher_spawned_worker_signals_a_terminal_provider_error(monkeypatch, reason): - """A revoked credential / missing model cannot be retried into working: the worker says so - with EX_CONFIG so the dispatcher parks the card after one spawn. A person's run keeps 1.""" + """A revoked credential / missing model / WAF User-Agent block cannot be retried into working: + the worker says so with EX_CONFIG so the dispatcher parks the card after one spawn. A person's + run keeps 1.""" monkeypatch.setenv("HERMES_KANBAN_TASK", "t_abc123") assert _run_non_quiet(monkeypatch, {"failed": True, "failure_reason": reason}) == KANBAN_TERMINAL_PROVIDER_EXIT_CODE monkeypatch.delenv("HERMES_KANBAN_TASK") assert _run_non_quiet(monkeypatch, {"failed": True, "failure_reason": reason}) == 1 +def test_dispatcher_spawned_worker_keeps_a_plain_failure_at_one(monkeypatch): + """Control: a task-level failure (or an unknown reason) is neither transient nor terminal — + the worker exits 1 and the dispatcher counts it against ``kanban.failure_limit`` as before.""" + monkeypatch.setenv("HERMES_KANBAN_TASK", "t_abc123") + assert _run_non_quiet(monkeypatch, {"failed": True, "failure_reason": "some_unknown_reason"}) == 1 + assert _run_non_quiet(monkeypatch, {"failed": True}) == 1 + + @pytest.mark.parametrize( ("turn_result", "expected"), [