# Section 12: NODE_OPS — Rename and Get Property Extensions

## Implementation Status: COMPLETE

## Overview

This section adds two new operations to the `node_ops` tool: `rename` and `get_property`. The `rename` operation allows renaming nodes within the editor scene tree (with undo/redo support). The `get_property` operation reads a property value from a node and converts it to a JSON-friendly format. Both require changes on the Python side (validation and docstring updates) and the GDScript side (new operation handlers).

No other sections need to be completed first; this section is fully independent.

---

## Files to Modify

| File | Change |
|------|--------|
| `src/godotiq/tools/bridge/node_ops.py` | Add validation for `rename` and `get_property` ops |
| `src/godotiq/tools/bridge/__init__.py` | Update `godotiq_node_ops` docstring to list new ops |
| `godot-addon/addons/godotiq/godotiq_server.gd` | Add `_op_rename` and `_op_get_property` handler functions; register them in the `_execute_node_op` match statement |
| `tests/test_bridge/test_node_ops.py` | Add tests for new operations |

---

## Tests FIRST

All tests go in `tests/test_bridge/test_node_ops.py`, extending the existing `TestNodeOps` class. The existing test file uses `unittest.mock.AsyncMock` and `patch` to mock `get_bridge`. Follow the same pattern for new tests.

```python
# --- New tests to ADD to tests/test_bridge/test_node_ops.py ---

# Test: rename operation in ops list passes to bridge
# A valid rename op {"op": "rename", "node": "Enemy", "new_name": "Boss"} should
# be forwarded to bridge.request("node_ops", ...) without error.
# Assert mock_bridge.request was called with the ops list containing the rename op.

# Test: rename requires node and new_name
# An op {"op": "rename", "node": "Enemy"} (missing new_name) should raise ValueError.
# An op {"op": "rename", "new_name": "Boss"} (missing node) should raise ValueError.

# Test: rename with empty new_name raises error
# An op {"op": "rename", "node": "Enemy", "new_name": ""} should raise ValueError
# because empty names are not valid Godot node names.

# Test: get_property operation passes to bridge
# A valid op {"op": "get_property", "node": "Player", "property": "position"}
# should be forwarded to bridge without error.

# Test: get_property requires node and property
# An op {"op": "get_property", "node": "Player"} (missing property) should raise ValueError.
# An op {"op": "get_property", "property": "position"} (missing node) should raise ValueError.

# Test: get_property with empty property raises error
# An op {"op": "get_property", "node": "Player", "property": ""} should raise ValueError.

# Test: node_ops docstring lists rename and get_property
# Import node_ops and check that its __doc__ contains "rename" and "get_property".
```

Test structure notes:
- Each test is an `async def` method on the existing `TestNodeOps` class.
- For validation tests, no bridge mock is needed since validation raises `ValueError` before the bridge call.
- For pass-through tests, use the same `AsyncMock` / `patch("godotiq.tools.bridge.node_ops.get_bridge", ...)` pattern already in the file.

---

## Python Side Implementation

### `src/godotiq/tools/bridge/node_ops.py`

**Validation logic**: Add pre-flight validation for `rename` and `get_property` operations inside the `node_ops()` function, after the existing empty/limit checks and before calling `bridge.request()`.

For each operation in the `operations` list:

1. If `op == "rename"`:
   - Require `node` key: non-empty string
   - Require `new_name` key: non-empty string
   - Raise `ValueError` with a clear message if either is missing or empty

2. If `op == "get_property"`:
   - Require `node` key: non-empty string
   - Require `property` key: non-empty string
   - Raise `ValueError` with a clear message if either is missing or empty
   - Note: `get_property` is a read-only operation -- no spatial validation is needed. The existing `validate` flag logic should skip it (it only applies to `move`, `scale`, and `add_child`).

**Docstring update**: In the `node_ops()` function docstring, update the "Supported operations" line from:

```
move, rotate, scale, set_property, add_child, delete, duplicate, reparent
```

to:

```
move, rotate, scale, set_property, add_child, delete, duplicate, reparent, rename, get_property
```

### `src/godotiq/tools/bridge/__init__.py`

Update the `godotiq_node_ops` MCP wrapper docstring (currently at approximately line 217) to include `rename` and `get_property` in the operation list. The current docstring reads:

```
Batch operations with Ctrl+Z undo: move, rotate, scale, set_property, add_child, delete, duplicate, reparent.
```

Change to:

```
Batch operations with Ctrl+Z undo: move, rotate, scale, set_property, add_child, delete, duplicate, reparent, rename, get_property.
```

---

## GDScript Side Implementation

### `godot-addon/addons/godotiq/godotiq_server.gd`

#### 1. Register new ops in `_execute_node_op`

In the `match op:` block (around line 724), add two new cases before the `_:` default:

```gdscript
"rename":
    return _op_rename(op_data, scene_root, ur)
"get_property":
    return _op_get_property(op_data, scene_root)
```

Note: `_op_get_property` does not take `ur` (the undo/redo manager) because it is a read-only operation that does not modify the scene.

#### 2. Implement `_op_rename`

Add a new function `_op_rename(op_data: Dictionary, scene_root: Node, ur) -> Dictionary`.

Logic:
- Extract `node` (string) and `new_name` (string) from `op_data`
- Find the node using `_find_node_by_name_or_path(node_name, scene_root)`
- If node not found, return error dict with `"status": "error"` and `"error": "Node not found: {node_name}"`
- If `new_name` is empty, return error
- Store `var old_name := node.name`
- Use undo/redo: `ur.add_do_property(node, "name", new_name)` and `ur.add_undo_property(node, "name", old_name)`
- Return `{"op": "rename", "node": node_name, "status": "ok", "new_name": new_name}`
- Important: Godot may adjust the name to avoid sibling duplicates (e.g., appending `@2`). The `new_name` in the response should reflect the value that was requested. The actual name can differ after commit. If more accuracy is needed, the caller can use `scene_tree` to verify.

#### 3. Implement `_op_get_property`

Add a new function `_op_get_property(op_data: Dictionary, scene_root: Node) -> Dictionary`.

Logic:
- Extract `node` (string) and `property` (string) from `op_data`
- Find the node using `_find_node_by_name_or_path(node_name, scene_root)`
- If node not found, return error dict
- Get property value: `var value = node.get(property_name)`
- Check if the property exists: if `value == null` and `not property_name in node`, return error `"Property not found: {property_name}"`
  - Be careful: `null` can be a legitimate property value, so also check `node.get_property_list()` or use the pattern `node.get(property_name)` and check if the property name is in the node's property list
- Convert the value to a JSON-friendly type using a helper (see below)
- Return `{"op": "get_property", "node": node_name, "status": "ok", "property": property_name, "value": converted_value}`

#### 4. Value conversion helper

Create `_convert_to_json(value) -> Variant` (or inline in `_op_get_property`).

Conversion rules:
- `int`, `float`, `bool`, `String` -- return as-is (native JSON types)
- `Vector2` -- return `[x, y]`
- `Vector3` -- return `[x, y, z]`
- `Color` -- return `[r, g, b, a]` (float array, 0.0-1.0 range)
- `Transform3D` -- return `{"origin": [x,y,z], "basis": [[xx,xy,xz],[yx,yy,yz],[zx,zy,zz]]}`
- `null` / nil -- return `null`
- Everything else -- return `str(value)` as a fallback string representation

Note: The existing `_convert_value()` function in the same file converts JSON values TO Godot types (the opposite direction). The new helper converts FROM Godot types TO JSON. Name it distinctly to avoid confusion (e.g., `_value_to_json`).

---

## Validation Summary

All checks verified:

1. `uv run pytest tests/test_bridge/test_node_ops.py -v` -- 16 tests pass (9 existing + 7 new)
2. The `node_ops()` function docstring includes `rename` and `get_property`
3. The MCP wrapper docstring in `__init__.py` includes the new ops
4. The GDScript `_execute_node_op` match block routes `"rename"` and `"get_property"` to their handlers
5. `_op_get_property` does NOT use undo/redo (it is read-only)
6. `_op_rename` DOES use undo/redo (it modifies the scene)
7. The `_value_to_json` helper handles all listed type conversions + Array/Dictionary recursion

## Code Review Fixes Applied

- **get_property excluded from undo tracking**: Pure get_property batches no longer create empty undo actions
- **_value_to_json extended**: Added recursive Array and Dictionary conversion
- **Color values snapped**: Consistent with Vector2/Vector3 snapping at 0.001