"""errors.py — typed errors with voice-friendly messages (plan §12).""" from mcp_server.errors import ( AnnotationError, ApiError, AuthError, ClarificationNeededError, ErrorKind, InsufficientStockError, LabVoiceError, NotFoundError, PermissionDeniedError, ) def test_every_error_kind_maps_to_its_class(): assert AuthError.kind is ErrorKind.AUTH assert PermissionDeniedError.kind is ErrorKind.PERMISSION assert NotFoundError.kind is ErrorKind.NOT_FOUND assert ApiError.kind is ErrorKind.API assert AnnotationError.kind is ErrorKind.ANNOTATION assert ClarificationNeededError.kind is ErrorKind.CLARIFICATION assert InsufficientStockError.kind is ErrorKind.INSUFFICIENT_STOCK assert LabVoiceError.kind is ErrorKind.API def test_kind_values_are_the_wire_strings(): assert ErrorKind.AUTH == "auth_error" assert ErrorKind.PERMISSION == "permission_error" assert ErrorKind.NOT_FOUND == "not_found" assert ErrorKind.CLARIFICATION == "clarification" assert ErrorKind.INSUFFICIENT_STOCK == "insufficient_stock" def test_message_and_detail_are_preserved(): err = NotFoundError("Experiment 123 not found.", detail="GET /experiments/123") assert str(err) == "Experiment 123 not found." assert err.message == "Experiment 123 not found." assert err.detail == "GET /experiments/123" def test_all_labvoice_errors_share_the_base(): for cls in ( AuthError, PermissionDeniedError, NotFoundError, ApiError, AnnotationError, ClarificationNeededError, InsufficientStockError, ): assert issubclass(cls, LabVoiceError) def test_clarification_options_default_empty_and_are_preserved(): err = ClarificationNeededError("Which ethanol?") assert err.options == [] err = ClarificationNeededError("Which ethanol?", options=["Absolute Ethanol", "96%"]) assert err.options == ["Absolute Ethanol", "96%"] def test_error_messages_are_voice_friendly_strings(): err = PermissionDeniedError("This API key is read-only; I can't finish the step.") assert isinstance(err.message, str) and err.message