# Issue #12: エラー表示の改善

**Phase:** 1b
**優先度:** 中
**依存関係:** #03, #07
**見積もり:** 0.5日

---

## 概要

ユーザーフレンドリーなエラーメッセージを実装する。
ログファイルの場所、解決ヒント、エラーコードを表示する。

---

## TDD実装手順

### Step 1: Red（テストを書く）

```python
# tests/unit/core/test_error_display.py
"""Tests for error display improvements."""
import pytest
from unittest.mock import patch, MagicMock


class TestErrorHints:
    """Test error hints functionality."""

    def test_connection_error_hint(self):
        """Should show hint for connection errors."""
        from railway.core.decorators import node

        @node
        def connection_node() -> str:
            raise ConnectionError("Unable to connect")

        with patch("railway.core.decorators.logger") as mock_logger:
            with pytest.raises(ConnectionError):
                connection_node()

            # Check for hint in error log
            error_calls = [str(c) for c in mock_logger.error.call_args_list]
            assert any("ヒント" in str(c) or "hint" in str(c).lower() for c in error_calls)

    def test_timeout_error_hint(self):
        """Should show hint for timeout errors."""
        from railway.core.decorators import node

        @node
        def timeout_node() -> str:
            raise TimeoutError("Request timed out")

        with patch("railway.core.decorators.logger") as mock_logger:
            with pytest.raises(TimeoutError):
                timeout_node()

            error_calls = [str(c) for c in mock_logger.error.call_args_list]
            assert any("ヒント" in str(c) or "timeout" in str(c).lower() for c in error_calls)

    def test_value_error_hint(self):
        """Should show hint for value errors."""
        from railway.core.decorators import node

        @node
        def value_node() -> str:
            raise ValueError("Invalid input")

        with patch("railway.core.decorators.logger") as mock_logger:
            with pytest.raises(ValueError):
                value_node()

            error_calls = [str(c) for c in mock_logger.error.call_args_list]
            assert any("ヒント" in str(c) for c in error_calls)


class TestLogFileReference:
    """Test log file reference in errors."""

    def test_error_shows_log_file(self):
        """Should reference log file location."""
        from railway.core.decorators import node

        @node
        def failing_node() -> str:
            raise RuntimeError("Something failed")

        with patch("railway.core.decorators.logger") as mock_logger:
            with pytest.raises(RuntimeError):
                failing_node()

            error_calls = [str(c) for c in mock_logger.error.call_args_list]
            assert any("log" in str(c).lower() for c in error_calls)


class TestErrorFormatting:
    """Test error message formatting."""

    def test_error_includes_exception_type(self):
        """Should include exception type in error."""
        from railway.core.decorators import node

        @node
        def type_error_node() -> str:
            raise TypeError("Wrong type")

        with patch("railway.core.decorators.logger") as mock_logger:
            with pytest.raises(TypeError):
                type_error_node()

            error_calls = [str(c) for c in mock_logger.error.call_args_list]
            assert any("TypeError" in str(c) for c in error_calls)

    def test_error_includes_node_name(self):
        """Should include node name in error."""
        from railway.core.decorators import node

        @node(name="my_special_node")
        def named_node() -> str:
            raise Exception("Error")

        with patch("railway.core.decorators.logger") as mock_logger:
            with pytest.raises(Exception):
                named_node()

            error_calls = [str(c) for c in mock_logger.error.call_args_list]
            assert any("my_special_node" in str(c) for c in error_calls)


class TestPipelineErrorDisplay:
    """Test pipeline error display."""

    def test_pipeline_shows_remaining_steps(self):
        """Should show remaining steps count on error."""
        from railway.core.pipeline import pipeline
        from railway.core.decorators import node

        @node
        def step1(x: int) -> int:
            return x + 1

        @node
        def step2(x: int) -> int:
            raise ValueError("Step 2 failed")

        @node
        def step3(x: int) -> int:
            return x * 2

        with patch("railway.core.decorators.logger"):
            with patch("railway.core.pipeline.logger") as mock_logger:
                with pytest.raises(ValueError):
                    pipeline(1, step1, step2, step3)

                info_calls = [str(c) for c in mock_logger.info.call_args_list]
                # Should mention skipping remaining steps
                assert any("skip" in str(c).lower() or "remaining" in str(c).lower()
                          for c in info_calls)
```

```bash
# 実行して失敗を確認
pytest tests/unit/core/test_error_display.py -v
# Expected: FAILED (partial)
```

### Step 2: Green（最小限の実装）

```python
# railway/core/decorators.py のエラーハンドリング部分を更新

def _get_error_hint(exception: Exception) -> str | None:
    """Get hint message for common errors."""
    if isinstance(exception, ConnectionError):
        return "ヒント: ネットワーク接続を確認してください。APIエンドポイントが正しいか確認してください。"
    elif isinstance(exception, TimeoutError):
        return "ヒント: タイムアウト値を増やすか、APIサーバーの状態を確認してください。"
    elif isinstance(exception, ValueError):
        return "ヒント: 入力データの形式や値を確認してください。"
    elif isinstance(exception, FileNotFoundError):
        return "ヒント: ファイルパスが正しいか確認してください。"
    elif isinstance(exception, PermissionError):
        return "ヒント: ファイルやディレクトリの権限を確認してください。"
    elif isinstance(exception, KeyError):
        return "ヒント: 必要なキーが存在するか確認してください。設定ファイルを確認してください。"

    # Check for API key related errors
    error_str = str(exception).upper()
    if "API_KEY" in error_str or "API_SECRET" in error_str or "UNAUTHORIZED" in error_str:
        return "ヒント: .envファイルでAPI認証情報が正しく設定されているか確認してください。"

    return None


def node(
    func: Callable[P, T] | None = None,
    *,
    retry: bool | Retry = False,
    log_input: bool = False,
    log_output: bool = False,
    name: str | None = None,
) -> Callable[P, T] | Callable[[Callable[P, T]], Callable[P, T]]:
    """Node decorator with improved error display."""

    def decorator(f: Callable[P, T]) -> Callable[P, T]:
        node_name = name or f.__name__

        @wraps(f)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            # ... existing code ...

            try:
                result = retryable_func(*args, **kwargs)
                # ... success handling ...
                return result

            except RetryError as e:
                original_exception = e.last_attempt.exception()
                attempt_count = e.last_attempt.attempt_number

                logger.error(
                    f"[{node_name}] ✗ Failed after {attempt_count} attempts: "
                    f"{type(original_exception).__name__}: {original_exception}"
                )
                logger.error("詳細は logs/error.log を確認してください")

                hint = _get_error_hint(original_exception)
                if hint:
                    logger.error(hint)

                raise original_exception

            except Exception as e:
                logger.error(f"[{node_name}] ✗ Failed: {type(e).__name__}: {e}")
                logger.error("詳細は logs/error.log を確認してください")

                hint = _get_error_hint(e)
                if hint:
                    logger.error(hint)

                raise

        # ... metadata ...
        return wrapper

    if func is None:
        return decorator
    return decorator(func)
```

```bash
# 実行して成功を確認
pytest tests/unit/core/test_error_display.py -v
# Expected: PASSED
```

---

## 完了条件

- [ ] ConnectionErrorに対するヒントが表示される
- [ ] TimeoutErrorに対するヒントが表示される
- [ ] ValueErrorに対するヒントが表示される
- [ ] ログファイルの場所が表示される
- [ ] 例外タイプがエラーメッセージに含まれる
- [ ] ノード名がエラーメッセージに含まれる
- [ ] パイプラインで残りステップ数が表示される
- [ ] テストカバレッジ90%以上

---

## 次のIssue

- #13: railway run コマンド
