# Sprint 5b Research — Editor Exec + Save Scene

## 1. Codebase Analysis

### Current exec_code.py
- **Signature**: `async def exec_code(code: str = "", timeout_ms: int = 5000) -> dict:`
- **Bridge call**: `await bridge.request("exec", params={"code": code, "timeout_ms": timeout_ms}, timeout=10.0)`
- **Import**: `from godotiq.bridge.session import get_bridge`
- **No `context` parameter yet** — always routes to game-side exec

### Bridge __init__.py Registration Pattern
- `@mcp.tool(name="godotiq_exec", annotations={...})` with error handling wrapper
- Error handling: try/except catching `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError`, `Exception`
- Import: `from godotiq.tools.bridge.exec_code import exec_code as _exec_code`
- Context parameter from FastMCP: `ctx: Context = None`

### godotiq_server.gd Dispatch Table (all 14 entries)
- `ping` → `_handle_ping`
- `editor_context` → `_handle_editor_context`
- `scene_tree` → `_handle_scene_tree`
- `node_ops` → `_handle_node_ops`
- `run` → `_handle_run`
- `stop` → `_handle_stop`
- `screenshot` → `_handle_game_forward("godotiq:screenshot", 30000)`
- `perf_snapshot` → `_handle_game_forward("godotiq:query_perf", 5000)`
- `input` → `_handle_game_forward("godotiq:input", 65000)`
- `exec` → `_handle_game_forward("godotiq:exec", 10000)`
- `state_inspect` → `_handle_game_forward("godotiq:query_state", 5000)`
- `nav_query` → `_handle_game_forward("godotiq:query_nav", 10000)`
- `watch` → `_handle_game_forward("godotiq:watch", 5000)`
- `undo_history` → `_handle_undo_history`
- `_` (default) → `_send_error("UNKNOWN_METHOD")`

### Key Method Signatures
- `send_response(peer_id: int, id: String, result: Dictionary)` → `{"id": id, "status": "ok", "result": {...}}`
- `_send_error(peer_id: int, id: String, code: String, message: String)` → `{"id": id, "status": "error", "error": {...}}`
- `_handle_game_forward(peer_id, id, msg_type, params, timeout_ms)` — stores pending request, forwards via debugger

### Test Patterns
- Mock: `AsyncMock()` for bridge, `patch("godotiq.tools.bridge.exec_code.get_bridge", return_value=mock_bridge)`
- Assert on `mock_bridge.request.call_args[0][0]` for method name
- Assert on return dict structure

### Existing Test File for exec
- `tests/test_bridge/test_exec_code.py` exists with tests for empty code and basic execution

## 2. Godot Bug #87046 — Dynamic GDScript in @tool Mode

### Bug Summary
`GDScript.new()` + `source_code` + `reload()` + `set_script()` in @tool mode: methods appear to exist (`has_method()` returns true) but calling them fails with "Nonexistent function" error.

### Root Cause
Without `@tool` in the dynamic script's source code, Godot creates a "placeholder instance" instead of a real one.

### THE FIX
**Add `@tool` to the dynamically generated source code:**
```gdscript
script.source_code = "@tool\nextends Node\n\n" + code
```

This resolves the issue completely. The spec already does this.

### Affected Versions
- Godot 4.2.1: Bug present, no crash
- Godot 4.3-dev1: Bug present, crashes after scene reloads
- Fix: Adding `@tool` to source works across versions

### Related Issue #65263
`reload()` on pathless scripts may produce "File not found" warning. Workaround: set a dummy `resource_path`. Not critical — doesn't prevent execution.

### Implications for Sprint 5b
1. **Primary approach (script.new()) SHOULD WORK** because spec already prepends `@tool`
2. The bug was specifically about `set_script()` — `script.new()` (direct instantiation) may avoid it entirely
3. **File-based fallback is still good insurance** but probably won't be needed
4. `extends Node` + `add_child()` is the correct pattern for get_tree() access

## 3. Testing Infrastructure
- pytest with `asyncio_mode = "auto"`
- AsyncMock-based bridge mocking
- Pattern: create mock → patch get_bridge → call function → assert
- 622 existing tests baseline
