infoxtractor/tests/unit/test_errors.py
Dirk Riemann ae595c937a
All checks were successful
tests / test (push) Successful in 1m2s
tests / test (pull_request) Successful in 59s
feat(errors): add IXException + IXErrorCode per spec §8
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>
2026-04-18 10:46:01 +02:00

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)