# Section 03: Scene Tree Python Tool

## Overview

This section creates the Python-side implementation of the `godotiq_scene_tree` bridge tool. The module lives at `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/scene_tree.py` and follows the established two-layer pattern: a pure async function that validates/clamps parameters and forwards them to the Godot addon via the WebSocket bridge.

## Dependencies

- **Section 01 (protocol-plugin-wiring)**: Must be complete. The `scene_tree` timeout entry (`5.0`) must already exist in `TOOL_TIMEOUTS` in `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/bridge/protocol.py`.
- **Section 02 (scene-tree-gdscript)**: The GDScript handler in `godotiq_server.gd` must handle the `scene_tree` method. However, the Python tool can be implemented and tested independently using mocked bridge calls.

## Tests First

Create the test file at `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_bridge/test_scene_tree.py`.

All tests use `AsyncMock` and `patch` to mock `get_bridge`, following the exact pattern established in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_bridge/test_tools.py`.

```python
"""Tests for the scene_tree bridge tool."""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, patch

from godotiq.tools.bridge.scene_tree import scene_tree

DEFAULT_INCLUDE = ["transform", "script", "groups", "visibility"]


class TestSceneTree:
    """scene_tree parameter validation and bridge delegation."""

    # Test: scene_tree with default params calls bridge.request("scene_tree", ...)
    #   with depth=3 and the default include list.
    async def test_default_params(self) -> None:
        """Default call forwards depth=3 and default include list to bridge."""
        mock_bridge = AsyncMock()
        mock_bridge.request.return_value = {"root": "Root", "tree": []}
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            result = await scene_tree()

        mock_bridge.request.assert_called_once_with(
            "scene_tree",
            params={
                "root": "",
                "depth": 3,
                "filter_type": "",
                "include": DEFAULT_INCLUDE,
            },
        )
        assert result == {"root": "Root", "tree": []}

    # Test: scene_tree clamps depth below 1 to 1
    async def test_depth_clamped_below_1(self) -> None:
        """Depth values below 1 are clamped to 1."""
        mock_bridge = AsyncMock()
        mock_bridge.request.return_value = {}
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            await scene_tree(depth=0)

        assert mock_bridge.request.call_args.kwargs["params"]["depth"] == 1

    # Test: scene_tree clamps depth above 10 to 10
    async def test_depth_clamped_above_10(self) -> None:
        """Depth values above 10 are clamped to 10."""
        mock_bridge = AsyncMock()
        mock_bridge.request.return_value = {}
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            await scene_tree(depth=50)

        assert mock_bridge.request.call_args.kwargs["params"]["depth"] == 10

    # Test: scene_tree passes root param to bridge request
    async def test_root_param_forwarded(self) -> None:
        """Custom root path is forwarded to the bridge."""
        mock_bridge = AsyncMock()
        mock_bridge.request.return_value = {}
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            await scene_tree(root="Level/Enemies")

        assert mock_bridge.request.call_args.kwargs["params"]["root"] == "Level/Enemies"

    # Test: scene_tree passes filter_type param to bridge request
    async def test_filter_type_forwarded(self) -> None:
        """filter_type string is forwarded to the bridge."""
        mock_bridge = AsyncMock()
        mock_bridge.request.return_value = {}
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            await scene_tree(filter_type="MeshInstance3D")

        assert mock_bridge.request.call_args.kwargs["params"]["filter_type"] == "MeshInstance3D"

    # Test: scene_tree passes custom include list to bridge request
    async def test_custom_include_list(self) -> None:
        """A caller-supplied include list overrides the default."""
        mock_bridge = AsyncMock()
        mock_bridge.request.return_value = {}
        custom = ["transform", "script"]
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            await scene_tree(include=custom)

        assert mock_bridge.request.call_args.kwargs["params"]["include"] == custom

    # Test: scene_tree returns bridge response dict as-is
    async def test_returns_bridge_response(self) -> None:
        """The bridge response dict is returned unmodified."""
        mock_bridge = AsyncMock()
        expected = {
            "root": "Main",
            "scene_path": "res://main.tscn",
            "total_nodes": 42,
            "returned_nodes": 10,
            "tree": [{"n": "Player", "t": "CharacterBody3D"}],
        }
        mock_bridge.request.return_value = expected
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            result = await scene_tree()

        assert result == expected

    # Test: scene_tree raises on BridgeConnectionError
    async def test_raises_bridge_connection_error(self) -> None:
        """BridgeConnectionError from get_bridge propagates."""
        from godotiq.bridge.connection import BridgeConnectionError

        with patch(
            "godotiq.tools.bridge.scene_tree.get_bridge",
            side_effect=BridgeConnectionError("not connected"),
        ):
            with pytest.raises(BridgeConnectionError):
                await scene_tree()

    # Test: scene_tree raises on BridgeTimeoutError
    async def test_raises_bridge_timeout_error(self) -> None:
        """BridgeTimeoutError from bridge.request propagates."""
        from godotiq.bridge.connection import BridgeTimeoutError

        mock_bridge = AsyncMock()
        mock_bridge.request.side_effect = BridgeTimeoutError("timed out")
        with patch("godotiq.tools.bridge.scene_tree.get_bridge", return_value=mock_bridge):
            with pytest.raises(BridgeTimeoutError):
                await scene_tree()
```

### Test pattern notes

- The mock target is always `"godotiq.tools.bridge.scene_tree.get_bridge"` because the implementation module imports `get_bridge` from `godotiq.bridge.session`. Patching at the point of use ensures the mock is effective.
- `mock_bridge.request` is used (not `request_with_retry`) because `scene_tree` is a simple single-shot request, not a retry-worthy operation like screenshots.
- Tests verify both the outgoing call arguments and the returned value to ensure the tool is a transparent pass-through with parameter validation.

## Implementation

Create the file `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/scene_tree.py`.

### Behavior specification

1. **Depth clamping**: Clamp the `depth` parameter to the range `[1, 10]`. Values below 1 become 1; values above 10 become 10. Use `max(1, min(10, depth))`.

2. **Default include list**: When `include` is `None`, default to `["transform", "script", "groups", "visibility"]`. This tells the GDScript handler which optional data fields to populate on each node in the response tree.

3. **Bridge delegation**: Call `bridge.request("scene_tree", params={...})` with all four parameters: `root`, `depth`, `filter_type`, and `include`. Return the result dict as-is.

4. **No error handling**: This is the implementation layer. Exceptions (`BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError`) propagate to the caller. The MCP registration wrapper in section-08 handles them.

### Function signature and structure

```python
"""Scene tree tool -- read the live editor scene tree via the bridge."""

from __future__ import annotations

from godotiq.bridge.session import get_bridge

DEFAULT_INCLUDE: list[str] = ["transform", "script", "groups", "visibility"]


async def scene_tree(
    root: str = "",
    depth: int = 3,
    filter_type: str = "",
    include: list[str] | None = None,
) -> dict:
    """Get the live scene tree from the Godot editor.

    Args:
        root: Optional node path to start from (e.g. "Level/Enemies").
              Empty string means the edited scene root.
        depth: Maximum tree depth to return. Clamped to [1, 10].
        filter_type: Only include nodes of this type (e.g. "MeshInstance3D").
                     Empty string means all types.
        include: Which optional data fields to include per node.
                 Defaults to ["transform", "script", "groups", "visibility"].

    Returns:
        Dict with root, scene_path, total_nodes, returned_nodes, and tree.
    """
    # Clamp depth to valid range
    depth = max(1, min(10, depth))

    # Apply default include list
    if include is None:
        include = list(DEFAULT_INCLUDE)

    bridge = await get_bridge()
    return await bridge.request(
        "scene_tree",
        params={
            "root": root,
            "depth": depth,
            "filter_type": filter_type,
            "include": include,
        },
    )
```

### Design rationale

- **Why `list(DEFAULT_INCLUDE)`**: Copies the default list to prevent mutation of the module-level constant if a caller later modifies the returned list reference.
- **Why no `request_with_retry`**: Scene tree reads are fast and deterministic. Unlike screenshots (which may need retries while the viewport initializes), scene tree queries either succeed immediately or fail due to connection issues. The 5-second timeout from `TOOL_TIMEOUTS` is sufficient.
- **Why parameters are forwarded verbatim**: The GDScript handler in `godotiq_server.gd` (section 02) owns the semantic interpretation. The Python side is a thin validation/forwarding layer. This keeps the two layers cleanly separated.

## File Listing

| File | Action |
|------|--------|
| `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/scene_tree.py` | Create |
| `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_bridge/test_scene_tree.py` | Create |

## Verification

After implementation, run:

```bash
cd /Users/salvatorefarruggio/Desktop/godotiq && uv run pytest tests/test_bridge/test_scene_tree.py -v
```

All 9 tests should pass. The tests do not require a live Godot connection because `get_bridge` is mocked throughout.

---

## Implementation Notes

Implemented exactly as specified. 9/9 tests pass. No deviations from plan. Registration deferred to section-08.
