feat: let every gateway error carry what the gateway said

Issue #10 Task 1: a rejected call's reason had nowhere to live. The
field goes on the base class because these errors all come from one HTTP
response - which class it is and what the peer said are orthogonal.
This commit is contained in:
2026-08-16 06:01:38 -04:00
parent 1489aab95d
commit e302247022
2 changed files with 70 additions and 5 deletions
+38 -3
View File
@@ -34,6 +34,43 @@ class TestBaseShape:
assert TransientError("429", retry_after_s=2.5).retry_after_s == 2.5
class TestBodyText:
"""issue #10: 非 2xx 的响应体摘要必须有承载处,否则拒绝理由事后不可查。"""
@pytest.mark.parametrize(
"cls", (PolyGatewayError, TransientError, SourceDeadError, RequestRejectedError)
)
def test_defaults_empty_and_accepts_summary(self, cls):
assert cls("boom").body_text == ""
assert cls("boom", body_text='{"error":{"code":"bad"}}').body_text == (
'{"error":{"code":"bad"}}'
)
def test_result_invalid_keeps_both_fields_apart(self):
"""`body_text`(非 2xx 的拒绝理由)与 `raw_text`(2xx 的不可解析输出)不得混用。"""
exc = ResultInvalidError("bad json", raw_text="{oops", body_text="")
assert exc.raw_text == "{oops"
assert exc.body_text == ""
@pytest.mark.parametrize(
"exc",
(
AllSourcesExhausted(scope="llm", reason="stalled", retry_after_s=1.0),
CircuitOpenError(scope="llm", retry_after_s=1.0),
GovernanceBackendError("redis down", scope="llm"),
),
)
def test_scope_level_errors_carry_no_body(self, exc):
"""scope 级失败没有单一响应体可言,空串是如实表达而非噪音。"""
assert exc.body_text == ""
def test_body_text_does_not_leak_into_str(self):
"""字段是旁路数据: 加了它不得改变任何既有异常的 str() 输出。"""
assert str(RequestRejectedError("qwen_1 请求被拒: 400", body_text="whatever")) == (
"qwen_1 请求被拒: 400"
)
class TestResultInvalid:
def test_carries_diagnosis(self):
exc = ResultInvalidError(
@@ -134,9 +171,7 @@ class TestGovernanceBackendReason:
assert "governance_backend_down" in SCOPE_REASONS
def test_gateway_unavailable_accepts_the_new_reason(self):
exc = AllSourcesExhausted(
scope="LLM", reason="governance_backend_down", retry_after_s=0.0
)
exc = AllSourcesExhausted(scope="LLM", reason="governance_backend_down", retry_after_s=0.0)
assert exc.reason == "governance_backend_down"
def test_retry_after_default_is_non_zero(self):