generated from coulomb/repo-seed
feat: WP-0001 foundation + WP-0002 core extensions
WP-0001 — Foundation & GAAF Baseline - SCOPE.md, ARCHITECTURE-LAYERS.md, contracts/ tree - .claude/rules/ stubs filled (architecture, stack, boundary) - 57 tests (pytest), pyproject.toml with ruff+mypy, CI workflow WP-0002 — Core Extensions (FR-4 + FR-3) - FR-4: BudgetTracker (thread-safe) + LLMBudgetExceededError + optional RunConfig.budget_tracker + enforcement in all adapters - FR-3: async_execute_prompt on LLMAdapter ABC (asyncio.to_thread fallback) + native asyncio.create_subprocess_exec in ClaudeCodeAdapter 81 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
96
tests/test_exceptions.py
Normal file
96
tests/test_exceptions.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Tests for the LLMError exception hierarchy (Core).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from llm_connect.exceptions import (
|
||||
LLMError,
|
||||
LLMConfigurationError,
|
||||
LLMAPIError,
|
||||
LLMRateLimitError,
|
||||
LLMTimeoutError,
|
||||
LLMSubprocessError,
|
||||
)
|
||||
|
||||
|
||||
class TestLLMErrorHierarchy:
|
||||
def test_all_are_subclasses_of_llm_error(self):
|
||||
assert issubclass(LLMConfigurationError, LLMError)
|
||||
assert issubclass(LLMAPIError, LLMError)
|
||||
assert issubclass(LLMRateLimitError, LLMError)
|
||||
assert issubclass(LLMTimeoutError, LLMError)
|
||||
assert issubclass(LLMSubprocessError, LLMError)
|
||||
|
||||
def test_rate_limit_is_api_error(self):
|
||||
assert issubclass(LLMRateLimitError, LLMAPIError)
|
||||
|
||||
def test_all_are_exceptions(self):
|
||||
assert issubclass(LLMError, Exception)
|
||||
|
||||
|
||||
class TestLLMError:
|
||||
def test_basic_message(self):
|
||||
err = LLMError("something went wrong")
|
||||
assert str(err) == "something went wrong"
|
||||
|
||||
def test_context_appears_in_str(self):
|
||||
err = LLMError("oops", context={"provider": "openai"})
|
||||
assert "provider=openai" in str(err)
|
||||
|
||||
def test_cause_is_chained(self):
|
||||
cause = ValueError("root cause")
|
||||
err = LLMError("wrapper", cause=cause)
|
||||
assert err.__cause__ is cause
|
||||
|
||||
def test_empty_context_does_not_appear(self):
|
||||
err = LLMError("clean message", context={})
|
||||
assert str(err) == "clean message"
|
||||
|
||||
|
||||
class TestLLMAPIError:
|
||||
def test_has_status_code(self):
|
||||
err = LLMAPIError("bad request", status_code=400)
|
||||
assert err.status_code == 400
|
||||
|
||||
def test_has_response_body(self):
|
||||
err = LLMAPIError("error", status_code=500, response_body='{"error": "oops"}')
|
||||
assert err.response_body == '{"error": "oops"}'
|
||||
|
||||
def test_defaults(self):
|
||||
err = LLMAPIError("minimal")
|
||||
assert err.status_code == 0
|
||||
assert err.response_body == ""
|
||||
|
||||
def test_rate_limit_inherits_status_code(self):
|
||||
err = LLMRateLimitError("too many", status_code=429)
|
||||
assert err.status_code == 429
|
||||
assert isinstance(err, LLMAPIError)
|
||||
|
||||
|
||||
class TestLLMSubprocessError:
|
||||
def test_has_return_code(self):
|
||||
err = LLMSubprocessError("cli failed", return_code=1)
|
||||
assert err.return_code == 1
|
||||
|
||||
def test_has_stderr(self):
|
||||
err = LLMSubprocessError("cli failed", stderr="error output")
|
||||
assert err.stderr == "error output"
|
||||
|
||||
def test_defaults(self):
|
||||
err = LLMSubprocessError("minimal")
|
||||
assert err.return_code == 1
|
||||
assert err.stderr == ""
|
||||
|
||||
|
||||
class TestRaiseAndCatch:
|
||||
def test_catch_as_llm_error(self):
|
||||
with pytest.raises(LLMError):
|
||||
raise LLMConfigurationError("no key")
|
||||
|
||||
def test_catch_api_error_as_llm_error(self):
|
||||
with pytest.raises(LLMError):
|
||||
raise LLMAPIError("http error", status_code=502)
|
||||
|
||||
def test_catch_rate_limit_as_api_error(self):
|
||||
with pytest.raises(LLMAPIError):
|
||||
raise LLMRateLimitError("429", status_code=429)
|
||||
Reference in New Issue
Block a user