Adds the single exception type used throughout the pipeline. Every failure maps to one of the ten IX_* codes from the MVP spec §8 with a stable machine-readable code and an optional free-form detail. The `str()` form is log-scrapable with a single regex (`IX_xxx_xxx: <msg> (detail=...)`), so mammon-side reliability UX can classify failures without brittle string parsing. Enum values equal names so callers can serialise either. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.8 KiB
Python
72 lines
2.8 KiB
Python
"""Tests for the IXException + IXErrorCode error model (spec §8)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from ix.errors import IXErrorCode, IXException
|
|
|
|
|
|
class TestIXErrorCode:
|
|
"""Every spec §8 code is present with its documented default message."""
|
|
|
|
def test_all_spec_codes_exist(self) -> None:
|
|
expected = {
|
|
"IX_000_000",
|
|
"IX_000_002",
|
|
"IX_000_004",
|
|
"IX_000_005",
|
|
"IX_000_006",
|
|
"IX_000_007",
|
|
"IX_001_000",
|
|
"IX_001_001",
|
|
"IX_002_000",
|
|
"IX_002_001",
|
|
}
|
|
actual = {code.name for code in IXErrorCode}
|
|
assert expected == actual
|
|
|
|
def test_code_value_matches_name(self) -> None:
|
|
# The enum value IS the string code. Tests at the call sites rely on
|
|
# e.g. `IXErrorCode.IX_000_000.value == "IX_000_000"`.
|
|
for code in IXErrorCode:
|
|
assert code.value == code.name
|
|
|
|
def test_default_messages_cover_spec_triggers(self) -> None:
|
|
# Check a subset of the spec §8 trigger phrases show up in the default
|
|
# message. We don't pin exact wording — just that the message is
|
|
# recognisable for log-scraping.
|
|
assert "request_ix" in IXErrorCode.IX_000_000.default_message
|
|
assert "context" in IXErrorCode.IX_000_002.default_message
|
|
assert "files" in IXErrorCode.IX_000_004.default_message
|
|
assert "MIME" in IXErrorCode.IX_000_005.default_message
|
|
assert "page" in IXErrorCode.IX_000_006.default_message.lower()
|
|
assert "fetch" in IXErrorCode.IX_000_007.default_message.lower()
|
|
assert "empty" in IXErrorCode.IX_001_000.default_message.lower()
|
|
assert "use case" in IXErrorCode.IX_001_001.default_message.lower()
|
|
assert "inference" in IXErrorCode.IX_002_000.default_message.lower()
|
|
assert "structured output" in IXErrorCode.IX_002_001.default_message.lower()
|
|
|
|
|
|
class TestIXException:
|
|
def test_raise_returns_code_and_default_message(self) -> None:
|
|
with pytest.raises(IXException) as exc_info:
|
|
raise IXException(IXErrorCode.IX_000_000)
|
|
|
|
exc = exc_info.value
|
|
assert exc.code is IXErrorCode.IX_000_000
|
|
assert exc.detail is None
|
|
s = str(exc)
|
|
assert s.startswith("IX_000_000: ")
|
|
assert "request_ix is None" in s
|
|
assert "(detail=" not in s
|
|
|
|
def test_exception_with_detail_includes_detail_in_str(self) -> None:
|
|
exc = IXException(IXErrorCode.IX_000_007, detail="https://example/x.pdf: 404")
|
|
s = str(exc)
|
|
assert s.startswith("IX_000_007: ")
|
|
assert "(detail=https://example/x.pdf: 404)" in s
|
|
|
|
def test_exception_is_an_exception(self) -> None:
|
|
exc = IXException(IXErrorCode.IX_001_001)
|
|
assert isinstance(exc, Exception)
|